feat: 新增误操作结束学习交互
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
continueSession,
|
continueSession,
|
||||||
pauseSession,
|
pauseSession,
|
||||||
endSession,
|
endSession,
|
||||||
|
abortSession,
|
||||||
startOrContinueStudySession,
|
startOrContinueStudySession,
|
||||||
getSessionDetail,
|
getSessionDetail,
|
||||||
} from '@/api/studySessions'
|
} from '@/api/studySessions'
|
||||||
@@ -69,6 +70,14 @@ describe('studySessions API', () => {
|
|||||||
{ content: '自定义结束' }
|
{ content: '自定义结束' }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
it('abortSession sends POST with confirmation phrase', async () => {
|
||||||
|
mockedRequest.post.mockResolvedValue({ code: 200, data: null })
|
||||||
|
await abortSession('S001', '我误操作导致开启了本次学习')
|
||||||
|
expect(mockedRequest.post).toHaveBeenCalledWith(
|
||||||
|
'/study-sessions/S001/study-sessions/abort',
|
||||||
|
{ confirmation: '我误操作导致开启了本次学习' }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('startOrContinueStudySession sends GET to correct endpoint', async () => {
|
it('startOrContinueStudySession sends GET to correct endpoint', async () => {
|
||||||
mockedRequest.get.mockResolvedValue({ code: 200, data: { sessionNum: 'S002' } })
|
mockedRequest.get.mockResolvedValue({ code: 200, data: { sessionNum: 'S002' } })
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createMemoryHistory, createRouter } from 'vue-router'
|
|||||||
import StartTask from '@/components/StartTask.vue'
|
import StartTask from '@/components/StartTask.vue'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import {
|
import {
|
||||||
|
abortSession,
|
||||||
continueSession,
|
continueSession,
|
||||||
endSession,
|
endSession,
|
||||||
getActiveSession,
|
getActiveSession,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
import { getFragmentsBySession } from '@/api/reportFragments'
|
import { getFragmentsBySession } from '@/api/reportFragments'
|
||||||
|
|
||||||
vi.mock('@/api/studySessions', () => ({
|
vi.mock('@/api/studySessions', () => ({
|
||||||
|
abortSession: vi.fn(),
|
||||||
continueSession: vi.fn(),
|
continueSession: vi.fn(),
|
||||||
endSession: vi.fn(),
|
endSession: vi.fn(),
|
||||||
getActiveSession: vi.fn(),
|
getActiveSession: vi.fn(),
|
||||||
@@ -222,4 +224,44 @@ describe('StartTask.vue', () => {
|
|||||||
expect(pauseSession).toHaveBeenCalledTimes(2)
|
expect(pauseSession).toHaveBeenCalledTimes(2)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('confirmAbort aborts session with correct phrase and navigates to study page', async () => {
|
||||||
|
vi.mocked(abortSession).mockResolvedValue({ code: 200, message: '请求成功' } as any)
|
||||||
|
const wrapper = await mountTask()
|
||||||
|
|
||||||
|
wrapper.vm.abortConfirmation = '我误操作导致开启了本次学习'
|
||||||
|
await wrapper.vm.confirmAbort()
|
||||||
|
|
||||||
|
expect(abortSession).toHaveBeenCalledWith('SESSION_001', '我误操作导致开启了本次学习')
|
||||||
|
expect(ElMessage.success).toHaveBeenCalledWith('本次学习已关闭,未产生任何数据')
|
||||||
|
expect(localStorage.getItem('activeSession')).toBeNull()
|
||||||
|
expect(router.currentRoute.value.path).toBe('/study')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('confirmAbort refuses wrong phrase without calling API', async () => {
|
||||||
|
const wrapper = await mountTask()
|
||||||
|
|
||||||
|
wrapper.vm.abortConfirmation = '不是确认语'
|
||||||
|
await wrapper.vm.confirmAbort()
|
||||||
|
|
||||||
|
expect(ElMessage.warning).toHaveBeenCalledWith('确认语输入不正确,请重新输入')
|
||||||
|
expect(abortSession).not.toHaveBeenCalled()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('confirmAbort shows backend message when session has fragments and stays on page', async () => {
|
||||||
|
vi.mocked(abortSession).mockRejectedValue({
|
||||||
|
code: 400,
|
||||||
|
message: '这次学习已经产生了学习残片,不能按误操作结束了哦',
|
||||||
|
} as any)
|
||||||
|
const wrapper = await mountTask()
|
||||||
|
|
||||||
|
wrapper.vm.abortConfirmation = '我误操作导致开启了本次学习'
|
||||||
|
await wrapper.vm.confirmAbort()
|
||||||
|
|
||||||
|
expect(ElMessage.error).toHaveBeenCalledWith('这次学习已经产生了学习残片,不能按误操作结束了哦')
|
||||||
|
expect(router.currentRoute.value.path).not.toBe('/study')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ export const endSession = (sessionNum: string, content: string = "任务结束")
|
|||||||
return request.post(`/study-sessions/${sessionNum}/study-sessions/ended`, { content });
|
return request.post(`/study-sessions/${sessionNum}/study-sessions/ended`, { content });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 误操作结束学习会话:需输入确认语,严格零数据关闭(删除会话与预期) */
|
||||||
|
export const abortSession = (sessionNum: string, confirmation: string) => {
|
||||||
|
return request.post(`/study-sessions/${sessionNum}/study-sessions/abort`, { confirmation });
|
||||||
|
};
|
||||||
|
|
||||||
export const startOrContinueStudySession = (taskNum: string) => {
|
export const startOrContinueStudySession = (taskNum: string) => {
|
||||||
return request.get(`/tasks/${taskNum}/study-sessions/start-or-continue`);
|
return request.get(`/tasks/${taskNum}/study-sessions/start-or-continue`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
|||||||
import request from "@/utils/request";
|
import request from "@/utils/request";
|
||||||
import { useTimer } from "@/components/composables/useTimer";
|
import { useTimer } from "@/components/composables/useTimer";
|
||||||
import {
|
import {
|
||||||
|
abortSession,
|
||||||
continueSession,
|
continueSession,
|
||||||
endSession,
|
endSession,
|
||||||
getActiveSession,
|
getActiveSession,
|
||||||
@@ -31,6 +32,14 @@ const {
|
|||||||
const pageLoading = ref(true);
|
const pageLoading = ref(true);
|
||||||
const timerActionLoading = ref(false);
|
const timerActionLoading = ref(false);
|
||||||
const endingSession = ref(false);
|
const endingSession = ref(false);
|
||||||
|
// 误操作结束(零数据关闭)
|
||||||
|
const MISOPERATION_PHRASE = "我误操作导致开启了本次学习";
|
||||||
|
const abortDialogVisible = ref(false);
|
||||||
|
const abortConfirmation = ref("");
|
||||||
|
const abortingSession = ref(false);
|
||||||
|
const canAbortSession = computed(
|
||||||
|
() => taskInfo.value.sessionState !== "ENDED" && fragmentsList.value.length === 0,
|
||||||
|
);
|
||||||
|
|
||||||
const summaryDialogVisible = ref(false);
|
const summaryDialogVisible = ref(false);
|
||||||
const summaryContent = ref("");
|
const summaryContent = ref("");
|
||||||
@@ -432,6 +441,41 @@ const closeSummaryDialog = () => {
|
|||||||
summaryDialogVisible.value = false;
|
summaryDialogVisible.value = false;
|
||||||
if (summaryLoadingTimer) { clearInterval(summaryLoadingTimer); summaryLoadingTimer = null; }
|
if (summaryLoadingTimer) { clearInterval(summaryLoadingTimer); summaryLoadingTimer = null; }
|
||||||
};
|
};
|
||||||
|
const openAbortDialog = () => {
|
||||||
|
abortConfirmation.value = "";
|
||||||
|
abortDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeExpectationAndOpenAbort = () => {
|
||||||
|
expectationDialogVisible.value = false;
|
||||||
|
openAbortDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmAbort = async () => {
|
||||||
|
if (abortConfirmation.value.trim() !== MISOPERATION_PHRASE) {
|
||||||
|
ElMessage.warning("确认语输入不正确,请重新输入");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (abortingSession.value) return;
|
||||||
|
abortingSession.value = true;
|
||||||
|
try {
|
||||||
|
const res = await abortSession(taskInfo.value.sessionNum, abortConfirmation.value.trim());
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("本次学习已关闭,未产生任何数据");
|
||||||
|
clear();
|
||||||
|
clearBreakState();
|
||||||
|
localStorage.removeItem("activeSession");
|
||||||
|
await router.push("/study");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || "误操作结束失败");
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message || "误操作结束失败,请重试");
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
abortingSession.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const restTimer = async () => {
|
const restTimer = async () => {
|
||||||
await stopTimer();
|
await stopTimer();
|
||||||
@@ -649,6 +693,9 @@ defineExpose({
|
|||||||
startTimer,
|
startTimer,
|
||||||
stopTimer,
|
stopTimer,
|
||||||
endTimer,
|
endTimer,
|
||||||
|
openAbortDialog,
|
||||||
|
confirmAbort,
|
||||||
|
abortConfirmation,
|
||||||
syncDisplay,
|
syncDisplay,
|
||||||
clear,
|
clear,
|
||||||
});
|
});
|
||||||
@@ -766,6 +813,7 @@ defineExpose({
|
|||||||
<el-button v-if="timerIsOver" plain type="success" @click="restTimer" :loading="timerActionLoading">休息</el-button>
|
<el-button v-if="timerIsOver" plain type="success" @click="restTimer" :loading="timerActionLoading">休息</el-button>
|
||||||
</template>
|
</template>
|
||||||
<el-button type="danger" plain @click="openSummaryDialog">结束会话</el-button>
|
<el-button type="danger" plain @click="openSummaryDialog">结束会话</el-button>
|
||||||
|
<el-button v-if="canAbortSession" text type="danger" @click="openAbortDialog">误操作结束</el-button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -866,6 +914,7 @@ defineExpose({
|
|||||||
:rows="4"
|
:rows="4"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<el-button v-if="!expectationSaved" text type="danger" @click="closeExpectationAndOpenAbort">误操作结束</el-button>
|
||||||
<el-button v-if="expectationSaved" @click="expectationDialogVisible = false">取消</el-button>
|
<el-button v-if="expectationSaved" @click="expectationDialogVisible = false">取消</el-button>
|
||||||
<el-button type="success" :loading="savingExpectation" @click="saveExpectation">确定</el-button>
|
<el-button type="success" :loading="savingExpectation" @click="saveExpectation">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -909,6 +958,32 @@ defineExpose({
|
|||||||
<el-button type="success" :loading="endingSession" @click="endTimer(summaryContent)">确认结束</el-button>
|
<el-button type="success" :loading="endingSession" @click="endTimer(summaryContent)">确认结束</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="abortDialogVisible"
|
||||||
|
title="误操作结束"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
>
|
||||||
|
<p class="dialog-hint">本次学习会话尚未产生学习残片,可以零数据关闭。关闭后会话与学习预期将被删除,不会留下任何学习记录。</p>
|
||||||
|
<p class="dialog-hint">如果是误操作开启了本次学习,请输入以下内容以确认:</p>
|
||||||
|
<div class="abort-phrase">{{ MISOPERATION_PHRASE }}</div>
|
||||||
|
<el-input
|
||||||
|
v-model="abortConfirmation"
|
||||||
|
placeholder="请输入上方确认语"
|
||||||
|
@keyup.enter="confirmAbort"
|
||||||
|
/>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="abortDialogVisible = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:loading="abortingSession"
|
||||||
|
:disabled="abortConfirmation.trim() !== MISOPERATION_PHRASE"
|
||||||
|
@click="confirmAbort"
|
||||||
|
>确认误操作结束</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1298,4 +1373,15 @@ defineExpose({
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.abort-phrase {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px dashed var(--el-color-danger);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user