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,
|
getFragmentDetail,
|
||||||
getReportDetail,
|
getReportDetail,
|
||||||
getReviewFeed,
|
getReviewFeed,
|
||||||
getReviewMindMap,
|
|
||||||
getReviewTaskStats,
|
getReviewTaskStats,
|
||||||
getReviewTaskStatsByTask,
|
getReviewTaskStatsByTask,
|
||||||
getTaskReview,
|
getTaskReview,
|
||||||
upsertReviewMindMap,
|
|
||||||
uploadReviewMindMap,
|
|
||||||
} from '@/api/review'
|
} from '@/api/review'
|
||||||
|
|
||||||
const mockedRequest = vi.mocked(request)
|
const mockedRequest = vi.mocked(request)
|
||||||
@@ -79,31 +76,4 @@ describe('review API', () => {
|
|||||||
expect(mockedRequest.get).toHaveBeenCalledWith('/review/task/T001')
|
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;
|
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") => {
|
export const getReviewFeed = (limit: number = 30, mode: "recent" | "random" | "smart" = "recent") => {
|
||||||
return request.get("/review/feed", { limit, mode });
|
return request.get("/review/feed", { limit, mode });
|
||||||
};
|
};
|
||||||
@@ -65,26 +46,3 @@ export const getFragmentDetail = (id: number) => {
|
|||||||
export const getTaskReview = (taskNum: string) => {
|
export const getTaskReview = (taskNum: string) => {
|
||||||
return request.get(`/review/task/${taskNum}`);
|
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 {
|
import {
|
||||||
getFragmentDetail,
|
getFragmentDetail,
|
||||||
getReportDetail,
|
getReportDetail,
|
||||||
getReviewMindMap,
|
|
||||||
getTaskReview,
|
getTaskReview,
|
||||||
uploadReviewMindMap,
|
|
||||||
type ReviewFeedItem,
|
type ReviewFeedItem,
|
||||||
type ReviewMindMap,
|
|
||||||
} from "@/api/review";
|
} from "@/api/review";
|
||||||
import { getSessionDetail } from "@/api/studySessions";
|
import { getSessionDetail } from "@/api/studySessions";
|
||||||
import { updateFragments } from "@/api/reportFragments";
|
import { updateFragments } from "@/api/reportFragments";
|
||||||
@@ -50,9 +47,6 @@ const isEditing = ref(false);
|
|||||||
const editContent = ref("");
|
const editContent = ref("");
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
|
||||||
const mindMap = ref<ReviewMindMap | null>(null);
|
|
||||||
const mindMapUploading = ref(false);
|
|
||||||
|
|
||||||
const sessionInfo = ref<{
|
const sessionInfo = ref<{
|
||||||
taskName: string;
|
taskName: string;
|
||||||
sessionNum: string;
|
sessionNum: string;
|
||||||
@@ -69,40 +63,6 @@ const taskSessions = ref<{
|
|||||||
fragments: ReviewFeedItem[];
|
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 => {
|
const formatDuration = (seconds?: number): string => {
|
||||||
if (seconds == null) return "-";
|
if (seconds == null) return "-";
|
||||||
const h = Math.floor(seconds / 3600);
|
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 () => {
|
const loadDetail = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
isEditing.value = false;
|
isEditing.value = false;
|
||||||
@@ -164,7 +118,6 @@ const loadDetail = async () => {
|
|||||||
taskNum.value = "";
|
taskNum.value = "";
|
||||||
sessionInfo.value = null;
|
sessionInfo.value = null;
|
||||||
taskSessions.value = [];
|
taskSessions.value = [];
|
||||||
mindMap.value = null;
|
|
||||||
try {
|
try {
|
||||||
let res;
|
let res;
|
||||||
if (contentType.value === "report") {
|
if (contentType.value === "report") {
|
||||||
@@ -187,7 +140,6 @@ const loadDetail = async () => {
|
|||||||
|
|
||||||
if (info?.taskNum) {
|
if (info?.taskNum) {
|
||||||
await loadTaskHistory(info.taskNum);
|
await loadTaskHistory(info.taskNum);
|
||||||
await loadReviewExtensions(info.taskNum);
|
|
||||||
}
|
}
|
||||||
} catch {
|
} 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(
|
watch(
|
||||||
() => [contentType.value, contentId.value],
|
() => [contentType.value, contentId.value],
|
||||||
() => {
|
() => {
|
||||||
@@ -299,6 +228,14 @@ watch(
|
|||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="taskNum"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="goToRecall"
|
||||||
|
>
|
||||||
|
回忆复习
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="sessionInfo" class="session-stats">
|
<div v-if="sessionInfo" class="session-stats">
|
||||||
<span>实际:{{ formatDuration(sessionInfo.actualTime) }}</span>
|
<span>实际:{{ formatDuration(sessionInfo.actualTime) }}</span>
|
||||||
@@ -328,59 +265,6 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
</article>
|
</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">
|
<article class="surface-card history-card" v-if="taskSessions.length > 0">
|
||||||
<h3>该任务下的所有学习记录</h3>
|
<h3>该任务下的所有学习记录</h3>
|
||||||
@@ -486,101 +370,6 @@ watch(
|
|||||||
padding: 40px 0;
|
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 {
|
.history-card {
|
||||||
padding: 20px 22px;
|
padding: 20px 22px;
|
||||||
@@ -692,10 +481,6 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.review-extension-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
.form-row {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
@@ -418,6 +418,11 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<!-- 折叠时引导:告知用户展开标准导图 -->
|
||||||
|
<div v-if="!focusPath && !hasCompared && !standardExpanded" class="start-guide surface-card">
|
||||||
|
<p>展开下方「标准思维导图」后,可点击节点选择复习起点,然后在此凭记忆补全知识点。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 回忆输入:可编辑思维导图 -->
|
<!-- 回忆输入:可编辑思维导图 -->
|
||||||
<article class="surface-card recall-panel">
|
<article class="surface-card recall-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
@@ -509,8 +514,8 @@ onMounted(() => {
|
|||||||
<span>来源:{{ standard.generator }}</span>
|
<span>来源:{{ standard.generator }}</span>
|
||||||
<span>参考:{{ standard.sourceReportCount }} 份报告 + {{ standard.sourceFragmentCount }} 份残片</span>
|
<span>参考:{{ standard.sourceReportCount }} 份报告 + {{ standard.sourceFragmentCount }} 份残片</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!hasCompared && !focusPath" class="select-hint">
|
<p v-if="!focusPath && !hasCompared" class="select-hint">
|
||||||
点击导图节点选择复习起点,选好后开始回忆填空
|
点击导图节点即可选择复习起点,选好后凭记忆在上方补全知识点。
|
||||||
</p>
|
</p>
|
||||||
<MindMapViewer
|
<MindMapViewer
|
||||||
v-if="standardTree"
|
v-if="standardTree"
|
||||||
@@ -838,13 +843,27 @@ onMounted(() => {
|
|||||||
flex-shrink: 0;
|
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 {
|
.select-hint {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #e6a23c;
|
color: #856404;
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
background: #fdf6ec;
|
background: #fdf6ec;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border-left: 3px solid #e6a23c;
|
border-left: 3px solid #ffc107;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user