xlx_teacher_app/src/components/class-analysis/AverageScoreChart.vue

307 lines
7.6 KiB
Vue
Raw Normal View History

2025-08-30 12:29:31 +08:00
<script lang="ts" setup>
import type * as API from '@/service/types'
2025-10-07 14:52:01 +08:00
import { useQuery } from '@tanstack/vue-query'
import { LineChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { computed, ref } from 'vue'
2025-08-30 12:29:31 +08:00
import { teacherAnalysisTrendUsingPost } from '@/service/laoshichengjifenxi'
import { useHomeStore } from '@/store/home'
const props = defineProps<{
selectedSubjectId: number
compareClassId: number | null
}>()
const emit = defineEmits<{
openCompareClassDialog: []
}>()
2025-10-07 14:52:01 +08:00
// 注册 ECharts 组件
echarts.use([
LineChart,
GridComponent,
LegendComponent,
TooltipComponent,
CanvasRenderer,
])
2025-08-30 12:29:31 +08:00
2025-10-07 14:52:01 +08:00
// 类型别名
type TrendInfo = API.TrendInfo
2025-08-30 12:29:31 +08:00
2025-10-07 14:52:01 +08:00
// 使用store
const homeStore = useHomeStore()
2025-08-30 12:29:31 +08:00
// 对比班级名称
const compareClassName = computed(() => {
if (!props.compareClassId) {
return '选择对比班级'
}
const cls = homeStore.classOptions.find(item => item.value === props.compareClassId)
return cls?.label || '选择对比班级'
})
2025-10-07 14:52:01 +08:00
// 使用 TanStack Query 获取本班级走势数据
const {
data: classTrendData,
isLoading: isLoadingClass,
refetch: refetchClassData,
} = useQuery({
queryKey: computed(() => [
'class-trend',
homeStore.selectedClassId,
homeStore.selectedGradeKey,
props.selectedSubjectId,
]),
queryFn: async () => {
const response = await teacherAnalysisTrendUsingPost({
2025-08-30 12:29:31 +08:00
body: {
class_key: homeStore.selectedClassId,
grade_key: homeStore.selectedGradeKey,
2025-10-07 14:52:01 +08:00
class_key_compare: props.compareClassId || homeStore.selectedClassId, // 如果没有对比班级,使用自己
2025-08-30 12:29:31 +08:00
subject_id: props.selectedSubjectId || undefined,
top_n: 50,
},
})
2025-10-07 14:52:01 +08:00
return response || { trend_list: [] }
},
enabled: computed(() => !!homeStore.selectedClassId && !!homeStore.selectedGradeKey),
staleTime: 30000, // 30秒内不重新请求
})
2025-08-30 12:29:31 +08:00
2025-10-07 14:52:01 +08:00
// 获取对比班级的 grade_key
const compareClassGradeKey = computed(() => {
if (!props.compareClassId)
return null
// 从 classList 中找到对应班级的 gradeKey
const classItem = homeStore.classOptions.find(item => item.value === props.compareClassId)
return classItem ? homeStore.classGradeMap.get(props.compareClassId) : null
})
2025-08-30 12:29:31 +08:00
2025-10-07 14:52:01 +08:00
// 使用 TanStack Query 获取对比班级走势数据
const {
data: compareTrendData,
isLoading: isLoadingCompare,
} = useQuery({
queryKey: computed(() => [
'class-trend',
props.compareClassId,
compareClassGradeKey.value,
props.selectedSubjectId,
]),
queryFn: async () => {
const response = await teacherAnalysisTrendUsingPost({
body: {
class_key: props.compareClassId,
grade_key: compareClassGradeKey.value,
class_key_compare: homeStore.selectedClassId || props.compareClassId, // 对比班级
subject_id: props.selectedSubjectId || undefined,
top_n: 50,
},
2025-08-30 12:29:31 +08:00
})
2025-10-07 14:52:01 +08:00
return response || { trend_list: [] }
},
enabled: computed(() => !!props.compareClassId && !!compareClassGradeKey.value),
staleTime: 30000,
})
2025-08-30 12:29:31 +08:00
2025-10-07 14:52:01 +08:00
// 计算加载状态
const loading = computed(() => isLoadingClass.value || isLoadingCompare.value)
2025-08-30 12:29:31 +08:00
2025-10-07 14:52:01 +08:00
// 计算走势列表
const classTrendList = computed(() => classTrendData.value?.trend_list || [])
const compareTrendList = computed(() => compareTrendData.value?.trend_list || [])
2025-08-30 12:29:31 +08:00
2025-10-07 14:52:01 +08:00
// uni-echarts 配置
const chartOption = computed(() => {
if (classTrendList.value.length === 0) {
return {}
}
2025-08-30 12:29:31 +08:00
// 获取考试名称作为横轴
2025-10-07 14:52:01 +08:00
const categories = classTrendList.value.map(item => item.exam_name || '')
// 从 trend_data 中提取数据
const classScores = classTrendList.value.map(item =>
item.trend_data?.[0]?.class_avg_score || 0,
)
// 计算年级平均分
const gradeScores = classTrendList.value.map((item) => {
const trendData = item.trend_data || []
if (trendData.length === 0)
return 0
const validScores = trendData
.map(td => td.class_avg_score || 0)
.filter(score => score > 0)
return validScores.length > 0
? validScores.reduce((sum, score) => sum + score, 0) / validScores.length
: 0
})
// 对比班级平均分
const compareScores = compareTrendList.value.map(item =>
item.trend_data?.[0]?.class_avg_score || 0,
)
2025-08-30 12:29:31 +08:00
const series: any[] = [
{
name: '本班级',
type: 'line',
data: classScores,
2025-10-07 14:52:01 +08:00
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#3b82f6' },
lineStyle: { width: 2 },
2025-08-30 12:29:31 +08:00
},
{
name: '年级平均',
type: 'line',
data: gradeScores,
2025-10-07 14:52:01 +08:00
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#f59e0b' },
lineStyle: { width: 2 },
2025-08-30 12:29:31 +08:00
},
]
const legendData = ['本班级', '年级平均']
// 如果有对比班级数据,添加到图表中
2025-10-07 14:52:01 +08:00
if (props.compareClassId && compareTrendList.value.length > 0) {
2025-08-30 12:29:31 +08:00
series.push({
2025-10-07 14:52:01 +08:00
name: compareClassName.value,
2025-08-30 12:29:31 +08:00
type: 'line',
data: compareScores,
2025-10-07 14:52:01 +08:00
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#10b981' },
lineStyle: { width: 2 },
2025-08-30 12:29:31 +08:00
})
2025-10-07 14:52:01 +08:00
legendData.push(compareClassName.value)
2025-08-30 12:29:31 +08:00
}
return {
tooltip: {
trigger: 'axis',
2025-10-07 14:52:01 +08:00
confine: true,
formatter: (params: any) => {
if (!Array.isArray(params))
return ''
let result = `${params[0].axisValue}<br/>`
params.forEach((param: any) => {
result += `${param.marker} ${param.seriesName}: ${param.value.toFixed(2)}分<br/>`
})
return result
2025-08-30 12:29:31 +08:00
},
},
legend: {
data: legendData,
2025-10-07 14:52:01 +08:00
top: 5,
textStyle: {
fontSize: 12,
},
2025-08-30 12:29:31 +08:00
},
grid: {
2025-10-07 14:52:01 +08:00
left: 40,
right: 15,
bottom: 30,
top: 40,
containLabel: false,
2025-08-30 12:29:31 +08:00
},
xAxis: {
type: 'category',
data: categories,
2025-10-07 14:52:01 +08:00
axisLabel: {
rotate: 30,
interval: 0,
fontSize: 10,
},
axisLine: {
lineStyle: {
color: '#cccccc',
},
},
2025-08-30 12:29:31 +08:00
},
yAxis: {
type: 'value',
name: '分数',
2025-10-07 14:52:01 +08:00
nameTextStyle: {
fontSize: 11,
},
axisLabel: {
fontSize: 10,
},
splitLine: {
lineStyle: {
type: 'dashed',
color: '#eeeeee',
},
},
2025-08-30 12:29:31 +08:00
},
series,
}
2025-10-07 14:52:01 +08:00
})
2025-08-30 12:29:31 +08:00
// 打开对比班级选择
function openCompareClassDialog() {
emit('openCompareClassDialog')
}
2025-10-07 14:52:01 +08:00
const showChart = computed(() => {
return !loading.value && classTrendList.value.length > 0
2025-08-30 12:29:31 +08:00
})
2025-10-07 14:52:01 +08:00
// 图表容器引用
const chartRef = ref(null)
2025-08-30 12:29:31 +08:00
// 暴露方法给父组件
defineExpose({
2025-10-07 14:52:01 +08:00
refetch: refetchClassData,
2025-08-30 12:29:31 +08:00
})
</script>
<template>
<view class="rounded-xl bg-white p-4 shadow-sm">
<view class="mb-4 flex items-center justify-between">
<text class="text-lg text-slate-800 font-semibold">均分对比</text>
<wd-button size="small" @click="openCompareClassDialog">
{{ compareClassName }}
</wd-button>
</view>
2025-10-07 14:52:01 +08:00
<!-- uni-echarts 图表组件 -->
<view v-if="showChart" class="chart-container">
<uni-echarts
ref="chartRef"
:option="chartOption"
custom-class="chart"
/>
</view>
2025-08-30 12:29:31 +08:00
<!-- 暂无数据提示 -->
2025-10-07 14:52:01 +08:00
<view v-if="classTrendList.length === 0 && !loading" class="py-8 text-center">
2025-08-30 12:29:31 +08:00
<text class="text-sm text-gray-500">暂无数据</text>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="h-80 flex items-center justify-center">
<wd-loading size="24" />
</view>
</view>
</template>
2025-10-07 14:52:01 +08:00
<style scoped>
.chart {
2025-08-30 12:29:31 +08:00
width: 100%;
2025-10-07 14:52:01 +08:00
height: 250px;
2025-08-30 12:29:31 +08:00
}
</style>