refactor: StartTask 抽取功能域组合式函数

This commit is contained in:
2026-08-27 21:48:32 +08:00
parent f1fcae0dac
commit d33bc11014
4 changed files with 268 additions and 151 deletions
+43 -151
View File
@@ -15,10 +15,12 @@ import {
} from "@/api/studySessions";
import { useStudyFragment } from "@/components/composables/fragment";
import { getFragmentsBySession, updateFragments } from "@/api/reportFragments";
import { getExpectation, getReportDraft, getTaskFragments, getTaskReports, upsertExpectation } from "@/api/studySessions";
import router from "@/router";
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
import { renderMarkdown } from "@/utils/markdown";
import { useSessionHistory } from "@/components/composables/useSessionHistory";
import { useSummaryReport } from "@/components/composables/useSummaryReport";
import { useSessionExpectation } from "@/components/composables/useSessionExpectation";
const {
fragmentsDialogVisible,
@@ -41,18 +43,29 @@ const canAbortSession = computed(
() => taskInfo.value.sessionState !== "ENDED" && fragmentsList.value.length === 0,
);
const summaryDialogVisible = ref(false);
const summaryContent = ref("");
const summaryPreview = ref(false);
const summaryLoading = ref(false);
const summaryLoadingSeconds = ref(0);
let summaryLoadingTimer: ReturnType<typeof setInterval> | null = null;
// 结束会话总结弹窗(有残片时先取 AI 草稿,等待期间展示已等待秒数)
const {
summaryDialogVisible,
summaryContent,
summaryPreview,
summaryLoading,
summaryLoadingSeconds,
openSummaryDialog,
closeSummaryDialog,
} = useSummaryReport({
getSessionNum: () => taskInfo.value.sessionNum,
hasFragments: () => fragmentsList.value.length > 0,
});
// 学习预期
const expectationDialogVisible = ref(false);
const expectationContent = ref("");
const expectationSaved = ref("");
const savingExpectation = ref(false);
// 学习预期:会话必须有预期才能开始计时
const {
expectationDialogVisible,
expectationContent,
expectationSaved,
savingExpectation,
loadExpectation,
saveExpectation,
} = useSessionExpectation(() => taskInfo.value.sessionNum);
// 当前会话碎片列表
interface FragmentItem {
@@ -64,88 +77,27 @@ const editingFragmentId = ref<number | null>(null);
const editFragmentContent = ref("");
const savingFragment = ref(false);
// 历史学习记录
const showHistory = ref(false);
const historyTab = ref("fragments");
const historyKeyword = ref("");
let historyDebounce: ReturnType<typeof setTimeout> | null = null;
const loadingHistory = ref(false);
const historyFragments = ref<any[]>([]);
const fragmentsTotal = ref(0);
const fragmentsPage = ref(1);
const historyReports = ref<any[]>([]);
const reportsTotal = ref(0);
const reportsPage = ref(1);
const PAGE_SIZE = 10;
watch(showHistory, (val) => {
if (val) {
// 展开时立即加载当前 tab 的数据
if (historyTab.value === "fragments") loadHistoryFragments();
else loadHistoryReports();
}
});
const loadHistoryFragments = async () => {
loadingHistory.value = true;
try {
const res = await getTaskFragments(taskNum, fragmentsPage.value, PAGE_SIZE, historyKeyword.value || undefined);
const data = res?.data;
historyFragments.value = data?.records || [];
fragmentsTotal.value = data?.total || 0;
} catch {
historyFragments.value = [];
fragmentsTotal.value = 0;
} finally {
loadingHistory.value = false;
}
};
const loadHistoryReports = async () => {
loadingHistory.value = true;
try {
const res = await getTaskReports(taskNum, reportsPage.value, PAGE_SIZE, historyKeyword.value || undefined);
const data = res?.data;
historyReports.value = data?.records || [];
reportsTotal.value = data?.total || 0;
} catch {
historyReports.value = [];
reportsTotal.value = 0;
} finally {
loadingHistory.value = false;
}
};
const searchHistory = () => {
if (historyDebounce) clearTimeout(historyDebounce);
historyDebounce = setTimeout(() => {
fragmentsPage.value = 1;
reportsPage.value = 1;
if (historyTab.value === "fragments") loadHistoryFragments();
else loadHistoryReports();
}, 300);
};
const onHistoryTabChange = () => {
fragmentsPage.value = 1;
reportsPage.value = 1;
if (historyTab.value === "fragments") loadHistoryFragments();
else loadHistoryReports();
};
const onFragmentsPageChange = (p: number) => {
fragmentsPage.value = p;
loadHistoryFragments();
};
const onReportsPageChange = (p: number) => {
reportsPage.value = p;
loadHistoryReports();
};
const route = useRoute();
const taskNum = route.params.taskNum as string;
// 历史学习记录面板(分页查询、搜索防抖、tab 切换)
const {
showHistory,
historyTab,
historyKeyword,
loadingHistory,
historyFragments,
fragmentsTotal,
fragmentsPage,
historyReports,
reportsTotal,
reportsPage,
searchHistory,
onHistoryTabChange,
onFragmentsPageChange,
onReportsPageChange,
} = useSessionHistory(taskNum);
const taskInfo = ref({
sessionNum: "",
sessionState: "--",
@@ -416,31 +368,6 @@ const endTimer = async (content: string) => {
}
};
const openSummaryDialog = async () => {
summaryDialogVisible.value = true;
// 有残片且未填写过总结时,用 AI 聚合草稿作为编辑起点
if (!summaryContent.value.trim() && fragmentsList.value.length > 0) {
summaryLoading.value = true;
summaryLoadingSeconds.value = 0;
summaryLoadingTimer = setInterval(() => { summaryLoadingSeconds.value++; }, 1000);
try {
const res = await getReportDraft(taskInfo.value.sessionNum);
if (res?.code === 200 && res.data) {
summaryContent.value = res.data;
}
} catch {
// 草稿失败不阻塞手写
} finally {
summaryLoading.value = false;
if (summaryLoadingTimer) { clearInterval(summaryLoadingTimer); summaryLoadingTimer = null; }
}
}
};
const closeSummaryDialog = () => {
summaryDialogVisible.value = false;
if (summaryLoadingTimer) { clearInterval(summaryLoadingTimer); summaryLoadingTimer = null; }
};
const openAbortDialog = () => {
abortConfirmation.value = "";
abortDialogVisible.value = true;
@@ -570,41 +497,6 @@ const loadTaskSession = async () => {
}
};
// 学习预期:会话必须有预期才能开始计时
const loadExpectation = async () => {
if (!taskInfo.value.sessionNum) return;
try {
const res = await getExpectation(taskInfo.value.sessionNum);
expectationSaved.value = res?.data?.description || "";
} catch {
expectationSaved.value = "";
}
if (!expectationSaved.value) {
expectationDialogVisible.value = true;
}
};
const saveExpectation = async () => {
if (!expectationContent.value.trim()) {
ElMessage.warning("学习预期不可为空");
return;
}
savingExpectation.value = true;
try {
const res = await upsertExpectation(taskInfo.value.sessionNum, expectationContent.value);
if (res?.code === 200) {
expectationSaved.value = expectationContent.value;
expectationDialogVisible.value = false;
} else {
ElMessage.error(res?.message || "保存失败");
}
} catch (e: any) {
ElMessage.error(e.message || "请求失败");
} finally {
savingExpectation.value = false;
}
};
const initPage = async () => {
try {
const hasPermission = await checkAudioPermission();
@@ -0,0 +1,58 @@
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import { getExpectation, upsertExpectation } from '@/api/studySessions';
/**
* 学习会话的学习预期:会话必须有预期才能开始计时。
* - loadExpectation:读取已保存预期,无预期时自动弹出填写弹窗
* - saveExpectation:保存预期并关闭弹窗
*/
export function useSessionExpectation(getSessionNum: () => string) {
const expectationDialogVisible = ref(false);
const expectationContent = ref("");
const expectationSaved = ref("");
const savingExpectation = ref(false);
const loadExpectation = async () => {
if (!getSessionNum()) return;
try {
const res = await getExpectation(getSessionNum());
expectationSaved.value = res?.data?.description || "";
} catch {
expectationSaved.value = "";
}
if (!expectationSaved.value) {
expectationDialogVisible.value = true;
}
};
const saveExpectation = async () => {
if (!expectationContent.value.trim()) {
ElMessage.warning("学习预期不可为空");
return;
}
savingExpectation.value = true;
try {
const res = await upsertExpectation(getSessionNum(), expectationContent.value);
if (res?.code === 200) {
expectationSaved.value = expectationContent.value;
expectationDialogVisible.value = false;
} else {
ElMessage.error(res?.message || "保存失败");
}
} catch (e: any) {
ElMessage.error(e.message || "请求失败");
} finally {
savingExpectation.value = false;
}
};
return {
expectationDialogVisible,
expectationContent,
expectationSaved,
savingExpectation,
loadExpectation,
saveExpectation,
};
}
@@ -0,0 +1,113 @@
import { ref, watch } from 'vue';
import { getTaskFragments, getTaskReports } from '@/api/studySessions';
/** 历史残片 / 历史报告共用的列表项形状(模板只消费这些字段) */
export interface SessionHistoryRecord {
id: number;
sessionNum: string;
content: string;
sessionExpectation?: string;
}
/**
* 学习会话页的“历史学习记录”面板:按任务分页查询历史残片与报告,
* 支持关键字搜索(300ms 防抖)与 tab 切换。
*/
export function useSessionHistory(taskNum: string) {
const PAGE_SIZE = 10;
const showHistory = ref(false);
const historyTab = ref("fragments");
const historyKeyword = ref("");
let historyDebounce: ReturnType<typeof setTimeout> | null = null;
const loadingHistory = ref(false);
const historyFragments = ref<SessionHistoryRecord[]>([]);
const fragmentsTotal = ref(0);
const fragmentsPage = ref(1);
const historyReports = ref<SessionHistoryRecord[]>([]);
const reportsTotal = ref(0);
const reportsPage = ref(1);
watch(showHistory, (val) => {
if (val) {
// 展开时立即加载当前 tab 的数据
if (historyTab.value === "fragments") loadHistoryFragments();
else loadHistoryReports();
}
});
const loadHistoryFragments = async () => {
loadingHistory.value = true;
try {
const res = await getTaskFragments(taskNum, fragmentsPage.value, PAGE_SIZE, historyKeyword.value || undefined);
const data = res?.data;
historyFragments.value = data?.records || [];
fragmentsTotal.value = data?.total || 0;
} catch {
historyFragments.value = [];
fragmentsTotal.value = 0;
} finally {
loadingHistory.value = false;
}
};
const loadHistoryReports = async () => {
loadingHistory.value = true;
try {
const res = await getTaskReports(taskNum, reportsPage.value, PAGE_SIZE, historyKeyword.value || undefined);
const data = res?.data;
historyReports.value = data?.records || [];
reportsTotal.value = data?.total || 0;
} catch {
historyReports.value = [];
reportsTotal.value = 0;
} finally {
loadingHistory.value = false;
}
};
const searchHistory = () => {
if (historyDebounce) clearTimeout(historyDebounce);
historyDebounce = setTimeout(() => {
fragmentsPage.value = 1;
reportsPage.value = 1;
if (historyTab.value === "fragments") loadHistoryFragments();
else loadHistoryReports();
}, 300);
};
const onHistoryTabChange = () => {
fragmentsPage.value = 1;
reportsPage.value = 1;
if (historyTab.value === "fragments") loadHistoryFragments();
else loadHistoryReports();
};
const onFragmentsPageChange = (p: number) => {
fragmentsPage.value = p;
loadHistoryFragments();
};
const onReportsPageChange = (p: number) => {
reportsPage.value = p;
loadHistoryReports();
};
return {
PAGE_SIZE,
showHistory,
historyTab,
historyKeyword,
loadingHistory,
historyFragments,
fragmentsTotal,
fragmentsPage,
historyReports,
reportsTotal,
reportsPage,
searchHistory,
onHistoryTabChange,
onFragmentsPageChange,
onReportsPageChange,
};
}
@@ -0,0 +1,54 @@
import { ref } from 'vue';
import { getReportDraft } from '@/api/studySessions';
import { useElapsedSeconds } from './useElapsedSeconds';
/**
* “结束会话总结”弹窗:编辑/预览总结内容。
* 有学习残片且尚未填写内容时,先取 AI 聚合草稿作为编辑起点(失败不阻塞手写),
* 等待期间展示“已等待 N 秒”提示。
*/
export function useSummaryReport(options: {
getSessionNum: () => string;
hasFragments: () => boolean;
}) {
const summaryDialogVisible = ref(false);
const summaryContent = ref("");
const summaryPreview = ref(false);
const summaryLoading = ref(false);
const { seconds: summaryLoadingSeconds, start: startLoadingTimer, stop: stopLoadingTimer } = useElapsedSeconds();
const openSummaryDialog = async () => {
summaryDialogVisible.value = true;
// 有残片且未填写过总结时,用 AI 聚合草稿作为编辑起点
if (!summaryContent.value.trim() && options.hasFragments()) {
summaryLoading.value = true;
startLoadingTimer();
try {
const res = await getReportDraft(options.getSessionNum());
if (res?.code === 200 && res.data) {
summaryContent.value = res.data;
}
} catch {
// 草稿失败不阻塞手写
} finally {
summaryLoading.value = false;
stopLoadingTimer();
}
}
};
const closeSummaryDialog = () => {
summaryDialogVisible.value = false;
stopLoadingTimer();
};
return {
summaryDialogVisible,
summaryContent,
summaryPreview,
summaryLoading,
summaryLoadingSeconds,
openSummaryDialog,
closeSummaryDialog,
};
}