fix(学习会话):休息倒计时刷新后丢失

根因:休息倒计时完全存在浏览器内存中(runCountdown),
刷新页面后 loadTaskSession() 读取会话为 PAUSED 状态,
走 else 分支 syncDisplay(pointerPosition) 重置为工作钟,
休息状态和剩余时间全部丢失。

修复方案(纯前端):
- restTimer() 将休息结束时间戳写入 localStorage
- loadTaskSession() 启动时检查 localStorage,
  若休息未到期则恢复 runCountdown 继续倒计时
- startTimer()/stopTimer()/endTimer() 清除休息状态
- 休息期间 UI:倒计时数字橙色、进度环金色、
  按钮区替换为休息中标签(隐藏开始/暂停/残片)
This commit is contained in:
2026-07-04 10:27:38 +08:00
parent cd68811dfb
commit ae796308a6
+88 -11
View File
@@ -72,7 +72,15 @@ const {
requestAudioPermission, requestAudioPermission,
} = useTimer(); } = useTimer();
const progress = computed(() => ((timerMinutes.value * 60 + timerSeconds.value) / (25 * 60)) * 100); // 休息状态持久化(刷新后恢复)
const BREAK_END_KEY = computed(() => `breakEnd_${taskNum}`);
const isBreak = ref(false);
const breakAudio = new Audio('/resource/notification.mp3');
const progress = computed(() => {
const total = isBreak.value ? 5 * 60 : 25 * 60;
return ((timerMinutes.value * 60 + timerSeconds.value) / total) * 100;
});
const nowTimestamp = ref(Date.now()); const nowTimestamp = ref(Date.now());
let displayInterval: number | undefined; let displayInterval: number | undefined;
@@ -170,6 +178,9 @@ const startTimer = async () => {
if (res.code === 200) ElMessage.success("任务继续"); if (res.code === 200) ElMessage.success("任务继续");
await loadTaskSession(); await loadTaskSession();
} }
// 手动开始 → 清除休息状态
localStorage.removeItem(BREAK_END_KEY.value);
isBreak.value = false;
const duration = taskInfo.value.pointerPosition || 25 * 60 * 1000; const duration = taskInfo.value.pointerPosition || 25 * 60 * 1000;
runCountdown(duration); runCountdown(duration);
}; };
@@ -181,6 +192,12 @@ const stopTimer = async () => {
taskInfo.value.sessionState = "PAUSED"; taskInfo.value.sessionState = "PAUSED";
} }
clear(); clear();
clearBreakState();
};
const clearBreakState = () => {
localStorage.removeItem(BREAK_END_KEY.value);
isBreak.value = false;
}; };
const endTimer = async (content: string) => { const endTimer = async (content: string) => {
@@ -211,6 +228,7 @@ const endTimer = async (content: string) => {
ElMessage.error(res.message || "结束会话失败"); ElMessage.error(res.message || "结束会话失败");
} }
clear(); clear();
clearBreakState();
await router.push("/study"); await router.push("/study");
} catch { } catch {
ElMessage.error("结束会话失败,请重试"); ElMessage.error("结束会话失败,请重试");
@@ -241,7 +259,23 @@ const closeSummaryDialog = () => {
const restTimer = async () => { const restTimer = async () => {
await stopTimer(); await stopTimer();
runCountdown(5 * 60 * 1000); const breakMs = 5 * 60 * 1000;
localStorage.setItem(BREAK_END_KEY.value, String(Date.now() + breakMs));
isBreak.value = true;
runCountdown(breakMs, () => {
localStorage.removeItem(BREAK_END_KEY.value);
isBreak.value = false;
breakAudio.loop = true;
breakAudio.play();
ElMessageBox.alert('休息结束!点击"开始"继续学习', '休息结束', {
confirmButtonText: '关闭铃声',
callback: () => {
breakAudio.pause();
breakAudio.currentTime = 0;
ElMessage.success('铃声已关闭');
},
});
});
}; };
const loadTaskSession = async () => { const loadTaskSession = async () => {
@@ -249,6 +283,34 @@ const loadTaskSession = async () => {
const res = await startOrContinueStudySession(taskNum); const res = await startOrContinueStudySession(taskNum);
Object.assign(taskInfo.value, res.data); Object.assign(taskInfo.value, res.data);
await loadExpectation(); await loadExpectation();
// 检查是否有未结束的休息
const storedEnd = localStorage.getItem(BREAK_END_KEY.value);
if (storedEnd) {
const remaining = parseInt(storedEnd, 10) - Date.now();
if (remaining > 0) {
isBreak.value = true;
runCountdown(remaining, () => {
localStorage.removeItem(BREAK_END_KEY.value);
isBreak.value = false;
breakAudio.loop = true;
breakAudio.play();
ElMessageBox.alert('休息结束!点击"开始"继续学习', '休息结束', {
confirmButtonText: '关闭铃声',
callback: () => {
breakAudio.pause();
breakAudio.currentTime = 0;
ElMessage.success('铃声已关闭');
},
});
});
loadFragments();
return;
} else {
localStorage.removeItem(BREAK_END_KEY.value);
}
}
if (taskInfo.value.sessionState === "ONGOING") { if (taskInfo.value.sessionState === "ONGOING") {
startTimer(); startTimer();
} else { } else {
@@ -404,13 +466,19 @@ onUnmounted(() => {
<div class="session-grid"> <div class="session-grid">
<article class="surface-card timer-card"> <article class="surface-card timer-card">
<p class="timer-title">当前计时</p> <p class="timer-title">{{ isBreak ? '☕ 休息中' : '当前计时' }}</p>
<el-progress type="dashboard" :percentage="progress" :stroke-width="14" :width="250" color="#2f8f68"> <el-progress
<div class="timer-number"> type="dashboard"
:percentage="progress"
:stroke-width="14"
:width="250"
:color="isBreak ? '#e6a23c' : '#2f8f68'"
>
<div class="timer-number" :class="{ 'break-color': isBreak }">
{{ timerMinutes.toString().padStart(2, "0") }}:{{ timerSeconds.toString().padStart(2, "0") }} {{ timerMinutes.toString().padStart(2, "0") }}:{{ timerSeconds.toString().padStart(2, "0") }}
</div> </div>
</el-progress> </el-progress>
<p class="timer-tip">番茄钟默认 25 分钟休息计时 5 分钟</p> <p class="timer-tip">{{ isBreak ? '休息倒计时' : '番茄钟默认 25 分钟,休息计时 5 分钟' }}</p>
</article> </article>
<article class="surface-card info-card"> <article class="surface-card info-card">
@@ -430,15 +498,20 @@ onUnmounted(() => {
</div> </div>
<div class="metric"> <div class="metric">
<span>会话状态</span> <span>会话状态</span>
<strong>{{ statusText }}</strong> <strong>{{ isBreak ? '休息中' : statusText }}</strong>
</div> </div>
</div> </div>
<div class="action-row"> <div class="action-row">
<el-button type="success" @click="startTimer" :disabled="timerRunning">开始</el-button> <template v-if="isBreak">
<el-button @click="stopTimer" :disabled="!timerRunning" v-if="!timerIsOver">暂停</el-button> <el-tag type="warning" effect="plain" size="large">休息中 · {{ timerMinutes }}:{{ timerSeconds.toString().padStart(2, "0") }}</el-tag>
<el-button @click="openFragmentDialog" :disabled="!timerRunning">生成残片</el-button> </template>
<el-button v-if="timerIsOver" plain type="success" @click="restTimer">休息</el-button> <template v-else>
<el-button type="success" @click="startTimer" :disabled="timerRunning">开始</el-button>
<el-button @click="stopTimer" :disabled="!timerRunning" v-if="!timerIsOver">暂停</el-button>
<el-button @click="openFragmentDialog" :disabled="!timerRunning">生成残片</el-button>
<el-button v-if="timerIsOver" plain type="success" @click="restTimer">休息</el-button>
</template>
<el-button type="danger" plain @click="openSummaryDialog">结束会话</el-button> <el-button type="danger" plain @click="openSummaryDialog">结束会话</el-button>
</div> </div>
</article> </article>
@@ -611,6 +684,10 @@ onUnmounted(() => {
font-weight: 700; font-weight: 700;
} }
.timer-number.break-color {
color: #d48806;
}
.timer-tip { .timer-tip {
margin: 14px 0 0; margin: 14px 0 0;
color: var(--text-secondary); color: var(--text-secondary);