新增用户和题目的数据展示
This commit is contained in:
2
src/components.d.ts
vendored
2
src/components.d.ts
vendored
@@ -34,6 +34,8 @@ declare module 'vue' {
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH2: typeof import('naive-ui')['NH2']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NH4: typeof import('naive-ui')['NH4']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
|
||||
@@ -70,6 +70,10 @@ export function getProblem(problemID: string, contestID: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getProblemBeatRate(problemID: number) {
|
||||
return http.get("problem/beat_count", { params: { problem_id: problemID } })
|
||||
}
|
||||
|
||||
export function getSubmission(id: string) {
|
||||
return http.get<Submission>("submission", {
|
||||
params: { id },
|
||||
@@ -220,3 +224,7 @@ export function getCommentStatistics(problemID: number) {
|
||||
export function refreshUserProblemDisplayIds() {
|
||||
return http.get("profile/fresh_display_id")
|
||||
}
|
||||
|
||||
export function getMetrics(userid: number) {
|
||||
return http.get("metrics", { params: { userid } })
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { problem } from "oj/composables/problem"
|
||||
import { DIFFICULTY, JUDGE_STATUS } from "utils/constants"
|
||||
import { getACRate, getTagColor, parseTime } from "utils/functions"
|
||||
import { Pie } from "vue-chartjs"
|
||||
import { getProblemBeatRate } from "~/oj/api"
|
||||
import { isDesktop } from "~/shared/composables/breakpoints"
|
||||
|
||||
const beatRate = ref("0%")
|
||||
|
||||
const data = computed(() => {
|
||||
const status = problem.value!.statistic_info
|
||||
const labels = []
|
||||
@@ -22,11 +26,46 @@ const data = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const numbers = computed(() => {
|
||||
return [
|
||||
{
|
||||
icon: "streamline-emojis:scroll",
|
||||
title: problem.value?.submission_number ?? 0,
|
||||
content: "总提交",
|
||||
},
|
||||
{
|
||||
icon: "streamline-emojis:woman-raising-hand-2",
|
||||
title: problem.value?.accepted_number ?? 0,
|
||||
content: "通过数",
|
||||
},
|
||||
{
|
||||
icon: "emojione:chart-increasing",
|
||||
title: getACRate(
|
||||
problem.value?.accepted_number ?? 0,
|
||||
problem.value?.submission_number ?? 0,
|
||||
),
|
||||
content: "通过率",
|
||||
},
|
||||
{
|
||||
icon: "streamline-emojis:sparkles",
|
||||
title: beatRate.value,
|
||||
content: "击败用户",
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
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>
|
||||
@@ -50,34 +89,45 @@ const options = {
|
||||
{{ DIFFICULTY[problem.difficulty] }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="时间限制">
|
||||
{{ problem.time_limit }}毫秒
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="内存限制">
|
||||
{{ problem.memory_limit }}MB
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="提交正确">
|
||||
{{ problem.accepted_number }}次
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="提交错误">
|
||||
{{ problem.submission_number - problem.accepted_number }}次
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="正确率">
|
||||
{{ getACRate(problem.accepted_number, problem.submission_number) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :span="3" label="标签">
|
||||
<n-descriptions-item :span="2" label="标签">
|
||||
<n-flex>
|
||||
<n-tag size="small" type="info" v-for="tag in problem.tags" :key="tag">
|
||||
<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.title">
|
||||
<n-card hoverable>
|
||||
<n-flex align="center">
|
||||
<Icon :icon="item.icon" width="40" />
|
||||
<div>
|
||||
<n-h2 class="number">{{ item.title }}</n-h2>
|
||||
<n-h4 class="number-label">{{ item.content }}</n-h4>
|
||||
</div>
|
||||
</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-bottom: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.number-label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pie {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from "@iconify/vue"
|
||||
import { NH2, NH3 } from "naive-ui"
|
||||
import { getProfile } from "~/shared/api"
|
||||
import { isDesktop } from "~/shared/composables/breakpoints"
|
||||
import { durationToDays, parseTime } from "~/utils/functions"
|
||||
import { Profile } from "~/utils/types"
|
||||
import { getMetrics } from "../api"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const profile = ref<Profile | null>(null)
|
||||
const problems = ref<string[]>([])
|
||||
const firstSubmissionAt = ref("")
|
||||
const latestSubmissionAt = ref("")
|
||||
const toLatestAt = ref("")
|
||||
const learnDuration = ref("")
|
||||
const [loading, toggle] = useToggle()
|
||||
|
||||
async function init() {
|
||||
@@ -25,11 +34,60 @@ async function init() {
|
||||
}
|
||||
ac.sort()
|
||||
problems.value = ac
|
||||
if (profile.value.submission_number > 0) {
|
||||
const metricsRes = await getMetrics(profile.value.user.id)
|
||||
firstSubmissionAt.value = parseTime(metricsRes.data.first)
|
||||
latestSubmissionAt.value = parseTime(metricsRes.data.latest)
|
||||
toLatestAt.value = durationToDays(
|
||||
metricsRes.data.latest,
|
||||
metricsRes.data.now,
|
||||
)
|
||||
learnDuration.value = durationToDays(
|
||||
metricsRes.data.first,
|
||||
metricsRes.data.latest,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
toggle(false)
|
||||
}
|
||||
}
|
||||
|
||||
const metrics = computed(() => {
|
||||
if (loading.value) return []
|
||||
return [
|
||||
{
|
||||
icon: "fluent-emoji:face-with-peeking-eye",
|
||||
title: learnDuration.value ?? "1天",
|
||||
content: "总共学习天数",
|
||||
},
|
||||
{
|
||||
icon: "fluent-emoji:cheese-wedge",
|
||||
title: toLatestAt.value,
|
||||
content: "距离上次提交",
|
||||
},
|
||||
{
|
||||
icon: "fluent-emoji:dog-face",
|
||||
title: latestSubmissionAt.value,
|
||||
content: "最新一次提交时间",
|
||||
},
|
||||
{
|
||||
icon: "fluent-emoji:cat-with-wry-smile",
|
||||
title: firstSubmissionAt.value,
|
||||
content: "第一次提交时间",
|
||||
},
|
||||
{
|
||||
icon: "fluent-emoji:candy",
|
||||
title: profile.value?.accepted_number ?? 0,
|
||||
content: "已解决的题目数量",
|
||||
},
|
||||
{
|
||||
icon: "fluent-emoji:thinking-face",
|
||||
title: profile.value?.submission_number ?? 0,
|
||||
content: "总提交数量",
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
onMounted(init)
|
||||
</script>
|
||||
<template>
|
||||
@@ -44,20 +102,31 @@ onMounted(init)
|
||||
<h2>{{ profile.user.username }}</h2>
|
||||
<p class="desc">{{ profile.mood }}</p>
|
||||
</n-flex>
|
||||
<n-descriptions
|
||||
v-if="!loading && profile"
|
||||
|
||||
<n-grid
|
||||
v-if="profile && profile.submission_number > 0"
|
||||
class="wrapper"
|
||||
bordered
|
||||
:column="2"
|
||||
label-style="width: 50%"
|
||||
:cols="2"
|
||||
:x-gap="10"
|
||||
:y-gap="10"
|
||||
>
|
||||
<n-descriptions-item label="已解决的题目数量">
|
||||
{{ profile.accepted_number }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="总提交数">
|
||||
{{ profile.submission_number }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item v-if="problems.length" label="已解决的题目" :span="2">
|
||||
<n-gi v-for="item in metrics" :key="item.title">
|
||||
<n-card hoverable>
|
||||
<n-flex align="center">
|
||||
<Icon v-if="isDesktop" :icon="item.icon" width="50" />
|
||||
<div>
|
||||
<Component :is="isDesktop ? NH2 : NH3" class="number">
|
||||
{{ item.title }}
|
||||
</Component>
|
||||
<n-h4 class="number-label">{{ item.content }}</n-h4>
|
||||
</div>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
<n-descriptions v-if="!loading && profile" class="wrapper" bordered>
|
||||
<n-descriptions-item v-if="problems.length" label="已解决的题目">
|
||||
<n-flex>
|
||||
<n-button
|
||||
v-for="id in problems"
|
||||
@@ -81,6 +150,15 @@ onMounted(init)
|
||||
margin: 16px auto 0;
|
||||
}
|
||||
|
||||
.number {
|
||||
margin-bottom: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.number-label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
|
||||
@@ -71,6 +71,27 @@ export function duration(
|
||||
return result
|
||||
}
|
||||
|
||||
export function durationToDays(
|
||||
start: Date | string,
|
||||
end: Date | string,
|
||||
): string {
|
||||
const duration = intervalToDuration({
|
||||
start: getTime(parseISO(start.toString())),
|
||||
end: getTime(parseISO(end.toString())),
|
||||
})
|
||||
let result = ""
|
||||
if (duration.years) {
|
||||
result += duration.years + "年"
|
||||
}
|
||||
if (duration.months) {
|
||||
result += duration.months + "月"
|
||||
}
|
||||
if (duration.days) {
|
||||
result += duration.days + "天"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function secondsToDuration(seconds: number): string {
|
||||
const duration = intervalToDuration({
|
||||
start: 0,
|
||||
|
||||
Reference in New Issue
Block a user