From 5dd29e8930110050cf708fa445e5ab36494b145a Mon Sep 17 00:00:00 2001 From: John Date: Thu, 4 Jun 2026 15:00:45 -0700 Subject: [PATCH] Add graph focus mode, hover tooltips, side panel, and targeted spell practice. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a node focuses its neighbor subgraph; side panel shows word stats and curve; 「练这个词」 prepends a spell question for the selected word. Co-authored-by: Cursor --- frontend/src/api/request.ts | 1 + frontend/src/components/WordGraphCanvas.vue | 62 ++++++- frontend/src/composables/useGraphFilter.ts | 97 +++++++++- frontend/src/pages/SpellQuiz.vue | 17 +- frontend/src/pages/WordLibrary.vue | 191 +++++++++++++++----- frontend/src/utils/practiceFocus.ts | 13 ++ frontend/src/utils/spellQuestion.ts | 14 ++ 7 files changed, 339 insertions(+), 56 deletions(-) create mode 100644 frontend/src/utils/practiceFocus.ts create mode 100644 frontend/src/utils/spellQuestion.ts diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts index f0142a9..7cf4702 100644 --- a/frontend/src/api/request.ts +++ b/frontend/src/api/request.ts @@ -182,6 +182,7 @@ export const api = { createWord: (data: Partial) => request.post('/words', data), listWords: (status?: string) => request.get('/words', { params: status ? { status } : {} }), + getWord: (id: number) => request.get(`/words/${id}`), memoryViz: () => request.get('/words/memory-viz'), wordMemory: (id: number) => request.get(`/words/${id}/memory`), deleteWord: (id: number) => request.delete(`/words/${id}`), diff --git a/frontend/src/components/WordGraphCanvas.vue b/frontend/src/components/WordGraphCanvas.vue index ae76936..916dbd0 100644 --- a/frontend/src/components/WordGraphCanvas.vue +++ b/frontend/src/components/WordGraphCanvas.vue @@ -12,8 +12,18 @@ const emit = defineEmits<{ }>() const canvasRef = ref(null) +const wrapRef = ref(null) const selectedId = ref(null) const cursorStyle = ref('default') +const hoverNode = ref(null) +const tooltipPos = ref({ x: 0, y: 0 }) + +const statusLabel: Record = { + new: '新词', + learning: '学习中', + mastered: '已掌握', + weak: '易错词', +} interface SimNode extends MemoryGraphNode { x: number @@ -232,6 +242,7 @@ function onPointerDown(e: PointerEvent) { const hit = hitNode(x, y) if (!hit) return + hoverNode.value = null draggingId = hit.id dragOffsetX = x - hit.x dragOffsetY = y - hit.y @@ -262,7 +273,21 @@ function onPointerMove(e: PointerEvent) { return } - cursorStyle.value = hitNode(x, y) ? 'grab' : 'default' + const hit = hitNode(x, y) + cursorStyle.value = hit ? 'grab' : 'default' + if (hit) { + hoverNode.value = hit + const wrap = wrapRef.value + if (wrap) { + const rect = wrap.getBoundingClientRect() + tooltipPos.value = { + x: Math.min(e.clientX - rect.left + 12, rect.width - 160), + y: Math.max(e.clientY - rect.top - 8, 8), + } + } + } else { + hoverNode.value = null + } } function onPointerUp(e: PointerEvent) { @@ -320,24 +345,55 @@ onBeforeUnmount(() => {