From c95e593cf17ef90e9a99f2e15cc3efc91c8f4057 Mon Sep 17 00:00:00 2001
From: cat-shark <1716967236@qq.com>
Date: Wed, 5 Aug 2026 21:42:45 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AD=A6=E4=B9=A0=E4=BC=9A=E8=AF=9D?=
=?UTF-8?q?=E6=8C=89=E9=92=AE=E8=A1=A5=E9=98=B2=E6=8A=96=E5=8A=A0=E8=BD=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/__tests__/composables/fragment.spec.ts | 21 ++++++
src/components/StartTask.vue | 83 +++++++++++++---------
src/components/composables/fragment.ts | 6 ++
3 files changed, 78 insertions(+), 32 deletions(-)
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,