68c5efe573
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>
117 lines
3.1 KiB
Vue
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>
|