452 lines
12 KiB
Vue
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>
|