Files
wordloop/frontend/src/components/MemoryCurveChart.vue
T
John 68c5efe573 Add spell practice, memory analytics, auth persistence, and word library UX.
Includes per-word training stats and curves, quiz session auto-save, remember-login,
paginated word list with floating page arrows, and Obsidian-style relationship graph baseline.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:57:39 -07:00

117 lines
3.1 KiB
Vue

<script setup lang="ts">
import * as echarts from 'echarts'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { MemoryCurvePoint, MemoryFutureRiskPoint } from '../api/request'
const props = defineProps<{
curvePoints: MemoryCurvePoint[]
futureRisk: MemoryFutureRiskPoint[]
}>()
const chartRef = ref<HTMLDivElement | null>(null)
let chart: echarts.ECharts | null = null
function render() {
if (!chartRef.value) return
if (!chart) chart = echarts.init(chartRef.value)
const dates = props.curvePoints.map((p) => p.date)
const futureDates = props.futureRisk.map((p) => p.date)
const allDates = [...dates, ...futureDates.slice(1)]
const forgetting = props.curvePoints.map((p) => p.forgetting)
const mastery = props.curvePoints.map((p) => p.mastery)
const riskHist = props.curvePoints.map((p) => p.risk)
const riskFuture = [
...Array(Math.max(0, dates.length - 1)).fill(null),
props.curvePoints.length ? props.curvePoints[props.curvePoints.length - 1].risk : null,
...props.futureRisk.map((p) => p.risk),
]
chart.setOption({
tooltip: { trigger: 'axis' },
legend: {
data: ['遗忘曲线', '熟练曲线', '可能遗忘(历史)', '可能遗忘(预测)'],
bottom: 0,
textStyle: { fontSize: 11 },
},
grid: { left: 48, right: 16, top: 24, bottom: 56 },
xAxis: {
type: 'category',
data: allDates,
axisLabel: { rotate: 35, fontSize: 10 },
},
yAxis: {
type: 'value',
min: 0,
max: 100,
name: '记忆指数 %',
nameTextStyle: { fontSize: 11 },
},
series: [
{
name: '遗忘曲线',
type: 'line',
smooth: true,
data: [...forgetting, ...Array(futureDates.length).fill(null)],
lineStyle: { color: '#ef4444', width: 2 },
itemStyle: { color: '#ef4444' },
areaStyle: { color: 'rgba(239,68,68,0.08)' },
},
{
name: '熟练曲线',
type: 'line',
smooth: true,
data: [...mastery, ...Array(futureDates.length).fill(null)],
lineStyle: { color: '#22c55e', width: 2 },
itemStyle: { color: '#22c55e' },
},
{
name: '可能遗忘(历史)',
type: 'line',
smooth: true,
data: [...riskHist, ...Array(futureDates.length).fill(null)],
lineStyle: { color: '#f59e0b', width: 2, type: 'dashed' },
itemStyle: { color: '#f59e0b' },
},
{
name: '可能遗忘(预测)',
type: 'line',
smooth: true,
data: riskFuture,
lineStyle: { color: '#a855f7', width: 2, type: 'dotted' },
itemStyle: { color: '#a855f7' },
},
],
})
}
function onResize() {
chart?.resize()
}
onMounted(() => {
render()
window.addEventListener('resize', onResize)
})
watch(() => [props.curvePoints, props.futureRisk], render, { deep: true })
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
chart?.dispose()
chart = null
})
</script>
<template>
<div ref="chartRef" class="memory-chart" />
</template>
<style scoped>
.memory-chart {
width: 100%;
height: 280px;
}
</style>