Improve word relationship graph with filters, legend, and performance tuning.
Add status/search filters, link-type toggles, 40-node cap with weak-word priority, distinct edge styles, and reduced physics cost for large graphs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
type WordMemoryDetail,
|
||||
} from '../api/request'
|
||||
import { formatTrainSeconds } from '../composables/useQuizTimer'
|
||||
import { LINK_KIND_META, useGraphFilter, type LinkKind } from '../composables/useGraphFilter'
|
||||
|
||||
const viewTabs = [
|
||||
{ key: 'list', label: '单词列表' },
|
||||
@@ -45,6 +46,16 @@ const detailExpanded = ref(true)
|
||||
const wordMemory = ref<WordMemoryDetail | null>(null)
|
||||
const wordMemoryLoading = ref(false)
|
||||
|
||||
const {
|
||||
graphStatusFilter,
|
||||
graphSearch,
|
||||
linkFilters,
|
||||
filteredGraphNodes,
|
||||
filteredGraphLinks,
|
||||
graphStatsText,
|
||||
isNodeVisible,
|
||||
} = useGraphFilter(viz)
|
||||
|
||||
const wordCurvePoints = computed((): MemoryCurvePoint[] => {
|
||||
if (!wordMemory.value?.curve_points.length) return []
|
||||
return wordMemory.value.curve_points.map((p, i) => ({
|
||||
@@ -142,6 +153,19 @@ function onGraphSelect(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
watch([graphStatusFilter, graphSearch, linkFilters], () => {
|
||||
if (selectedWord.value && !isNodeVisible(String(selectedWord.value.id))) {
|
||||
const first = filteredGraphNodes.value[0]
|
||||
if (first) {
|
||||
const w = viz.value?.words.find((x) => String(x.id) === first.id)
|
||||
if (w) {
|
||||
selectedWord.value = w
|
||||
loadWordMemory(w.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
watch(selectedWord, (w) => {
|
||||
if (w && activeView.value === 'memory') loadWordMemory(w.id)
|
||||
})
|
||||
@@ -251,12 +275,52 @@ onMounted(() => {
|
||||
|
||||
<div class="card graph-card">
|
||||
<h3 class="section-title">单词关系图</h3>
|
||||
<p class="section-desc">类似 Obsidian 的力导向图,展示词与词之间的记忆关联</p>
|
||||
<p class="section-desc">筛选后展示子图,减轻卡顿并突出记忆关联</p>
|
||||
|
||||
<div class="graph-toolbar">
|
||||
<input
|
||||
v-model="graphSearch"
|
||||
class="graph-search input"
|
||||
type="search"
|
||||
placeholder="搜索英文或中文"
|
||||
/>
|
||||
<select v-model="graphStatusFilter" class="graph-select">
|
||||
<option value="">全部状态</option>
|
||||
<option value="new">新词</option>
|
||||
<option value="learning">学习中</option>
|
||||
<option value="mastered">已掌握</option>
|
||||
<option value="weak">易错词</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="link-filters">
|
||||
<label
|
||||
v-for="(meta, kind) in LINK_KIND_META"
|
||||
:key="kind"
|
||||
class="link-filter-item"
|
||||
>
|
||||
<input v-model="linkFilters[kind as LinkKind]" type="checkbox" />
|
||||
<span class="link-swatch" :style="{ background: meta.color }" />
|
||||
{{ meta.label }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="graph-stats">{{ graphStatsText }}</p>
|
||||
|
||||
<WordGraphCanvas
|
||||
:nodes="viz.graph.nodes"
|
||||
:links="viz.graph.links"
|
||||
v-if="filteredGraphNodes.length"
|
||||
:nodes="filteredGraphNodes"
|
||||
:links="filteredGraphLinks"
|
||||
@select="onGraphSelect"
|
||||
/>
|
||||
<p v-else class="curve-loading">当前筛选无匹配单词,请调整条件</p>
|
||||
|
||||
<div class="graph-legend">
|
||||
<span v-for="(meta, kind) in LINK_KIND_META" :key="kind" class="legend-item">
|
||||
<span class="link-swatch" :style="{ background: meta.color }" />
|
||||
{{ meta.label }}:{{ meta.desc }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedWord" class="card word-detail">
|
||||
@@ -361,6 +425,62 @@ onMounted(() => {
|
||||
.graph-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.graph-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.graph-search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.graph-select {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
background: #fff;
|
||||
}
|
||||
.link-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.link-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.link-swatch {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.graph-stats {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.graph-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.legend-item {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.word-detail {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user