refactor: 成绩报告
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
9fc79d7b7e
commit
967d14efa2
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
|
||||
// 选中的科目ID,0表示全科
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue