2 Commits
Author SHA1 Message Date
cat-shark 96cc57ae06 chore: 添加 .gitattributes 规范行尾 2026-08-26 21:13:19 +08:00
cat-shark f37a92c240 feat: 新增误操作结束学习交互 2026-08-15 21:25:01 +08:00
5 changed files with 146 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# 行尾规范:文本文件统一 LF,Windows 批处理保留 CRLF
* text=auto eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
+9
View File
@@ -14,6 +14,7 @@ import {
continueSession,
pauseSession,
endSession,
abortSession,
startOrContinueStudySession,
getSessionDetail,
} from '@/api/studySessions'
@@ -69,6 +70,14 @@ describe('studySessions API', () => {
{ 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 () => {
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 router from '@/router'
import {
abortSession,
continueSession,
endSession,
getActiveSession,
@@ -17,6 +18,7 @@ import {
import { getFragmentsBySession } from '@/api/reportFragments'
vi.mock('@/api/studySessions', () => ({
abortSession: vi.fn(),
continueSession: vi.fn(),
endSession: vi.fn(),
getActiveSession: vi.fn(),
@@ -222,4 +224,44 @@ describe('StartTask.vue', () => {
expect(pauseSession).toHaveBeenCalledTimes(2)
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()
})
})
+5
View File
@@ -14,6 +14,11 @@ export const endSession = (sessionNum: string, content: string = "任务结束")
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) => {
return request.get(`/tasks/${taskNum}/study-sessions/start-or-continue`);
};
+86
View File
@@ -5,6 +5,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
import request from "@/utils/request";
import { useTimer } from "@/components/composables/useTimer";
import {
abortSession,
continueSession,
endSession,
getActiveSession,
@@ -31,6 +32,14 @@ const {
const pageLoading = ref(true);
const timerActionLoading = 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 summaryContent = ref("");
@@ -432,6 +441,41 @@ const closeSummaryDialog = () => {
summaryDialogVisible.value = false;
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 () => {
await stopTimer();
@@ -649,6 +693,9 @@ defineExpose({
startTimer,
stopTimer,
endTimer,
openAbortDialog,
confirmAbort,
abortConfirmation,
syncDisplay,
clear,
});
@@ -766,6 +813,7 @@ defineExpose({
<el-button v-if="timerIsOver" plain type="success" @click="restTimer" :loading="timerActionLoading">休息</el-button>
</template>
<el-button type="danger" plain @click="openSummaryDialog">结束会话</el-button>
<el-button v-if="canAbortSession" text type="danger" @click="openAbortDialog">误操作结束</el-button>
</div>
</article>
</div>
@@ -866,6 +914,7 @@ defineExpose({
:rows="4"
/>
<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 type="success" :loading="savingExpectation" @click="saveExpectation">确定</el-button>
</template>
@@ -909,6 +958,32 @@ defineExpose({
<el-button type="success" :loading="endingSession" @click="endTimer(summaryContent)">确认结束</el-button>
</template>
</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>
</template>
@@ -1298,4 +1373,15 @@ defineExpose({
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>