Files
ojnext/src/oj/problem/components/ProblemInfo.vue
yuetsh b14316b919
Some checks failed
Deploy / deploy (push) Has been cancelled
batch update
2025-10-08 00:46:49 +08:00

166 lines
3.9 KiB
Vue

<script setup lang="ts">
import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import { useProblemStore } from "oj/store/problem"
import { DIFFICULTY, JUDGE_STATUS } from "utils/constants"
import { getACRateNumber, getTagColor, parseTime } from "utils/functions"
import { Pie } from "vue-chartjs"
import {
Chart as ChartJS,
ArcElement,
Title,
Tooltip,
Legend,
Colors,
} from "chart.js"
import { getProblemBeatRate } from "oj/api"
import { useBreakpoints } from "shared/composables/breakpoints"
// 仅注册饼图所需的 Chart.js 组件
ChartJS.register(ArcElement, Title, Tooltip, Legend, Colors)
const problemStore = useProblemStore()
const { problem } = storeToRefs(problemStore)
const { isDesktop } = useBreakpoints()
const beatRate = ref("0")
const data = computed(() => {
const status = problem.value!.statistic_info
const labels = []
for (let i in status) {
if (status[i] !== 0) {
// @ts-ignore
labels.push(JUDGE_STATUS[i]["name"])
}
}
return {
labels,
datasets: [
{ data: Object.values(status), hoverOffset: 5, borderRadius: 10 },
],
}
})
const numbers = computed(() => {
return [
{
icon: "streamline-emojis:scroll",
title: problem.value?.submission_number ?? 0,
content: "总提交",
int: true,
suffix: "",
},
{
icon: "streamline-emojis:woman-raising-hand-2",
title: problem.value?.accepted_number ?? 0,
content: "通过数",
int: true,
suffix: "",
},
{
icon: "emojione:chart-increasing",
title: getACRateNumber(
problem.value?.accepted_number ?? 0,
problem.value?.submission_number ?? 0,
),
content: "通过率",
int: false,
suffix: "%",
},
{
icon: "streamline-emojis:sparkles",
title: parseFloat(beatRate.value),
content: "击败用户",
int: false,
suffix: "%",
},
]
})
const options = {
plugins: {
title: { text: "提交结果的比例", display: true, font: { size: 20 } },
},
}
async function getBeatRate() {
const res = await getProblemBeatRate(problem.value!.id)
beatRate.value = res.data
}
onMounted(getBeatRate)
</script>
<template>
<n-descriptions
bordered
label-placement="left"
:column="isDesktop ? 3 : 1"
v-if="problem"
>
<n-descriptions-item label="编号">
{{ problem._id }}
</n-descriptions-item>
<n-descriptions-item label="出题人">
{{ problem.created_by.username }}
</n-descriptions-item>
<n-descriptions-item label="创建时间">
{{ parseTime(problem.create_time) }}
</n-descriptions-item>
<n-descriptions-item label="难度">
<n-tag :type="getTagColor(problem.difficulty)">
{{ DIFFICULTY[problem.difficulty] }}
</n-tag>
</n-descriptions-item>
<n-descriptions-item :span="2" label="标签">
<n-flex>
<n-tag type="info" v-for="tag in problem.tags" :key="tag">
{{ tag }}
</n-tag>
</n-flex>
</n-descriptions-item>
</n-descriptions>
<n-grid :cols="isDesktop ? 4 : 2" :x-gap="10" :y-gap="10" class="cards">
<n-gi v-for="item in numbers" :key="item.content">
<n-card hoverable>
<n-flex vertical align="center">
<Icon v-if="isDesktop" :icon="item.icon" width="40" />
<n-h2 class="number">
<n-number-animation
:to="item.title"
:precision="item.int ? 0 : 2"
/>
<span v-if="item.suffix">{{ item.suffix }}</span>
</n-h2>
<n-h4 class="number-label">{{ item.content }}</n-h4>
</n-flex>
</n-card>
</n-gi>
</n-grid>
<div class="pie" v-if="problem && problem.submission_number > 0">
<Pie :data="data" :options="options" />
</div>
</template>
<style scoped>
.cards {
margin-top: 24px;
}
.number {
margin: 0;
font-weight: bold;
}
.number-label {
margin: 0;
}
.pie {
width: 100%;
max-width: 500px;
margin: 24px auto;
}
</style>