feat(复习): 支持会话维度复习 + 标准导图按会话过滤

- MindMapNode 新增 sessionNum 字段,追踪节点来源会话
- BuiltinMindMapGenerator 生成节点时填充 sessionNum
- MindMapTreeTool.filterBySession() 按会话过滤标准导图
- recallCompare 接受 sessionNum 参数,筛选后对比
- 新增 review_recall_records.session_num 字段(Flyway V20260705_1)
- 新增 GET /study-sessions/tasks/{taskNum}/sessions 端点
- 前端 ReviewRecall.vue 增加复习范围选择器(全部/指定会话)
- 历史报告条目展示学习预期目标
This commit is contained in:
2026-07-05 14:39:01 +08:00
parent 62e12686e3
commit e5adb4095b
4 changed files with 108 additions and 5 deletions
+73 -1
View File
@@ -12,6 +12,7 @@ import {
} from "@/api/standardMindMap";
import { ElMessage, ElMessageBox } from "element-plus";
import MindMapViewer, { type MindMapTreeNode } from "@/components/MindMapViewer.vue";
import { getTaskSessions } from "@/api/studySessions";
const route = useRoute();
const router = useRouter();
@@ -25,6 +26,11 @@ let comparingTimer: ReturnType<typeof setInterval> | null = null;
const lastResult = ref<any>(null);
const hasCompared = ref(false);
// 复习维度选择
const sessions = ref<{ sessionNum: string; startTime: string; expectation: string; reportCount: number; fragmentCount: number }[]>([]);
const selectedSession = ref("");
const loadingSessions = ref(false);
// 回忆输入(思维导图形式)
const recallTree = ref<MindMapTreeNode | null>(null);
const recallViewer = ref<InstanceType<typeof MindMapViewer> | null>(null);
@@ -144,7 +150,7 @@ const handleRecallCompare = async () => {
comparingSeconds.value = 0;
comparingTimer = setInterval(() => { comparingSeconds.value++; }, 1000);
try {
const res = await recallCompare(taskNum.value, outline);
const res = await recallCompare(taskNum.value, outline, selectedSession.value || undefined);
standard.value = res?.data || null;
hasCompared.value = true;
// 解析对比结果
@@ -270,8 +276,29 @@ const extraNodes = computed(() => {
return raw.map((item: any) => typeof item === "string" ? { title: item } : item);
});
// 加载会话列表(复习维度选择器用)
const loadSessions = async () => {
loadingSessions.value = true;
try {
const res = await getTaskSessions(taskNum.value);
sessions.value = res?.data || [];
} catch {
sessions.value = [];
} finally {
loadingSessions.value = false;
}
};
// 格式化会话标签
const formatSessionLabel = (s: { sessionNum: string; startTime: string; expectation: string; reportCount: number; fragmentCount: number }) => {
const date = s.startTime ? s.startTime.replace("T", " ").substring(0, 16) : s.sessionNum;
const hint = s.expectation ? s.expectation.substring(0, 30) : "";
return `${date} ${hint ? "— " + hint : ""}`;
};
onMounted(() => {
loadData();
loadSessions();
});
</script>
@@ -286,6 +313,24 @@ onMounted(() => {
</el-button>
</div>
<!-- 复习维度选择器 -->
<div class="review-scope surface-card" v-if="sessions.length > 0 || loadingSessions">
<span class="scope-label">复习范围</span>
<el-select v-model="selectedSession" placeholder="选择复习范围" clearable size="small" :loading="loadingSessions">
<el-option label="全部任务" value="" />
<el-option
v-for="s in sessions"
:key="s.sessionNum"
:label="formatSessionLabel(s)"
:value="s.sessionNum"
/>
</el-select>
<el-tag v-if="selectedSession" size="small" type="success" effect="light">
已过滤仅匹配该会话知识点
</el-tag>
<span v-else-if="!selectedSession" class="scope-hint">不过滤对比全部标准导图</span>
</div>
<!-- 统计卡片 -->
<div class="stats-card surface-card" v-if="hasCompared && lastResult">
<div class="stat-item">
@@ -317,6 +362,8 @@ onMounted(() => {
@click="selectHistoryRecord(record)"
>
<span class="history-time">{{ record.createdTime?.replace("T", " ")?.substring(0, 16) }}</span>
<el-tag v-if="record.sessionNum" size="small" type="success" effect="plain" class="session-badge">会话</el-tag>
<el-tag v-else size="small" type="info" effect="plain" class="session-badge">全部</el-tag>
<span class="history-ratio">覆盖率 {{ (record.recallRatio * 100).toFixed(1) }}%</span>
<span class="history-detail">{{ record.matchedCount }} {{ record.missedCount }} {{ record.extraCount }}</span>
</div>
@@ -709,4 +756,29 @@ onMounted(() => {
.history-detail {
color: var(--text-secondary, #666);
}
/* 复习范围选择器 */
.review-scope {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
padding: 12px 22px;
}
.scope-label {
font-size: 13px;
font-weight: 600;
color: var(--green-800);
white-space: nowrap;
}
.scope-hint {
font-size: 12px;
color: var(--text-secondary, #999);
}
.session-badge {
flex-shrink: 0;
}
</style>