feat(复习):滚动条回忆卡片——先回忆再对照

- 点击滚动内容不再直接跳详情,先弹回忆卡片展示单条片段
- 用户自行回忆后点击'展开对照'查看该会话全部报告/残片
- 滚动 feed 切换为 smart 模式(时间衰减+掌握度加权)
- 引入主动回忆环节,将 incidental exposure 转化为 testing effect
This commit is contained in:
2026-07-03 23:59:39 +08:00
parent 6346120805
commit 5616399108
4 changed files with 1980 additions and 10 deletions
+1851 -7
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -21,6 +21,7 @@
"axios": "^1.7.7",
"css-loader": "^7.1.2",
"element-plus": "^2.8.7",
"mind-elixir": "^5.13.0",
"normalize": "^0.3.1",
"normalize.css": "^8.0.1",
"style-loader": "^4.0.0",
@@ -42,4 +43,4 @@
"vitest": "^2.1.9",
"vue-tsc": "^2.1.6"
}
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ export interface ReviewMindMap {
lastModifiedTime?: string;
}
export const getReviewFeed = (limit: number = 30, mode: "recent" | "random" = "recent") => {
export const getReviewFeed = (limit: number = 30, mode: "recent" | "random" | "smart" = "recent") => {
return request.get("/review/feed", { limit, mode });
};
+126 -1
View File
@@ -63,14 +63,39 @@ const duplicatedGroups = computed(() => [...sessionGroups.value, ...sessionGroup
const loadReviewFeed = async () => {
try {
const res = await getReviewFeed(100, "random");
const res = await getReviewFeed(100, "smart");
reviewItems.value = res?.data || [];
} catch {
// feed 加载失败不影响页面主体功能
}
};
// 回忆卡片:点击滚动内容先尝试回忆,再看原文
const recallCard = ref<{ visible: boolean; group: SessionGroup | null; revealed: boolean }>({
visible: false,
group: null,
revealed: false,
});
// 当前回忆卡片对应会话的全部记录
const recallSessionItems = computed(() => {
const group = recallCard.value.group;
if (!group) return [];
return reviewItems.value.filter((i) => i.sessionNum === group.sessionNum);
});
const goToReviewDetail = (group: SessionGroup) => {
recallCard.value = { visible: true, group, revealed: false };
};
const revealRecallContent = () => {
recallCard.value.revealed = true;
};
const openRecallDetail = () => {
const group = recallCard.value.group;
if (!group) return;
recallCard.value.visible = false;
router.push(`/review/detail/${group.navigateType}/${group.navigateId}`);
};
@@ -150,6 +175,40 @@ onMounted(() => {
</ol>
</div>
</section>
<!-- 回忆卡片先回忆再对照 -->
<el-dialog
v-model="recallCard.visible"
:title="recallCard.group?.taskName || '回忆一下'"
width="560px"
>
<template v-if="recallCard.group">
<p class="recall-question">
这次学习共有 {{ recallSessionItems.length }} 条记录看着下面这个片段先试着回忆当时还学了什么
</p>
<blockquote class="recall-snippet">{{ recallCard.group.content }}</blockquote>
<div v-if="recallCard.revealed" class="recall-full-list">
<div
v-for="item in recallSessionItems"
:key="`${item.sourceType}-${item.id}`"
class="recall-full-item"
>
<span class="recall-item-tag" :class="item.sourceType === 'REPORT' ? 'tag-report' : 'tag-fragment'">
{{ item.sourceType === "REPORT" ? "报告" : "残片" }}
</span>
<span class="recall-item-content">{{ item.content }}</span>
</div>
</div>
</template>
<template #footer>
<el-button v-if="!recallCard.revealed" type="success" @click="revealRecallContent">
回忆好了展开对照
</el-button>
<el-button v-else type="success" @click="openRecallDetail">进入详情页</el-button>
<el-button @click="recallCard.visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<style scoped>
@@ -325,6 +384,72 @@ onMounted(() => {
margin-top: 8px;
}
/* 回忆卡片 */
.recall-question {
margin: 0 0 12px;
font-size: 14px;
color: var(--text-primary);
line-height: 1.6;
}
.recall-snippet {
margin: 0 0 14px;
padding: 12px 16px;
border-left: 3px solid var(--green-600);
background: #f1f8e9;
border-radius: 0 6px 6px 0;
font-size: 14px;
color: var(--text-primary);
line-height: 1.7;
word-break: break-word;
white-space: normal;
}
.recall-full-list {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 320px;
overflow-y: auto;
}
.recall-full-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px 10px;
background: #fbfefc;
border: 1px solid var(--border-soft);
border-radius: 6px;
}
.recall-item-tag {
flex-shrink: 0;
font-size: 11px;
font-weight: 700;
padding: 1px 6px;
border-radius: 3px;
}
.tag-report {
background: #e8f5e9;
color: var(--green-700);
}
.tag-fragment {
background: #fff8e1;
color: #b8860b;
}
.recall-item-content {
flex: 1;
font-size: 13px;
line-height: 1.6;
color: var(--text-primary);
word-break: break-word;
white-space: normal;
}
@media (max-width: 900px) {
.welcome-banner {
flex-direction: column;