hide who is leaving
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-05-24 23:43:14 -06:00
parent 5a378b095c
commit 46c3176cd2

View File

@@ -1,15 +1,15 @@
<template> <template>
<n-flex> <n-flex align="center">
<n-input <n-input
placeholder="用户(可选)" placeholder="用户(可选)"
v-model:value="query.username" v-model:value="query.username"
style="width: 160px" style="width: 150px"
clearable clearable
/> />
<n-input <n-input
placeholder="题号(可选)" placeholder="题号(可选)"
v-model:value="query.problem" v-model:value="query.problem"
style="width: 160px" style="width: 120px"
clearable clearable
/> />
<n-select <n-select
@@ -22,79 +22,95 @@
前往提交列表 前往提交列表
</n-button> </n-button>
</n-flex> </n-flex>
<n-flex style="margin: 20px 0" v-if="count.total > 0">
<n-gradient-text font-size="24" type="primary">
正确提交数{{ count.accepted }}
</n-gradient-text>
<n-gradient-text font-size="24" type="info">
总提交数{{ count.total }}
</n-gradient-text>
<n-gradient-text font-size="24" type="warning">
正确率{{ count.rate }}
</n-gradient-text>
</n-flex>
<n-flex style="margin: 20px 0" v-if="count.total > 0">
<n-gradient-text font-size="24" type="error">
回答正确的人数{{ list.length }}
</n-gradient-text>
<n-gradient-text font-size="24" v-if="person.count > 0" type="warning">
班级人数{{ person.count }}
</n-gradient-text>
<n-gradient-text font-size="24" v-if="person.count > 0" type="success">
班级完成度{{ person.rate }}
</n-gradient-text>
</n-flex>
<n-flex style="margin: 20px 0" v-if="count.total > 0">
<n-button type="warning" @click="toggleUnaccepted(!unaccepted)">
{{ unaccepted ? "隐藏没有完成的" : "显示没有完成的" }}
</n-button>
</n-flex>
<n-flex style="margin-top: 20px">
<n-gradient-text font-size="24" v-if="count.total === 0" type="primary">
暂无数据统计
</n-gradient-text>
</n-flex>
<n-flex style="margin-bottom: 20px" v-if="unaccepted" size="large"> <n-empty v-if="count.total === 0" description="暂无数据" style="margin: 40px 0" />
<span style="font-size: 24px">
{{ listUnaccepted.length }} 位没有完成
</span>
<span style="font-size: 24px" v-for="name in listUnaccepted" :key="name">
{{ name }}
</span>
</n-flex>
<n-tabs animated v-if="count.total > 0"> <template v-if="count.total > 0">
<n-tab-pane name="charts" tab="数据图表"> <n-divider style="margin: 16px 0" />
<n-grid :cols="2" :x-gap="20" :y-gap="20" style="margin-top: 20px"> <n-flex justify="space-around">
<n-gi> <div class="stat-item">
<n-card title="提交正确率"> <div class="stat-label">总提交</div>
<Doughnut :data="pieChartData" :options="pieChartOptions" /> <n-gradient-text type="info" font-size="28">{{ count.total }}</n-gradient-text>
</n-card> </div>
</n-gi> <div class="stat-item">
<n-gi v-if="person.count > 0"> <div class="stat-label">正确提交</div>
<n-card title="班级完成度"> <n-gradient-text type="primary" font-size="28">{{ count.accepted }}</n-gradient-text>
<Doughnut </div>
:data="completionChartData" <div class="stat-item">
:options="completionChartOptions" <div class="stat-label">正确率</div>
/> <n-gradient-text type="warning" font-size="28">{{ count.rate }}</n-gradient-text>
</n-card> </div>
</n-gi> <template v-if="person.count > 0">
</n-grid> <div class="stat-item">
</n-tab-pane> <div class="stat-label">完成人数</div>
<n-tab-pane name="submissions" tab="提交记录"> <n-gradient-text type="error" font-size="28">{{ list.length }}</n-gradient-text>
<n-data-table </div>
v-if="list.length" <div class="stat-item">
striped <div class="stat-label">班级人数</div>
:columns="columns" <n-gradient-text type="warning" font-size="28">{{ adjustedPersonCount }}</n-gradient-text>
:data="list" </div>
:row-key="rowKey" <div class="stat-item">
:expanded-row-keys="expandedRowKeys" <div class="stat-label">完成度</div>
@update:expanded-row-keys="updateExpandedRowKeys" <n-gradient-text type="success" font-size="28">{{ adjustedPersonRate }}</n-gradient-text>
:row-props="rowProps" </div>
/> </template>
</n-tab-pane> </n-flex>
</n-tabs> <n-divider style="margin: 16px 0" />
<n-tabs animated type="line">
<n-tab-pane name="charts" tab="数据图表">
<n-grid :cols="2" :x-gap="20" :y-gap="20" style="margin-top: 12px">
<n-gi>
<n-card title="提交正确率">
<Doughnut :data="pieChartData" :options="pieChartOptions" />
</n-card>
</n-gi>
<n-gi v-if="person.count > 0">
<n-card title="班级完成度">
<Doughnut :data="completionChartData" :options="completionChartOptions" />
</n-card>
</n-gi>
</n-grid>
</n-tab-pane>
<n-tab-pane name="submissions" tab="提交记录">
<n-data-table
v-if="list.length"
striped
:columns="columns"
:data="list"
:row-key="rowKey"
:expanded-row-keys="expandedRowKeys"
@update:expanded-row-keys="updateExpandedRowKeys"
:row-props="rowProps"
style="margin-top: 12px"
/>
</n-tab-pane>
<n-tab-pane name="unaccepted" :tab="`未完成(${visibleUnaccepted.length}`">
<n-flex align="center" style="margin: 12px 0">
<n-switch v-model:value="hideMode" size="large">
<template #checked>请假隐藏中</template>
<template #unchecked>请假隐藏</template>
</n-switch>
<n-button v-if="hiddenCount > 0" size="small" type="info" @click="showAll">
恢复 {{ hiddenCount }}
</n-button>
</n-flex>
<n-flex size="large" align="center">
<n-gradient-text v-if="visibleUnaccepted.length === 0" font-size="24" type="success">
全都完成了
</n-gradient-text>
<template v-for="item in visibleUnaccepted" :key="item.username">
<n-tag v-if="hideMode" closable size="large" style="font-size: 20px" @close="hideStudent(item.username)">
{{ item.real_name }}
</n-tag>
<span v-else style="font-size: 24px">{{ item.real_name }}</span>
</template>
</n-flex>
</n-tab-pane>
</n-tabs>
</template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { h } from "vue" import { h } from "vue"
@@ -188,11 +204,79 @@ interface UserStatistic {
}> }>
} }
interface UnacceptedItem {
username: string
real_name: string
}
const list = ref<UserStatistic[]>([]) const list = ref<UserStatistic[]>([])
const listUnaccepted = ref<string[]>([]) const listUnaccepted = ref<UnacceptedItem[]>([])
const [unaccepted, toggleUnaccepted] = useToggle()
const expandedRowKeys = ref<DataTableRowKey[]>([]) const expandedRowKeys = ref<DataTableRowKey[]>([])
const HIDE_DURATION = 2 * 60 * 60 * 1000
const STORAGE_KEY = "oj_hidden_students"
function loadHidden(): Record<string, number> {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}")
} catch {
return {}
}
}
const hiddenStudents = ref<Record<string, number>>(loadHidden())
const hideMode = ref(false)
function saveHidden(data: Record<string, number>) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
}
function hideStudent(username: string) {
hiddenStudents.value = {
...hiddenStudents.value,
[username]: Date.now() + HIDE_DURATION,
}
saveHidden(hiddenStudents.value)
}
function showAll() {
hiddenStudents.value = {}
saveHidden({})
}
const visibleUnaccepted = computed(() => {
const now = Date.now()
return listUnaccepted.value.filter((item) => {
const exp = hiddenStudents.value[item.username]
return !exp || exp <= now
})
})
const hiddenCount = computed(() => {
const now = Date.now()
return listUnaccepted.value.filter((item) => {
const exp = hiddenStudents.value[item.username]
return !!exp && exp > now
}).length
})
const adjustedPersonCount = computed(() => person.count - hiddenCount.value)
const adjustedPersonRate = computed(() => {
if (adjustedPersonCount.value <= 0) return "0%"
const rate = Math.min(100, (list.value.length / adjustedPersonCount.value) * 100)
return `${Math.round(rate * 100) / 100}%`
})
onMounted(() => {
const now = Date.now()
const cleaned = Object.fromEntries(
Object.entries(hiddenStudents.value).filter(([, exp]) => exp > now),
)
hiddenStudents.value = cleaned
saveHidden(cleaned)
})
// 饼图数据 - 提交正确率分布 // 饼图数据 - 提交正确率分布
const pieChartData = computed(() => { const pieChartData = computed(() => {
const wrongCount = count.total - count.accepted const wrongCount = count.total - count.accepted
@@ -237,7 +321,7 @@ const pieChartOptions = {
// 环形图数据 - 班级完成度 // 环形图数据 - 班级完成度
const completionChartData = computed(() => { const completionChartData = computed(() => {
const completedCount = list.value.length const completedCount = list.value.length
const uncompletedCount = person.count - completedCount const uncompletedCount = Math.max(0, adjustedPersonCount.value - completedCount)
return { return {
labels: ["已完成", "未完成"], labels: ["已完成", "未完成"],
datasets: [ datasets: [
@@ -312,8 +396,6 @@ async function handleStatistics() {
listUnaccepted.value = res.data.data_unaccepted listUnaccepted.value = res.data.data_unaccepted
person.count = res.data.person_count person.count = res.data.person_count
person.rate = res.data.person_rate person.rate = res.data.person_rate
toggleUnaccepted(false)
} }
function rowKey(row: UserStatistic): DataTableRowKey { function rowKey(row: UserStatistic): DataTableRowKey {
@@ -335,4 +417,15 @@ function rowProps(row: UserStatistic) {
} }
} }
</script> </script>
<style scoped></style> <style scoped>
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.stat-label {
font-size: 13px;
color: var(--n-text-color-3, #999);
}
</style>