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
+135
View File
@@ -4,6 +4,11 @@ import request from "@/utils/request";
import router from "@/router";
import { ElMessage } from "element-plus";
import { getReviewTaskStatsByTask } from "@/api/review";
import {
getTaskApplications,
updateTaskApplication,
type TaskApplication,
} from "@/api/tasks";
interface TaskItem {
title: string;
@@ -27,6 +32,14 @@ interface TaskItem {
const tasks = ref<TaskItem[]>([]);
const selectedTaskId = ref<number | null>(null);
const loading = ref(false);
const taskApplications = ref<TaskApplication[]>([]);
const applicationsLoading = ref(false);
const applicationStatusOptions: { label: string; value: TaskApplication["status"] }[] = [
{ label: "待应用", value: "TODO" },
{ label: "进行中", value: "DOING" },
{ label: "已完成", value: "DONE" },
];
const selectedTask = computed(() =>
tasks.value.find((item) => item.taskId === selectedTaskId.value) || null
@@ -62,6 +75,36 @@ async function loadTaskStats(taskNum: string) {
}
}
async function loadTaskApplications(taskNum: string) {
applicationsLoading.value = true;
try {
const res = await getTaskApplications(taskNum);
taskApplications.value = res?.data || [];
} catch (error: any) {
taskApplications.value = [];
ElMessage.error(error?.message || "应用场景加载失败");
} finally {
applicationsLoading.value = false;
}
}
const changeApplicationStatus = async (item: TaskApplication) => {
try {
await updateTaskApplication(item.id, {
title: item.title,
description: item.description,
resourceUrl: item.resourceUrl,
status: item.status,
});
ElMessage.success("应用场景状态已更新");
} catch (error: any) {
ElMessage.error(error?.message || "状态更新失败");
if (selectedTask.value) {
await loadTaskApplications(selectedTask.value.taskNum);
}
}
};
const fetchTasks = async () => {
loading.value = true;
try {
@@ -91,10 +134,12 @@ const fetchTasks = async () => {
selectedTaskId.value = nextSelectedTaskId;
if (targetTask && shouldLoadStats) {
await loadTaskStats(targetTask.taskNum);
await loadTaskApplications(targetTask.taskNum);
}
} catch (error: any) {
tasks.value = [];
selectedTaskId.value = null;
taskApplications.value = [];
ElMessage.error(error?.message || "任务列表加载失败,请重试");
} finally {
loading.value = false;
@@ -105,6 +150,9 @@ watch(selectedTaskId, (newId) => {
const task = tasks.value.find(t => t.taskId === newId);
if (task) {
loadTaskStats(task.taskNum);
loadTaskApplications(task.taskNum);
} else {
taskApplications.value = [];
}
});
@@ -229,6 +277,39 @@ onMounted(() => {
</div>
</div>
</div>
<div class="detail-block" v-loading="applicationsLoading">
<p class="label">应用场景</p>
<div v-if="taskApplications.length > 0" class="application-list">
<div
v-for="item in taskApplications"
:key="item.id"
class="application-item"
>
<div class="application-main">
<strong>{{ item.title }}</strong>
<el-select
v-model="item.status"
size="small"
class="application-status"
@change="changeApplicationStatus(item)"
>
<el-option
v-for="option in applicationStatusOptions"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
</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>
</div>
<p v-else class="empty-text">暂未设置应用场景</p>
</div>
</template>
<el-empty v-else description="暂无任务,请先创建任务" />
</article>
@@ -365,6 +446,51 @@ onMounted(() => {
color: var(--text-primary);
}
.application-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.application-item {
padding: 12px 0;
border-bottom: 1px solid var(--border-soft);
}
.application-item:first-child {
padding-top: 0;
}
.application-item:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.application-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.application-main strong {
min-width: 0;
color: var(--text-primary);
word-break: break-word;
}
.application-status {
width: 112px;
flex: 0 0 auto;
}
.application-item p {
margin: 8px 0 0;
color: var(--text-secondary);
font-size: 13px;
word-break: break-word;
}
.action-card {
padding: 16px;
display: flex;
@@ -404,5 +530,14 @@ onMounted(() => {
.action-card {
grid-template-columns: 1fr;
}
.application-main {
align-items: flex-start;
flex-direction: column;
}
.application-status {
width: 100%;
}
}
</style>