chore: 移除上传导图 + 优化复习引导提示
- 删除 ReviewMindMap 接口及 upload/get/upsert API - 删除 ReviewDetail 上传 UI 和思维导图预览 - 回忆复习按钮移至内容元信息区 - 引导提示改为渐进式:折叠时引导展开,展开后引导点击节点 - 提示语用告知语气,无表情符号 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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' } },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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" },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user