feat:1. 调整项目结构,将API接口定义单独抽离 2. 将倒计时计算逻辑调整到后端进行处理计算 3. 增加铃声提示功能
This commit is contained in:
@@ -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`);
|
||||||
|
};
|
||||||
+81
-131
@@ -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();
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
timerRunning.value = true;
|
const duration = taskInfo.value.pointerPosition || 25 * 60 * 1000;
|
||||||
|
runCountdown(duration);
|
||||||
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();
|
Object.assign(taskInfo.value, res.data);
|
||||||
const now = new Date().getTime();
|
if (taskInfo.value.sessionState === 'ONGOING') {
|
||||||
let diff = Math.floor((now - lastStart) / 1000);
|
startTimer();
|
||||||
// 判断任务是否超时(状态为正在进行,且最后一次开始时间到现在超过25分钟)
|
}
|
||||||
console.log(new Date().toISOString())
|
if (taskInfo.value.systemMessage) {
|
||||||
if (taskInfo.value.sessionState === 'ONGOING' && now - lastStart > 25 * 60 * 1000) {
|
ElMessage.warning(taskInfo.value.systemMessage);
|
||||||
// 超时了,触发暂停
|
}
|
||||||
const adjustedEnd = new Date(lastStart + 25 * 60000);
|
} catch (err: any) {
|
||||||
await handleTimerException(adjustedEnd);
|
ElMessage.error(err.message || '加载任务失败');
|
||||||
// 准备开始下一次倒计时
|
|
||||||
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 = () => {
|
const initPage = async () => {
|
||||||
request.get(`/tasks/${taskNum}/study-sessions/start-or-continue`, {})
|
const hasPermission = await checkAudioPermission();
|
||||||
.then(res => {
|
if (!hasPermission) {
|
||||||
Object.assign(taskInfo.value, res.data);
|
const activated = await requestAudioPermission();
|
||||||
initClocker();
|
if (!activated) {
|
||||||
})
|
console.warn('用户拒绝或未能激活权限');
|
||||||
.catch(err => {
|
return;
|
||||||
ElMessage.error(err.message);
|
}
|
||||||
});
|
}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user