feat: 学习页展示任务学习统计

This commit is contained in:
2026-06-18 00:09:52 +08:00
parent 000b1aa9f4
commit 7878242596
+125 -2
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref, watch } from "vue";
import request from "@/utils/request"; import request from "@/utils/request";
import router from "@/router"; import router from "@/router";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
@@ -12,6 +12,15 @@ interface TaskItem {
priority: number | string; priority: number | string;
taskNum: string; taskNum: string;
taskId: number; taskId: number;
reportCount: number;
fragmentCount: number;
effectiveTime: number;
statsLoaded: boolean;
sessionCount: number;
todayEffectiveTime: number;
weekEffectiveTime: number;
avgEffectiveTime: number;
avgEffectivenessRatio: number;
} }
const tasks = ref<TaskItem[]>([]); const tasks = ref<TaskItem[]>([]);
@@ -22,9 +31,40 @@ const selectedTask = computed(() =>
tasks.value.find((item) => item.taskId === selectedTaskId.value) || null tasks.value.find((item) => item.taskId === selectedTaskId.value) || null
); );
const formatEffectiveTime = (seconds: number): string => {
if (!seconds || seconds <= 0) return "0分钟";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}小时${minutes}分钟`;
return `${minutes}分钟`;
};
async function loadTaskStats(taskNum: string) {
const task = tasks.value.find(t => t.taskNum === taskNum);
if (!task || task.statsLoaded) return;
try {
const res = await request.get(`/review/tasks/${taskNum}`);
if (res?.data) {
task.reportCount = res.data.reportCount ?? 0;
task.fragmentCount = res.data.fragmentCount ?? 0;
task.effectiveTime = res.data.effectiveTime ?? 0;
task.sessionCount = res.data.sessionCount ?? 0;
task.todayEffectiveTime = res.data.todayEffectiveTime ?? 0;
task.weekEffectiveTime = res.data.weekEffectiveTime ?? 0;
task.avgEffectiveTime = res.data.avgEffectiveTime ?? 0;
task.avgEffectivenessRatio = res.data.avgEffectivenessRatio ?? 0;
task.statsLoaded = true;
}
} catch {
// 静默处理,不影响主流程
}
}
const fetchTasks = async () => { const fetchTasks = async () => {
loading.value = true; loading.value = true;
try { try {
const previousSelectedTaskId = selectedTaskId.value;
const res = await request.get("/tasks", { pageSize: 100, pageNum: 1 }); const res = await request.get("/tasks", { pageSize: 100, pageNum: 1 });
tasks.value = (res?.data?.records || []).map((record: any) => ({ tasks.value = (res?.data?.records || []).map((record: any) => ({
title: record.taskName, title: record.taskName,
@@ -34,8 +74,23 @@ const fetchTasks = async () => {
priority: Number.isFinite(record.taskPriority) ? Math.round(record.taskPriority) : "-", priority: Number.isFinite(record.taskPriority) ? Math.round(record.taskPriority) : "-",
taskNum: record.taskNum, taskNum: record.taskNum,
taskId: record.id, taskId: record.id,
reportCount: 0,
fragmentCount: 0,
effectiveTime: 0,
statsLoaded: false,
sessionCount: 0,
todayEffectiveTime: 0,
weekEffectiveTime: 0,
avgEffectiveTime: 0,
avgEffectivenessRatio: 0,
})); }));
selectedTaskId.value = tasks.value[0]?.taskId ?? null; const targetTask = tasks.value.find(item => item.taskId === previousSelectedTaskId) || tasks.value[0];
const nextSelectedTaskId = targetTask?.taskId ?? null;
const shouldLoadStats = nextSelectedTaskId === selectedTaskId.value;
selectedTaskId.value = nextSelectedTaskId;
if (targetTask && shouldLoadStats) {
await loadTaskStats(targetTask.taskNum);
}
} catch (error: any) { } catch (error: any) {
tasks.value = []; tasks.value = [];
selectedTaskId.value = null; selectedTaskId.value = null;
@@ -45,6 +100,13 @@ const fetchTasks = async () => {
} }
}; };
watch(selectedTaskId, (newId) => {
const task = tasks.value.find(t => t.taskId === newId);
if (task) {
loadTaskStats(task.taskNum);
}
});
const startTask = () => { const startTask = () => {
if (!selectedTask.value) { if (!selectedTask.value) {
ElMessage.warning("请先选择一个任务"); ElMessage.warning("请先选择一个任务");
@@ -128,6 +190,44 @@ onMounted(() => {
</el-link> </el-link>
<p v-else class="empty-text">暂未填写材料地址</p> <p v-else class="empty-text">暂未填写材料地址</p>
</div> </div>
<div class="detail-block" v-if="selectedTask.statsLoaded">
<p class="label">学习统计</p>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">学习次数</span>
<span class="stat-value">{{ selectedTask.sessionCount }} </span>
</div>
<div class="stat-item">
<span class="stat-label">今日有效学习</span>
<span class="stat-value">{{ formatEffectiveTime(selectedTask.todayEffectiveTime) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">本周有效学习</span>
<span class="stat-value">{{ formatEffectiveTime(selectedTask.weekEffectiveTime) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">累计有效学习</span>
<span class="stat-value">{{ formatEffectiveTime(selectedTask.effectiveTime) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">平均学习时长</span>
<span class="stat-value">{{ formatEffectiveTime(selectedTask.avgEffectiveTime) }}</span>
</div>
<div class="stat-item">
<span class="stat-label">平均有效时间比</span>
<span class="stat-value">{{ (selectedTask.avgEffectivenessRatio * 100).toFixed(1) }}%</span>
</div>
<div class="stat-item">
<span class="stat-label">学习报告</span>
<span class="stat-value">{{ selectedTask.reportCount }} </span>
</div>
<div class="stat-item">
<span class="stat-label">学习残片</span>
<span class="stat-value">{{ selectedTask.fragmentCount }} </span>
</div>
</div>
</div>
</template> </template>
<el-empty v-else description="暂无任务,请先创建任务" /> <el-empty v-else description="暂无任务,请先创建任务" />
</article> </article>
@@ -241,6 +341,29 @@ onMounted(() => {
color: var(--text-secondary); color: var(--text-secondary);
} }
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: var(--text-secondary);
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
}
.action-card { .action-card {
padding: 16px; padding: 16px;
display: flex; display: flex;