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

This commit is contained in:
2025-09-24 16:20:48 +08:00
parent de6ac4bd85
commit 0a9d49b526
7 changed files with 211 additions and 119 deletions

View File

@@ -6,7 +6,7 @@
<n-select <n-select
style="width: 140px" style="width: 140px"
:options="options" :options="options"
v-model:value="query.duration" v-model:value="aiStore.duration"
/> />
<n-flex> <n-flex>
<n-input <n-input
@@ -18,20 +18,14 @@
<n-button @click="search">查询</n-button> <n-button @click="search">查询</n-button>
</n-flex> </n-flex>
</n-flex> </n-flex>
<Details <Details :start="start" :end="end" />
:start="start"
:end="end"
:duration="query.duration"
:username="query.username"
/>
</n-flex> </n-flex>
</n-gi> </n-gi>
<n-gi :span="3"> <n-gi :span="3">
<WeeklyChart <n-flex vertical size="large">
:end="end" <WeeklyChart :end="end" />
:username="query.username" <AI v-if="aiStore.detailsData.solved.length" />
:duration="query.duration" </n-flex>
/>
</n-gi> </n-gi>
</n-grid> </n-grid>
</template> </template>
@@ -41,17 +35,16 @@ import { formatISO, sub, type Duration } from "date-fns"
import { NButton } from "naive-ui" import { NButton } from "naive-ui"
import WeeklyChart from "./components/WeeklyChart.vue" import WeeklyChart from "./components/WeeklyChart.vue"
import Details from "./components/Details.vue" import Details from "./components/Details.vue"
import AI from "./components/AI.vue"
import { useUserStore } from "~/shared/store/user" import { useUserStore } from "~/shared/store/user"
import { useAIStore } from "../store/ai"
const userStore = useUserStore() const userStore = useUserStore()
const aiStore = useAIStore()
const start = ref("") const start = ref("")
const end = ref("") const end = ref("")
const username = ref("") const username = ref("")
const query = reactive({
username: "",
duration: "months:6",
})
const options: SelectOption[] = [ const options: SelectOption[] = [
{ label: "一节课内", value: "hours:1" }, { label: "一节课内", value: "hours:1" },
@@ -65,7 +58,7 @@ const options: SelectOption[] = [
] ]
const subOptions = computed<Duration>(() => { const subOptions = computed<Duration>(() => {
let dur = options.find((it) => it.value === query.duration) ?? options[0] let dur = options.find((it) => it.value === aiStore.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]
@@ -79,8 +72,8 @@ function updateRange() {
} }
function search() { function search() {
query.username = username.value aiStore.username = username.value
} }
watch(() => query.duration, updateRange, { immediate: true }) watch(() => aiStore.duration, updateRange, { immediate: true })
</script> </script>

View File

@@ -0,0 +1,25 @@
<template>
<n-spin :show="aiStore.loading.ai">
<div>AI 小助手友情提醒</div>
</n-spin>
</template>
<script setup lang="ts">
import { useAIStore } from "~/oj/store/ai"
const aiStore = useAIStore()
watch(
() => [
aiStore.loading.details,
aiStore.loading.weekly,
],
(newVal, oldVal) => {
if (!oldVal) return
const loaded = newVal.some((val, idx) => val === false && oldVal[idx] === true)
if (loaded) {
aiStore.fetchAIAnalysis()
}
},
{ immediate: true },
)
</script>

View File

@@ -1,33 +1,40 @@
<template> <template>
<n-spin :show="detailLoading"> <n-spin :show="aiStore.loading.details">
<n-flex vertical size="large"> <n-flex vertical size="large">
<n-alert :show-icon="false" type="success" v-if="solvedProblems.length"> <n-alert
:show-icon="false"
type="success"
v-if="aiStore.detailsData.solved.length"
>
<span>{{ durationLabel }}</span> <span>{{ durationLabel }}</span>
<span>{{ !!username ? username : "你" }}一共解决 </span> <span>{{ aiStore.theFirstPerson }}一共解决 </span>
<b class="charming"> {{ solvedProblems.length }} </b> <b class="charming"> {{ aiStore.detailsData.solved.length }} </b>
<span> 道题</span> <span> 道题</span>
<span v-if="contest_count > 0"> <span v-if="aiStore.detailsData.contest_count > 0">
并且参加 并且参加
<b class="charming"> {{ contest_count }} </b> 次比赛 <b class="charming"> {{ aiStore.detailsData.contest_count }} </b>
次比赛
</span> </span>
<span>综合评价给到</span> <span>综合评价给到</span>
<Grade :grade="grade" /> <Grade :grade="aiStore.detailsData.grade" />
<span>{{ greeting }}</span> <span>{{ greeting }}</span>
</n-alert> </n-alert>
<n-alert <n-flex vertical size="large" v-else>
type="error" <n-alert
v-else type="error"
:title="(!!username ? username : '你') + '还没有完成任何题目'" :title="aiStore.theFirstPerson + '还没有完成任何题目'"
></n-alert> ></n-alert>
<AI />
</n-flex>
<n-flex> <n-flex>
<TagsChart :tags="tags" /> <TagsChart :tags="aiStore.detailsData.tags" />
<DifficultyChart :difficulty="difficulty" /> <DifficultyChart :difficulty="aiStore.detailsData.difficulty" />
</n-flex> </n-flex>
<n-data-table <n-data-table
v-if="solvedProblems.length" v-if="aiStore.detailsData.solved.length"
striped striped
:max-height="400" :max-height="400"
:data="solvedProblems" :data="aiStore.detailsData.solved"
:columns="columns" :columns="columns"
/> />
</n-flex> </n-flex>
@@ -39,43 +46,18 @@ import Grade from "./Grade.vue"
import TagsChart from "./TagsChart.vue" import TagsChart from "./TagsChart.vue"
import DifficultyChart from "./DifficultyChart.vue" import DifficultyChart from "./DifficultyChart.vue"
import TagTitle from "./TagTitle.vue" import TagTitle from "./TagTitle.vue"
import AI from "./AI.vue"
import { parseTime } from "~/utils/functions" import { parseTime } from "~/utils/functions"
import { getAIDetailData } from "~/oj/api" import { SolvedProblem } from "~/utils/types"
import { useAIStore } from "~/oj/store/ai"
const props = defineProps<{ const props = defineProps<{
start: string start: string
end: string end: string
duration: string
username: string
}>() }>()
const router = useRouter() const router = useRouter()
const aiStore = useAIStore()
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>[] = [ const columns: DataTableColumn<SolvedProblem>[] = [
{ {
@@ -109,7 +91,7 @@ const columns: DataTableColumn<SolvedProblem>[] = [
), ),
}, },
{ {
title: () => (class_name ? "班级排名" : "全服排名"), title: () => (aiStore.detailsData.class_name ? "班级排名" : "全服排名"),
key: "rank", key: "rank",
width: 100, width: 100,
align: "center", align: "center",
@@ -124,17 +106,17 @@ const columns: DataTableColumn<SolvedProblem>[] = [
] ]
const durationLabel = computed(() => { const durationLabel = computed(() => {
if (props.duration.includes("hours")) { if (aiStore.duration.includes("hours")) {
return `${parseTime(startLabel.value, "HH:mm")} - ${parseTime(endLabel.value, "HH:mm")} 期间` return `${parseTime(aiStore.detailsData.start, "HH:mm")} - ${parseTime(aiStore.detailsData.end, "HH:mm")} 期间`
} else if (props.duration.includes("days")) { } else if (aiStore.duration.includes("days")) {
return `${parseTime(endLabel.value, "MM月DD日")}` return `${parseTime(aiStore.detailsData.end, "MM月DD日")}`
} else if ( } else if (
props.duration.includes("weeks") || aiStore.duration.includes("weeks") ||
props.duration.includes("months") aiStore.duration.includes("months")
) { ) {
return `${parseTime(startLabel.value, "MM月DD日")} - ${parseTime(endLabel.value, "MM月DD日")} 期间` return `${parseTime(aiStore.detailsData.start, "MM月DD日")} - ${parseTime(aiStore.detailsData.end, "MM月DD日")} 期间`
} else { } else {
return `${parseTime(startLabel.value, "YYYY年MM月DD日")} - ${parseTime(endLabel.value, "YYYY年MM月DD日")} 期间` return `${parseTime(aiStore.detailsData.start, "YYYY年MM月DD日")} - ${parseTime(aiStore.detailsData.end, "YYYY年MM月DD日")} 期间`
} }
}) })
@@ -144,25 +126,16 @@ const greeting = computed(() => {
A: "你很棒,继续保持!", A: "你很棒,继续保持!",
B: "请再接再厉!", B: "请再接再厉!",
C: "你还需要努力!", C: "你还需要努力!",
}[grade.value] }[aiStore.detailsData.grade]
}) })
async function getDetail() { watch(
detailLoading.value = true () => [aiStore.duration, aiStore.username],
const res = await getAIDetailData(props.start, props.end, props.username) () => {
detailLoading.value = false aiStore.fetchDetailsData(props.start, props.end, aiStore.username)
},
startLabel.value = res.data.start { immediate: true },
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> </script>
<style scoped> <style scoped>
.charming { .charming {

View File

@@ -1,5 +1,5 @@
<template> <template>
<n-spin :show="weeklyLoading"> <n-spin :show="aiStore.loading.weekly">
<div class="chart"> <div class="chart">
<Chart type="bar" :data="data" :options="options" /> <Chart type="bar" :data="data" :options="options" />
</div> </div>
@@ -8,26 +8,23 @@
<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 { useAIStore } from "~/oj/store/ai"
import { parseTime } from "~/utils/functions" import { parseTime } from "~/utils/functions"
import { WeeklyData } from "~/utils/types"
import { getAIWeeklyData } from "~/oj/api"
const props = defineProps<{ const props = defineProps<{
duration: string
end: string end: string
username: string
}>() }>()
const weeklyLoading = ref(false) const aiStore = useAIStore()
const gradeOrder = ["C", "B", "A", "S"] as const const gradeOrder = ["C", "B", "A", "S"] as const
const title = computed(() => { const title = computed(() => {
if (props.duration === "months:2") { if (aiStore.duration === "months:2") {
return "过去两个月的每周综合情况一览图" return "过去两个月的每周综合情况一览图"
} else if (props.duration === "months:6") { } else if (aiStore.duration === "months:6") {
return "过去半年的每月综合情况一览图" return "过去半年的每月综合情况一览图"
} else if (props.duration === "years:1") { } else if (aiStore.duration === "years:1") {
return "过去一年的每月综合情况一览图" return "过去一年的每月综合情况一览图"
} else { } else {
return "过去四周的综合情况一览图" return "过去四周的综合情况一览图"
@@ -36,12 +33,11 @@ const title = computed(() => {
const data = computed<ChartData<"bar" | "line">>(() => { const data = computed<ChartData<"bar" | "line">>(() => {
return { return {
labels: weeklyData.value.map((weekly) => { labels: aiStore.weeklyData.map((weekly) => {
let prefix = "周" let prefix = "周"
if (weekly.unit === "months") { if (weekly.unit === "months") {
prefix = "月" prefix = "月"
} }
return [ return [
parseTime(weekly.start, "M月D日"), parseTime(weekly.start, "M月D日"),
parseTime(weekly.end, "M月D日"), parseTime(weekly.end, "M月D日"),
@@ -51,19 +47,19 @@ const data = computed<ChartData<"bar" | "line">>(() => {
{ {
type: "bar", type: "bar",
label: "完成题目数量", label: "完成题目数量",
data: weeklyData.value.map((weekly) => weekly.problem_count), data: aiStore.weeklyData.map((weekly) => weekly.problem_count),
yAxisID: "y", yAxisID: "y",
}, },
{ {
type: "bar", type: "bar",
label: "总提交次数", label: "总提交次数",
data: weeklyData.value.map((weekly) => weekly.submission_count), data: aiStore.weeklyData.map((weekly) => weekly.submission_count),
yAxisID: "y", yAxisID: "y",
}, },
{ {
type: "line", type: "line",
label: "等级", label: "等级",
data: weeklyData.value.map((weekly) => data: aiStore.weeklyData.map((weekly) =>
gradeOrder.indexOf(weekly.grade || "C"), gradeOrder.indexOf(weekly.grade || "C"),
), ),
tension: 0.4, tension: 0.4,
@@ -124,18 +120,13 @@ const options = computed<ChartOptions<"bar" | "line">>(() => {
} }
}) })
const weeklyData = ref<WeeklyData[]>([]) watch(
() => [aiStore.duration, aiStore.username],
async function getWeeklyData() { () => {
weeklyLoading.value = true aiStore.fetchWeeklyData(props.end, aiStore.duration, aiStore.username)
const res = await getAIWeeklyData(props.end, props.duration, props.username) },
weeklyData.value = res.data { immediate: true },
weeklyLoading.value = false )
}
watch(() => [props.duration, props.username], getWeeklyData, {
immediate: true,
})
</script> </script>
<style scoped> <style scoped>
.chart { .chart {

View File

@@ -2,10 +2,12 @@ import { DIFFICULTY } from "utils/constants"
import { getACRate } from "utils/functions" import { getACRate } from "utils/functions"
import http from "utils/http" import http from "utils/http"
import { import {
DetailsData,
Problem, Problem,
Submission, Submission,
SubmissionListPayload, SubmissionListPayload,
SubmitCodePayload, SubmitCodePayload,
WeeklyData,
} from "utils/types" } from "utils/types"
function filterResult(result: Problem) { function filterResult(result: Problem) {
@@ -257,6 +259,12 @@ export function getAIWeeklyData(
return http.get("ai/weekly", { params: { end, duration, username } }) return http.get("ai/weekly", { params: { end, duration, username } })
} }
export function getAIAnalysis() { export function getAIAnalysis(
return http.get("ai/analysis") detailsData: DetailsData,
weeklyData: WeeklyData[],
) {
return http.post("ai/analysis", {
details: detailsData,
weekly: weeklyData,
})
} }

76
src/oj/store/ai.ts Normal file
View File

@@ -0,0 +1,76 @@
import { DetailsData, WeeklyData } from "~/utils/types"
import { getAIAnalysis, getAIDetailData, getAIWeeklyData } from "../api"
export const useAIStore = defineStore("ai", () => {
const duration = ref("months:6")
const username = ref("")
const weeklyData = ref<WeeklyData[]>([])
const detailsData = reactive<DetailsData>({
start: "",
end: "",
grade: "B",
class_name: "",
tags: {},
difficulty: {},
contest_count: 0,
solved: [],
})
const loading = reactive({
details: false,
weekly: false,
ai: false,
})
const theFirstPerson = computed(() => {
return !!username.value ? username.value : "你"
})
async function fetchDetailsData(
start: string,
end: string,
username?: string,
) {
loading.details = true
const res = await getAIDetailData(start, end, username)
detailsData.start = res.data.start
detailsData.end = res.data.end
detailsData.solved = res.data.solved
detailsData.grade = res.data.grade
detailsData.class_name = res.data.class_name
detailsData.tags = res.data.tags
detailsData.difficulty = res.data.difficulty
detailsData.contest_count = res.data.contest_count
loading.details = false
}
async function fetchWeeklyData(
end: string,
duration: string,
username?: string,
) {
loading.weekly = true
const res = await getAIWeeklyData(end, duration, username)
weeklyData.value = res.data
loading.weekly = false
}
async function fetchAIAnalysis() {
loading.ai = true
const res = await getAIAnalysis(detailsData, weeklyData.value)
console.log(res.data)
loading.ai = false
}
return {
fetchWeeklyData,
fetchDetailsData,
fetchAIAnalysis,
weeklyData,
detailsData,
duration,
username,
theFirstPerson,
loading,
}
})

View File

@@ -394,7 +394,33 @@ export interface WeeklyData {
index: number index: number
start: string start: string
end: string end: string
grade: "S" | "A" | "B" | "C" grade: Grade
problem_count: number problem_count: number
submission_count: number submission_count: number
} }
export interface SolvedProblem {
problem: {
title: string
display_id: string
contest_title: string
contest_id: number
}
ac_time: string
rank: number
ac_count: number
grade: Grade
}
export interface DetailsData {
start: string
end: string
grade: Grade
class_name: string
tags: { [key: string]: number }
difficulty: { [key: string]: number }
contest_count: number
solved: SolvedProblem[]
}
export type Grade = "S" | "A" | "B" | "C"