2026-05-29 20:39:12 +08:00

452 lines
12 KiB
Vue

<template>
<ParentPage active="reports" :show-tab="false" :use-scroll-view="false">
<ParentFilterRow :items="filterItems" @change="onFilterChange" />
<view class="module-section">
<ParentCard title="错题知识点占比" subtitle="按错题占比识别最近需要优先补强的知识点">
<view v-if="filteredKnowledge.length" class="ratio-layout">
<view class="ratio-main">
<view class="ratio-total">
<text class="ratio-number">{{ mainWrongPercent }}%</text>
<text class="ratio-label">最高错题占比</text>
</view>
<view
v-for="item in filteredKnowledge"
:key="`bar-${item.name}`"
class="ratio-bar"
>
<view class="ratio-fill" :class="item.tone" :style="{ width: `${item.wrongPercent}%` }"></view>
</view>
</view>
<view class="legend-list">
<view v-for="item in filteredKnowledge" :key="item.name" class="legend-item">
<view class="legend-dot" :class="item.tone"></view>
<text class="legend-name ellipsis">{{ item.name }}</text>
<text class="legend-percent">{{ item.wrongPercent }}%</text>
</view>
</view>
</view>
<xy-Empty v-else type="task" content="暂无符合条件的知识点数据" />
</ParentCard>
</view>
<view class="module-section">
<ParentCard title="知识点得分" subtitle="可切换得分等级,快速定位当前薄弱段">
<view class="level-tabs">
<view
v-for="level in scoreLevels"
:key="level.key"
class="level-tab"
:class="{ active: activeLevel === level.key }"
@click="activeLevel = level.key"
>
{{ level.label }}
</view>
</view>
<view v-if="levelKnowledge.length" class="score-table">
<view class="score-row table-head">
<text>知识点</text>
<text>满分</text>
<text>得分</text>
</view>
<view v-for="item in levelKnowledge" :key="`${item.name}-${activeLevel}`" class="score-row">
<text class="knowledge-name ellipsis">{{ item.name }}</text>
<text>{{ item.fullScore }}</text>
<text class="score-value">{{ item.score }}<text class="score-rate">({{ scoreRate(item) }}%)</text></text>
</view>
</view>
<xy-Empty v-else type="learn" content="当前等级暂无知识点" />
</ParentCard>
</view>
<view class="module-section">
<ParentCard title="得分能力" subtitle="综合最近多次练习得分,观察能力变化和稳定性">
<view class="ucharts-wrap">
<qiun-data-charts
class="ability-chart"
type="line"
canvasId="parentAbilityTrendCanvas2dLineChart"
background="rgba(255,255,255,0)"
:canvas2d="true"
:in-scroll-view="false"
:ontouch="false"
:disable-scroll="false"
:reshow="chartReshow"
:opts="abilityChartOpts"
:chartData="abilityChartData"
/>
</view>
<view class="summary-grid">
<view v-for="item in abilitySummary" :key="item.label" class="summary-item">
<text class="summary-label">{{ item.label }}</text>
<text class="summary-value">{{ item.value }}</text>
<text class="summary-desc">{{ item.desc }}</text>
</view>
</view>
</ParentCard>
</view>
</ParentPage>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { onReady, onShow } from '@dcloudio/uni-app';
import ParentCard from '../components/ParentCard.vue';
import ParentFilterRow from '../components/ParentFilterRow.vue';
import ParentPage from '../components/ParentPage.vue';
import {
abilitySummary,
abilityTrend,
categoryOptions,
gradeOptions,
knowledgeWeakPoints,
subjectOptions,
type KnowledgeWeakPoint,
} from '../utils/data';
type ScoreLevel = 'weak' | 'pass' | 'good' | 'excellent';
const subjectIndex = ref(0);
const gradeIndex = ref(1);
const categoryIndex = ref(0);
const activeLevel = ref<ScoreLevel>('weak');
const filterItems = computed(() => [
{ key: 'subject', range: subjectOptions, value: subjectIndex.value },
{ key: 'grade', range: gradeOptions, value: gradeIndex.value },
{ key: 'category', range: categoryOptions, value: categoryIndex.value },
]);
const onFilterChange = (payload: { key: string; value: number }) => {
if (payload.key === 'subject') {
subjectIndex.value = payload.value;
return;
}
if (payload.key === 'grade') {
gradeIndex.value = payload.value;
return;
}
if (payload.key === 'category') {
categoryIndex.value = payload.value;
}
};
const scoreLevels: { key: ScoreLevel; label: string }[] = [
{ key: 'weak', label: '薄弱' },
{ key: 'pass', label: '合格' },
{ key: 'good', label: '良好' },
{ key: 'excellent', label: '优秀' },
];
const filteredKnowledge = computed(() => {
const subject = subjectOptions[subjectIndex.value];
const grade = gradeOptions[gradeIndex.value];
const category = categoryOptions[categoryIndex.value];
return knowledgeWeakPoints.filter((item) => {
const subjectMatched = subject === '全部学科' || item.subject === subject;
const gradeMatched = grade === '全部年级' || item.grade === grade;
const categoryMatched = category === '全部类型' || item.category === category;
return subjectMatched && gradeMatched && categoryMatched;
});
});
const mainWrongPercent = computed(() => Math.max(...filteredKnowledge.value.map((item) => item.wrongPercent), 0));
const scoreRate = (item: KnowledgeWeakPoint) => Math.round((item.score / item.fullScore) * 100);
const levelOf = (item: KnowledgeWeakPoint): ScoreLevel => {
const rate = scoreRate(item);
if (rate < 60) return 'weak';
if (rate < 75) return 'pass';
if (rate < 90) return 'good';
return 'excellent';
};
const levelKnowledge = computed(() => filteredKnowledge.value.filter((item) => levelOf(item) === activeLevel.value));
const abilityChartData = ref({});
const chartReshow = ref(false);
const buildAbilityChartData = () => ({
categories: abilityTrend.map((item) => item.date),
series: [
{
name: '得分能力',
data: abilityTrend.map((item) => item.score),
color: '#0891b2',
},
],
});
const fillChart = () => {
abilityChartData.value = JSON.parse(JSON.stringify(buildAbilityChartData()));
chartReshow.value = !chartReshow.value;
};
onReady(() => {
setTimeout(fillChart, 500);
});
onShow(() => {
if (Object.keys(abilityChartData.value).length) {
chartReshow.value = !chartReshow.value;
}
});
const abilityChartOpts = {
color: ['#0891b2'],
padding: [18, 12, 0, 12],
enableScroll: false,
legend: { show: false },
xAxis: {
disableGrid: false,
gridColor: '#eef2f7',
fontColor: '#64748b',
fontSize: 10,
boundaryGap: 'justify',
},
yAxis: {
gridType: 'dash',
dashLength: 2,
gridColor: '#eef2f7',
data: [
{
min: 40,
max: 100,
splitNumber: 4,
fontColor: '#94a3b8',
fontSize: 10,
},
],
},
extra: {
line: {
type: 'curve',
width: 3,
activeType: 'hollow',
},
markLine: {
type: 'dash',
dashLength: 6,
data: [
{ value: 85, lineColor: 'rgba(244, 63, 94, 0.32)', showLabel: true, labelText: '优秀' },
{ value: 70, lineColor: 'rgba(244, 63, 94, 0.28)', showLabel: true, labelText: '良好' },
{ value: 60, lineColor: 'rgba(244, 63, 94, 0.22)', showLabel: true, labelText: '一般' },
],
},
},
};
</script>
<style lang="scss" scoped>
.module-section {
margin-top: 44rpx;
}
.filter-row + .module-section {
margin-top: 36rpx;
}
.ratio-layout {
display: flex;
align-items: center;
gap: 28rpx;
}
.ratio-main {
width: 232rpx;
flex-shrink: 0;
}
.ratio-total {
min-height: 116rpx;
border-radius: 28rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #ecfeff 0%, #ffffff 100%);
box-shadow: inset 0 -2rpx 0 rgba(34, 211, 238, 0.24);
}
.ratio-number {
color: #164e63;
font-size: 42rpx;
font-weight: 900;
}
.ratio-label {
margin-top: 2rpx;
color: #64748b;
font-size: 20rpx;
}
.ratio-bar {
height: 16rpx;
margin-top: 14rpx;
border-radius: 999rpx;
overflow: hidden;
background: #e2e8f0;
}
.ratio-fill {
height: 100%;
border-radius: 999rpx;
&.blue { background: #3b82f6; }
&.orange { background: #f97316; }
&.green { background: #22c55e; }
&.rose { background: #f43f5e; }
}
.legend-list {
flex: 1;
min-width: 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 12rpx;
min-height: 46rpx;
color: #164e63;
font-size: 24rpx;
font-weight: 700;
}
.legend-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
flex-shrink: 0;
&.blue { background: #3b82f6; }
&.orange { background: #f97316; }
&.green { background: #22c55e; }
&.rose { background: #f43f5e; }
}
.legend-name {
flex: 1;
}
.legend-percent {
color: #0891b2;
}
.level-tabs {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14rpx;
margin-bottom: 20rpx;
}
.level-tab {
height: 64rpx;
border: 2rpx solid #e2e8f0;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
color: #475569;
background: #ffffff;
font-size: 24rpx;
font-weight: 800;
&.active {
border-color: #0891b2;
color: #ffffff;
background: linear-gradient(180deg, #22d3ee 0%, #0891b2 100%);
box-shadow: 0 10rpx 20rpx rgba(8, 145, 178, 0.2);
}
}
.score-table {
border: 2rpx solid #e2e8f0;
border-radius: 24rpx;
overflow: hidden;
}
.score-row {
display: grid;
grid-template-columns: 1fr 112rpx 150rpx;
align-items: center;
min-height: 76rpx;
padding: 0 22rpx;
color: #164e63;
font-size: 24rpx;
text-align: center;
border-top: 2rpx solid #f1f5f9;
&:first-child {
border-top: 0;
}
}
.table-head {
color: #475569;
background: #f8fafc;
font-weight: 900;
}
.knowledge-name {
text-align: left;
}
.score-value {
color: #ea580c;
font-weight: 900;
}
.score-rate {
margin-left: 2rpx;
font-size: 22rpx;
font-weight: 700;
}
.ucharts-wrap {
width: 100%;
height: 360rpx;
margin-top: 4rpx;
overflow: hidden;
}
.ability-chart {
width: 100%;
height: 100%;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
margin-top: 26rpx;
}
.summary-item {
padding: 18rpx;
border-radius: 24rpx;
background: #f8fafc;
text-align: center;
}
.summary-label,
.summary-desc {
display: block;
color: #64748b;
font-size: 22rpx;
line-height: 1.45;
}
.summary-value {
display: block;
margin: 8rpx 0;
color: #0891b2;
font-size: 32rpx;
font-weight: 900;
}
</style>