refactor: 新增 useElapsedSeconds 统一秒表计时
This commit is contained in:
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user