BIN
public/A.png
Normal file
BIN
public/A.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/B.png
Normal file
BIN
public/B.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
public/C.png
Normal file
BIN
public/C.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/S.png
Normal file
BIN
public/S.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -2,9 +2,8 @@
|
|||||||
import { importUsers } from "../api"
|
import { importUsers } from "../api"
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const prefix = ref("")
|
const prefix = ref(0)
|
||||||
const rawInput = ref("")
|
const rawInput = ref("")
|
||||||
const [needKs] = useToggle(true)
|
|
||||||
const [loading, toggleLoading] = useToggle()
|
const [loading, toggleLoading] = useToggle()
|
||||||
const users = shallowRef<string[][]>([])
|
const users = shallowRef<string[][]>([])
|
||||||
|
|
||||||
@@ -13,25 +12,17 @@ function generateUsers() {
|
|||||||
message.info("请填写相关内容")
|
message.info("请填写相关内容")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 自动加上 ks 的开头
|
let className = !!prefix.value ? `ks${prefix.value}` : ""
|
||||||
let myClass = ""
|
|
||||||
if (prefix.value) {
|
|
||||||
if (needKs.value && !prefix.value.startsWith("ks")) {
|
|
||||||
myClass = "ks" + prefix.value
|
|
||||||
} else {
|
|
||||||
myClass = prefix.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rawInput.value = rawInput.value.trim()
|
rawInput.value = rawInput.value.trim()
|
||||||
const inputs = rawInput.value.split("\n")
|
const inputs = rawInput.value.split("\n")
|
||||||
users.value = inputs.map((u, i) => {
|
users.value = inputs.map((u, i) => {
|
||||||
const username = myClass + u
|
const username = className + u
|
||||||
let password = ""
|
let password = ""
|
||||||
for (let j = 0; j < 6; j++) {
|
for (let j = 0; j < 6; j++) {
|
||||||
password += "123456789".charAt(Math.floor(Math.random() * 9))
|
password += "123456789".charAt(Math.floor(Math.random() * 9))
|
||||||
}
|
}
|
||||||
const realName = u
|
const realName = u
|
||||||
const email = `${myClass}.${i + 1}@example.com`
|
const email = `${className}.${i + 1}@example.com`
|
||||||
return [username, password, email, realName]
|
return [username, password, email, realName]
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
@@ -68,14 +59,16 @@ async function submit() {
|
|||||||
<n-space>
|
<n-space>
|
||||||
<n-flex vertical>
|
<n-flex vertical>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<n-switch v-model:value="needKs" />
|
<div style="width: 18px; font-size: 1.2rem">ks</div>
|
||||||
<span>前面带上 ks</span>
|
<n-input-number
|
||||||
|
style="width: 170px"
|
||||||
|
v-model:value="prefix"
|
||||||
|
clearable
|
||||||
|
:max="9999"
|
||||||
|
:min="0"
|
||||||
|
placeholder="班级号"
|
||||||
|
/>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-input
|
|
||||||
style="width: 200px"
|
|
||||||
v-model:value="prefix"
|
|
||||||
placeholder="班级号"
|
|
||||||
/>
|
|
||||||
<n-input
|
<n-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
class="inputArea"
|
class="inputArea"
|
||||||
|
|||||||
24
src/components.d.ts
vendored
24
src/components.d.ts
vendored
@@ -9,32 +9,56 @@ export {}
|
|||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCode: typeof import('naive-ui')['NCode']
|
NCode: typeof import('naive-ui')['NCode']
|
||||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
|
NDescriptions: typeof import('naive-ui')['NDescriptions']
|
||||||
|
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
|
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||||
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NFlex: typeof import('naive-ui')['NFlex']
|
NFlex: typeof import('naive-ui')['NFlex']
|
||||||
NForm: typeof import('naive-ui')['NForm']
|
NForm: typeof import('naive-ui')['NForm']
|
||||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||||
|
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||||
|
NGi: typeof import('naive-ui')['NGi']
|
||||||
NGradientText: typeof import('naive-ui')['NGradientText']
|
NGradientText: typeof import('naive-ui')['NGradientText']
|
||||||
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
NH1: typeof import('naive-ui')['NH1']
|
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']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||||
|
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||||
NMenu: typeof import('naive-ui')['NMenu']
|
NMenu: typeof import('naive-ui')['NMenu']
|
||||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
NModal: typeof import('naive-ui')['NModal']
|
NModal: typeof import('naive-ui')['NModal']
|
||||||
|
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||||
NPagination: typeof import('naive-ui')['NPagination']
|
NPagination: typeof import('naive-ui')['NPagination']
|
||||||
|
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||||
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
|
NRate: typeof import('naive-ui')['NRate']
|
||||||
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
NSpan: typeof import('naive-ui')['NSpan']
|
||||||
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||||
|
NTabs: typeof import('naive-ui')['NTabs']
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
NText: typeof import('naive-ui')['NText']
|
NText: typeof import('naive-ui')['NText']
|
||||||
|
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
}
|
}
|
||||||
|
|||||||
240
src/oj/ai/analysis.vue
Normal file
240
src/oj/ai/analysis.vue
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
<template>
|
||||||
|
<n-grid :cols="5" :x-gap="20">
|
||||||
|
<n-gi :span="2">
|
||||||
|
<n-flex vertical size="large">
|
||||||
|
<n-flex align="center">
|
||||||
|
<n-select
|
||||||
|
style="width: 140px"
|
||||||
|
:options="options"
|
||||||
|
v-model:value="duration"
|
||||||
|
/>
|
||||||
|
<n-flex>
|
||||||
|
<n-input
|
||||||
|
clearable
|
||||||
|
style="width: 140px"
|
||||||
|
v-if="userStore.isSuperAdmin"
|
||||||
|
v-model:value="username"
|
||||||
|
/>
|
||||||
|
<n-button @click="init">查询</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-flex>
|
||||||
|
<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="你还没有完成任何题目"></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-gi>
|
||||||
|
<n-gi :span="3">
|
||||||
|
<n-spin :show="weeklyLoading">
|
||||||
|
<WeeklyChart :weeklyData="weeklyData" :duration="duration" />
|
||||||
|
</n-spin>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch } from "vue"
|
||||||
|
import { formatISO, sub, type Duration } from "date-fns"
|
||||||
|
import { getAIDetailData, getAIWeeklyData } from "../api"
|
||||||
|
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 Grade from "./components/Grade.vue"
|
||||||
|
import { WeeklyData } from "~/utils/types"
|
||||||
|
import { useUserStore } from "~/shared/store/user"
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const start = ref("")
|
||||||
|
const end = ref("")
|
||||||
|
const duration = ref("months:6")
|
||||||
|
const username = ref("")
|
||||||
|
|
||||||
|
const startLabel = ref("")
|
||||||
|
const endLabel = ref("")
|
||||||
|
|
||||||
|
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[] = [
|
||||||
|
{ label: "一节课内", value: "hours:1" },
|
||||||
|
{ label: "两节课内", value: "hours:2" },
|
||||||
|
{ label: "一天内", value: "days:1" },
|
||||||
|
{ label: "一周内", value: "weeks:1" },
|
||||||
|
{ label: "一个月内", value: "months:1" },
|
||||||
|
{ label: "两个月内", value: "months:2" },
|
||||||
|
{ label: "半年内", value: "months:6" },
|
||||||
|
{ label: "一年内", value: "years:1" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const subOptions = computed<Duration>(() => {
|
||||||
|
let dur = options.find((it) => it.value === duration.value) ?? options[0]
|
||||||
|
const x = dur.value!.toString().split(":")
|
||||||
|
const unit = x[0]
|
||||||
|
const n = x[1]
|
||||||
|
return { [unit]: parseInt(n) } as Duration
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateRange() {
|
||||||
|
const current = new Date()
|
||||||
|
end.value = formatISO(current)
|
||||||
|
start.value = formatISO(sub(current, subOptions.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDetail() {
|
||||||
|
detailLoading.value = true
|
||||||
|
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[]>([])
|
||||||
|
|
||||||
|
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>
|
||||||
|
<style scoped>
|
||||||
|
.charming {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
58
src/oj/ai/components/DifficultyChart.vue
Normal file
58
src/oj/ai/components/DifficultyChart.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart" v-if="show">
|
||||||
|
<Bar :data="data" :options="options" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Bar } from "vue-chartjs"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
difficulty: { [key: string]: number }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const show = computed(() => {
|
||||||
|
return Object.values(props.difficulty).reduce((a, b) => a + b, 0) > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = computed(() => {
|
||||||
|
return {
|
||||||
|
labels: Object.keys(props.difficulty),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: Object.values(props.difficulty),
|
||||||
|
backgroundColor: ["#FF6384", "#36A2EB", "#FFCE56"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: "题目的难度统计",
|
||||||
|
display: true,
|
||||||
|
font: {
|
||||||
|
size: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
height: 300px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
src/oj/ai/components/Grade.vue
Normal file
31
src/oj/ai/components/Grade.vue
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<template>
|
||||||
|
<img src="/S.png" alt="S Grade" v-if="props.grade === 'S'" />
|
||||||
|
<img src="/A.png" alt="A Grade" v-if="props.grade === 'A'" />
|
||||||
|
<img src="/B.png" alt="B Grade" v-if="props.grade === 'B'" />
|
||||||
|
<img src="/C.png" alt="C Grade" v-if="props.grade === 'C'" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
grade: "S" | "A" | "B" | "C"
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
img {
|
||||||
|
animation: shake 0.5s infinite;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin: 0 10px -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shake {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px) scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
21
src/oj/ai/components/TagTitle.vue
Normal file
21
src/oj/ai/components/TagTitle.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<n-flex vertical align="start">
|
||||||
|
<n-flex align="center">
|
||||||
|
<n-tag type="info" size="small" :bordered="false">比赛</n-tag>
|
||||||
|
<span>{{ problem.contest_title }}</span>
|
||||||
|
</n-flex>
|
||||||
|
<span>{{ problem.display_id }} {{ problem.title }}</span>
|
||||||
|
</n-flex>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
problem: {
|
||||||
|
title: string
|
||||||
|
display_id: string
|
||||||
|
contest_title: string
|
||||||
|
contest_id: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
</script>
|
||||||
57
src/oj/ai/components/TagsChart.vue
Normal file
57
src/oj/ai/components/TagsChart.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart" v-if="show">
|
||||||
|
<Pie :data="data" :options="options" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Pie } from "vue-chartjs"
|
||||||
|
const props = defineProps<{
|
||||||
|
tags: { [key: string]: number }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const show = computed(() => {
|
||||||
|
return Object.keys(props.tags).length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = computed(() => {
|
||||||
|
return {
|
||||||
|
labels: Object.keys(props.tags),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: Object.values(props.tags),
|
||||||
|
backgroundColor: [
|
||||||
|
"#FF6384",
|
||||||
|
"#36A2EB",
|
||||||
|
"#FFCE56",
|
||||||
|
"#4BC0C0",
|
||||||
|
"#9966FF",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
return {
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
text: `题目的标签分布(前${Object.keys(props.tags).length}个)`,
|
||||||
|
display: true,
|
||||||
|
font: {
|
||||||
|
size: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
height: 300px;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
126
src/oj/ai/components/WeeklyChart.vue
Normal file
126
src/oj/ai/components/WeeklyChart.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chart">
|
||||||
|
<Chart type="bar" :data="data" :options="options" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions, TooltipItem } from "chart.js"
|
||||||
|
import { Chart } from "vue-chartjs"
|
||||||
|
import { parseTime } from "~/utils/functions"
|
||||||
|
import { WeeklyData } from "~/utils/types"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
weeklyData: WeeklyData[]
|
||||||
|
duration: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const gradeOrder = ["C", "B", "A", "S"] as const
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
if (props.duration === "months:2") {
|
||||||
|
return "过去两个月的每周综合情况一览图"
|
||||||
|
} else if (props.duration === "months:6") {
|
||||||
|
return "过去半年的每月综合情况一览图"
|
||||||
|
} else if (props.duration === "years:1") {
|
||||||
|
return "过去一年的每月综合情况一览图"
|
||||||
|
} else {
|
||||||
|
return "过去四周的综合情况一览图"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = computed<ChartData<"bar" | "line">>(() => {
|
||||||
|
return {
|
||||||
|
labels: props.weeklyData.map((weekly) => {
|
||||||
|
let prefix = "周"
|
||||||
|
if (weekly.unit === "months") {
|
||||||
|
prefix = "月"
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
parseTime(weekly.start, "M月D日"),
|
||||||
|
parseTime(weekly.end, "M月D日"),
|
||||||
|
].join("~")
|
||||||
|
}),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
type: "bar",
|
||||||
|
label: "完成题目数量",
|
||||||
|
data: props.weeklyData.map((weekly) => weekly.problem_count),
|
||||||
|
yAxisID: "y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "bar",
|
||||||
|
label: "总提交次数",
|
||||||
|
data: props.weeklyData.map((weekly) => weekly.submission_count),
|
||||||
|
yAxisID: "y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "line",
|
||||||
|
label: "等级",
|
||||||
|
data: props.weeklyData.map((weekly) =>
|
||||||
|
gradeOrder.indexOf(weekly.grade || "C"),
|
||||||
|
),
|
||||||
|
tension: 0.4,
|
||||||
|
yAxisID: "y1",
|
||||||
|
barThickness: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = computed<ChartOptions<"bar" | "line">>(() => {
|
||||||
|
return {
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
type: "linear",
|
||||||
|
position: "right",
|
||||||
|
min: -1,
|
||||||
|
max: gradeOrder.length,
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1,
|
||||||
|
callback: (v) => {
|
||||||
|
const idx = Number(v)
|
||||||
|
return gradeOrder[idx] || ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
text: title.value,
|
||||||
|
display: true,
|
||||||
|
font: {
|
||||||
|
size: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (ctx: TooltipItem<"bar">) => {
|
||||||
|
const dsLabel = ctx.dataset.label || ""
|
||||||
|
if ((ctx.dataset as any).yAxisID === "y1") {
|
||||||
|
const idx = Number(ctx.parsed.y)
|
||||||
|
return `${dsLabel}: ${gradeOrder[idx] || ""}`
|
||||||
|
}
|
||||||
|
return `${dsLabel}: ${ctx.formattedValue}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -90,7 +90,7 @@ export function getSubmissions(params: SubmissionListPayload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getRankOfProblem(problem_id: string) {
|
export function getRankOfProblem(problem_id: string) {
|
||||||
return http.get("user_problem_rank", { params: {problem_id: problem_id} })
|
return http.get("user_problem_rank", { params: { problem_id: problem_id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTodaySubmissionCount() {
|
export function getTodaySubmissionCount() {
|
||||||
@@ -244,3 +244,19 @@ export function getTutorial(id: number) {
|
|||||||
export function getTutorials() {
|
export function getTutorials() {
|
||||||
return http.get("tutorials")
|
return http.get("tutorials")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAIDetailData(start: string, end: string, username?: string) {
|
||||||
|
return http.get("ai/detail", { params: { start, end, username } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAIWeeklyData(
|
||||||
|
end: string,
|
||||||
|
duration: string,
|
||||||
|
username?: string,
|
||||||
|
) {
|
||||||
|
return http.get("ai/weekly", { params: { end, duration, username } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAIAnalysis() {
|
||||||
|
return http.get("ai/analysis")
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const class_name = ref("")
|
|||||||
const rank = ref(-1)
|
const rank = ref(-1)
|
||||||
const class_ac_count = ref(0)
|
const class_ac_count = ref(0)
|
||||||
const all_ac_count = ref(0)
|
const all_ac_count = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
const submissions = ref<Submission[]>([])
|
const submissions = ref<Submission[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -82,7 +83,8 @@ const showList = computed(() => {
|
|||||||
|
|
||||||
const errorMsg = computed(() => {
|
const errorMsg = computed(() => {
|
||||||
if (!userStore.isAuthed) return "请先登录"
|
if (!userStore.isAuthed) return "请先登录"
|
||||||
else if (!configStore.config.submission_list_show_all) return "不让看了"
|
else if (!configStore.config.submission_list_show_all)
|
||||||
|
return "提交列表已被管理员关闭"
|
||||||
else return ""
|
else return ""
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -100,7 +102,10 @@ async function listSubmissions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getRankOfThisProblem() {
|
async function getRankOfThisProblem() {
|
||||||
|
loading.value = true
|
||||||
const res = await getRankOfProblem(<string>route.params.problemID ?? "")
|
const res = await getRankOfProblem(<string>route.params.problemID ?? "")
|
||||||
|
loading.value = false
|
||||||
|
|
||||||
class_name.value = res.data.class_name
|
class_name.value = res.data.class_name
|
||||||
rank.value = res.data.rank
|
rank.value = res.data.rank
|
||||||
class_ac_count.value = res.data.class_ac_count
|
class_ac_count.value = res.data.class_ac_count
|
||||||
@@ -114,8 +119,9 @@ onMounted(() => {
|
|||||||
watch(query, listSubmissions)
|
watch(query, listSubmissions)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<n-alert type="error" v-if="!showList" :title="errorMsg" />
|
<n-alert class="tip" type="error" v-if="!showList" :title="errorMsg" />
|
||||||
<template v-if="route.name === 'problem'">
|
|
||||||
|
<template v-if="!loading && route.name === 'problem' && userStore.isAuthed">
|
||||||
<template v-if="class_name">
|
<template v-if="class_name">
|
||||||
<n-alert class="tip" type="success" :show-icon="false" v-if="rank !== -1">
|
<n-alert class="tip" type="success" :show-icon="false" v-if="rank !== -1">
|
||||||
<template #header>
|
<template #header>
|
||||||
@@ -125,6 +131,7 @@ watch(query, listSubmissions)
|
|||||||
>,你们班共有 <b>{{ class_ac_count }}</b> 人答案正确
|
>,你们班共有 <b>{{ class_ac_count }}</b> 人答案正确
|
||||||
</div>
|
</div>
|
||||||
<n-button
|
<n-button
|
||||||
|
v-if="showList"
|
||||||
@click="
|
@click="
|
||||||
router.push({
|
router.push({
|
||||||
name: 'submissions',
|
name: 'submissions',
|
||||||
@@ -152,6 +159,8 @@ watch(query, listSubmissions)
|
|||||||
共有 <b>{{ class_ac_count }}</b> 人答案正确
|
共有 <b>{{ class_ac_count }}</b> 人答案正确
|
||||||
</div>
|
</div>
|
||||||
<n-button
|
<n-button
|
||||||
|
v-if="showList"
|
||||||
|
secondary
|
||||||
@click="
|
@click="
|
||||||
router.push({
|
router.push({
|
||||||
name: 'submissions',
|
name: 'submissions',
|
||||||
@@ -182,6 +191,7 @@ watch(query, listSubmissions)
|
|||||||
<div></div>
|
<div></div>
|
||||||
<n-button
|
<n-button
|
||||||
secondary
|
secondary
|
||||||
|
v-if="showList"
|
||||||
@click="
|
@click="
|
||||||
router.push({
|
router.push({
|
||||||
name: 'submissions',
|
name: 'submissions',
|
||||||
@@ -203,9 +213,12 @@ watch(query, listSubmissions)
|
|||||||
<template #header>
|
<template #header>
|
||||||
<n-flex align="center">
|
<n-flex align="center">
|
||||||
<div>
|
<div>
|
||||||
本道题你还没有解决,全服共有 <b>{{ all_ac_count }}</b> 人答案正确
|
本道题你还没有解决,全服共有
|
||||||
|
<b>{{ all_ac_count }}</b> 人答案正确
|
||||||
</div>
|
</div>
|
||||||
<n-button
|
<n-button
|
||||||
|
v-if="showList"
|
||||||
|
secondary
|
||||||
@click="
|
@click="
|
||||||
router.push({
|
router.push({
|
||||||
name: 'submissions',
|
name: 'submissions',
|
||||||
@@ -225,6 +238,7 @@ watch(query, listSubmissions)
|
|||||||
</n-alert>
|
</n-alert>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="showList">
|
<template v-if="showList">
|
||||||
<n-data-table striped :columns="columns" :data="submissions" />
|
<n-data-table striped :columns="columns" :data="submissions" />
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -65,7 +65,6 @@
|
|||||||
<n-button type="warning" @click="toggleUnaccepted(!unaccepted)">
|
<n-button type="warning" @click="toggleUnaccepted(!unaccepted)">
|
||||||
{{ unaccepted ? "隐藏没有完成的" : "显示没有完成的" }}
|
{{ unaccepted ? "隐藏没有完成的" : "显示没有完成的" }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="copyUnaccepted">手动复制到微信群</n-button>
|
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-h1 v-if="count.total === 0">
|
<n-h1 v-if="count.total === 0">
|
||||||
@@ -73,8 +72,8 @@
|
|||||||
</n-h1>
|
</n-h1>
|
||||||
<n-space v-if="unaccepted" size="large">
|
<n-space v-if="unaccepted" size="large">
|
||||||
<n-h1>这{{ listUnaccepted.length }}位没有完成:</n-h1>
|
<n-h1>这{{ listUnaccepted.length }}位没有完成:</n-h1>
|
||||||
<n-h1 v-for="item in listUnaccepted" :key="item">
|
<n-h1 v-for="name in listUnaccepted" :key="name">
|
||||||
{{ removeClassname(item) }}
|
{{ name }}
|
||||||
</n-h1>
|
</n-h1>
|
||||||
<n-text v-if="message">{{ message }}</n-text>
|
<n-text v-if="message">{{ message }}</n-text>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -159,20 +158,4 @@ async function handleStatistics() {
|
|||||||
toggleUnaccepted(false)
|
toggleUnaccepted(false)
|
||||||
message.value = ""
|
message.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeClassname(name: string) {
|
|
||||||
if (name.startsWith("ks")) {
|
|
||||||
return name.slice(5)
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyUnaccepted() {
|
|
||||||
const grade = query.username.slice(2, 4)
|
|
||||||
const classname = query.username.slice(4, 5)
|
|
||||||
const prefix = `${grade}计算机${classname}班${query.problem}这道题有${listUnaccepted.value.length}人没有完成,分别是:`
|
|
||||||
const names = listUnaccepted.value.map(removeClassname).join("、")
|
|
||||||
const suffix = "。请以上同学尽快完成!"
|
|
||||||
message.value = prefix + names + suffix
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ export const ojs: RouteRecordRaw = {
|
|||||||
component: () => import("oj/learn/index.vue"),
|
component: () => import("oj/learn/index.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "ai-analysis",
|
||||||
|
component: () => import("oj/ai/analysis.vue"),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
beforeEnter: loadChart,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,11 +123,19 @@ const options: Array<DropdownOption | DropdownDividerOption> = [
|
|||||||
{
|
{
|
||||||
label: "我的设置",
|
label: "我的设置",
|
||||||
key: "setting",
|
key: "setting",
|
||||||
icon: renderIcon("streamline-emojis:robot-face-1"),
|
icon: renderIcon("streamline-emojis:ferris-wheel"),
|
||||||
props: {
|
props: {
|
||||||
onClick: () => router.push("/setting"),
|
onClick: () => router.push("/setting"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "智能分析",
|
||||||
|
key: "ai-analysis",
|
||||||
|
icon: renderIcon("streamline-emojis:floppy-disk"),
|
||||||
|
props: {
|
||||||
|
onClick: () => router.push("/ai-analysis"),
|
||||||
|
},
|
||||||
|
},
|
||||||
{ type: "divider" },
|
{ type: "divider" },
|
||||||
{
|
{
|
||||||
label: "退出",
|
label: "退出",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
LinearScale,
|
LinearScale,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
} from "chart.js"
|
} from "chart.js"
|
||||||
|
|
||||||
const [isLoaded] = useToggle()
|
const [isLoaded] = useToggle()
|
||||||
@@ -19,6 +21,8 @@ export function loadChart() {
|
|||||||
LinearScale,
|
LinearScale,
|
||||||
BarElement,
|
BarElement,
|
||||||
ArcElement,
|
ArcElement,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
Colors,
|
Colors,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
|||||||
@@ -388,3 +388,13 @@ export interface Tutorial {
|
|||||||
updated_at?: Date
|
updated_at?: Date
|
||||||
created_at?: Date
|
created_at?: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WeeklyData {
|
||||||
|
unit: string
|
||||||
|
index: number
|
||||||
|
start: string
|
||||||
|
end: string
|
||||||
|
grade: "S" | "A" | "B" | "C"
|
||||||
|
problem_count: number
|
||||||
|
submission_count: number
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user