refactor: StartTask 抽取功能域组合式函数
This commit is contained in:
+43
-151
@@ -15,10 +15,12 @@ import {
|
|||||||
} from "@/api/studySessions";
|
} from "@/api/studySessions";
|
||||||
import { useStudyFragment } from "@/components/composables/fragment";
|
import { useStudyFragment } from "@/components/composables/fragment";
|
||||||
import { getFragmentsBySession, updateFragments } from "@/api/reportFragments";
|
import { getFragmentsBySession, updateFragments } from "@/api/reportFragments";
|
||||||
import { getExpectation, getReportDraft, getTaskFragments, getTaskReports, upsertExpectation } from "@/api/studySessions";
|
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
|
import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
|
||||||
import { renderMarkdown } from "@/utils/markdown";
|
import { renderMarkdown } from "@/utils/markdown";
|
||||||
|
import { useSessionHistory } from "@/components/composables/useSessionHistory";
|
||||||
|
import { useSummaryReport } from "@/components/composables/useSummaryReport";
|
||||||
|
import { useSessionExpectation } from "@/components/composables/useSessionExpectation";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fragmentsDialogVisible,
|
fragmentsDialogVisible,
|
||||||
@@ -41,18 +43,29 @@ const canAbortSession = computed(
|
|||||||
() => taskInfo.value.sessionState !== "ENDED" && fragmentsList.value.length === 0,
|
() => taskInfo.value.sessionState !== "ENDED" && fragmentsList.value.length === 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const summaryDialogVisible = ref(false);
|
// 结束会话总结弹窗(有残片时先取 AI 草稿,等待期间展示已等待秒数)
|
||||||
const summaryContent = ref("");
|
const {
|
||||||
const summaryPreview = ref(false);
|
summaryDialogVisible,
|
||||||
const summaryLoading = ref(false);
|
summaryContent,
|
||||||
const summaryLoadingSeconds = ref(0);
|
summaryPreview,
|
||||||
let summaryLoadingTimer: ReturnType<typeof setInterval> | null = null;
|
summaryLoading,
|
||||||
|
summaryLoadingSeconds,
|
||||||
|
openSummaryDialog,
|
||||||
|
closeSummaryDialog,
|
||||||
|
} = useSummaryReport({
|
||||||
|
getSessionNum: () => taskInfo.value.sessionNum,
|
||||||
|
hasFragments: () => fragmentsList.value.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
// 学习预期
|
// 学习预期:会话必须有预期才能开始计时
|
||||||
const expectationDialogVisible = ref(false);
|
const {
|
||||||
const expectationContent = ref("");
|
expectationDialogVisible,
|
||||||
const expectationSaved = ref("");
|
expectationContent,
|
||||||
const savingExpectation = ref(false);
|
expectationSaved,
|
||||||
|
savingExpectation,
|
||||||
|
loadExpectation,
|
||||||
|
saveExpectation,
|
||||||
|
} = useSessionExpectation(() => taskInfo.value.sessionNum);
|
||||||
|
|
||||||
// 当前会话碎片列表
|
// 当前会话碎片列表
|
||||||
interface FragmentItem {
|
interface FragmentItem {
|
||||||
@@ -64,88 +77,27 @@ const editingFragmentId = ref<number | null>(null);
|
|||||||
const editFragmentContent = ref("");
|
const editFragmentContent = ref("");
|
||||||
const savingFragment = ref(false);
|
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 route = useRoute();
|
||||||
const taskNum = route.params.taskNum as string;
|
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({
|
const taskInfo = ref({
|
||||||
sessionNum: "",
|
sessionNum: "",
|
||||||
sessionState: "--",
|
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 = () => {
|
const openAbortDialog = () => {
|
||||||
abortConfirmation.value = "";
|
abortConfirmation.value = "";
|
||||||
abortDialogVisible.value = true;
|
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 () => {
|
const initPage = async () => {
|
||||||
try {
|
try {
|
||||||
const hasPermission = await checkAudioPermission();
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user