chore: 移除上传导图 + 优化复习引导提示

- 删除 ReviewMindMap 接口及 upload/get/upsert API
- 删除 ReviewDetail 上传 UI 和思维导图预览
- 回忆复习按钮移至内容元信息区
- 引导提示改为渐进式:折叠时引导展开,展开后引导点击节点
- 提示语用告知语气,无表情符号

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cat-win
2026-07-08 23:56:03 +08:00
parent 1ea15db504
commit e65b800e01
4 changed files with 31 additions and 299 deletions
-30
View File
@@ -14,12 +14,9 @@ import {
getFragmentDetail,
getReportDetail,
getReviewFeed,
getReviewMindMap,
getReviewTaskStats,
getReviewTaskStatsByTask,
getTaskReview,
upsertReviewMindMap,
uploadReviewMindMap,
} from '@/api/review'
const mockedRequest = vi.mocked(request)
@@ -79,31 +76,4 @@ describe('review API', () => {
expect(mockedRequest.get).toHaveBeenCalledWith('/review/task/T001')
})
it('review mind map API calls expected endpoints', async () => {
mockedRequest.get.mockResolvedValue({ code: 200, data: null })
mockedRequest.put.mockResolvedValue({ code: 200, data: { id: 1 } })
mockedRequest.post.mockResolvedValue({ code: 200, data: { id: 2 } })
await getReviewMindMap('T001')
expect(mockedRequest.get).toHaveBeenCalledWith('/review/mind-map/T001')
await upsertReviewMindMap('T001', {
title: 'Map',
content: 'content',
contentFormat: 'TEXT',
})
expect(mockedRequest.put).toHaveBeenCalledWith('/review/mind-map/T001', {
title: 'Map',
content: 'content',
contentFormat: 'TEXT',
})
const file = new File(['# Java'], 'java.md', { type: 'text/markdown' })
await uploadReviewMindMap('T001', file)
expect(mockedRequest.post).toHaveBeenCalledWith(
'/review/mind-map/T001/upload',
expect.any(FormData),
{ headers: { 'Content-Type': 'multipart/form-data' } },
)
})
})
-42
View File
@@ -23,25 +23,6 @@ export interface ReviewTaskStats {
avgEffectivenessRatio: number;
}
export interface ReviewMindMap {
id: number;
taskNum: string;
title: string;
content: string;
contentFormat: "TEXT" | "MERMAID" | "JSON";
sourceType?: "MANUAL" | "FILE";
fileName?: string;
filePath?: string;
fileFormat?: "XMIND" | "MARKDOWN" | "OPML" | "FREEMIND" | "TEXT";
parsedContent?: string;
parseStatus?: "SUCCESS" | "FAILED";
parseError?: string;
summary?: string;
lastParsedTime?: string;
createdTime?: string;
lastModifiedTime?: string;
}
export const getReviewFeed = (limit: number = 30, mode: "recent" | "random" | "smart" = "recent") => {
return request.get("/review/feed", { limit, mode });
};
@@ -65,26 +46,3 @@ export const getFragmentDetail = (id: number) => {
export const getTaskReview = (taskNum: string) => {
return request.get(`/review/task/${taskNum}`);
};
export const getReviewMindMap = (taskNum: string) => {
return request.get(`/review/mind-map/${taskNum}`);
};
export const upsertReviewMindMap = (
taskNum: string,
payload: {
title: string;
content: string;
contentFormat?: ReviewMindMap["contentFormat"];
},
) => {
return request.put(`/review/mind-map/${taskNum}`, payload);
};
export const uploadReviewMindMap = (taskNum: string, file: File) => {
const formData = new FormData();
formData.append("file", file);
return request.post(`/review/mind-map/${taskNum}/upload`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
};
+8 -223
View File
@@ -4,11 +4,8 @@ import { useRoute, useRouter } from "vue-router";
import {
getFragmentDetail,
getReportDetail,
getReviewMindMap,
getTaskReview,
uploadReviewMindMap,
type ReviewFeedItem,
type ReviewMindMap,
} from "@/api/review";
import { getSessionDetail } from "@/api/studySessions";
import { updateFragments } from "@/api/reportFragments";
@@ -50,9 +47,6 @@ const isEditing = ref(false);
const editContent = ref("");
const saving = ref(false);
const mindMap = ref<ReviewMindMap | null>(null);
const mindMapUploading = ref(false);
const sessionInfo = ref<{
taskName: string;
sessionNum: string;
@@ -69,40 +63,6 @@ const taskSessions = ref<{
fragments: ReviewFeedItem[];
}[]>([]);
type MindMapNode = {
title?: string;
notes?: string;
children?: MindMapNode[];
};
const flattenedMindMapNodes = computed(() => {
const parsedContent = mindMap.value?.parsedContent;
if (!parsedContent) {
return [];
}
try {
const parsed = JSON.parse(parsedContent) as MindMapNode & { nodes?: MindMapNode[] };
const rows: { title: string; level: number }[] = [];
const walk = (node: MindMapNode, level: number) => {
if (node.title) {
rows.push({ title: node.title, level });
}
(node.children || []).forEach(child => walk(child, level + 1));
};
rows.push({ title: parsed.title || mindMap.value?.title || "思维导图", level: 0 });
(parsed.nodes || parsed.children || []).forEach(node => walk(node, 1));
return rows;
} catch {
return [];
}
});
const mindMapStatusType = computed(() => {
if (mindMap.value?.parseStatus === "FAILED") return "danger";
if (mindMap.value?.parseStatus === "SUCCESS") return "success";
return "info";
});
const formatDuration = (seconds?: number): string => {
if (seconds == null) return "-";
const h = Math.floor(seconds / 3600);
@@ -149,12 +109,6 @@ const loadTaskHistory = async (tn: string) => {
}
};
const loadReviewExtensions = async (tn: string) => {
const mindMapRes = await getReviewMindMap(tn).catch(() => null);
const loadedMindMap: ReviewMindMap | null = mindMapRes?.data || null;
mindMap.value = loadedMindMap;
};
const loadDetail = async () => {
loading.value = true;
isEditing.value = false;
@@ -164,7 +118,6 @@ const loadDetail = async () => {
taskNum.value = "";
sessionInfo.value = null;
taskSessions.value = [];
mindMap.value = null;
try {
let res;
if (contentType.value === "report") {
@@ -187,7 +140,6 @@ const loadDetail = async () => {
if (info?.taskNum) {
await loadTaskHistory(info.taskNum);
await loadReviewExtensions(info.taskNum);
}
} catch {
// 非关键数据
@@ -241,29 +193,6 @@ const saveEdit = async () => {
}
};
const handleMindMapFileChange = async (uploadFile: any) => {
if (!taskNum.value) {
ElMessage.warning("未找到关联任务");
return;
}
const file = uploadFile.raw as File | undefined;
if (!file) {
return;
}
mindMapUploading.value = true;
try {
const res = await uploadReviewMindMap(taskNum.value, file);
mindMap.value = res?.data || null;
if (mindMap.value?.parseStatus === "FAILED") {
ElMessage.warning("文件已上传,但解析失败");
} else {
ElMessage.success("思维导图已解析");
}
} finally {
mindMapUploading.value = false;
}
};
watch(
() => [contentType.value, contentId.value],
() => {
@@ -299,6 +228,14 @@ watch(
>
编辑
</el-button>
<el-button
v-if="taskNum"
type="success"
size="small"
@click="goToRecall"
>
回忆复习
</el-button>
</div>
<div v-if="sessionInfo" class="session-stats">
<span>实际{{ formatDuration(sessionInfo.actualTime) }}</span>
@@ -328,59 +265,6 @@ watch(
</template>
</article>
<section class="review-extension-grid" v-if="taskNum">
<article class="surface-card extension-card">
<div class="extension-head">
<h3>思维导图</h3>
<el-tag size="small" :type="mindMapStatusType" effect="plain">
{{ mindMap?.parseStatus || "未上传" }}
</el-tag>
</div>
<div class="mind-map-form">
<div class="mind-map-upload-row">
<el-upload
:auto-upload="false"
:show-file-list="false"
accept=".xmind,.md,.markdown,.opml,.mm,.txt"
:on-change="handleMindMapFileChange"
>
<el-button type="success" :loading="mindMapUploading">上传导图文件</el-button>
</el-upload>
<el-button type="success" size="small" @click="goToRecall">
回忆复习
</el-button>
<el-tag v-if="mindMap?.fileFormat" size="small" effect="plain">
{{ mindMap.fileFormat }}
</el-tag>
</div>
<div v-if="mindMap" class="mind-map-preview">
<div class="mind-map-meta">
<strong>{{ mindMap.title }}</strong>
<span v-if="mindMap.fileName">{{ mindMap.fileName }}</span>
</div>
<p v-if="mindMap.summary" class="mind-map-summary">{{ mindMap.summary }}</p>
<p v-if="mindMap.parseStatus === 'FAILED'" class="mind-map-error">
{{ mindMap.parseError || "解析失败" }}
</p>
<div v-if="flattenedMindMapNodes.length > 0" class="mind-map-outline">
<div
v-for="(node, idx) in flattenedMindMapNodes"
:key="`${idx}-${node.title}`"
class="mind-map-node"
:style="{ paddingLeft: `${node.level * 16}px` }"
>
{{ node.title }}
</div>
</div>
<pre v-else-if="mindMap.content" class="mind-map-content">{{ mindMap.content }}</pre>
</div>
<el-empty v-else description="暂无思维导图" :image-size="64" />
</div>
</article>
</section>
<!-- 该任务下的所有学习记录 -->
<article class="surface-card history-card" v-if="taskSessions.length > 0">
<h3>该任务下的所有学习记录</h3>
@@ -486,101 +370,6 @@ watch(
padding: 40px 0;
}
.review-extension-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 14px;
}
.extension-card {
padding: 20px 22px;
}
.extension-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.extension-head h3 {
margin: 0;
color: var(--green-900);
font-size: 16px;
}
.mind-map-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.mind-map-upload-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.mind-map-preview {
border: 1px solid var(--border-soft);
border-radius: 8px;
background: #fbfefc;
padding: 12px;
}
.mind-map-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.mind-map-meta strong {
color: var(--text-primary);
word-break: break-word;
}
.mind-map-meta span,
.mind-map-summary {
color: var(--text-secondary);
font-size: 13px;
word-break: break-word;
}
.mind-map-summary,
.mind-map-error {
margin: 8px 0 0;
}
.mind-map-error {
color: #c62828;
font-size: 13px;
word-break: break-word;
}
.mind-map-outline {
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
.mind-map-node {
font-size: 13px;
line-height: 1.5;
color: var(--text-primary);
word-break: break-word;
}
.mind-map-content {
margin: 10px 0 0;
white-space: pre-wrap;
word-break: break-word;
font-size: 13px;
color: var(--text-primary);
}
/* 历史记录卡片 */
.history-card {
padding: 20px 22px;
@@ -692,10 +481,6 @@ watch(
}
@media (max-width: 900px) {
.review-extension-grid {
grid-template-columns: 1fr;
}
.form-row {
flex-direction: column;
align-items: stretch;
+23 -4
View File
@@ -418,6 +418,11 @@ onMounted(() => {
</div>
</article>
<!-- 折叠时引导告知用户展开标准导图 -->
<div v-if="!focusPath && !hasCompared && !standardExpanded" class="start-guide surface-card">
<p>展开下方标准思维导图可点击节点选择复习起点然后在此凭记忆补全知识点</p>
</div>
<!-- 回忆输入可编辑思维导图 -->
<article class="surface-card recall-panel">
<div class="panel-header">
@@ -509,8 +514,8 @@ onMounted(() => {
<span>来源{{ standard.generator }}</span>
<span>参考{{ standard.sourceReportCount }} 份报告 + {{ standard.sourceFragmentCount }} 份残片</span>
</div>
<p v-if="!hasCompared && !focusPath" class="select-hint">
点击导图节点选择复习起点选好后开始回忆填空
<p v-if="!focusPath && !hasCompared" class="select-hint">
点击导图节点即可选择复习起点选好后凭记忆在上方补全知识点
</p>
<MindMapViewer
v-if="standardTree"
@@ -838,13 +843,27 @@ onMounted(() => {
flex-shrink: 0;
}
.start-guide {
padding: 14px 22px;
background: linear-gradient(135deg, #fff8e1 0%, #fff3cd 100%);
border-left: 4px solid #ffc107;
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
color: #856404;
}
.start-guide p {
margin: 0;
}
.select-hint {
font-size: 13px;
color: #e6a23c;
color: #856404;
margin: 0 0 8px;
padding: 6px 12px;
background: #fdf6ec;
border-radius: 4px;
border-left: 3px solid #e6a23c;
border-left: 3px solid #ffc107;
}
</style>