diff --git a/src/components/StartTask.vue b/src/components/StartTask.vue index e4ec0e7..f42a092 100644 --- a/src/components/StartTask.vue +++ b/src/components/StartTask.vue @@ -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 | 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(null); const editFragmentContent = ref(""); const savingFragment = ref(false); -// 历史学习记录 -const showHistory = ref(false); -const historyTab = ref("fragments"); -const historyKeyword = ref(""); -let historyDebounce: ReturnType | null = null; -const loadingHistory = ref(false); -const historyFragments = ref([]); -const fragmentsTotal = ref(0); -const fragmentsPage = ref(1); -const historyReports = ref([]); -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(); diff --git a/src/components/composables/useSessionExpectation.ts b/src/components/composables/useSessionExpectation.ts new file mode 100644 index 0000000..06cbd74 --- /dev/null +++ b/src/components/composables/useSessionExpectation.ts @@ -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, + }; +} diff --git a/src/components/composables/useSessionHistory.ts b/src/components/composables/useSessionHistory.ts new file mode 100644 index 0000000..81fe8db --- /dev/null +++ b/src/components/composables/useSessionHistory.ts @@ -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 | null = null; + const loadingHistory = ref(false); + const historyFragments = ref([]); + const fragmentsTotal = ref(0); + const fragmentsPage = ref(1); + const historyReports = ref([]); + 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, + }; +} diff --git a/src/components/composables/useSummaryReport.ts b/src/components/composables/useSummaryReport.ts new file mode 100644 index 0000000..3d29c93 --- /dev/null +++ b/src/components/composables/useSummaryReport.ts @@ -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, + }; +}