From 468bba86702afde3962d06798af871abf63f1f97 Mon Sep 17 00:00:00 2001 From: cat-shark <1716967236@qq.com> Date: Thu, 27 Aug 2026 21:48:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=96=B0=E5=A2=9E=20useElapsedSeco?= =?UTF-8?q?nds=20=E7=BB=9F=E4=B8=80=E7=A7=92=E8=A1=A8=E8=AE=A1=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../composables/useElapsedSeconds.spec.ts | 78 +++++++++++++++++++ src/components/ReviewRecall.vue | 29 ++++--- .../composables/useElapsedSeconds.ts | 40 ++++++++++ 3 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 src/__tests__/composables/useElapsedSeconds.spec.ts create mode 100644 src/components/composables/useElapsedSeconds.ts diff --git a/src/__tests__/composables/useElapsedSeconds.spec.ts b/src/__tests__/composables/useElapsedSeconds.spec.ts new file mode 100644 index 0000000..ebe54b8 --- /dev/null +++ b/src/__tests__/composables/useElapsedSeconds.spec.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { useElapsedSeconds } from '@/components/composables/useElapsedSeconds' + +describe('useElapsedSeconds', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + it('start 从 0 开始每秒递增', () => { + const { seconds, start, stop } = useElapsedSeconds() + + start() + expect(seconds.value).toBe(0) + + vi.advanceTimersByTime(3000) + expect(seconds.value).toBe(3) + stop() + }) + + it('重复 start 会归零并重新计时', () => { + const { seconds, start, stop } = useElapsedSeconds() + + start() + vi.advanceTimersByTime(5000) + + start() + expect(seconds.value).toBe(0) + vi.advanceTimersByTime(1000) + expect(seconds.value).toBe(1) + stop() + }) + + it('stop 停止递增但保留秒数', () => { + const { seconds, start, stop } = useElapsedSeconds() + + start() + vi.advanceTimersByTime(2000) + stop() + expect(seconds.value).toBe(2) + + vi.advanceTimersByTime(5000) + expect(seconds.value).toBe(2) + }) + + it('reset 停止并归零', () => { + const { seconds, start, reset } = useElapsedSeconds() + + start() + vi.advanceTimersByTime(2000) + reset() + expect(seconds.value).toBe(0) + + vi.advanceTimersByTime(5000) + expect(seconds.value).toBe(0) + }) + + it('在组件内使用时卸载自动清理定时器', async () => { + const { mount } = await import('@vue/test-utils') + const { defineComponent, h } = await import('vue') + + let exposed!: ReturnType + const Host = defineComponent({ + setup() { + exposed = useElapsedSeconds() + return () => h('div') + }, + }) + const wrapper = mount(Host) + + exposed.start() + vi.advanceTimersByTime(2000) + expect(exposed.seconds.value).toBe(2) + + wrapper.unmount() + vi.advanceTimersByTime(5000) + expect(exposed.seconds.value).toBe(2) + }) +}) diff --git a/src/components/ReviewRecall.vue b/src/components/ReviewRecall.vue index 714e3aa..a95abbf 100644 --- a/src/components/ReviewRecall.vue +++ b/src/components/ReviewRecall.vue @@ -13,6 +13,7 @@ import { } from "@/api/standardMindMap"; import { ElMessage, ElMessageBox } from "element-plus"; import MindMapViewer, { type MindMapTreeNode } from "@/components/MindMapViewer.vue"; +import { useElapsedSeconds } from "@/components/composables/useElapsedSeconds"; const route = useRoute(); const router = useRouter(); @@ -21,12 +22,20 @@ const taskNum = computed(() => route.params.taskNum as string); const loading = ref(false); const standard = ref(null); const comparing = ref(false); -const comparingSeconds = ref(0); -let comparingTimer: ReturnType | null = null; +const { + seconds: comparingSeconds, + start: startComparingTimer, + stop: stopComparingTimer, +} = useElapsedSeconds(); +// 重新生成标准导图 const regenerating = ref(false); -const regeneratingSeconds = ref(0); -let regeneratingTimer: ReturnType | null = null; + +const { + seconds: regeneratingSeconds, + start: startRegeneratingTimer, + stop: stopRegeneratingTimer, +} = useElapsedSeconds(); const lastResult = ref(null); const hasCompared = ref(false); @@ -127,7 +136,7 @@ const loadData = async () => { // 匹配失败时仍可进入回忆页 } } - } catch (e: any) { + } catch { standard.value = null; } finally { loading.value = false; @@ -201,8 +210,7 @@ const handleRegenerate = async () => { // 开始生成 regenerating.value = true; - regeneratingSeconds.value = 0; - regeneratingTimer = setInterval(() => { regeneratingSeconds.value++; }, 1000); + startRegeneratingTimer(); try { const res = await regenerateStandardMindMap(taskNum.value, mode); standard.value = res?.data || null; @@ -211,7 +219,7 @@ const handleRegenerate = async () => { ElMessage.error(e.message || "重新生成失败"); } finally { regenerating.value = false; - if (regeneratingTimer) { clearInterval(regeneratingTimer); regeneratingTimer = null; } + stopRegeneratingTimer(); } }; @@ -223,8 +231,7 @@ const handleRecallCompare = async () => { return; } comparing.value = true; - comparingSeconds.value = 0; - comparingTimer = setInterval(() => { comparingSeconds.value++; }, 1000); + startComparingTimer(); try { const res = await recallCompare(taskNum.value, outline, focusPath.value || undefined); standard.value = res?.data || null; @@ -248,7 +255,7 @@ const handleRecallCompare = async () => { ElMessage.error(e.message || "对比失败"); } finally { comparing.value = false; - if (comparingTimer) { clearInterval(comparingTimer); comparingTimer = null; } + stopComparingTimer(); } }; diff --git a/src/components/composables/useElapsedSeconds.ts b/src/components/composables/useElapsedSeconds.ts new file mode 100644 index 0000000..8539662 --- /dev/null +++ b/src/components/composables/useElapsedSeconds.ts @@ -0,0 +1,40 @@ +// src/components/composables/useElapsedSeconds.ts +import { getCurrentInstance, onUnmounted, ref } from 'vue'; + +/** + * 统一的秒表计时:记录异步操作已耗时秒数,用于“已等待 N 秒”等界面提示。 + * - start():归零并开始每秒递增(重复调用会先停止上一次计时) + * - stop():停止计时,保留当前秒数 + * - reset():停止并归零 + * 组件卸载时自动清理定时器;在组件外部调用仅作纯状态机使用。 + */ +export function useElapsedSeconds() { + const seconds = ref(0); + let timer: ReturnType | null = null; + + const stop = () => { + if (timer) { + clearInterval(timer); + timer = null; + } + }; + + const start = () => { + stop(); + seconds.value = 0; + timer = setInterval(() => { + seconds.value++; + }, 1000); + }; + + const reset = () => { + stop(); + seconds.value = 0; + }; + + if (getCurrentInstance()) { + onUnmounted(stop); + } + + return { seconds, start, stop, reset }; +}