feat: add task learning history to review detail page
This commit is contained in:
@@ -21,3 +21,7 @@ export const getReportDetail = (id: number) => {
|
||||
export const getFragmentDetail = (id: number) => {
|
||||
return request.get(`/review/fragment/${id}`);
|
||||
};
|
||||
|
||||
export const getTaskReview = (taskNum: string) => {
|
||||
return request.get(`/review/task/${taskNum}`);
|
||||
};
|
||||
|
||||
+174
-49
@@ -1,20 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getReportDetail, getFragmentDetail } from "@/api/review";
|
||||
import { getReportDetail, getFragmentDetail, getTaskReview, type ReviewFeedItem } from "@/api/review";
|
||||
import { getSessionDetail } from "@/api/studySessions";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const contentType = computed(() => route.params.type as string); // 'report' | 'fragment'
|
||||
const contentType = computed(() => route.params.type as string);
|
||||
const contentId = computed(() => Number(route.params.id));
|
||||
|
||||
const loading = ref(true);
|
||||
const content = ref("");
|
||||
const sourceTypeLabel = ref("");
|
||||
const taskNum = ref("");
|
||||
|
||||
// 会话相关信息(用于回忆上下文)
|
||||
// 当前会话信息
|
||||
const sessionInfo = ref<{
|
||||
taskName: string;
|
||||
sessionNum: string;
|
||||
@@ -24,6 +25,14 @@ const sessionInfo = ref<{
|
||||
startTime?: string;
|
||||
} | null>(null);
|
||||
|
||||
// 该任务下的所有学习记录(按 session 分组)
|
||||
const taskSessions = ref<{
|
||||
sessionNum: string;
|
||||
startTime: string;
|
||||
report: ReviewFeedItem | null;
|
||||
fragments: ReviewFeedItem[];
|
||||
}[]>([]);
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
if (seconds == null) return "-";
|
||||
const h = Math.floor(seconds / 3600);
|
||||
@@ -48,14 +57,20 @@ const loadDetail = async () => {
|
||||
const data = res?.data;
|
||||
content.value = data?.content || "";
|
||||
|
||||
// 通过 sessionNum 获取会话上下文信息
|
||||
const sessionNum = data?.sessionNum;
|
||||
if (sessionNum) {
|
||||
try {
|
||||
const sessionRes = await getSessionDetail(sessionNum);
|
||||
sessionInfo.value = sessionRes?.data || null;
|
||||
const info = sessionRes?.data;
|
||||
sessionInfo.value = info || null;
|
||||
taskNum.value = info?.taskNum || "";
|
||||
|
||||
// 加载该任务下的所有报告和残片
|
||||
if (info?.taskNum) {
|
||||
await loadTaskHistory(info.taskNum, sessionNum);
|
||||
}
|
||||
} catch {
|
||||
// 会话信息非关键,加载失败不影响内容展示
|
||||
// 非关键数据
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -63,6 +78,42 @@ const loadDetail = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadTaskHistory = async (tn: string, currentSessionNum: string) => {
|
||||
try {
|
||||
const res = await getTaskReview(tn);
|
||||
const items: ReviewFeedItem[] = res?.data || [];
|
||||
|
||||
// 按 sessionNum 分组
|
||||
const map = new Map<string, { report: ReviewFeedItem | null; fragments: ReviewFeedItem[]; startTime: string }>();
|
||||
for (const item of items) {
|
||||
const entry = map.get(item.sessionNum) || { report: null, fragments: [], startTime: item.createdTime };
|
||||
if (item.sourceType === "REPORT") {
|
||||
entry.report = item;
|
||||
} else {
|
||||
entry.fragments.push(item);
|
||||
}
|
||||
if (!map.has(item.sessionNum)) {
|
||||
map.set(item.sessionNum, entry);
|
||||
}
|
||||
}
|
||||
|
||||
taskSessions.value = Array.from(map.entries())
|
||||
.map(([sessionNum, data]) => ({
|
||||
sessionNum,
|
||||
startTime: data.startTime,
|
||||
report: data.report,
|
||||
fragments: data.fragments,
|
||||
}))
|
||||
.sort((a, b) => b.startTime.localeCompare(a.startTime));
|
||||
} catch {
|
||||
// 非关键数据
|
||||
}
|
||||
};
|
||||
|
||||
const goToDetail = (type: string, id: number) => {
|
||||
router.push(`/review/detail/${type}/${id}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDetail();
|
||||
});
|
||||
@@ -78,6 +129,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 当前条目内容 -->
|
||||
<article class="surface-card content-card" v-loading="loading">
|
||||
<div class="content-meta">
|
||||
<el-tag
|
||||
@@ -103,31 +155,44 @@ onMounted(() => {
|
||||
<div v-else class="empty-content">暂无内容</div>
|
||||
</article>
|
||||
|
||||
<article
|
||||
class="surface-card session-card"
|
||||
v-if="sessionInfo"
|
||||
>
|
||||
<h3>会话上下文</h3>
|
||||
<div class="session-grid">
|
||||
<div class="session-item">
|
||||
<span class="label">任务名称</span>
|
||||
<strong>{{ sessionInfo.taskName }}</strong>
|
||||
<!-- 该任务下的所有学习记录 -->
|
||||
<article class="surface-card history-card" v-if="taskSessions.length > 0">
|
||||
<h3>该任务下的所有学习记录</h3>
|
||||
<div
|
||||
v-for="session in taskSessions"
|
||||
:key="session.sessionNum"
|
||||
class="history-session"
|
||||
:class="{ 'is-current': session.sessionNum === sessionInfo?.sessionNum }"
|
||||
>
|
||||
<div class="session-header">
|
||||
<span class="session-date">
|
||||
{{ session.startTime?.replace("T", " ")?.substring(0, 16) }}
|
||||
</span>
|
||||
<span class="session-badge" v-if="session.sessionNum === sessionInfo?.sessionNum">当前</span>
|
||||
</div>
|
||||
<div class="session-item">
|
||||
<span class="label">会话编号</span>
|
||||
<strong>{{ sessionInfo.sessionNum }}</strong>
|
||||
|
||||
<div class="session-report" v-if="session.report">
|
||||
<span
|
||||
class="clickable-text"
|
||||
:class="{ active: contentType === 'report' && session.report.id === contentId }"
|
||||
@click="goToDetail('report', session.report!.id)"
|
||||
>
|
||||
<span class="label-tag report-tag">报告</span>
|
||||
{{ session.report.content.length > 150 ? session.report.content.substring(0, 150) + "..." : session.report.content }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="session-item" v-if="sessionInfo.actualTime != null">
|
||||
<span class="label">实际学习时间</span>
|
||||
<strong>{{ formatDuration(sessionInfo.actualTime) }}</strong>
|
||||
</div>
|
||||
<div class="session-item" v-if="sessionInfo.effectiveTime != null">
|
||||
<span class="label">有效学习时间</span>
|
||||
<strong>{{ formatDuration(sessionInfo.effectiveTime) }}</strong>
|
||||
</div>
|
||||
<div class="session-item" v-if="sessionInfo.effectivenessRatio != null">
|
||||
<span class="label">效率比</span>
|
||||
<strong>{{ (sessionInfo.effectivenessRatio * 100).toFixed(1) }}%</strong>
|
||||
|
||||
<div class="session-fragments" v-if="session.fragments.length > 0">
|
||||
<span
|
||||
v-for="f 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>
|
||||
{{ f.content.length > 150 ? f.content.substring(0, 150) + "..." : f.content }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -203,46 +268,106 @@ onMounted(() => {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* 会话上下文卡片 */
|
||||
.session-card {
|
||||
/* 历史记录卡片 */
|
||||
.history-card {
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.session-card h3 {
|
||||
margin: 0 0 14px;
|
||||
.history-card h3 {
|
||||
margin: 0 0 16px;
|
||||
color: var(--green-900);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.session-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 14px;
|
||||
.history-session {
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.session-item {
|
||||
.history-session:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.history-session.is-current {
|
||||
background: #f1f8e9;
|
||||
margin: 0 -10px;
|
||||
padding: 14px 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.session-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.session-date {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-badge {
|
||||
font-size: 11px;
|
||||
background: var(--green-600);
|
||||
color: #fff;
|
||||
padding: 1px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.session-report {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.session-fragments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.session-item .label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
.clickable-text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.session-item strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
.clickable-text:hover {
|
||||
background: var(--surface-strong);
|
||||
}
|
||||
|
||||
.clickable-text.active {
|
||||
background: #e8f5e9;
|
||||
border-left: 3px solid var(--green-600);
|
||||
}
|
||||
|
||||
.label-tag {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
margin-right: 6px;
|
||||
font-weight: 600;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.report-tag {
|
||||
background: #e8f5e9;
|
||||
color: var(--green-700);
|
||||
}
|
||||
|
||||
.fragment-tag {
|
||||
background: #fff8e1;
|
||||
color: #b8860b;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-head {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.session-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user