272 lines
6.6 KiB
Vue
272 lines
6.6 KiB
Vue
<template>
|
||
<n-card :title="title" size="small" v-if="show">
|
||
<template #header-extra>
|
||
<n-text depth="3" style="font-size: 12px">追踪学习成长轨迹</n-text>
|
||
</template>
|
||
<div class="chart">
|
||
<Chart type="line" :data="data" :options="options" />
|
||
</div>
|
||
</n-card>
|
||
</template>
|
||
<script setup lang="ts">
|
||
import type { ChartData, ChartOptions, TooltipItem } from "chart.js"
|
||
import { Chart } from "vue-chartjs"
|
||
import {
|
||
Chart as ChartJS,
|
||
CategoryScale,
|
||
LinearScale,
|
||
PointElement,
|
||
LineElement,
|
||
Title,
|
||
Tooltip,
|
||
Legend,
|
||
Colors,
|
||
Filler,
|
||
} from "chart.js"
|
||
import { useAIStore } from "oj/store/ai"
|
||
import { parseTime } from "utils/functions"
|
||
import type { Grade } from "utils/types"
|
||
|
||
// 注册折线图所需的 Chart.js 组件
|
||
ChartJS.register(
|
||
CategoryScale,
|
||
LinearScale,
|
||
PointElement,
|
||
LineElement,
|
||
Title,
|
||
Tooltip,
|
||
Legend,
|
||
Colors,
|
||
Filler,
|
||
)
|
||
|
||
const aiStore = useAIStore()
|
||
|
||
const gradeOrder = ["C", "B", "A", "S"] as const
|
||
const gradeColors: Record<Grade, string> = {
|
||
C: "#95F204",
|
||
B: "#36A2EB",
|
||
A: "#FFCE56",
|
||
S: "#FF6384",
|
||
}
|
||
|
||
const title = computed(() => {
|
||
if (aiStore.duration === "months:2") {
|
||
return "过去两个月的进步曲线"
|
||
} else if (aiStore.duration === "months:6") {
|
||
return "过去半年的进步曲线"
|
||
} else if (aiStore.duration === "years:1") {
|
||
return "过去一年的进步曲线"
|
||
} else {
|
||
return "过去四周的进步曲线"
|
||
}
|
||
})
|
||
|
||
// 判断是否有数据
|
||
const show = computed(() => {
|
||
return aiStore.durationData.length > 0
|
||
})
|
||
|
||
// 计算累计题目数量和等级趋势
|
||
const progressData = computed(() => {
|
||
let cumulativeCount = 0
|
||
let totalWeightedGrade = 0 // 累计加权等级
|
||
let totalProblems = 0 // 累计题目总数
|
||
|
||
return aiStore.durationData.map((duration) => {
|
||
const problemCount = duration.problem_count || 0
|
||
cumulativeCount += problemCount
|
||
|
||
// 计算本期等级的权重值
|
||
const currentGradeValue = gradeOrder.indexOf(duration.grade || "C")
|
||
|
||
// 累加加权等级
|
||
totalWeightedGrade += currentGradeValue * problemCount
|
||
totalProblems += problemCount
|
||
|
||
// 计算累计平均等级
|
||
const avgGradeValue =
|
||
totalProblems > 0 ? totalWeightedGrade / totalProblems : 0
|
||
|
||
return {
|
||
label: [
|
||
parseTime(duration.start, "M月D日"),
|
||
parseTime(duration.end, "M月D日"),
|
||
].join("~"),
|
||
start: parseTime(duration.start, "YYYY-MM-DD"),
|
||
end: parseTime(duration.end, "YYYY-MM-DD"),
|
||
count: cumulativeCount,
|
||
grade: duration.grade || "C",
|
||
gradeValue: currentGradeValue,
|
||
avgGradeValue: avgGradeValue, // 累计平均等级
|
||
problemCount: problemCount,
|
||
}
|
||
})
|
||
})
|
||
|
||
// 图表数据
|
||
const data = computed<ChartData<"line">>(() => {
|
||
const progress = progressData.value
|
||
|
||
return {
|
||
labels: progress.map((p) => p.label),
|
||
datasets: [
|
||
{
|
||
type: "line",
|
||
label: "累计完成题目",
|
||
data: progress.map((p) => p.count),
|
||
borderColor: "#4CAF50",
|
||
backgroundColor: "rgba(76, 175, 80, 0.1)",
|
||
tension: 0.4,
|
||
yAxisID: "y",
|
||
fill: true,
|
||
pointRadius: 5,
|
||
pointHoverRadius: 7,
|
||
borderWidth: 2.5,
|
||
pointBackgroundColor: "#4CAF50",
|
||
pointBorderColor: "#fff",
|
||
pointBorderWidth: 2,
|
||
},
|
||
{
|
||
type: "line",
|
||
label: "累计平均等级",
|
||
data: progress.map((p) => p.avgGradeValue),
|
||
borderColor: "#FF9800",
|
||
backgroundColor: "rgba(255, 152, 0, 0.1)",
|
||
tension: 0.4,
|
||
yAxisID: "y1",
|
||
fill: false,
|
||
pointRadius: 5,
|
||
pointHoverRadius: 7,
|
||
borderWidth: 2.5,
|
||
pointBackgroundColor: progress.map((p) => gradeColors[p.grade]),
|
||
pointBorderColor: "#fff",
|
||
pointBorderWidth: 2,
|
||
},
|
||
],
|
||
}
|
||
})
|
||
|
||
// 图表配置
|
||
const options = computed<ChartOptions<"line">>(() => {
|
||
return {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
interaction: {
|
||
mode: "index",
|
||
intersect: false,
|
||
},
|
||
scales: {
|
||
x: {
|
||
ticks: {
|
||
maxRotation: 0,
|
||
minRotation: 0,
|
||
autoSkip: true,
|
||
maxTicksLimit: 15,
|
||
},
|
||
},
|
||
y: {
|
||
type: "linear",
|
||
position: "left",
|
||
title: {
|
||
display: true,
|
||
text: "累计题目数",
|
||
font: {
|
||
size: 14,
|
||
},
|
||
},
|
||
ticks: {
|
||
stepSize: 1,
|
||
},
|
||
beginAtZero: true,
|
||
},
|
||
y1: {
|
||
type: "linear",
|
||
position: "right",
|
||
min: -0.5,
|
||
max: gradeOrder.length - 0.5,
|
||
title: {
|
||
display: true,
|
||
text: "累计平均等级",
|
||
font: {
|
||
size: 14,
|
||
},
|
||
},
|
||
ticks: {
|
||
stepSize: 1,
|
||
callback: (v: string | number) => {
|
||
const idx = Math.round(Number(v))
|
||
return gradeOrder[idx] || ""
|
||
},
|
||
},
|
||
grid: {
|
||
drawOnChartArea: false,
|
||
},
|
||
},
|
||
},
|
||
plugins: {
|
||
title: {
|
||
display: false,
|
||
},
|
||
tooltip: {
|
||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||
padding: 12,
|
||
callbacks: {
|
||
title: (items: TooltipItem<"line">[]) => {
|
||
if (items.length > 0) {
|
||
const idx = items[0].dataIndex
|
||
const progress = progressData.value[idx]
|
||
return progress ? `${progress.start} ~ ${progress.end}` : ""
|
||
}
|
||
return ""
|
||
},
|
||
label: (ctx: TooltipItem<"line">) => {
|
||
const dsLabel = ctx.dataset.label || ""
|
||
const idx = ctx.dataIndex
|
||
const progress = progressData.value[idx]
|
||
|
||
if (!progress) {
|
||
return `${dsLabel}: ${ctx.formattedValue}`
|
||
}
|
||
|
||
if ((ctx.dataset as any).yAxisID === "y1") {
|
||
// 累计平均等级轴
|
||
const avgIdx = Math.round(Number(ctx.parsed.y))
|
||
return [
|
||
`${dsLabel}: ${gradeOrder[avgIdx] || ""}`,
|
||
`本期等级: ${progress.grade}`,
|
||
`本期完成: ${progress.problemCount} 题`,
|
||
]
|
||
} else {
|
||
// 累计题目数轴
|
||
return [
|
||
`${dsLabel}: ${ctx.formattedValue} 题`,
|
||
`本期完成: ${progress.problemCount} 题`,
|
||
]
|
||
}
|
||
},
|
||
},
|
||
},
|
||
legend: {
|
||
display: true,
|
||
position: "bottom" as const,
|
||
labels: {
|
||
boxWidth: 12,
|
||
boxHeight: 12,
|
||
padding: 8,
|
||
font: {
|
||
size: 12,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
})
|
||
</script>
|
||
<style scoped>
|
||
.chart {
|
||
height: 300px;
|
||
width: 100%;
|
||
}
|
||
</style>
|