Merge branch 'feature/fragment-edit' into dev

学习碎片编辑功能:
- 新增碎片更新和会话查询前端 API
- 碎片详情页支持内联编辑
- 学习会话页展示碎片列表并支持编辑
- 碎片列表增加序号标识
- 修复 RouterView 嵌套 Transition 弃用警告
- 修复 StartTask 多根节点导致 Transition 动画异常
This commit is contained in:
2026-05-30 16:06:52 +08:00
5 changed files with 271 additions and 34 deletions
+5 -3
View File
@@ -15,9 +15,11 @@ const route = useRoute();
<MyHead />
</el-header>
<el-main class="shell-main">
<Transition name="fade-up" mode="out-in">
<RouterView :key="route.fullPath" />
</Transition>
<RouterView v-slot="{ Component }">
<Transition name="fade-up" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</Transition>
</RouterView>
</el-main>
<el-footer class="shell-footer">
<MyFooter />
+8
View File
@@ -3,4 +3,12 @@ import request from "@/utils/request";
export const createFragments = (sessionNum: string, content: string) => {
return request.post(`/report-fragments`, {sessionNum, content})
}
export const updateFragments = (id: number, content: string) => {
return request.put(`/report-fragments/${id}`, {content})
}
export const getFragmentsBySession = (sessionNum: string) => {
return request.get(`/report-fragments/session/${sessionNum}`)
}
+85 -7
View File
@@ -3,6 +3,8 @@ import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getReportDetail, getFragmentDetail, getTaskReview, type ReviewFeedItem } from "@/api/review";
import { getSessionDetail } from "@/api/studySessions";
import { updateFragments } from "@/api/reportFragments";
import { ElMessage } from "element-plus";
const route = useRoute();
const router = useRouter();
@@ -15,6 +17,11 @@ const content = ref("");
const sourceTypeLabel = ref("");
const taskNum = ref("");
// 编辑状态
const isEditing = ref(false);
const editContent = ref("");
const saving = ref(false);
// 当前会话信息
const sessionInfo = ref<{
taskName: string;
@@ -114,6 +121,42 @@ const goToDetail = (type: string, id: number) => {
router.push(`/review/detail/${type}/${id}`);
};
const enterEdit = () => {
editContent.value = content.value;
isEditing.value = true;
};
const cancelEdit = () => {
isEditing.value = false;
editContent.value = "";
};
const saveEdit = async () => {
if (!editContent.value.trim()) {
ElMessage.warning("内容不能为空");
return;
}
if (editContent.value === content.value) {
isEditing.value = false;
return;
}
saving.value = true;
try {
const res = await updateFragments(contentId.value, editContent.value);
if (res?.code === 200) {
content.value = editContent.value;
isEditing.value = false;
ElMessage.success("保存成功");
} else {
ElMessage.error(res?.message || "保存失败");
}
} catch (e: any) {
ElMessage.error(e.message || "请求失败");
} finally {
saving.value = false;
}
};
onMounted(() => {
loadDetail();
});
@@ -137,14 +180,38 @@ onMounted(() => {
<span v-if="sessionInfo?.startTime" class="meta-time">
学习时间{{ sessionInfo.startTime?.replace("T", " ")?.substring(0, 16) }}
</span>
<el-button
v-if="contentType === 'fragment' && !isEditing"
type="primary"
size="small"
@click="enterEdit"
>
编辑
</el-button>
</div>
<div class="content-body" v-if="content">
<p v-for="(line, idx) in content.split('\n')" :key="idx">
{{ line || " " }}
</p>
<!-- 编辑模式 -->
<div v-if="isEditing" class="edit-area">
<el-input
v-model="editContent"
type="textarea"
:rows="6"
placeholder="请输入学习内容"
/>
<div class="edit-actions">
<el-button @click="cancelEdit">取消</el-button>
<el-button type="primary" :loading="saving" @click="saveEdit">保存</el-button>
</div>
</div>
<div v-else class="empty-content">暂无内容</div>
<!-- 只读模式 -->
<template v-else>
<div class="content-body" v-if="content">
<p v-for="(line, idx) in content.split('\n')" :key="idx">
{{ line || " " }}
</p>
</div>
<div v-else class="empty-content">暂无内容</div>
</template>
</article>
<!-- 该任务下的所有学习记录 -->
@@ -176,13 +243,13 @@ onMounted(() => {
<div class="session-fragments" v-if="session.fragments.length > 0">
<span
v-for="f in session.fragments"
v-for="(f, idx) in session.fragments"
:key="f.id"
class="clickable-text"
:class="{ active: contentType === 'fragment' && f.id === contentId }"
@click="goToDetail('fragment', f.id)"
>
<span class="label-tag fragment-tag">残片</span>
<span class="label-tag fragment-tag">残片{{ idx + 1 }}</span>
{{ f.content.length > 150 ? f.content.substring(0, 150) + "..." : f.content }}
</span>
</div>
@@ -342,4 +409,15 @@ onMounted(() => {
color: #b8860b;
}
.edit-area {
margin-top: 12px;
}
.edit-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 12px;
}
</style>
+147 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import { useTimer } from "@/components/composables/useTimer";
@@ -10,6 +10,7 @@ import {
startOrContinueStudySession,
} from "@/api/studySessions";
import { useStudyFragment } from "@/components/composables/fragment";
import { getFragmentsBySession, updateFragments } from "@/api/reportFragments";
import router from "@/router";
const {
@@ -23,6 +24,16 @@ const {
const summaryDialogVisible = ref(false);
const summaryContent = ref("");
// 当前会话碎片列表
interface FragmentItem {
id: number;
content: string;
}
const fragmentsList = ref<FragmentItem[]>([]);
const editingFragmentId = ref<number | null>(null);
const editFragmentContent = ref("");
const savingFragment = ref(false);
const route = useRoute();
const taskNum = route.params.taskNum as string;
@@ -213,6 +224,7 @@ const loadTaskSession = async () => {
} else {
syncDisplay(taskInfo.value.pointerPosition || 25 * 60 * 1000);
}
loadFragments();
} catch (err: any) {
ElMessage.error(err.message || "加载任务失败");
}
@@ -227,6 +239,53 @@ const initPage = async () => {
await loadTaskSession();
};
// 碎片列表
const loadFragments = async () => {
if (!taskInfo.value.sessionNum) return;
try {
const res = await getFragmentsBySession(taskInfo.value.sessionNum);
if (res?.code === 200) {
fragmentsList.value = res.data || [];
}
} catch {
// 非关键数据
}
};
const startEditFragment = (fragment: FragmentItem) => {
editingFragmentId.value = fragment.id;
editFragmentContent.value = fragment.content;
};
const cancelEditFragment = () => {
editingFragmentId.value = null;
editFragmentContent.value = "";
};
const saveEditFragment = async (id: number) => {
if (!editFragmentContent.value.trim()) {
ElMessage.warning("内容不能为空");
return;
}
savingFragment.value = true;
try {
const res = await updateFragments(id, editFragmentContent.value);
if (res?.code === 200) {
const item = fragmentsList.value.find((f) => f.id === id);
if (item) item.content = editFragmentContent.value;
editingFragmentId.value = null;
editFragmentContent.value = "";
ElMessage.success("保存成功");
} else {
ElMessage.error(res?.message || "保存失败");
}
} catch (e: any) {
ElMessage.error(e.message || "请求失败");
} finally {
savingFragment.value = false;
}
};
onMounted(() => {
displayInterval = window.setInterval(() => {
nowTimestamp.value = Date.now();
@@ -234,6 +293,13 @@ onMounted(() => {
initPage();
});
// 碎片创建成功后刷新列表
watch(fragmentsDialogVisible, (newVal, oldVal) => {
if (oldVal === true && newVal === false) {
loadFragments();
}
});
onUnmounted(() => {
if (displayInterval) {
clearInterval(displayInterval);
@@ -243,6 +309,7 @@ onUnmounted(() => {
</script>
<template>
<div>
<section class="start-page">
<div class="task-info-bar">
<el-tag type="success" effect="plain">任务编号{{ taskInfo.taskNum }}</el-tag>
@@ -298,6 +365,34 @@ onUnmounted(() => {
</div>
</article>
</div>
<!-- 当前会话学习残片 -->
<article class="surface-card fragments-card" v-if="fragmentsList.length > 0">
<h3>当前会话学习残片</h3>
<div
v-for="(fragment, index) in fragmentsList"
:key="fragment.id"
class="fragment-item"
>
<span class="fragment-index">{{ index + 1 }}</span>
<template v-if="editingFragmentId === fragment.id">
<el-input
v-model="editFragmentContent"
type="textarea"
:rows="3"
placeholder="请输入学习内容"
/>
<div class="fragment-edit-actions">
<el-button size="small" @click="cancelEditFragment">取消</el-button>
<el-button size="small" type="primary" :loading="savingFragment" @click="saveEditFragment(fragment.id)">保存</el-button>
</div>
</template>
<template v-else>
<span class="fragment-content">{{ fragment.content }}</span>
<el-button size="small" @click="startEditFragment(fragment)">编辑</el-button>
</template>
</div>
</article>
</section>
<el-dialog v-model="fragmentsDialogVisible" title="生成学习残片" width="520px">
@@ -325,6 +420,7 @@ onUnmounted(() => {
<el-button type="success" @click="endTimer(summaryContent)">确认结束</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
@@ -425,6 +521,56 @@ onUnmounted(() => {
margin: 0;
}
.fragments-card {
padding: 20px;
}
.fragments-card h3 {
margin: 0 0 14px;
color: var(--green-900);
}
.fragment-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid var(--border-soft);
}
.fragment-item:last-child {
border-bottom: none;
}
.fragment-index {
flex-shrink: 0;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--green-600);
color: #fff;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.fragment-content {
flex: 1;
font-size: 14px;
line-height: 1.6;
color: var(--text-primary);
word-break: break-word;
}
.fragment-edit-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 8px;
}
@media (max-width: 1100px) {
.session-grid {
grid-template-columns: 1fr;
+26 -23
View File
@@ -25,34 +25,37 @@ export const useStudyFragment = () => {
});
};
const confirmGenerateFragment = async (sessionNum: string) => {
const confirmGenerateFragment = async (sessionNum: string): Promise<boolean> => {
if (!fragmentContent.value.trim()) {
ElMessage.warning('请输入学习内容!');
return;
return false;
}
ElMessageBox.confirm('确定要生成学习残片吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
})
.then(async () => {
try {
const res = await createFragments(sessionNum, fragmentContent.value);
if (res.code === 200) {
ElMessage.success('学习残片生成成功!');
fragmentsDialogVisible.value = false;
} else {
ElMessage.error(res.message || '生成学习残片失败');
}
} catch (error: any) {
ElMessage.error(error.message || '请求失败');
}
})
.catch(() => {
ElMessage.info('已取消生成');
try {
await ElMessageBox.confirm('确定要生成学习残片吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
});
} catch {
ElMessage.info('已取消生成');
return false;
}
try {
const res = await createFragments(sessionNum, fragmentContent.value);
if (res.code === 200) {
ElMessage.success('学习残片生成成功!');
fragmentsDialogVisible.value = false;
return true;
} else {
ElMessage.error(res.message || '生成学习残片失败');
return false;
}
} catch (error: any) {
ElMessage.error(error.message || '请求失败');
return false;
}
};
return {