feat:1. 调整项目结构,将API接口定义单独抽离 2. 将倒计时计算逻辑调整到后端进行处理计算 3. 增加铃声提示功能

This commit is contained in:
2025-07-26 12:53:33 +08:00
parent 07bf872149
commit 4faf2db730
6 changed files with 206 additions and 135 deletions
+19
View File
@@ -0,0 +1,19 @@
// src/api/studySession.ts
import request from "@/utils/request";
export const continueSession = (sessionNum: string) => {
return request.post(`/study-sessions/${sessionNum}/study-sessions/continue`);
};
export const pauseSession = (sessionNum: string, endTime?: Date) => {
const params = endTime ? { endTime: endTime.toISOString() } : null;
return request.post(`/study-sessions/${sessionNum}/study-sessions/pause`, null, { params });
};
export const endSession = (sessionNum: string, content: string = "任务结束") => {
return request.post(`/study-sessions/${sessionNum}/study-sessions/ended`, { content });
};
export const startOrContinueStudySession = (taskNum: string) => {
return request.get(`/tasks/${taskNum}/study-sessions/start-or-continue`);
};
+83 -133
View File
@@ -1,38 +1,50 @@
<!-- src/views/StudyTimer.vue -->
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import {ElButton, ElMessage, ElMessageBox, ElProgress} from 'element-plus'; import { useRoute } from 'vue-router';
import { useRoute } from "vue-router"; import { ElMessage } from 'element-plus';
import request from "@/utils/request"; import { useTimer } from '@/components/composables/useTimer';
import { continueSession, pauseSession, endSession, startOrContinueStudySession } from '@/api/studySessions';
const route = useRoute(); const route = useRoute();
const taskNum = route.params.taskNum; const taskNum = route.params.taskNum as string;
const taskInfo = ref({ const taskInfo = ref({
sessionNum: '', sessionNum: '',
sessionState: '--', sessionState: '--',
taskName: '', taskName: '',
taskNum: taskNum, taskNum,
startTime: '', startTime: '',
endTime: '', endTime: '',
lastStartTime: '', lastStartTime: '',
actualTime: 0, actualTime: 0,
effectiveTime: 0, effectiveTime: 0,
effectivenessRatio: '--', effectivenessRatio: '--',
pointerPosition: 0,
systemMessage: '',
}); });
const timerMinutes = ref(25); const {
const timerSeconds = ref(0); timerMinutes,
const timerRunning = ref(false); timerSeconds,
let timerInterval: number; runCountdown,
let timer : number; clear,
timerRunning,
timerIsOver,
checkAudioPermission,
requestAudioPermission
} = useTimer();
const currentCycle = computed(() => Math.floor(taskInfo.value.effectiveTime/60 / 25) + 1); const globalTimer = ref(Date.now());
const currentTime = ref(Date.now());
const currentCycle = computed(() =>
Math.floor(taskInfo.value.effectiveTime / 60 / 25) + 1
);
const elapsedTime = computed(() => { const elapsedTime = computed(() => {
if (!taskInfo.value.startTime) return { hours: 0, minutes: 0, seconds: 0 }; if (!taskInfo.value.startTime || !timerRunning.value) return { hours: 0, minutes: 0, seconds: 0 };
const start = new Date(taskInfo.value.startTime).getTime(); const start = new Date(taskInfo.value.startTime).getTime();
const now = currentTime.value; const now = globalTimer.value;
const diff = Math.max(Math.floor((now - start) / 1000), 0); const diff = Math.max(Math.floor((now - start) / 1000), 0);
return { return {
hours: Math.floor(diff / 3600), hours: Math.floor(diff / 3600),
@@ -41,134 +53,73 @@ const elapsedTime = computed(() => {
}; };
}); });
const formatTime = (time: { hours: number; minutes: number; seconds: number }) => { const formatTime = (t: { hours: number; minutes: number; seconds: number }) =>
return `${time.hours}${time.minutes}${time.seconds}`; `${t.hours}${t.minutes}${t.seconds}`;
};
const progress = computed(() => ((timerMinutes.value * 60 + timerSeconds.value) / (25 * 60)) * 100); const progress = computed(() =>
((timerMinutes.value * 60 + timerSeconds.value) / (25 * 60)) * 100
const audio = new Audio('/resource/notification.mp3'); );
const startTimer = async () => { const startTimer = async () => {
if ("PAUSED" === taskInfo.value.sessionState) { if (taskInfo.value.sessionState === "PAUSED") {
await request.post(`/study-sessions/${taskInfo.value.sessionNum}/study-sessions/continue`, null, {params: null}).then(req => { const res = await continueSession(taskInfo.value.sessionNum);
if (req.code === 200) { if (res.code === 200) ElMessage.success('任务继续');
ElMessage.success('任务开始'); await loadTaskSession();
} }
}); const duration = taskInfo.value.pointerPosition || 25 * 60 * 1000;
} runCountdown(duration);
timerRunning.value = true;
timerInterval = window.setInterval(() => {
if (timerRunning.value) {
if (timerSeconds.value === 0) {
if (timerMinutes.value === 0) {
endTimer();
// 弹窗 + 播放铃声
audio.loop = true; // 循环播放,避免弹窗没关闭铃声自动停
audio.play();
ElMessageBox.alert('本次番茄钟结束!', '提醒', {
confirmButtonText: '关闭铃声',
callback: () => {
audio.pause();
audio.currentTime = 0; // 重置音频进度
ElMessage.success('铃声已关闭');
}
});
} else {
timerMinutes.value--;
timerSeconds.value = 59;
}
} else {
timerSeconds.value--;
}
}else {
clearInterval(timerInterval);
}
}, 1000);
}; };
const stopTimer = async () => { const stopTimer = async () => {
await request.post(`/study-sessions/${taskInfo.value.sessionNum}/study-sessions/pause`, null, {params:null}).then(req =>{ const res = await pauseSession(taskInfo.value.sessionNum);
if (req.code === 200) { if (res.code === 200) ElMessage.success('任务暂停');
ElMessage.success('任务暂停'); clear();
}
});
timerRunning.value = false;
}; };
const endTimer = async () => { const endTimer = async () => {
await request.post(`/study-sessions/${taskInfo.value.sessionNum}/study-sessions/ended`, {content:"测试关闭"}, {params:null}).then(req =>{ const res = await endSession(taskInfo.value.sessionNum, "用户手动结束");
if (req.code === 200) { if (res.code === 200) ElMessage.success('任务结束');
ElMessage.success('任务结束'); clear();
}
});
clearInterval(timerInterval);
timerRunning.value = false;
}; };
const handleTimerException = async (adjustedEnd: Date) => { const restTimer = async () => {
await request.post(`/study-sessions/${taskInfo.value.sessionNum}/study-sessions/pause`, null, { await stopTimer();
params: { runCountdown(5 * 60 * 1000);
endTime: adjustedEnd.toISOString(),
}
});
ElMessage.warning('上次学习任务开始时间过长,未及时结束,有效时间仅增加25分钟');
// 重新加载数据
startOrContinueStudySession()
}; };
const initClocker = async () => { const loadTaskSession = async () => {
try {
const end = new Date(taskInfo.value.endTime).getTime(); const res = await startOrContinueStudySession(taskNum);
const lastStart = new Date(taskInfo.value.lastStartTime).getTime();
const now = new Date().getTime();
let diff = Math.floor((now - lastStart) / 1000);
// 判断任务是否超时(状态为正在进行,且最后一次开始时间到现在超过25分钟)
console.log(new Date().toISOString())
if (taskInfo.value.sessionState === 'ONGOING' && now - lastStart > 25 * 60 * 1000) {
// 超时了,触发暂停
const adjustedEnd = new Date(lastStart + 25 * 60000);
await handleTimerException(adjustedEnd);
// 准备开始下一次倒计时
timerMinutes.value = 25;
timerSeconds.value = 0;
} else if (taskInfo.value.sessionState === 'ONGOING' ) {
// 没超时,恢复进度
timerMinutes.value = 25 - Math.floor(diff / 60);
timerSeconds.value = 60 - diff % 60;
startTimer();
} else {
// 准备开始下一次倒计时
timerMinutes.value = 25;
timerSeconds.value = 0;
}
};
const startOrContinueStudySession = () => {
request.get(`/tasks/${taskNum}/study-sessions/start-or-continue`, {})
.then(res => {
Object.assign(taskInfo.value, res.data); Object.assign(taskInfo.value, res.data);
initClocker(); if (taskInfo.value.sessionState === 'ONGOING') {
}) startTimer();
.catch(err => { }
ElMessage.error(err.message); if (taskInfo.value.systemMessage) {
}); ElMessage.warning(taskInfo.value.systemMessage);
}
} catch (err: any) {
ElMessage.error(err.message || '加载任务失败');
}
};
const initPage = async () => {
const hasPermission = await checkAudioPermission();
if (!hasPermission) {
const activated = await requestAudioPermission();
if (!activated) {
console.warn('用户拒绝或未能激活权限');
return;
}
}
await loadTaskSession();
setInterval(() => {
globalTimer.value = Date.now();
}, 1000);
}; };
onMounted(() => { onMounted(() => {
startOrContinueStudySession(); initPage();
timer = setInterval(() => {
currentTime.value = Date.now();
}, 1000);
});
onUnmounted(() => {
clearInterval(timerInterval)
clearInterval(timer);
}); });
</script> </script>
@@ -180,17 +131,19 @@ onUnmounted(() => {
<p>有效时间{{ taskInfo.effectiveTime / 60 }}分钟</p> <p>有效时间{{ taskInfo.effectiveTime / 60 }}分钟</p>
<p>本次运行时间{{ formatTime(elapsedTime) }}</p> <p>本次运行时间{{ formatTime(elapsedTime) }}</p>
</div> </div>
<div class="timer-section"> <div class="timer-section">
<el-progress type="circle" :percentage="progress" :width="200"> <el-progress type="circle" :percentage="progress" :width="200">
<div>{{ timerMinutes.toString().padStart(2, '0') }} : {{ timerSeconds.toString().padStart(2, '0') }}</div> <div>
{{ timerMinutes.toString().padStart(2, '0') }} :
{{ timerSeconds.toString().padStart(2, '0') }}
</div>
</el-progress> </el-progress>
</div> </div>
<div class="actions"> <div class="actions">
<el-button @click="startTimer" :disabled="timerRunning">开始</el-button> <el-button @click="startTimer" :disabled="timerRunning">开始</el-button>
<el-button @click="stopTimer" :disabled = "!timerRunning">暂停</el-button> <el-button @click="stopTimer" :disabled="!timerRunning" v-if="!timerIsOver">暂停</el-button>
<el-button @click="endTimer" >结束</el-button> <el-button @click="restTimer" v-if="timerIsOver">休息</el-button>
<el-button @click="endTimer">结束</el-button>
</div> </div>
</div> </div>
</template> </template>
@@ -203,16 +156,13 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
margin: 20px; margin: 20px;
} }
.task-info { .task-info {
font-size: 18px; font-size: 18px;
margin-bottom: 30px; margin-bottom: 30px;
} }
.timer-section { .timer-section {
margin: 20px; margin: 20px;
} }
.actions { .actions {
display: flex; display: flex;
gap: 10px; gap: 10px;
+1 -1
View File
@@ -84,7 +84,7 @@ onMounted(() => {
<el-card class="task-card"> <el-card class="task-card">
<div v-if="selectedTask"> <div v-if="selectedTask">
<h3>任务描述</h3> <h3>任务描述</h3>
<el-input type="textarea" v-model="selectedTask.description" rows="4" /> <el-input type="textarea" v-model="selectedTask.description" :rows="4" />
<h3 class="section-title">任务优先级</h3> <h3 class="section-title">任务优先级</h3>
<el-radio-group v-model="selectedTask.priority"> <el-radio-group v-model="selectedTask.priority">
+99
View File
@@ -0,0 +1,99 @@
// src/composables/useTimer.ts
import { ref } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus';
export function useTimer(audioUrl: string = '/public/resource/notification.mp3') {
const timerMinutes = ref(25);
const timerSeconds = ref(0);
const remainingTime = ref(25 * 60 * 1000);
const timerRunning = ref(false);
const timerIsOver = ref(false);
const audioActivated = ref(false);
let timerInterval: number;
const audio = new Audio(audioUrl);
const clear = () => {
clearInterval(timerInterval);
timerRunning.value = false;
};
const handleCountdownComplete = () => {
audio.loop = true;
audio.play();
ElMessageBox.alert('本次番茄钟结束!请点击“休息”按钮开始休息倒计时', '提醒', {
confirmButtonText: '关闭铃声',
callback: () => {
audio.pause();
audio.currentTime = 0;
ElMessage.success('铃声已关闭');
},
});
};
const runCountdown = (durationMs: number, onComplete: () => void = handleCountdownComplete) => {
clearInterval(timerInterval);
const endTimeStamp = Date.now() + durationMs;
remainingTime.value = durationMs;
timerIsOver.value = false;
timerRunning.value = true;
timerInterval = window.setInterval(() => {
const now = Date.now();
remainingTime.value = Math.max(endTimeStamp - now, 0);
timerMinutes.value = Math.floor(remainingTime.value / 1000 / 60);
timerSeconds.value = Math.floor((remainingTime.value / 1000) % 60);
if (remainingTime.value === 0 && !timerIsOver.value) {
timerIsOver.value = true;
timerRunning.value = false;
onComplete();
}
}, 1000);
};
const checkAudioPermission = async () => {
try {
await audio.play();
audio.pause();
audio.currentTime = 0;
audioActivated.value = true;
return true;
} catch {
audioActivated.value = false;
return false;
}
};
const requestAudioPermission = () => {
return new Promise<boolean>((resolve) => {
ElMessageBox.alert('点击确认激活提示音权限。', '权限激活', {
confirmButtonText: '确认',
callback: async () => {
try {
await audio.play();
audio.pause();
audio.currentTime = 0;
audioActivated.value = true;
resolve(true);
} catch {
resolve(false);
}
},
});
});
};
return {
timerMinutes,
timerSeconds,
remainingTime,
timerRunning,
timerIsOver,
runCountdown,
clear,
checkAudioPermission,
requestAudioPermission,
audioActivated,
};
}
+6 -3
View File
@@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
const basic_url = "http://localhost:8081"; const basic_url = "http://localhost:5157";
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: basic_url, baseURL: basic_url,
@@ -27,12 +27,15 @@ const validateResponse = (res: any) => {
return result; return result;
}; };
const get = (url: string, params: {}) => { function get(url: string, params: Record<string, any>): Promise<any>;
function get(url: string): Promise<any>;
function get(url: string, params: Record<string, any> = {}) {
console.log("开始GET请求", basic_url + url, "参数:", params); console.log("开始GET请求", basic_url + url, "参数:", params);
return axiosInstance.get(url, { params }) return axiosInstance.get(url, { params })
.then(validateResponse) .then(validateResponse)
.catch(handleError); .catch(handleError);
}; }
const post = (url: string, data: any = null, config: any = {}) => { const post = (url: string, data: any = null, config: any = {}) => {
console.log("开始POST请求", url, "参数:", data, "配置:", config); console.log("开始POST请求", url, "参数:", data, "配置:", config);