feat(预期):学习预期闭环 + 权重配置 UI

- StartTask:新会话强制填写学习预期(不可跳过),页面顶部常驻展示可修改
- 结束会话弹窗:展示本次预期供对照,有残片时自动拉取 AI 聚合草稿作为编辑起点
- Study:常用操作增加权重配置——五维度滑块、合计校验 100%、保存后重算并刷新
- api:expectation / report-draft / priority-weights 接口
This commit is contained in:
2026-07-03 23:56:52 +08:00
parent b24483b03b
commit 6346120805
4 changed files with 299 additions and 4 deletions
+140 -4
View File
@@ -11,6 +11,7 @@ import {
} from "@/api/studySessions";
import { useStudyFragment } from "@/components/composables/fragment";
import { getFragmentsBySession, updateFragments } from "@/api/reportFragments";
import { getExpectation, getReportDraft, upsertExpectation } from "@/api/studySessions";
import router from "@/router";
const {
@@ -23,6 +24,13 @@ const {
const summaryDialogVisible = ref(false);
const summaryContent = ref("");
const summaryLoading = ref(false);
// 学习预期
const expectationDialogVisible = ref(false);
const expectationContent = ref("");
const expectationSaved = ref("");
const savingExpectation = ref(false);
// 当前会话碎片列表
interface FragmentItem {
@@ -209,8 +217,22 @@ const endTimer = async (content: string) => {
}
};
const openSummaryDialog = () => {
const openSummaryDialog = async () => {
summaryDialogVisible.value = true;
// 有残片且未填写过总结时,用 AI 聚合草稿作为编辑起点
if (!summaryContent.value.trim() && fragmentsList.value.length > 0) {
summaryLoading.value = true;
try {
const res = await getReportDraft(taskInfo.value.sessionNum);
if (res?.code === 200 && res.data) {
summaryContent.value = res.data;
}
} catch {
// 草稿失败不阻塞手写
} finally {
summaryLoading.value = false;
}
}
};
const closeSummaryDialog = () => {
@@ -226,6 +248,7 @@ const loadTaskSession = async () => {
try {
const res = await startOrContinueStudySession(taskNum);
Object.assign(taskInfo.value, res.data);
await loadExpectation();
if (taskInfo.value.sessionState === "ONGOING") {
startTimer();
} else {
@@ -237,6 +260,41 @@ const loadTaskSession = async () => {
}
};
// 学习预期:会话必须有预期才能开始计时
const loadExpectation = async () => {
if (!taskInfo.value.sessionNum) return;
try {
const res = await getExpectation(taskInfo.value.sessionNum);
expectationSaved.value = res?.data?.description || "";
} catch {
expectationSaved.value = "";
}
if (!expectationSaved.value) {
expectationDialogVisible.value = true;
}
};
const saveExpectation = async () => {
if (!expectationContent.value.trim()) {
ElMessage.warning("学习预期不可为空");
return;
}
savingExpectation.value = true;
try {
const res = await upsertExpectation(taskInfo.value.sessionNum, expectationContent.value);
if (res?.code === 200) {
expectationSaved.value = expectationContent.value;
expectationDialogVisible.value = false;
} else {
ElMessage.error(res?.message || "保存失败");
}
} catch (e: any) {
ElMessage.error(e.message || "请求失败");
} finally {
savingExpectation.value = false;
}
};
const initPage = async () => {
const hasPermission = await checkAudioPermission();
if (!hasPermission) {
@@ -323,6 +381,19 @@ onUnmounted(() => {
<span class="status-text">状态{{ statusText }}</span>
</div>
<!-- 学习预期 -->
<article class="surface-card expectation-card" v-if="expectationSaved">
<span class="expectation-label">本次预期</span>
<span class="expectation-text">{{ expectationSaved }}</span>
<el-button
text
size="small"
@click="expectationContent = expectationSaved; expectationDialogVisible = true"
>
修改
</el-button>
</article>
<el-alert
v-if="taskInfo.systemMessage"
:title="taskInfo.systemMessage"
@@ -415,12 +486,38 @@ onUnmounted(() => {
</template>
</el-dialog>
<el-dialog v-model="summaryDialogVisible" title="结束会话总结" width="520px">
<el-dialog
v-model="expectationDialogVisible"
title="学习预期"
width="520px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="!!expectationSaved"
>
<p class="dialog-hint">开始学习前写下这次想学会什么结束时会与实际学习内容对照</p>
<el-input
type="textarea"
v-model="expectationContent"
placeholder="例如:理解线程池的核心参数和拒绝策略"
:rows="4"
/>
<template #footer>
<el-button v-if="expectationSaved" @click="expectationDialogVisible = false">取消</el-button>
<el-button type="success" :loading="savingExpectation" @click="saveExpectation">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="summaryDialogVisible" title="结束会话总结" width="560px">
<div v-if="expectationSaved" class="summary-expectation">
<span class="expectation-label">本次预期</span>
<span>{{ expectationSaved }}</span>
</div>
<el-input
v-loading="summaryLoading"
type="textarea"
v-model="summaryContent"
placeholder="请输入本次学习内容总结"
:rows="5"
placeholder="总结本次学到的内容(已有残片时自动生成草稿,可修改)"
:rows="8"
/>
<template #footer>
<el-button @click="closeSummaryDialog">取消</el-button>
@@ -443,6 +540,45 @@ onUnmounted(() => {
gap: 12px;
}
.expectation-card {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
}
.expectation-label {
flex-shrink: 0;
font-size: 12px;
font-weight: 700;
color: var(--green-700);
background: #e8f5e9;
padding: 2px 8px;
border-radius: 4px;
}
.expectation-text {
flex: 1;
font-size: 14px;
color: var(--text-primary);
word-break: break-word;
}
.dialog-hint {
margin: 0 0 10px;
font-size: 13px;
color: var(--text-secondary);
}
.summary-expectation {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 10px;
font-size: 13px;
color: var(--text-secondary);
}
.status-text {
font-size: 14px;
color: var(--text-secondary);