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>
|