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:
@@ -23,6 +23,7 @@ export interface RecallRecord {
|
|||||||
id: number;
|
id: number;
|
||||||
taskNum: string;
|
taskNum: string;
|
||||||
standardMapId: number;
|
standardMapId: number;
|
||||||
|
sessionNum?: string;
|
||||||
recallContent: string;
|
recallContent: string;
|
||||||
compareResult: string;
|
compareResult: string;
|
||||||
recallRatio: number;
|
recallRatio: number;
|
||||||
@@ -47,9 +48,11 @@ export const updateStandardMindMap = (taskNum: string, outline: string) => {
|
|||||||
return request.put(`/review/standard-mind-map/${taskNum}`, { outline });
|
return request.put(`/review/standard-mind-map/${taskNum}`, { outline });
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 用户提交回忆大纲,与标准导图对比 */
|
/** 用户提交回忆大纲,与标准导图对比(可选指定会话维度) */
|
||||||
export const recallCompare = (taskNum: string, recallOutline: string) => {
|
export const recallCompare = (taskNum: string, recallOutline: string, sessionNum?: string) => {
|
||||||
return request.post(`/review/standard-mind-map/${taskNum}/recall`, { recallOutline });
|
const body: Record<string, any> = { recallOutline };
|
||||||
|
if (sessionNum) body.sessionNum = sessionNum;
|
||||||
|
return request.post(`/review/standard-mind-map/${taskNum}/recall`, body);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 获取该任务的所有回忆对比记录 */
|
/** 获取该任务的所有回忆对比记录 */
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ export const getActiveSession = (excludeTaskNum?: string) => {
|
|||||||
return request.get('/study-sessions/active', params);
|
return request.get('/study-sessions/active', params);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 获取任务的所有会话简要信息(复习维度选择用) */
|
||||||
|
export const getTaskSessions = (taskNum: string) => {
|
||||||
|
return request.get(`/study-sessions/tasks/${taskNum}/sessions`);
|
||||||
|
};
|
||||||
|
|
||||||
/** 分页查询任务的历史残片 */
|
/** 分页查询任务的历史残片 */
|
||||||
export const getTaskFragments = (taskNum: string, page: number, size: number, keyword?: string) => {
|
export const getTaskFragments = (taskNum: string, page: number, size: number, keyword?: string) => {
|
||||||
const params: Record<string, any> = { page, size };
|
const params: Record<string, any> = { page, size };
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "@/api/standardMindMap";
|
} from "@/api/standardMindMap";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import MindMapViewer, { type MindMapTreeNode } from "@/components/MindMapViewer.vue";
|
import MindMapViewer, { type MindMapTreeNode } from "@/components/MindMapViewer.vue";
|
||||||
|
import { getTaskSessions } from "@/api/studySessions";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -25,6 +26,11 @@ let comparingTimer: ReturnType<typeof setInterval> | null = null;
|
|||||||
const lastResult = ref<any>(null);
|
const lastResult = ref<any>(null);
|
||||||
const hasCompared = ref(false);
|
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 recallTree = ref<MindMapTreeNode | null>(null);
|
||||||
const recallViewer = ref<InstanceType<typeof MindMapViewer> | null>(null);
|
const recallViewer = ref<InstanceType<typeof MindMapViewer> | null>(null);
|
||||||
@@ -144,7 +150,7 @@ const handleRecallCompare = async () => {
|
|||||||
comparingSeconds.value = 0;
|
comparingSeconds.value = 0;
|
||||||
comparingTimer = setInterval(() => { comparingSeconds.value++; }, 1000);
|
comparingTimer = setInterval(() => { comparingSeconds.value++; }, 1000);
|
||||||
try {
|
try {
|
||||||
const res = await recallCompare(taskNum.value, outline);
|
const res = await recallCompare(taskNum.value, outline, selectedSession.value || undefined);
|
||||||
standard.value = res?.data || null;
|
standard.value = res?.data || null;
|
||||||
hasCompared.value = true;
|
hasCompared.value = true;
|
||||||
// 解析对比结果
|
// 解析对比结果
|
||||||
@@ -270,8 +276,29 @@ const extraNodes = computed(() => {
|
|||||||
return raw.map((item: any) => typeof item === "string" ? { title: item } : item);
|
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(() => {
|
onMounted(() => {
|
||||||
loadData();
|
loadData();
|
||||||
|
loadSessions();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -286,6 +313,24 @@ onMounted(() => {
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</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="stats-card surface-card" v-if="hasCompared && lastResult">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
@@ -317,6 +362,8 @@ onMounted(() => {
|
|||||||
@click="selectHistoryRecord(record)"
|
@click="selectHistoryRecord(record)"
|
||||||
>
|
>
|
||||||
<span class="history-time">{{ record.createdTime?.replace("T", " ")?.substring(0, 16) }}</span>
|
<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-ratio">覆盖率 {{ (record.recallRatio * 100).toFixed(1) }}%</span>
|
||||||
<span class="history-detail">✅{{ record.matchedCount }} ❌{{ record.missedCount }} ➕{{ record.extraCount }}</span>
|
<span class="history-detail">✅{{ record.matchedCount }} ❌{{ record.missedCount }} ➕{{ record.extraCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -709,4 +756,29 @@ onMounted(() => {
|
|||||||
.history-detail {
|
.history-detail {
|
||||||
color: var(--text-secondary, #666);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -677,7 +677,10 @@ onUnmounted(() => {
|
|||||||
<el-tab-pane label="学习报告" name="reports">
|
<el-tab-pane label="学习报告" name="reports">
|
||||||
<div v-loading="loadingHistory" class="history-list">
|
<div v-loading="loadingHistory" class="history-list">
|
||||||
<div v-for="item in historyReports" :key="item.id" class="history-item report-item">
|
<div v-for="item in historyReports" :key="item.id" class="history-item report-item">
|
||||||
|
<div class="report-header">
|
||||||
<span class="session-tag">{{ item.sessionNum }}</span>
|
<span class="session-tag">{{ item.sessionNum }}</span>
|
||||||
|
<span v-if="item.sessionExpectation" class="expectation-hint">{{ item.sessionExpectation }}</span>
|
||||||
|
</div>
|
||||||
<MarkdownRenderer :content="item.content" />
|
<MarkdownRenderer :content="item.content" />
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-if="!loadingHistory && historyReports.length === 0" description="暂未找到匹配的报告" :image-size="48" />
|
<el-empty v-if="!loadingHistory && historyReports.length === 0" description="暂未找到匹配的报告" :image-size="48" />
|
||||||
@@ -1098,6 +1101,26 @@ onUnmounted(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.report-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expectation-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
background: #f5f7f5;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.history-content {
|
.history-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
Reference in New Issue
Block a user