fix(学习会话):休息倒计时刷新后丢失
根因:休息倒计时完全存在浏览器内存中(runCountdown), 刷新页面后 loadTaskSession() 读取会话为 PAUSED 状态, 走 else 分支 syncDisplay(pointerPosition) 重置为工作钟, 休息状态和剩余时间全部丢失。 修复方案(纯前端): - restTimer() 将休息结束时间戳写入 localStorage - loadTaskSession() 启动时检查 localStorage, 若休息未到期则恢复 runCountdown 继续倒计时 - startTimer()/stopTimer()/endTimer() 清除休息状态 - 休息期间 UI:倒计时数字橙色、进度环金色、 按钮区替换为休息中标签(隐藏开始/暂停/残片)
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user