feat(复习):回忆复习页面与入口

- 新增 ReviewRecall.vue 回忆复习页面(双栏布局)
  - 左栏:回忆大纲输入区、对比结果展示(着色树)
  - 右栏:标准导图(默认折叠)、编辑、重新生成
  - 遗漏项列表,可点击回看原文
  - 回忆历史记录查询
- api/standardMindMap.ts:标准导图与回忆对比 API
- router:新增 /review/recall/:taskNum 路由
- Review.vue 行操作中增加回忆复习按钮
- ReviewDetail.vue 思维导图区增加回忆复习入口
This commit is contained in:
2026-07-03 08:54:16 +08:00
parent dc7469ec3f
commit b24483b03b
11 changed files with 1205 additions and 260 deletions
+4 -188
View File
@@ -2,16 +2,11 @@
import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
createReviewApplication,
deleteReviewApplication,
getFragmentDetail,
getReportDetail,
getReviewApplications,
getReviewMindMap,
getTaskReview,
updateReviewApplication,
uploadReviewMindMap,
type ReviewApplication,
type ReviewFeedItem,
type ReviewMindMap,
} from "@/api/review";
@@ -34,21 +29,6 @@ const isEditing = ref(false);
const editContent = ref("");
const saving = ref(false);
const applications = ref<ReviewApplication[]>([]);
const applicationSaving = ref(false);
const editingApplicationId = ref<number | null>(null);
const applicationForm = ref<{
title: string;
description: string;
resourceUrl: string;
status: ReviewApplication["status"];
}>({
title: "",
description: "",
resourceUrl: "",
status: "TODO",
});
const mindMap = ref<ReviewMindMap | null>(null);
const mindMapUploading = ref(false);
@@ -68,12 +48,6 @@ const taskSessions = ref<{
fragments: ReviewFeedItem[];
}[]>([]);
const statusText: Record<ReviewApplication["status"], string> = {
TODO: "待应用",
DOING: "进行中",
DONE: "已完成",
};
type MindMapNode = {
title?: string;
notes?: string;
@@ -123,16 +97,6 @@ const formatRatio = (ratio?: number): string => {
return `${(ratio * 100).toFixed(1)}%`;
};
const resetApplicationForm = () => {
editingApplicationId.value = null;
applicationForm.value = {
title: "",
description: "",
resourceUrl: "",
status: "TODO",
};
};
const loadTaskHistory = async (tn: string) => {
try {
const res = await getTaskReview(tn);
@@ -165,9 +129,6 @@ const loadTaskHistory = async (tn: string) => {
};
const loadReviewExtensions = async (tn: string) => {
const applicationsRes = await getReviewApplications(tn).catch(() => null);
applications.value = applicationsRes?.data || [];
const mindMapRes = await getReviewMindMap(tn).catch(() => null);
const loadedMindMap: ReviewMindMap | null = mindMapRes?.data || null;
mindMap.value = loadedMindMap;
@@ -182,7 +143,6 @@ const loadDetail = async () => {
taskNum.value = "";
sessionInfo.value = null;
taskSessions.value = [];
applications.value = [];
mindMap.value = null;
try {
let res;
@@ -260,58 +220,6 @@ const saveEdit = async () => {
}
};
const saveApplication = async () => {
if (!taskNum.value) {
ElMessage.warning("未找到关联任务");
return;
}
if (!applicationForm.value.title.trim()) {
ElMessage.warning("应用项目标题不能为空");
return;
}
applicationSaving.value = true;
try {
const payload = {
title: applicationForm.value.title,
description: applicationForm.value.description,
resourceUrl: applicationForm.value.resourceUrl,
status: applicationForm.value.status,
};
if (editingApplicationId.value) {
await updateReviewApplication(editingApplicationId.value, payload);
ElMessage.success("应用场景已更新");
} else {
await createReviewApplication({ taskNum: taskNum.value, ...payload });
ElMessage.success("应用场景已添加");
}
resetApplicationForm();
await loadReviewExtensions(taskNum.value);
} finally {
applicationSaving.value = false;
}
};
const editApplication = (item: ReviewApplication) => {
editingApplicationId.value = item.id;
applicationForm.value = {
title: item.title || "",
description: item.description || "",
resourceUrl: item.resourceUrl || "",
status: item.status || "TODO",
};
};
const removeApplication = async (item: ReviewApplication) => {
await deleteReviewApplication(item.id);
ElMessage.success("应用场景已删除");
if (editingApplicationId.value === item.id) {
resetApplicationForm();
}
if (taskNum.value) {
await loadReviewExtensions(taskNum.value);
}
};
const handleMindMapFileChange = async (uploadFile: any) => {
if (!taskNum.value) {
ElMessage.warning("未找到关联任务");
@@ -402,53 +310,6 @@ watch(
</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="success" effect="plain">{{ applications.length }}</el-tag>
</div>
<div class="application-list" v-if="applications.length > 0">
<div class="application-item" v-for="item in applications" :key="item.id">
<div class="application-main">
<strong>{{ item.title }}</strong>
<el-tag size="small" effect="plain">{{ statusText[item.status] || item.status }}</el-tag>
</div>
<p v-if="item.description">{{ item.description }}</p>
<el-link v-if="item.resourceUrl" :href="item.resourceUrl" target="_blank" type="primary">
{{ item.resourceUrl }}
</el-link>
<div class="item-actions">
<el-button text type="primary" size="small" @click="editApplication(item)">编辑</el-button>
<el-button text type="danger" size="small" @click="removeApplication(item)">删除</el-button>
</div>
</div>
</div>
<el-empty v-else description="暂无应用场景" :image-size="64" />
<div class="application-form">
<el-input v-model="applicationForm.title" placeholder="应用项目" />
<el-input
v-model="applicationForm.description"
type="textarea"
:rows="3"
placeholder="应用描述"
/>
<el-input v-model="applicationForm.resourceUrl" placeholder="相关链接" />
<div class="form-row">
<el-select v-model="applicationForm.status" placeholder="状态">
<el-option label="待应用" value="TODO" />
<el-option label="进行中" value="DOING" />
<el-option label="已完成" value="DONE" />
</el-select>
<el-button type="success" :loading="applicationSaving" @click="saveApplication">
{{ editingApplicationId ? "更新" : "添加" }}
</el-button>
<el-button v-if="editingApplicationId" @click="resetApplicationForm">取消</el-button>
</div>
</div>
</article>
<article class="surface-card extension-card">
<div class="extension-head">
<h3>思维导图</h3>
@@ -467,6 +328,9 @@ watch(
>
<el-button type="success" :loading="mindMapUploading">上传导图文件</el-button>
</el-upload>
<el-button type="primary" size="small" @click="router.push(`/review/recall/${taskNum}`)">
回忆复习
</el-button>
<el-tag v-if="mindMap?.fileFormat" size="small" effect="plain">
{{ mindMap.fileFormat }}
</el-tag>
@@ -605,7 +469,7 @@ watch(
.review-extension-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr);
gap: 14px;
}
@@ -627,48 +491,6 @@ watch(
font-size: 16px;
}
.application-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 14px;
}
.application-item {
padding: 12px;
border: 1px solid var(--border-soft);
border-radius: 8px;
background: #fbfefc;
}
.application-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.application-main strong {
min-width: 0;
color: var(--text-primary);
word-break: break-word;
}
.application-item p {
margin: 8px 0 0;
color: var(--text-secondary);
font-size: 13px;
word-break: break-word;
}
.item-actions {
display: flex;
justify-content: flex-end;
gap: 4px;
margin-top: 8px;
}
.application-form,
.mind-map-form {
display: flex;
flex-direction: column;
@@ -740,12 +562,6 @@ watch(
color: var(--text-primary);
}
.form-row {
display: flex;
align-items: center;
gap: 10px;
}
/* 历史记录卡片 */
.history-card {
padding: 20px 22px;