refactor: 新增 useElapsedSeconds 统一秒表计时

This commit is contained in:
2026-08-27 21:48:28 +08:00
parent 96cc57ae06
commit 468bba8670
3 changed files with 136 additions and 11 deletions
@@ -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<typeof useElapsedSeconds>
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)
})
})
+18 -11
View File
@@ -13,6 +13,7 @@ import {
} from "@/api/standardMindMap"; } from "@/api/standardMindMap";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import MindMapViewer, { type MindMapTreeNode } from "@/components/MindMapViewer.vue"; import MindMapViewer, { type MindMapTreeNode } from "@/components/MindMapViewer.vue";
import { useElapsedSeconds } from "@/components/composables/useElapsedSeconds";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -21,12 +22,20 @@ const taskNum = computed(() => route.params.taskNum as string);
const loading = ref(false); const loading = ref(false);
const standard = ref<StandardMindMap | null>(null); const standard = ref<StandardMindMap | null>(null);
const comparing = ref(false); const comparing = ref(false);
const comparingSeconds = ref(0); const {
let comparingTimer: ReturnType<typeof setInterval> | null = null; seconds: comparingSeconds,
start: startComparingTimer,
stop: stopComparingTimer,
} = useElapsedSeconds();
// 重新生成标准导图
const regenerating = ref(false); const regenerating = ref(false);
const regeneratingSeconds = ref(0);
let regeneratingTimer: ReturnType<typeof setInterval> | null = null; const {
seconds: regeneratingSeconds,
start: startRegeneratingTimer,
stop: stopRegeneratingTimer,
} = useElapsedSeconds();
const lastResult = ref<any>(null); const lastResult = ref<any>(null);
const hasCompared = ref(false); const hasCompared = ref(false);
@@ -127,7 +136,7 @@ const loadData = async () => {
// 匹配失败时仍可进入回忆页 // 匹配失败时仍可进入回忆页
} }
} }
} catch (e: any) { } catch {
standard.value = null; standard.value = null;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -201,8 +210,7 @@ const handleRegenerate = async () => {
// 开始生成 // 开始生成
regenerating.value = true; regenerating.value = true;
regeneratingSeconds.value = 0; startRegeneratingTimer();
regeneratingTimer = setInterval(() => { regeneratingSeconds.value++; }, 1000);
try { try {
const res = await regenerateStandardMindMap(taskNum.value, mode); const res = await regenerateStandardMindMap(taskNum.value, mode);
standard.value = res?.data || null; standard.value = res?.data || null;
@@ -211,7 +219,7 @@ const handleRegenerate = async () => {
ElMessage.error(e.message || "重新生成失败"); ElMessage.error(e.message || "重新生成失败");
} finally { } finally {
regenerating.value = false; regenerating.value = false;
if (regeneratingTimer) { clearInterval(regeneratingTimer); regeneratingTimer = null; } stopRegeneratingTimer();
} }
}; };
@@ -223,8 +231,7 @@ const handleRecallCompare = async () => {
return; return;
} }
comparing.value = true; comparing.value = true;
comparingSeconds.value = 0; startComparingTimer();
comparingTimer = setInterval(() => { comparingSeconds.value++; }, 1000);
try { try {
const res = await recallCompare(taskNum.value, outline, focusPath.value || undefined); const res = await recallCompare(taskNum.value, outline, focusPath.value || undefined);
standard.value = res?.data || null; standard.value = res?.data || null;
@@ -248,7 +255,7 @@ const handleRecallCompare = async () => {
ElMessage.error(e.message || "对比失败"); ElMessage.error(e.message || "对比失败");
} finally { } finally {
comparing.value = false; comparing.value = false;
if (comparingTimer) { clearInterval(comparingTimer); comparingTimer = null; } stopComparingTimer();
} }
}; };
@@ -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<typeof setInterval> | 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 };
}