-
Notifications
You must be signed in to change notification settings - Fork 724
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6587ab7
commit ab99692
Showing
9 changed files
with
525 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
<template> | ||
<div class="flex items-center font-sans text-gray-300 dark:text-gray-500"> | ||
<div | ||
ref="clockIcon" | ||
class="mr-1 flex items-center justify-center" | ||
> | ||
<span | ||
class="i-ph-alarm-bold" | ||
style="width: 30px; height: 30px" | ||
></span> | ||
</div> | ||
<p class="text-lg font-bold">{{ formattedTime }}</p> | ||
</div> | ||
</template> | ||
|
||
<script setup> | ||
import { computed, onMounted, onUnmounted, ref, watch } from "vue"; | ||
import { useLearningTimeTracker } from "~/composables/main/learningTimeTracker"; | ||
const { $anime } = useNuxtApp(); | ||
const { totalSeconds, startTracking, stopTracking } = useLearningTimeTracker(); | ||
const clockIcon = ref(null); | ||
const formattedTime = computed(() => { | ||
const hours = Math.floor(totalSeconds.value / 3600); | ||
const minutes = Math.floor((totalSeconds.value % 3600) / 60); | ||
const seconds = totalSeconds.value % 60; | ||
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; | ||
}); | ||
function animateClock() { | ||
$anime({ | ||
targets: clockIcon.value, | ||
translateY: [ | ||
{ value: -4, duration: 100, easing: "easeInOutQuad" }, | ||
{ value: 4, duration: 200, easing: "easeInOutQuad" }, | ||
{ value: -4, duration: 200, easing: "easeInOutQuad" }, | ||
{ value: 4, duration: 200, easing: "easeInOutQuad" }, | ||
{ value: 0, duration: 100, easing: "easeInOutQuad" }, | ||
], | ||
rotate: [ | ||
{ value: -5, duration: 100, easing: "easeInOutQuad" }, | ||
{ value: 5, duration: 200, easing: "easeInOutQuad" }, | ||
{ value: -5, duration: 200, easing: "easeInOutQuad" }, | ||
{ value: 5, duration: 200, easing: "easeInOutQuad" }, | ||
{ value: 0, duration: 100, easing: "easeInOutQuad" }, | ||
], | ||
scale: [ | ||
{ value: 1.1, duration: 400, easing: "easeInOutQuad" }, | ||
{ value: 1, duration: 400, easing: "easeInOutQuad" }, | ||
], | ||
duration: 800, | ||
loop: 1, | ||
}); | ||
} | ||
watch(totalSeconds, (newValue) => { | ||
if (newValue % 60 === 0 && newValue !== 0) { | ||
animateClock(); | ||
} | ||
}); | ||
function handleVisibilityChange() { | ||
if (document.hidden) { | ||
stopTracking(); | ||
} else { | ||
startTracking(); | ||
} | ||
} | ||
onMounted(() => { | ||
document.addEventListener("visibilitychange", handleVisibilityChange); | ||
window.addEventListener("beforeunload", stopTracking); | ||
}); | ||
onUnmounted(() => { | ||
document.removeEventListener("visibilitychange", handleVisibilityChange); | ||
window.removeEventListener("beforeunload", stopTracking); | ||
}); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { ref } from "vue"; | ||
|
||
import { useUserStore } from "~/store/user"; | ||
|
||
const totalSeconds = ref(0); | ||
const isTracking = ref(false); | ||
let timer: NodeJS.Timeout | null = null; | ||
|
||
export function useLearningTimeTracker() { | ||
const userStore = useUserStore(); | ||
const userId = userStore.user?.id; | ||
|
||
function getStorageKey() { | ||
const today = new Date().toISOString().split("T")[0]; | ||
return `learningTime_${userId}_${today}`; | ||
} | ||
|
||
function loadTime() { | ||
const savedTime = localStorage.getItem(getStorageKey()); | ||
totalSeconds.value = savedTime ? parseInt(savedTime) : 0; | ||
} | ||
|
||
function saveTime() { | ||
localStorage.setItem(getStorageKey(), totalSeconds.value.toString()); | ||
uploadTime(); | ||
} | ||
|
||
function uploadTime() { | ||
console.log("Uploading learning time:", { | ||
userId, | ||
date: new Date().toISOString().split("T")[0], | ||
seconds: totalSeconds.value, | ||
}); | ||
// Here we'll implement the actual API call later | ||
} | ||
|
||
function startTracking() { | ||
if (isTracking.value) return; | ||
loadTime(); | ||
if (totalSeconds.value === 0) { | ||
saveTime(); // 如果是新的一天,立即创建新的存储项 | ||
} | ||
isTracking.value = true; | ||
timer = setInterval(() => { | ||
totalSeconds.value++; | ||
if (totalSeconds.value % 30 === 0) { | ||
saveTime(); | ||
} | ||
}, 1000); | ||
} | ||
|
||
function stopTracking() { | ||
if (!isTracking.value) return; | ||
if (timer) { | ||
clearInterval(timer); | ||
timer = null; | ||
} | ||
isTracking.value = false; | ||
saveTime(); | ||
} | ||
|
||
return { | ||
totalSeconds, | ||
isTracking, | ||
startTracking, | ||
stopTracking, | ||
}; | ||
} |
134 changes: 134 additions & 0 deletions
134
apps/client/composables/main/tests/learningTimeTracker.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
import { nextTick } from "vue"; | ||
|
||
import { useLearningTimeTracker } from "../learningTimeTracker"; | ||
|
||
vi.mock("~/store/user", () => ({ | ||
useUserStore: vi.fn(() => ({ | ||
user: { id: "testUser" }, | ||
})), | ||
})); | ||
|
||
describe("useLearningTimeTracker", () => { | ||
let tracker: ReturnType<typeof useLearningTimeTracker>; | ||
tracker = useLearningTimeTracker(); | ||
|
||
beforeEach(() => { | ||
// 清除 localStorage | ||
localStorage.clear(); | ||
// 重置计时器 | ||
vi.useFakeTimers(); | ||
// 重置 | ||
tracker.totalSeconds.value = 0; | ||
tracker.isTracking.value = false; | ||
}); | ||
|
||
afterEach(() => { | ||
vi.restoreAllMocks(); | ||
tracker.stopTracking(); | ||
}); | ||
|
||
it("should initialize with zero seconds and not tracking", () => { | ||
expect(tracker.totalSeconds.value).toBe(0); | ||
expect(tracker.isTracking.value).toBe(false); | ||
}); | ||
|
||
it("should start tracking when startTracking is called", () => { | ||
tracker.startTracking(); | ||
expect(tracker.isTracking.value).toBe(true); | ||
}); | ||
[]; | ||
it("should stop tracking when stopTracking is called", () => { | ||
tracker.startTracking(); | ||
tracker.stopTracking(); | ||
expect(tracker.isTracking.value).toBe(false); | ||
}); | ||
|
||
it("should increment totalSeconds every second when tracking", async () => { | ||
tracker.startTracking(); | ||
vi.advanceTimersByTime(3000); | ||
await nextTick(); | ||
expect(tracker.totalSeconds.value).toBe(3); | ||
}); | ||
|
||
it("should save time to localStorage every 30 seconds", () => { | ||
const setItemSpy = vi.spyOn(Storage.prototype, "setItem"); | ||
tracker.startTracking(); | ||
vi.advanceTimersByTime(30000); | ||
expect(setItemSpy).toHaveBeenCalledWith(expect.any(String), "30"); | ||
}); | ||
|
||
it("should load time from localStorage when starting tracking", () => { | ||
const date = new Date().toISOString().split("T")[0]; | ||
localStorage.setItem(`learningTime_testUser_${date}`, "50"); | ||
tracker.startTracking(); | ||
expect(tracker.totalSeconds.value).toBe(50); | ||
}); | ||
|
||
it("should not start tracking if already tracking", () => { | ||
tracker.startTracking(); | ||
const initialSeconds = tracker.totalSeconds.value; | ||
tracker.startTracking(); // 尝试再次启动 | ||
vi.advanceTimersByTime(1000); | ||
expect(tracker.totalSeconds.value).toBe(initialSeconds + 1); // 只增加了1秒 | ||
}); | ||
|
||
it("should not stop tracking if not tracking", () => { | ||
const consoleSpy = vi.spyOn(console, "log"); | ||
tracker.stopTracking(); // 尝试停止未启动的跟踪 | ||
expect(consoleSpy).not.toHaveBeenCalled(); // 确保没有调用 uploadTime | ||
}); | ||
|
||
it("should upload time when stopping tracking", () => { | ||
const consoleSpy = vi.spyOn(console, "log"); | ||
tracker.startTracking(); | ||
vi.advanceTimersByTime(5000); | ||
tracker.stopTracking(); | ||
expect(consoleSpy).toHaveBeenCalledWith( | ||
"Uploading learning time:", | ||
expect.objectContaining({ | ||
userId: "testUser", | ||
seconds: 5, | ||
}), | ||
); | ||
}); | ||
|
||
it("should reset totalSeconds to zero when a new day starts", async () => { | ||
// 模拟当前日期 | ||
const day1 = new Date("2023-07-23T12:00:00"); | ||
vi.setSystemTime(day1); | ||
|
||
// 开始跟踪并设置一些初始时间 | ||
tracker.startTracking(); | ||
vi.advanceTimersByTime(5000); // 5 seconds | ||
tracker.stopTracking(); | ||
expect(tracker.totalSeconds.value).toBe(5); | ||
|
||
// 保存当前的存储键和值 | ||
const day1StorageKey = `learningTime_testUser_2023-07-23`; | ||
const day1StoredValue = localStorage.getItem(day1StorageKey); | ||
|
||
// 模拟时间跨越到下一天 | ||
const day2 = new Date("2023-07-24T12:00:00"); | ||
vi.setSystemTime(day2); | ||
|
||
// 再次开始跟踪 | ||
tracker.startTracking(); | ||
|
||
// 检查 totalSeconds 是否重置为 0 | ||
expect(tracker.totalSeconds.value).toBe(0); | ||
|
||
// 检查前一天的存储值是否保持不变 | ||
expect(localStorage.getItem(day1StorageKey)).toBe(day1StoredValue); | ||
|
||
// 检查新的一天是否创建了新的存储项 | ||
const day2StorageKey = `learningTime_testUser_2023-07-24`; | ||
expect(localStorage.getItem(day2StorageKey)).toBe("0"); | ||
|
||
// 添加一些时间后再次检查 | ||
vi.advanceTimersByTime(3000); // 3 seconds | ||
tracker.stopTracking(); | ||
expect(tracker.totalSeconds.value).toBe(3); | ||
expect(localStorage.getItem(day2StorageKey)).toBe("3"); | ||
}); | ||
}); |
Oops, something went wrong.