update ui
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
2025-09-24 14:30:17 +08:00
parent 7bd6f9170a
commit 67a3b7fb26
4 changed files with 222 additions and 183 deletions

3
.gitignore vendored
View File

@@ -22,3 +22,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
src/components.d.ts
src/auto-imports.d.ts

View File

@@ -6,7 +6,7 @@
<n-select <n-select
style="width: 140px" style="width: 140px"
:options="options" :options="options"
v-model:value="duration" v-model:value="query.duration"
/> />
<n-flex> <n-flex>
<n-input <n-input
@@ -15,168 +15,44 @@
v-if="userStore.isSuperAdmin" v-if="userStore.isSuperAdmin"
v-model:value="username" v-model:value="username"
/> />
<n-button @click="init">查询</n-button> <n-button @click="search">查询</n-button>
</n-flex> </n-flex>
</n-flex> </n-flex>
<n-spin :show="detailLoading"> <Details
<n-flex vertical size="large"> :start="start"
<n-alert :end="end"
:show-icon="false" :duration="query.duration"
type="success" :username="query.username"
v-if="solvedProblems.length" />
>
<span>{{ durationLabel }}</span>
<span>{{ !!username ? username : "你" }}一共解决 </span>
<b class="charming"> {{ solvedProblems.length }} </b>
<span> 道题</span>
<span v-if="contest_count > 0">
并且参加
<b class="charming"> {{ contest_count }} </b> 次比赛
</span>
<span>综合评价给到</span>
<Grade :grade="grade" />
<span>{{ greeting }}</span>
</n-alert>
<n-alert type="error" v-else title="你还没有完成任何题目"></n-alert>
<n-flex>
<TagsChart :tags="tags" />
<DifficultyChart :difficulty="difficulty" />
</n-flex>
<n-data-table
v-if="solvedProblems.length"
striped
:max-height="400"
:data="solvedProblems"
:columns="columns"
/>
</n-flex>
</n-spin>
</n-flex> </n-flex>
</n-gi> </n-gi>
<n-gi :span="3"> <n-gi :span="3">
<n-spin :show="weeklyLoading"> <WeeklyChart
<WeeklyChart :weeklyData="weeklyData" :duration="duration" /> :end="end"
</n-spin> :username="query.username"
:duration="query.duration"
/>
</n-gi> </n-gi>
</n-grid> </n-grid>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from "vue" import { ref, computed, watch } from "vue"
import { formatISO, sub, type Duration } from "date-fns" import { formatISO, sub, type Duration } from "date-fns"
import { getAIDetailData, getAIWeeklyData } from "../api"
import { NButton } from "naive-ui" import { NButton } from "naive-ui"
import { parseTime } from "~/utils/functions"
import TagTitle from "./components/TagTitle.vue"
import TagsChart from "./components/TagsChart.vue"
import DifficultyChart from "./components/DifficultyChart.vue"
import WeeklyChart from "./components/WeeklyChart.vue" import WeeklyChart from "./components/WeeklyChart.vue"
import Grade from "./components/Grade.vue" import Details from "./components/Details.vue"
import { WeeklyData } from "~/utils/types"
import { useUserStore } from "~/shared/store/user" import { useUserStore } from "~/shared/store/user"
const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const start = ref("") const start = ref("")
const end = ref("") const end = ref("")
const duration = ref("months:6")
const username = ref("") const username = ref("")
const query = reactive({
const startLabel = ref("") username: "",
const endLabel = ref("") duration: "months:6",
const weeklyLoading = ref(false)
const detailLoading = ref(false)
const durationLabel = computed(() => {
if (duration.value.includes("hours")) {
return `${parseTime(startLabel.value, "HH:mm")} - ${parseTime(endLabel.value, "HH:mm")} 期间`
} else if (duration.value.includes("days")) {
return `${parseTime(endLabel.value, "MM月DD日")}`
} else if (
duration.value.includes("weeks") ||
duration.value.includes("months")
) {
return `${parseTime(startLabel.value, "MM月DD日")} - ${parseTime(endLabel.value, "MM月DD日")} 期间`
} else {
return `${parseTime(startLabel.value, "YYYY年MM月DD日")} - ${parseTime(endLabel.value, "YYYY年MM月DD日")} 期间`
}
}) })
const greeting = computed(() => {
return {
S: "要不试试高难度题目?",
A: "你很棒,继续保持!",
B: "请再接再厉!",
C: "你还需要努力!",
}[grade.value]
})
interface SolvedProblem {
problem: {
title: string
display_id: string
contest_title: string
contest_id: number
}
ac_time: string
rank: number
ac_count: number
grade: "S" | "A" | "B" | "C"
}
const solvedProblems = ref<SolvedProblem[]>([])
const grade = ref<"S" | "A" | "B" | "C">("B")
const class_name = ref("")
const tags = ref<{ [key: string]: number }>({})
const difficulty = ref<{ [key: string]: number }>({})
const contest_count = ref(0)
const columns: DataTableColumn<SolvedProblem>[] = [
{
title: "完成的题目",
key: "problem.title",
render: (row) =>
h(
NButton,
{
text: true,
onClick: () => {
if (row.problem.contest_id) {
router.push(
"/contest/" +
row.problem.contest_id +
"/problem/" +
row.problem.display_id,
)
} else {
router.push("/problem/" + row.problem.display_id)
}
},
},
() => {
if (row.problem.contest_id) {
return h(TagTitle, { problem: row.problem })
} else {
return row.problem.display_id + " " + row.problem.title
}
},
),
},
{
title: () => (class_name ? "班级排名" : "全服排名"),
key: "rank",
width: 100,
align: "center",
render: (row) => row.rank + " / " + row.ac_count,
},
{
title: "等级",
key: "grade",
width: 100,
align: "center",
},
]
const options: SelectOption[] = [ const options: SelectOption[] = [
{ label: "一节课内", value: "hours:1" }, { label: "一节课内", value: "hours:1" },
{ label: "两节课内", value: "hours:2" }, { label: "两节课内", value: "hours:2" },
@@ -189,7 +65,7 @@ const options: SelectOption[] = [
] ]
const subOptions = computed<Duration>(() => { const subOptions = computed<Duration>(() => {
let dur = options.find((it) => it.value === duration.value) ?? options[0] let dur = options.find((it) => it.value === query.duration) ?? options[0]
const x = dur.value!.toString().split(":") const x = dur.value!.toString().split(":")
const unit = x[0] const unit = x[0]
const n = x[1] const n = x[1]
@@ -202,39 +78,9 @@ function updateRange() {
start.value = formatISO(sub(current, subOptions.value)) start.value = formatISO(sub(current, subOptions.value))
} }
async function getDetail() { function search() {
detailLoading.value = true query.username = username.value
const res = await getAIDetailData(start.value, end.value, username.value)
detailLoading.value = false
startLabel.value = res.data.start
endLabel.value = res.data.end
solvedProblems.value = res.data.solved
grade.value = res.data.grade
class_name.value = res.data.class_name
tags.value = res.data.tags
difficulty.value = res.data.difficulty
contest_count.value = res.data.contest_count
} }
const weeklyData = ref<WeeklyData[]>([]) watch(() => query.duration, updateRange, { immediate: true })
async function getWeeklyData() {
weeklyLoading.value = true
const res = await getAIWeeklyData(end.value, duration.value, username.value)
weeklyData.value = res.data
weeklyLoading.value = false
}
function init() {
updateRange()
getDetail()
getWeeklyData()
}
watch(duration, init, { immediate: true })
</script> </script>
<style scoped>
.charming {
font-size: 1.2rem;
}
</style>

View File

@@ -0,0 +1,171 @@
<template>
<n-spin :show="detailLoading">
<n-flex vertical size="large">
<n-alert :show-icon="false" type="success" v-if="solvedProblems.length">
<span>{{ durationLabel }}</span>
<span>{{ !!username ? username : "你" }}一共解决 </span>
<b class="charming"> {{ solvedProblems.length }} </b>
<span> 道题</span>
<span v-if="contest_count > 0">
并且参加
<b class="charming"> {{ contest_count }} </b> 次比赛
</span>
<span>综合评价给到</span>
<Grade :grade="grade" />
<span>{{ greeting }}</span>
</n-alert>
<n-alert
type="error"
v-else
:title="(!!username ? username : '你') + '还没有完成任何题目'"
></n-alert>
<n-flex>
<TagsChart :tags="tags" />
<DifficultyChart :difficulty="difficulty" />
</n-flex>
<n-data-table
v-if="solvedProblems.length"
striped
:max-height="400"
:data="solvedProblems"
:columns="columns"
/>
</n-flex>
</n-spin>
</template>
<script lang="ts" setup>
import { NButton } from "naive-ui"
import Grade from "./Grade.vue"
import TagsChart from "./TagsChart.vue"
import DifficultyChart from "./DifficultyChart.vue"
import TagTitle from "./TagTitle.vue"
import { parseTime } from "~/utils/functions"
import { getAIDetailData } from "~/oj/api"
const props = defineProps<{
start: string
end: string
duration: string
username: string
}>()
const router = useRouter()
interface SolvedProblem {
problem: {
title: string
display_id: string
contest_title: string
contest_id: number
}
ac_time: string
rank: number
ac_count: number
grade: "S" | "A" | "B" | "C"
}
const detailLoading = ref(false)
const startLabel = ref("")
const endLabel = ref("")
const grade = ref<"S" | "A" | "B" | "C">("B")
const class_name = ref("")
const tags = ref<{ [key: string]: number }>({})
const difficulty = ref<{ [key: string]: number }>({})
const contest_count = ref(0)
const solvedProblems = ref<SolvedProblem[]>([])
const columns: DataTableColumn<SolvedProblem>[] = [
{
title: "完成的题目",
key: "problem.title",
render: (row) =>
h(
NButton,
{
text: true,
onClick: () => {
if (row.problem.contest_id) {
router.push(
"/contest/" +
row.problem.contest_id +
"/problem/" +
row.problem.display_id,
)
} else {
router.push("/problem/" + row.problem.display_id)
}
},
},
() => {
if (row.problem.contest_id) {
return h(TagTitle, { problem: row.problem })
} else {
return row.problem.display_id + " " + row.problem.title
}
},
),
},
{
title: () => (class_name ? "班级排名" : "全服排名"),
key: "rank",
width: 100,
align: "center",
render: (row) => row.rank + " / " + row.ac_count,
},
{
title: "等级",
key: "grade",
width: 100,
align: "center",
},
]
const durationLabel = computed(() => {
if (props.duration.includes("hours")) {
return `${parseTime(startLabel.value, "HH:mm")} - ${parseTime(endLabel.value, "HH:mm")} 期间`
} else if (props.duration.includes("days")) {
return `${parseTime(endLabel.value, "MM月DD日")}`
} else if (
props.duration.includes("weeks") ||
props.duration.includes("months")
) {
return `${parseTime(startLabel.value, "MM月DD日")} - ${parseTime(endLabel.value, "MM月DD日")} 期间`
} else {
return `${parseTime(startLabel.value, "YYYY年MM月DD日")} - ${parseTime(endLabel.value, "YYYY年MM月DD日")} 期间`
}
})
const greeting = computed(() => {
return {
S: "要不试试高难度题目?",
A: "你很棒,继续保持!",
B: "请再接再厉!",
C: "你还需要努力!",
}[grade.value]
})
async function getDetail() {
detailLoading.value = true
const res = await getAIDetailData(props.start, props.end, props.username)
detailLoading.value = false
startLabel.value = res.data.start
endLabel.value = res.data.end
solvedProblems.value = res.data.solved
grade.value = res.data.grade
class_name.value = res.data.class_name
tags.value = res.data.tags
difficulty.value = res.data.difficulty
contest_count.value = res.data.contest_count
}
watch(() => [props.duration, props.username], getDetail, { immediate: true })
</script>
<style scoped>
.charming {
font-size: 1.2rem;
}
</style>

View File

@@ -1,19 +1,25 @@
<template> <template>
<div class="chart"> <n-spin :show="weeklyLoading">
<Chart type="bar" :data="data" :options="options" /> <div class="chart">
</div> <Chart type="bar" :data="data" :options="options" />
</div>
</n-spin>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { ChartData, ChartOptions, TooltipItem } from "chart.js" import type { ChartData, ChartOptions, TooltipItem } from "chart.js"
import { Chart } from "vue-chartjs" import { Chart } from "vue-chartjs"
import { parseTime } from "~/utils/functions" import { parseTime } from "~/utils/functions"
import { WeeklyData } from "~/utils/types" import { WeeklyData } from "~/utils/types"
import { getAIWeeklyData } from "~/oj/api"
const props = defineProps<{ const props = defineProps<{
weeklyData: WeeklyData[]
duration: string duration: string
end: string
username: string
}>() }>()
const weeklyLoading = ref(false)
const gradeOrder = ["C", "B", "A", "S"] as const const gradeOrder = ["C", "B", "A", "S"] as const
const title = computed(() => { const title = computed(() => {
@@ -30,7 +36,7 @@ const title = computed(() => {
const data = computed<ChartData<"bar" | "line">>(() => { const data = computed<ChartData<"bar" | "line">>(() => {
return { return {
labels: props.weeklyData.map((weekly) => { labels: weeklyData.value.map((weekly) => {
let prefix = "周" let prefix = "周"
if (weekly.unit === "months") { if (weekly.unit === "months") {
prefix = "月" prefix = "月"
@@ -45,19 +51,19 @@ const data = computed<ChartData<"bar" | "line">>(() => {
{ {
type: "bar", type: "bar",
label: "完成题目数量", label: "完成题目数量",
data: props.weeklyData.map((weekly) => weekly.problem_count), data: weeklyData.value.map((weekly) => weekly.problem_count),
yAxisID: "y", yAxisID: "y",
}, },
{ {
type: "bar", type: "bar",
label: "总提交次数", label: "总提交次数",
data: props.weeklyData.map((weekly) => weekly.submission_count), data: weeklyData.value.map((weekly) => weekly.submission_count),
yAxisID: "y", yAxisID: "y",
}, },
{ {
type: "line", type: "line",
label: "等级", label: "等级",
data: props.weeklyData.map((weekly) => data: weeklyData.value.map((weekly) =>
gradeOrder.indexOf(weekly.grade || "C"), gradeOrder.indexOf(weekly.grade || "C"),
), ),
tension: 0.4, tension: 0.4,
@@ -117,6 +123,19 @@ const options = computed<ChartOptions<"bar" | "line">>(() => {
}, },
} }
}) })
const weeklyData = ref<WeeklyData[]>([])
async function getWeeklyData() {
weeklyLoading.value = true
const res = await getAIWeeklyData(props.end, props.duration, props.username)
weeklyData.value = res.data
weeklyLoading.value = false
}
watch(() => [props.duration, props.username], getWeeklyData, {
immediate: true,
})
</script> </script>
<style scoped> <style scoped>
.chart { .chart {