diff --git a/src/api/studySessions.ts b/src/api/studySessions.ts index 9fa8477..a75bde0 100644 --- a/src/api/studySessions.ts +++ b/src/api/studySessions.ts @@ -21,3 +21,18 @@ export const startOrContinueStudySession = (taskNum: string) => { export const getSessionDetail = (sessionNum: string) => { return request.get(`/study-sessions/${sessionNum}`); }; + +/** 获取会话的学习预期 */ +export const getExpectation = (sessionNum: string) => { + return request.get(`/study-sessions/${sessionNum}/expectation`); +}; + +/** 创建/更新会话的学习预期 */ +export const upsertExpectation = (sessionNum: string, description: string) => { + return request.put(`/study-sessions/${sessionNum}/expectation`, { description }); +}; + +/** 获取报告草稿(AI 聚合残片,不可用时为拼接) */ +export const getReportDraft = (sessionNum: string) => { + return request.get(`/study-sessions/${sessionNum}/report-draft`); +}; diff --git a/src/api/tasks.ts b/src/api/tasks.ts index b4af7ef..f5dd0b1 100644 --- a/src/api/tasks.ts +++ b/src/api/tasks.ts @@ -42,3 +42,22 @@ export const updateTaskApplication = ( export const deleteTaskApplication = (id: number) => { return request.del(`/tasks/applications/${id}`); }; + +// ============ 优先级权重配置 ============ + +export interface PriorityWeights { + urgencyWeight: number; + importanceWeight: number; + contentDifficultyWeight: number; + futureValueWeight: number; + subjectivePriorityWeight: number; +} + +export const getPriorityWeights = () => { + return request.get("/tasks/priority-weights"); +}; + +/** 保存权重配置并触发全部任务优先级重算 */ +export const savePriorityWeights = (weights: PriorityWeights) => { + return request.put("/tasks/priority-weights", weights); +}; diff --git a/src/components/StartTask.vue b/src/components/StartTask.vue index a017f2f..4a4eb09 100644 --- a/src/components/StartTask.vue +++ b/src/components/StartTask.vue @@ -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(() => { 状态:{{ statusText }} + + + 本次预期 + {{ expectationSaved }} + + 修改 + + + { - + + 开始学习前,写下这次想学会什么。结束时会与实际学习内容对照。 + + 取消 + 确定 + + + + + + 本次预期 + {{ expectationSaved }} + + 取消 @@ -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); diff --git a/src/components/Study.vue b/src/components/Study.vue index aa5fdc3..78be29a 100644 --- a/src/components/Study.vue +++ b/src/components/Study.vue @@ -5,8 +5,11 @@ import router from "@/router"; import { ElMessage } from "element-plus"; import { getReviewTaskStatsByTask } from "@/api/review"; import { + getPriorityWeights, getTaskApplications, + savePriorityWeights, updateTaskApplication, + type PriorityWeights, type TaskApplication, } from "@/api/tasks"; @@ -190,6 +193,62 @@ const removeTask = async () => { } }; +// ============ 优先级权重配置 ============ +const weightsDialogVisible = ref(false); +const savingWeights = ref(false); +const weightItems = ref([ + { key: "urgencyWeight", label: "紧急性", value: 35 }, + { key: "importanceWeight", label: "重要性", value: 25 }, + { key: "contentDifficultyWeight", label: "内容难度", value: 20 }, + { key: "futureValueWeight", label: "未来价值", value: 10 }, + { key: "subjectivePriorityWeight", label: "主观优先级", value: 10 }, +]); + +const weightsSum = computed(() => + weightItems.value.reduce((acc, item) => acc + item.value, 0) +); + +const openWeightsDialog = async () => { + try { + const res = await getPriorityWeights(); + const data = res?.data; + if (data) { + for (const item of weightItems.value) { + const v = data[item.key]; + if (typeof v === "number") item.value = Math.round(v * 100); + } + } + } catch { + // 读取失败时保持默认值 + } + weightsDialogVisible.value = true; +}; + +const saveWeights = async () => { + if (weightsSum.value !== 100) { + ElMessage.warning(`五项权重之和需为 100%,当前 ${weightsSum.value}%`); + return; + } + savingWeights.value = true; + try { + const payload = Object.fromEntries( + weightItems.value.map((item) => [item.key, item.value / 100]) + ) as unknown as PriorityWeights; + const res = await savePriorityWeights(payload); + if (res?.code === 200) { + weightsDialogVisible.value = false; + ElMessage.success("权重已保存,任务优先级已重算"); + await fetchTasks(); + } else { + ElMessage.error(res?.message || "保存失败"); + } + } catch (e: any) { + ElMessage.error(e.message || "请求失败"); + } finally { + savingWeights.value = false; + } +}; + onMounted(() => { fetchTasks(); }); @@ -320,11 +379,32 @@ onMounted(() => { 开始任务 添加任务 更新任务 + 权重配置 删除任务 + + + + 调整五个维度在优先级计算中的占比,保存后全部任务将按新权重重新排序。 + + + {{ item.label }} + + {{ item.value }}% + + + 合计:{{ weightsSum }}%(需为 100%) + + + 取消 + + 保存并重算 + + +
开始学习前,写下这次想学会什么。结束时会与实际学习内容对照。
+ 调整五个维度在优先级计算中的占比,保存后全部任务将按新权重重新排序。 +