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) => {
|
export const getFragmentDetail = (id: number) => {
|
||||||
return request.get(`/review/fragment/${id}`);
|
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">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
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";
|
import { getSessionDetail } from "@/api/studySessions";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
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 contentId = computed(() => Number(route.params.id));
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const content = ref("");
|
const content = ref("");
|
||||||
const sourceTypeLabel = ref("");
|
const sourceTypeLabel = ref("");
|
||||||
|
const taskNum = ref("");
|
||||||
|
|
||||||
// 会话相关信息(用于回忆上下文)
|
// 当前会话信息
|
||||||
const sessionInfo = ref<{
|
const sessionInfo = ref<{
|
||||||
taskName: string;
|
taskName: string;
|
||||||
sessionNum: string;
|
sessionNum: string;
|
||||||
@@ -24,6 +25,14 @@ const sessionInfo = ref<{
|
|||||||
startTime?: string;
|
startTime?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
// 该任务下的所有学习记录(按 session 分组)
|
||||||
|
const taskSessions = ref<{
|
||||||
|
sessionNum: string;
|
||||||
|
startTime: string;
|
||||||
|
report: ReviewFeedItem | null;
|
||||||
|
fragments: ReviewFeedItem[];
|
||||||
|
}[]>([]);
|
||||||
|
|
||||||
const formatDuration = (seconds: number): string => {
|
const formatDuration = (seconds: number): string => {
|
||||||
if (seconds == null) return "-";
|
if (seconds == null) return "-";
|
||||||
const h = Math.floor(seconds / 3600);
|
const h = Math.floor(seconds / 3600);
|
||||||
@@ -48,14 +57,20 @@ const loadDetail = async () => {
|
|||||||
const data = res?.data;
|
const data = res?.data;
|
||||||
content.value = data?.content || "";
|
content.value = data?.content || "";
|
||||||
|
|
||||||
// 通过 sessionNum 获取会话上下文信息
|
|
||||||
const sessionNum = data?.sessionNum;
|
const sessionNum = data?.sessionNum;
|
||||||
if (sessionNum) {
|
if (sessionNum) {
|
||||||
try {
|
try {
|
||||||
const sessionRes = await getSessionDetail(sessionNum);
|
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 {
|
} catch {
|
||||||
// 会话信息非关键,加载失败不影响内容展示
|
// 非关键数据
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} 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(() => {
|
onMounted(() => {
|
||||||
loadDetail();
|
loadDetail();
|
||||||
});
|
});
|
||||||
@@ -78,6 +129,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- 当前条目内容 -->
|
||||||
<article class="surface-card content-card" v-loading="loading">
|
<article class="surface-card content-card" v-loading="loading">
|
||||||
<div class="content-meta">
|
<div class="content-meta">
|
||||||
<el-tag
|
<el-tag
|
||||||
@@ -103,31 +155,44 @@ onMounted(() => {
|
|||||||
<div v-else class="empty-content">暂无内容</div>
|
<div v-else class="empty-content">暂无内容</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article
|
<!-- 该任务下的所有学习记录 -->
|
||||||
class="surface-card session-card"
|
<article class="surface-card history-card" v-if="taskSessions.length > 0">
|
||||||
v-if="sessionInfo"
|
<h3>该任务下的所有学习记录</h3>
|
||||||
>
|
<div
|
||||||
<h3>会话上下文</h3>
|
v-for="session in taskSessions"
|
||||||
<div class="session-grid">
|
:key="session.sessionNum"
|
||||||
<div class="session-item">
|
class="history-session"
|
||||||
<span class="label">任务名称</span>
|
:class="{ 'is-current': session.sessionNum === sessionInfo?.sessionNum }"
|
||||||
<strong>{{ sessionInfo.taskName }}</strong>
|
>
|
||||||
|
<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>
|
||||||
<div class="session-item">
|
|
||||||
<span class="label">会话编号</span>
|
<div class="session-report" v-if="session.report">
|
||||||
<strong>{{ sessionInfo.sessionNum }}</strong>
|
<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>
|
||||||
<div class="session-item" v-if="sessionInfo.actualTime != null">
|
|
||||||
<span class="label">实际学习时间</span>
|
<div class="session-fragments" v-if="session.fragments.length > 0">
|
||||||
<strong>{{ formatDuration(sessionInfo.actualTime) }}</strong>
|
<span
|
||||||
</div>
|
v-for="f in session.fragments"
|
||||||
<div class="session-item" v-if="sessionInfo.effectiveTime != null">
|
:key="f.id"
|
||||||
<span class="label">有效学习时间</span>
|
class="clickable-text"
|
||||||
<strong>{{ formatDuration(sessionInfo.effectiveTime) }}</strong>
|
:class="{ active: contentType === 'fragment' && f.id === contentId }"
|
||||||
</div>
|
@click="goToDetail('fragment', f.id)"
|
||||||
<div class="session-item" v-if="sessionInfo.effectivenessRatio != null">
|
>
|
||||||
<span class="label">效率比</span>
|
<span class="label-tag fragment-tag">残片</span>
|
||||||
<strong>{{ (sessionInfo.effectivenessRatio * 100).toFixed(1) }}%</strong>
|
{{ f.content.length > 150 ? f.content.substring(0, 150) + "..." : f.content }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -203,46 +268,106 @@ onMounted(() => {
|
|||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 会话上下文卡片 */
|
/* 历史记录卡片 */
|
||||||
.session-card {
|
.history-card {
|
||||||
padding: 20px 22px;
|
padding: 20px 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-card h3 {
|
.history-card h3 {
|
||||||
margin: 0 0 14px;
|
margin: 0 0 16px;
|
||||||
color: var(--green-900);
|
color: var(--green-900);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-grid {
|
.history-session {
|
||||||
display: grid;
|
padding: 14px 0;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
border-bottom: 1px solid var(--border-soft);
|
||||||
gap: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-item .label {
|
.clickable-text {
|
||||||
color: var(--text-secondary);
|
display: block;
|
||||||
font-size: 12px;
|
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 {
|
.clickable-text:hover {
|
||||||
color: var(--text-primary);
|
background: var(--surface-strong);
|
||||||
font-size: 15px;
|
}
|
||||||
|
|
||||||
|
.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) {
|
@media (max-width: 768px) {
|
||||||
.detail-head {
|
.detail-head {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-grid {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user