hide who is leaving
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user