268 lines
7.3 KiB
Vue
268 lines
7.3 KiB
Vue
<script setup lang="ts">
|
|
import { errorMessage } from "utils/api"
|
|
import { h, computed, ref, onMounted, watch } from "vue"
|
|
import { watchDebounced } from "@vueuse/core"
|
|
import { parseTime } from "utils/functions"
|
|
import type { ProblemSetProgress } from "utils/types"
|
|
import { getProblemSetUserProgress } from "../../api"
|
|
import { NFlex, NTag } from "naive-ui"
|
|
import { usePagination } from "shared/composables/pagination"
|
|
import Pagination from "shared/components/Pagination.vue"
|
|
|
|
const route = useRoute()
|
|
const message = useMessage()
|
|
const problemSetId = computed(() => Number(route.params.problemSetId))
|
|
const progress = ref<ProblemSetProgress[]>([])
|
|
const loading = ref(false)
|
|
const total = ref(0)
|
|
const statistics = ref<{
|
|
total: number
|
|
completed: number
|
|
avgProgress: number
|
|
} | null>(null)
|
|
const classFilter = ref<string>("")
|
|
const completionFilter = ref<"" | "completed" | "in_progress" | "not_started">("")
|
|
const allProblems = ref<Array<{ id: number; _id: string; title: string }>>([])
|
|
|
|
// 完成度筛选选项
|
|
const completionOptions = [
|
|
{ label: "全部", value: "" },
|
|
{ label: "未开始", value: "not_started" },
|
|
{ label: "进行中", value: "in_progress" },
|
|
{ label: "已完成", value: "completed" },
|
|
]
|
|
|
|
// 使用分页 composable
|
|
const { query } = usePagination({}, { defaultLimit: 50 })
|
|
|
|
// 加载用户进度数据
|
|
async function loadUserProgress() {
|
|
loading.value = true
|
|
const offset = (query.page - 1) * query.limit
|
|
const params: {
|
|
limit?: number
|
|
offset?: number
|
|
className?: string
|
|
completionStatus?: "" | "completed" | "in_progress" | "not_started"
|
|
} = {
|
|
limit: query.limit,
|
|
offset,
|
|
}
|
|
if (classFilter.value.trim()) {
|
|
params.className = classFilter.value.trim()
|
|
}
|
|
if (completionFilter.value) {
|
|
params.completionStatus = completionFilter.value
|
|
}
|
|
try {
|
|
const res = await getProblemSetUserProgress(problemSetId.value, params)
|
|
|
|
progress.value = res.results
|
|
total.value = res.total
|
|
// 使用后端返回的统计数据(基于所有数据)
|
|
if (res.statistics) {
|
|
statistics.value = res.statistics
|
|
}
|
|
// 保存所有题目信息
|
|
if (res.problems) {
|
|
allProblems.value = res.problems
|
|
}
|
|
} catch (err) {
|
|
// finally 里收掉 loading:以前 loading.value = false 写在 await 之后,
|
|
// 请求一失败(403、断网)转圈就永远停不下来
|
|
message.error("加载用户进度失败:" + errorMessage(err, "未知错误"))
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 监听分页参数变化
|
|
watch([() => query.page, () => query.limit], loadUserProgress)
|
|
// 监听班级过滤变化(防抖)
|
|
watchDebounced(
|
|
classFilter,
|
|
() => {
|
|
query.page = 1 // 重置到第一页
|
|
loadUserProgress()
|
|
},
|
|
{ debounce: 500 },
|
|
)
|
|
// 监听完成度筛选变化
|
|
watch(completionFilter, () => {
|
|
query.page = 1 // 重置到第一页
|
|
loadUserProgress()
|
|
})
|
|
|
|
// 使用后端返回的统计数据
|
|
const stats = computed(() => {
|
|
if (statistics.value) {
|
|
return {
|
|
total: statistics.value.total,
|
|
completed: statistics.value.completed,
|
|
avgProgress: Math.round(statistics.value.avgProgress),
|
|
}
|
|
}
|
|
// 如果后端还没有返回统计数据,使用默认值
|
|
return {
|
|
total: total.value,
|
|
completed: 0,
|
|
avgProgress: 0,
|
|
}
|
|
})
|
|
|
|
onMounted(loadUserProgress)
|
|
|
|
// 定义表格列
|
|
const progressColumns = [
|
|
{
|
|
title: "排名",
|
|
key: "rank",
|
|
width: 80,
|
|
render: (_row: ProblemSetProgress, index: number) => {
|
|
// 计算全局排名:当前页偏移 + 当前行索引 + 1
|
|
const globalRank = (query.page - 1) * query.limit + index + 1
|
|
return globalRank
|
|
},
|
|
},
|
|
{
|
|
title: "用户",
|
|
key: "user.username",
|
|
width: 120,
|
|
render: (row: ProblemSetProgress) => row.user.username,
|
|
},
|
|
{
|
|
title: "加入时间",
|
|
key: "join_time",
|
|
width: 180,
|
|
render: (row: ProblemSetProgress) => parseTime(row.joinTime, "YYYY-MM-DD HH:mm:ss"),
|
|
},
|
|
{
|
|
title: "已完成数量",
|
|
key: "completedProblemsCount",
|
|
width: 100,
|
|
},
|
|
{
|
|
title: "已/未完成题目",
|
|
key: "completed_problems",
|
|
width: 300,
|
|
render: (row: ProblemSetProgress) => {
|
|
if (row.progressPercentage === 100) {
|
|
return "全部题目已完成"
|
|
}
|
|
if (row.progressPercentage > 50 && row.progressPercentage < 100) {
|
|
const completedProblemIds = new Set(row.completedProblems.map((p: any) => p.id))
|
|
const incompleteProblems = allProblems.value.filter((p) => !completedProblemIds.has(p.id))
|
|
return h("div", { style: "max-height: 120px; overflow-y: auto" }, [
|
|
h(NFlex, {}, () =>
|
|
incompleteProblems.map((problem) =>
|
|
h(
|
|
NTag,
|
|
{ type: "warning", size: "small", style: "margin: 2px" },
|
|
() => `${problem._id}: ${problem.title}`,
|
|
),
|
|
),
|
|
),
|
|
])
|
|
}
|
|
return h("div", { style: "max-height: 120px; overflow-y: auto" }, [
|
|
h(NFlex, {}, () =>
|
|
row.completedProblems.map((problem: any) =>
|
|
h(
|
|
NTag,
|
|
{
|
|
type: "success",
|
|
size: "small",
|
|
style: "margin: 2px",
|
|
},
|
|
() => `${problem._id}: ${problem.title}`,
|
|
),
|
|
),
|
|
),
|
|
])
|
|
},
|
|
},
|
|
{
|
|
title: "进度",
|
|
key: "progress_percentage",
|
|
width: 120,
|
|
render: (row: ProblemSetProgress) => {
|
|
return `${row.progressPercentage.toFixed(0)}%`
|
|
},
|
|
},
|
|
{
|
|
title: "状态",
|
|
key: "is_completed",
|
|
width: 100,
|
|
render: (row: ProblemSetProgress) => {
|
|
if (row.isCompleted) {
|
|
return h(NTag, { type: "success" }, () => "已完成")
|
|
} else {
|
|
return h(NTag, { type: "warning" }, () => "进行中")
|
|
}
|
|
},
|
|
},
|
|
]
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<!-- 过滤条件 -->
|
|
<n-form label-placement="left" inline>
|
|
<n-form-item label="班级">
|
|
<n-input
|
|
v-model:value="classFilter"
|
|
placeholder="输入班级名称"
|
|
style="width: 200px"
|
|
clearable
|
|
/>
|
|
</n-form-item>
|
|
|
|
<n-form-item label="完成度:">
|
|
<n-select
|
|
v-model:value="completionFilter"
|
|
:options="completionOptions"
|
|
placeholder="完成度"
|
|
style="width: 160px"
|
|
clearable
|
|
/>
|
|
</n-form-item>
|
|
</n-form>
|
|
|
|
<!-- 统计信息卡片 -->
|
|
<n-grid :cols="3" :x-gap="16" style="margin-bottom: 16px">
|
|
<n-grid-item>
|
|
<n-card size="small">
|
|
<n-statistic label="总参与人数" :value="stats.total" />
|
|
</n-card>
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-card size="small">
|
|
<n-statistic label="已完成人数" :value="stats.completed" />
|
|
</n-card>
|
|
</n-grid-item>
|
|
<n-grid-item>
|
|
<n-card size="small">
|
|
<n-statistic label="平均进度" :value="stats.avgProgress.toFixed(0) + '%'" />
|
|
</n-card>
|
|
</n-grid-item>
|
|
</n-grid>
|
|
|
|
<n-data-table
|
|
:loading="loading"
|
|
:columns="progressColumns"
|
|
:data="progress"
|
|
:pagination="false"
|
|
:bordered="false"
|
|
:single-line="false"
|
|
/>
|
|
|
|
<Pagination
|
|
:total="total"
|
|
:limit="query.limit"
|
|
:page="query.page"
|
|
@update:limit="(limit: number) => (query.limit = limit)"
|
|
@update:page="(page: number) => (query.page = page)"
|
|
/>
|
|
</div>
|
|
</template>
|