diff --git a/src/__tests__/composables/fragment.spec.ts b/src/__tests__/composables/fragment.spec.ts index efc1e9e..d410dcb 100644 --- a/src/__tests__/composables/fragment.spec.ts +++ b/src/__tests__/composables/fragment.spec.ts @@ -88,4 +88,25 @@ describe('useStudyFragment', () => { expect(result).toBe(false) expect(ElMessage.error).toHaveBeenCalledWith('网络超时') }) + + it('prevents duplicate submission while creating fragment', async () => { + let resolveCreate!: (value: any) => void + vi.mocked(createFragments).mockImplementation( + () => new Promise((resolve) => { resolveCreate = resolve }), + ) + const { fragmentContent, creatingFragment, confirmGenerateFragment } = useStudyFragment() + fragmentContent.value = '学习内容' + + const first = confirmGenerateFragment('S001') + const second = confirmGenerateFragment('S001') + await Promise.resolve() + + expect(createFragments).toHaveBeenCalledTimes(1) + expect(creatingFragment.value).toBe(true) + + resolveCreate({ code: 200 }) + await Promise.all([first, second]) + + expect(creatingFragment.value).toBe(false) + }) }) diff --git a/src/components/StartTask.vue b/src/components/StartTask.vue index af5d3b5..99985d1 100644 --- a/src/components/StartTask.vue +++ b/src/components/StartTask.vue @@ -25,9 +25,12 @@ const { openFragmentDialog, closeFragmentDialog, confirmGenerateFragment, + creatingFragment, } = useStudyFragment(); const pageLoading = ref(true); +const timerActionLoading = ref(false); +const endingSession = ref(false); const summaryDialogVisible = ref(false); const summaryContent = ref(""); @@ -309,39 +312,51 @@ const statusText = computed(() => { }); const startTimer = async () => { - if (taskInfo.value.sessionState === "PAUSED") { - const res = await continueSession(taskInfo.value.sessionNum); - if (res.code === 200) ElMessage.success("任务继续"); - await loadTaskSession(); + if (timerActionLoading.value) return; + timerActionLoading.value = true; + try { + if (taskInfo.value.sessionState === "PAUSED") { + const res = await continueSession(taskInfo.value.sessionNum); + if (res.code === 200) ElMessage.success("任务继续"); + await loadTaskSession(); + } + // 手动开始 → 清除休息状态 + localStorage.removeItem(BREAK_END_KEY.value); + isBreak.value = false; + const duration = taskInfo.value.pointerPosition || 25 * 60 * 1000; + runCountdown(duration); + } finally { + timerActionLoading.value = false; } - // 手动开始 → 清除休息状态 - localStorage.removeItem(BREAK_END_KEY.value); - isBreak.value = false; - const duration = taskInfo.value.pointerPosition || 25 * 60 * 1000; - runCountdown(duration); }; const stopTimer = async () => { - const res = await pauseSession(taskInfo.value.sessionNum); - if (res.code === 200) { - ElMessage.success("任务暂停"); - // 重新获取会话数据,同步后端计算后的时间字段 - try { - const detail = await getSessionDetail(taskInfo.value.sessionNum); - if (detail?.data) { - const d = detail.data; - taskInfo.value.sessionState = d.sessionState ?? "PAUSED"; - taskInfo.value.actualTime = d.actualTime ?? 0; - taskInfo.value.effectiveTime = d.effectiveTime ?? 0; - taskInfo.value.effectivenessRatio = d.effectivenessRatio ?? 0; - taskInfo.value.startTime = d.startTime ?? taskInfo.value.startTime; - taskInfo.value.endTime = d.endTime ?? ""; - taskInfo.value.lastStartTime = d.lastStartTime ?? ""; - taskInfo.value.pointerPosition = d.pointerPosition ?? 0; + if (timerActionLoading.value) return; + timerActionLoading.value = true; + try { + const res = await pauseSession(taskInfo.value.sessionNum); + if (res.code === 200) { + ElMessage.success("任务暂停"); + // 重新获取会话数据,同步后端计算后的时间字段 + try { + const detail = await getSessionDetail(taskInfo.value.sessionNum); + if (detail?.data) { + const d = detail.data; + taskInfo.value.sessionState = d.sessionState ?? "PAUSED"; + taskInfo.value.actualTime = d.actualTime ?? 0; + taskInfo.value.effectiveTime = d.effectiveTime ?? 0; + taskInfo.value.effectivenessRatio = d.effectivenessRatio ?? 0; + taskInfo.value.startTime = d.startTime ?? taskInfo.value.startTime; + taskInfo.value.endTime = d.endTime ?? ""; + taskInfo.value.lastStartTime = d.lastStartTime ?? ""; + taskInfo.value.pointerPosition = d.pointerPosition ?? 0; + } + } catch { + taskInfo.value.sessionState = "PAUSED"; } - } catch { - taskInfo.value.sessionState = "PAUSED"; } + } finally { + timerActionLoading.value = false; } clear(); clearBreakState(); @@ -367,6 +382,8 @@ const endTimer = async (content: string) => { // 用户取消结束会话,不做任何操作 return; } + if (endingSession.value) return; + endingSession.value = true; try { const res = await endSession(taskInfo.value.sessionNum, content); @@ -385,6 +402,8 @@ const endTimer = async (content: string) => { await router.push("/study"); } catch { ElMessage.error("结束会话失败,请重试"); + } finally { + endingSession.value = false; } }; @@ -741,10 +760,10 @@ defineExpose({ 生成残片 结束会话 @@ -827,7 +846,7 @@ defineExpose({ /> @@ -887,7 +906,7 @@ defineExpose({ diff --git a/src/components/composables/fragment.ts b/src/components/composables/fragment.ts index f9ceea3..7e644de 100644 --- a/src/components/composables/fragment.ts +++ b/src/components/composables/fragment.ts @@ -5,6 +5,7 @@ import {createFragments} from '@/api/reportFragments'; export const useStudyFragment = () => { const fragmentsDialogVisible = ref(false); const fragmentContent = ref(''); + const creatingFragment = ref(false); const openFragmentDialog = () => { fragmentContent.value = ''; @@ -26,6 +27,7 @@ export const useStudyFragment = () => { }; const confirmGenerateFragment = async (sessionNum: string): Promise => { + if (creatingFragment.value) return false; if (!fragmentContent.value.trim()) { ElMessage.warning('请输入学习内容!'); return false; @@ -43,6 +45,7 @@ export const useStudyFragment = () => { } try { + creatingFragment.value = true; const res = await createFragments(sessionNum, fragmentContent.value); if (res.code === 200) { ElMessage.success('学习残片生成成功!'); @@ -55,12 +58,15 @@ export const useStudyFragment = () => { } catch (error: any) { ElMessage.error(error.message || '请求失败'); return false; + } finally { + creatingFragment.value = false; } }; return { fragmentsDialogVisible, fragmentContent, + creatingFragment, openFragmentDialog, closeFragmentDialog, confirmGenerateFragment,