<template>
<Layout>
<div class="score-container">
<div class="score-header">
<h2>成绩查询</h2>
<p>查看你的考试成绩和学习进度</p>
</div>
<!-- 增强的筛选区域 -->
<div class="score-filters">
<el-select
v-model="selectedSubject"
placeholder="选择科目"
class="subject-filter"
@change="handleFilterChange"
>
<el-option label="全部科目" value="" />
<el-option label="前端开发" value="frontend" />
<el-option label="后端开发" value="backend" />
<el-option label="数据库" value="database" />
</el-select>
<el-select
v-model="scoreRange"
placeholder="成绩范围"
class="range-filter"
@change="handleFilterChange"
>
<el-option label="全部成绩" value="" />
<el-option label="未通过(0-59)" value="fail" />
<el-option label="及格(60-79)" value="pass" />
<el-option label="优秀(80-100)" value="excellent" />
</el-select>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="date-filter"
@change="handleFilterChange"
/>
</div>
<!-- 加载状态 -->
<el-skeleton v-if="loading" class="score-skeleton" :rows="8" />
<template v-else>
<el-card class="score-overview">
<div slot="header">
<h3>成绩概览</h3>
</div>
<div class="overview-stats">
<div class="stat-item">
<div class="stat-value">{{ totalExams }}</div>
<div class="stat-label">总考试次数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ passRate }}%</div>
<div class="stat-label">通过率</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ avgScore }}</div>
<div class="stat-label">平均得分</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ highestScore }}</div>
<div class="stat-label">最高得分</div>
</div>
</div>
<!-- 优化的成绩趋势图 -->
<div class="score-chart">
<h4>成绩趋势</h4>
<el-card>
<template #header>
<div class="chart-header">
<span>成绩趋势图</span>
<el-select
v-model="chartType"
size="small"
@change="updateChart"
>
<el-option label="折线图" value="line" />
<el-option label="柱状图" value="bar" />
</el-select>
</div>
</template>
<v-chart :option="chartOption" height="400px" />
</el-card>
</div>
</el-card>
<!-- 成绩明细表格(带分页) -->
<el-card class="score-list" style="margin-top: 20px;">
<div slot="header">
<h3>考试成绩明细</h3>
</div>
<!-- 空状态提示 -->
<el-empty
v-if="filteredScores.length === 0"
description="没有找到匹配的成绩记录"
class="empty-state"
/>
<el-table
v-else
:data="pagedScores"
border
style="width: 100%"
:default-sort="{prop: 'examDate', order: 'descending'}"
@sort-change="handleSortChange"
row-class-name="table-row-class"
>
<el-table-column prop="examId" label="考试ID" width="100" />
<el-table-column prop="title" label="考试名称" sortable />
<el-table-column prop="subjectName" label="科目" width="120" />
<el-table-column
prop="score"
label="得分"
width="100"
sortable
>
<template #default="scope">
<span :class="scope.row.score >= 60 ? 'pass-score' : 'fail-score'">
{{ scope.row.score }}
</span>
</template>
</el-table-column>
<el-table-column prop="totalScore" label="总分" width="80" />
<el-table-column
prop="rank"
label="排名"
width="80"
sortable
>
<template #default="scope">
<el-badge :value="scope.row.rank" type="primary" />
</template>
</el-table-column>
<el-table-column
prop="examDate"
label="考试日期"
width="180"
sortable
/>
<el-table-column prop="duration" label="用时(分钟)" width="120" />
<el-table-column label="操作" width="220">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="viewDetails(scope.row.examId)"
style="margin-right: 5px;"
>
详情
</el-button>
<el-button
type="success"
size="small"
@click="viewAnalysis(scope.row.examId)"
style="margin-right: 5px;"
>
分析
</el-button>
<el-button
type="warning"
size="small"
@click="retryExam(scope.row.examId)"
:disabled="scope.row.score >= 60"
>
重考
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页控件 -->
<div class="pagination-container" v-if="filteredScores.length > 0">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20]"
:page-size="pageSize"
:total="filteredScores.length"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
</el-card>
</template>
</div>
</Layout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import VChart from 'vue-echarts'
import { use } from 'echarts/core'
import { LineChart, BarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'
import Layout from '@/components/layout/Layout.vue'
import { ElMessage } from 'element-plus'
// 注册所需的图表模块
use([LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent, CanvasRenderer])
// 类型定义
interface ScoreItem {
examId: string
title: string
subject: string
subjectName: string
score: number
totalScore: number
rank: number
examDate: string
duration: number
allowRetry?: boolean // 是否允许重考
}
// 图表配置
const chartOption = ref<any>({
tooltip: {
trigger: 'axis'
},
legend: {
data: ['你的成绩', '平均分']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value',
min: 0,
max: 100,
axisLabel: {
formatter: '{value} 分'
}
},
series: [
{
name: '你的成绩',
type: 'line',
data: [],
markPoint: {
data: [
{ type: 'max', name: '最高分' },
{ type: 'min', name: '最低分' }
]
},
markLine: {
data: [
{ type: 'average', name: '平均分' }
]
}
},
{
name: '平均分',
type: 'line',
data: [],
lineStyle: {
type: 'dashed'
}
}
]
})
const router = useRouter()
// 筛选条件
const selectedSubject = ref('')
const dateRange = ref<any>(null)
const scoreRange = ref('') // 新增:成绩范围筛选
const currentPage = ref(1)
const pageSize = ref(10)
const sortProp = ref('examDate')
const sortOrder = ref('descending')
const chartType = ref('line') // 图表类型切换
const loading = ref(false)
// 成绩数据
const scoreData = ref<ScoreItem[]>([
{
examId: 'e001',
title: 'JavaScript 基础测试',
subject: 'frontend',
subjectName: '前端开发',
score: 85,
totalScore: 100,
rank: 3,
examDate: '2023-09-15',
duration: 45,
allowRetry: true
},
{
examId: 'e002',
title: 'HTML/CSS 技能考核',
subject: 'frontend',
subjectName: '前端开发',
score: 78,
totalScore: 100,
rank: 7,
examDate: '2023-09-20',
duration: 55,
allowRetry: true
},
{
examId: 'e003',
title: 'Node.js 入门测试',
subject: 'backend',
subjectName: '后端开发',
score: 58,
totalScore: 100,
rank: 15,
examDate: '2023-09-25',
duration: 60,
allowRetry: true
},
{
examId: 'e004',
title: 'MySQL 基础测试',
subject: 'database',
subjectName: '数据库',
score: 92,
totalScore: 100,
rank: 1,
examDate: '2023-10-05',
duration: 40,
allowRetry: false
}
])
// 模拟平均分数据(实际项目可从接口获取)
const avgScoreData = ref([72, 75, 60, 88])
// 处理筛选变化
const handleFilterChange = () => {
currentPage.value = 1 // 筛选变化时重置到第一页
loading.value = true
// 模拟加载延迟
setTimeout(() => {
loading.value = false
updateChart() // 筛选后更新图表
}, 500)
}
// 筛选后的成绩
const filteredScores = computed<ScoreItem[]>(() => {
return scoreData.value.filter(item => {
// 科目筛选
const subjectMatch = !selectedSubject.value || item.subject === selectedSubject.value
// 日期筛选
let dateMatch = true
if (dateRange.value && dateRange.value.length === 2) {
const examDate = new Date(item.examDate)
dateMatch = examDate >= dateRange.value[0] && examDate <= dateRange.value[1]
}
// 成绩范围筛选
let scoreMatch = true
if (scoreRange.value) {
switch(scoreRange.value) {
case 'fail':
scoreMatch = item.score < 60
break
case 'pass':
scoreMatch = item.score >= 60 && item.score < 80
break
case 'excellent':
scoreMatch = item.score >= 80
break
}
}
return subjectMatch && dateMatch && scoreMatch
}).sort((a, b) => {
// 排序处理
if (sortProp.value) {
const prop = sortProp.value as keyof ScoreItem
if (a[prop] < b[prop]) return sortOrder.value === 'ascending' ? -1 : 1
if (a[prop] > b[prop]) return sortOrder.value === 'ascending' ? 1 : -1
}
return 0
})
})
// 分页处理
const pagedScores = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value
return filteredScores.value.slice(startIndex, startIndex + pageSize.value)
})
// 统计数据
const totalExams = computed(() => filteredScores.value.length)
const passRate = computed(() => {
if (totalExams.value === 0) return 0
const passCount = filteredScores.value.filter(item => item.score >= 60).length
return Math.round((passCount / totalExams.value) * 100)
})
const avgScore = computed(() => {
if (totalExams.value === 0) return 0
const sum = filteredScores.value.reduce((acc, item) => acc + item.score, 0)
return Math.round(sum / totalExams.value)
})
const highestScore = computed(() => {
if (totalExams.value === 0) return 0
return Math.max(...filteredScores.value.map(item => item.score))
})
// 图表数据更新
const updateChart = () => {
const sortedData = [...filteredScores.value].sort(
(a, b) => new Date(a.examDate).getTime() - new Date(b.examDate).getTime()
)
chartOption.value.xAxis.data = sortedData.map(item => item.title)
chartOption.value.series[0].data = sortedData.map(item => item.score)
chartOption.value.series[0].type = chartType.value
// 截取对应长度的平均分数据(实际项目应从接口获取对应考试的平均分)
chartOption.value.series[1].data = avgScoreData.value.slice(0, sortedData.length)
chartOption.value.series[1].type = chartType.value
}
// 表格排序
const handleSortChange = (sort: { prop: string, order: string }) => {
sortProp.value = sort.prop
sortOrder.value = sort.order === 'ascending' ? 'ascending' : 'descending'
}
// 分页事件
const handleSizeChange = (val: number) => {
pageSize.value = val
currentPage.value = 1
}
const handleCurrentChange = (val: number) => {
currentPage.value = val
}
// 查看详情
const viewDetails = (examId: string) => {
router.push(`/score/details/${examId}`)
}
// 查看分析
const viewAnalysis = (examId: string) => {
router.push(`/score/analysis/${examId}`)
}
// 重考功能
const retryExam = (examId: string) => {
const exam = scoreData.value.find(item => item.examId === examId)
if (exam && exam.allowRetry) {
router.push(`/examination/${examId}?retry=true`)
} else {
ElMessage.warning('该考试不允许重考')
}
}
// 初始加载
onMounted(() => {
loading.value = true
// 模拟数据加载
setTimeout(() => {
loading.value = false
updateChart()
}, 800)
})
// 监听筛选条件变化更新图表
watch([selectedSubject, dateRange, scoreRange], updateChart)
</script>
<style scoped>
.score-container {
padding: 20px;
}
.score-header {
margin-bottom: 20px;
}
.score-header h2 {
margin: 0 0 10px 0;
color: #303133;
}
.score-header p {
margin: 0;
color: #606266;
}
.score-filters {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.subject-filter, .range-filter {
width: 200px;
}
.date-filter {
width: 350px;
}
.overview-stats {
display: flex;
justify-content: space-around;
margin-bottom: 30px;
flex-wrap: wrap;
gap: 15px;
}
.stat-item {
text-align: center;
padding: 15px;
flex: 1;
min-width: 120px;
background-color: #f5f7fa;
border-radius: 6px;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #4b0082;
margin-bottom: 5px;
}
.stat-label {
color: #606266;
font-size: 14px;
}
.score-chart {
padding: 10px 0;
}
.score-chart h4 {
margin: 0 0 15px 0;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.pass-score {
color: #67c23a;
font-weight: 500;
}
.fail-score {
color: #f56c6c;
font-weight: 500;
}
.pagination-container {
margin-top: 15px;
text-align: right;
}
.empty-state {
margin: 40px 0;
}
.score-skeleton {
margin-bottom: 20px;
}
/* 表格行样式 */
:deep(.table-row-class) {
&:nth-child(even) {
background-color: #fafafa;
}
&:hover {
background-color: #f5f7fa;
}
}
/* 未通过行高亮 */
:deep(.el-table__row) {
&:has(.fail-score) {
background-color: #fff5f5 !important;
}
}
</style>根据代码修复控制台告警[ECharts] Can't get DOM width or height. Please check dom.clientWidth and dom.clientHeight. They should not be 0.For example, you may need to call this in the callback of window.onload.
outputLog @ chunk-F3YHV5CU.js?v=3fbc4ae4:604
warn @ chunk-F3YHV5CU.js?v=3fbc4ae4:611
init2 @ chunk-F3YHV5CU.js?v=3fbc4ae4:14540
init$1 @ vue-echarts.js?v=3fbc4ae4:243
(anonymous) @ vue-echarts.js?v=3fbc4ae4:365
(anonymous) @ chunk-JDSL2H4L.js?v=3fbc4ae4:4998
callWithErrorHandling @ chunk-JDSL2H4L.js?v=3fbc4ae4:2296
callWithAsyncErrorHandling @ chunk-JDSL2H4L.js?v=3fbc4ae4:2303
hook.__weh.hook.__weh @ chunk-JDSL2H4L.js?v=3fbc4ae4:4978
flushPostFlushCbs @ chunk-JDSL2H4L.js?v=3fbc4ae4:2481
flushJobs @ chunk-JDSL2H4L.js?v=3fbc4ae4:2523
Promise.then
queueFlush @ chunk-JDSL2H4L.js?v=3fbc4ae4:2418
queueJob @ chunk-JDSL2H4L.js?v=3fbc4ae4:2413
effect2.scheduler @ chunk-JDSL2H4L.js?v=3fbc4ae4:7639
trigger @ chunk-JDSL2H4L.js?v=3fbc4ae4:533
endBatch @ chunk-JDSL2H4L.js?v=3fbc4ae4:591
notify @ chunk-JDSL2H4L.js?v=3fbc4ae4:853
trigger @ chunk-JDSL2H4L.js?v=3fbc4ae4:827
set value @ chunk-JDSL2H4L.js?v=3fbc4ae4:1706
(anonymous) @ ScoreInquiry.vue:479
setTimeout
(anonymous) @ ScoreInquiry.vue:478
(anonymous) @ chunk-JDSL2H4L.js?v=3fbc4ae4:4998
callWithErrorHandling @ chunk-JDSL2H4L.js?v=3fbc4ae4:2296
callWithAsyncErrorHandling @ chunk-JDSL2H4L.js?v=3fbc4ae4:2303
hook.__weh.hook.__weh @ chunk-JDSL2H4L.js?v=3fbc4ae4:4978
flushPostFlushCbs @ chunk-JDSL2H4L.js?v=3fbc4ae4:2481
flushJobs @ chunk-JDSL2H4L.js?v=3fbc4ae4:2523
Promise.then
queueFlush @ chunk-JDSL2H4L.js?v=3fbc4ae4:2418
queuePostFlushCb @ chunk-JDSL2H4L.js?v=3fbc4ae4:2432
queueEffectWithSuspense @ chunk-JDSL2H4L.js?v=3fbc4ae4:9523
baseWatchOptions.scheduler @ chunk-JDSL2H4L.js?v=3fbc4ae4:8402
effect2.scheduler @ chunk-JDSL2H4L.js?v=3fbc4ae4:2042
trigger @ chunk-JDSL2H4L.js?v=3fbc4ae4:533
endBatch @ chunk-JDSL2H4L.js?v=3fbc4ae4:591
notify @ chunk-JDSL2H4L.js?v=3fbc4ae4:853
trigger @ chunk-JDSL2H4L.js?v=3fbc4ae4:827
set value @ chunk-JDSL2H4L.js?v=3fbc4ae4:1706
finalizeNavigation @ vue-router.js?v=3fbc4ae4:2213
(anonymous) @ vue-router.js?v=3fbc4ae4:2151
Promise.then
pushWithRedirect @ vue-router.js?v=3fbc4ae4:2138
push @ vue-router.js?v=3fbc4ae4:2089
handleMenuSelect @ Header.vue:176
callWithErrorHandling @ chunk-JDSL2H4L.js?v=3fbc4ae4:2296
callWithAsyncErrorHandling @ chunk-JDSL2H4L.js?v=3fbc4ae4:2303
emit @ chunk-JDSL2H4L.js?v=3fbc4ae4:8589
(anonymous) @ chunk-JDSL2H4L.js?v=3fbc4ae4:10300
handleMenuItemClick @ chunk-IO2JD3WY.js?v=3fbc4ae4:36331
handleClick @ chunk-IO2JD3WY.js?v=3fbc4ae4:36568
callWithErrorHandling @ chunk-JDSL2H4L.js?v=3fbc4ae4:2296
callWithAsyncErrorHandling @ chunk-JDSL2H4L.js?v=3fbc4ae4:2303
invoker @ chunk-JDSL2H4L.js?v=3fbc4ae4:11335
最新发布