add charts.

This commit is contained in:
2023-01-24 20:51:07 +08:00
parent 7def5a4d83
commit f557135075
16 changed files with 381 additions and 24 deletions

46
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@monaco-editor/loader": "^1.3.2", "@monaco-editor/loader": "^1.3.2",
"@vueuse/core": "^9.11.1", "@vueuse/core": "^9.11.1",
"axios": "1.2.3", "axios": "1.2.3",
"chart.js": "^4.2.0",
"copy-text-to-clipboard": "^3.0.1", "copy-text-to-clipboard": "^3.0.1",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
@@ -19,6 +20,7 @@
"party-js": "^2.2.0", "party-js": "^2.2.0",
"pinia": "^2.0.29", "pinia": "^2.0.29",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-chartjs": "^5.2.0",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {
@@ -485,6 +487,11 @@
"resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
}, },
"node_modules/@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"node_modules/@mdit-vue/plugin-component": { "node_modules/@mdit-vue/plugin-component": {
"version": "0.11.2", "version": "0.11.2",
"resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-component/-/plugin-component-0.11.2.tgz", "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-component/-/plugin-component-0.11.2.tgz",
@@ -1008,6 +1015,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/chart.js": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.2.0.tgz",
"integrity": "sha512-wbtcV+QKeH0F7gQZaCJEIpsNriFheacouJQTVIjITi3eQA8bTlIBoknz0+dgV79aeKLNMAX+nDslIVE/nJ3rzA==",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": "^7.0.0"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz",
@@ -2491,6 +2509,15 @@
"@vue/shared": "3.2.45" "@vue/shared": "3.2.45"
} }
}, },
"node_modules/vue-chartjs": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/vue-chartjs/-/vue-chartjs-5.2.0.tgz",
"integrity": "sha512-d3zpKmGZr2OWHQ1xmxBcAn5ShTG917+/UCLaSpaCDDqT0U7DBsvFzTs69ZnHCgKoXT55GZDW8YEj9Av+dlONLA==",
"peerDependencies": {
"chart.js": "^4.1.1",
"vue": "^3.0.0-0 || ^2.7.0"
}
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz",
@@ -2824,6 +2851,11 @@
"resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", "resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==" "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
}, },
"@kurkle/color": {
"version": "0.3.2",
"resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.2.tgz",
"integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw=="
},
"@mdit-vue/plugin-component": { "@mdit-vue/plugin-component": {
"version": "0.11.2", "version": "0.11.2",
"resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-component/-/plugin-component-0.11.2.tgz", "resolved": "https://registry.npmmirror.com/@mdit-vue/plugin-component/-/plugin-component-0.11.2.tgz",
@@ -3275,6 +3307,14 @@
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
} }
}, },
"chart.js": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.2.0.tgz",
"integrity": "sha512-wbtcV+QKeH0F7gQZaCJEIpsNriFheacouJQTVIjITi3eQA8bTlIBoknz0+dgV79aeKLNMAX+nDslIVE/nJ3rzA==",
"requires": {
"@kurkle/color": "^0.3.0"
}
},
"chokidar": { "chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz",
@@ -4375,6 +4415,12 @@
"@vue/shared": "3.2.45" "@vue/shared": "3.2.45"
} }
}, },
"vue-chartjs": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/vue-chartjs/-/vue-chartjs-5.2.0.tgz",
"integrity": "sha512-d3zpKmGZr2OWHQ1xmxBcAn5ShTG917+/UCLaSpaCDDqT0U7DBsvFzTs69ZnHCgKoXT55GZDW8YEj9Av+dlONLA==",
"requires": {}
},
"vue-router": { "vue-router": {
"version": "4.1.6", "version": "4.1.6",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.1.6.tgz",

View File

@@ -14,6 +14,7 @@
"@monaco-editor/loader": "^1.3.2", "@monaco-editor/loader": "^1.3.2",
"@vueuse/core": "^9.11.1", "@vueuse/core": "^9.11.1",
"axios": "1.2.3", "axios": "1.2.3",
"chart.js": "^4.2.0",
"copy-text-to-clipboard": "^3.0.1", "copy-text-to-clipboard": "^3.0.1",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
@@ -21,6 +22,7 @@
"party-js": "^2.2.0", "party-js": "^2.2.0",
"pinia": "^2.0.29", "pinia": "^2.0.29",
"vue": "^3.2.45", "vue": "^3.2.45",
"vue-chartjs": "^5.2.0",
"vue-router": "^4.1.6" "vue-router": "^4.1.6"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,32 +1,72 @@
<script setup lang="ts"> <script setup lang="ts">
import Monaco from "../shared/Monaco.vue" import Monaco from "~/shared/Monaco.vue"
import raw from "./step-1/1.c?raw" import { isDesktop } from "~/shared/composables/breakpoints"
const route = useRoute() const route = useRoute()
const id = route.hash.replace("#step-", "") || "1" const router = useRouter()
const Md = defineAsyncComponent(() => import(`./step-${id}/index.md`)) const TOTAL = 3
const code = ref(raw) const Mds = Array.from({ length: TOTAL }, (_, i) => i + 1).map((v) =>
defineAsyncComponent(() => import(`./step-${v}/index.md`))
)
const code = ref("")
const step = computed(() => {
if (!route.params.step || !route.params.step.length) return 1
else {
return parseInt(route.params.step[0].split("-")[1])
}
})
function change(value: string) { watch(
code.value = value () => route.params.step,
async (value) => {
if (route.name !== "learn") return
try {
const raw = await import(`./${value[0]}/main.c?raw`)
code.value = raw.default
} catch (err) {
router.replace("/learn/step-1")
}
},
{ immediate: true }
)
function change(v: string) {
code.value = v
}
function prev() {
router.push(`/learn/step-${step.value - 1}`)
}
function next() {
router.push(`/learn/step-${step.value + 1}`)
}
function run() {
console.log(code.value)
} }
</script> </script>
<template> <template>
<n-grid :cols="2"> <n-grid v-if="isDesktop" :cols="24">
<n-gi> <n-gi :span="10">
<Md /> <n-scrollbar style="max-height: calc(100vh - 92px)">
<n-space justify="space-between"> <component :is="Mds[step - 1]"></component>
<n-button text type="primary">上一步</n-button> <n-space justify="space-around">
<n-button text type="primary">下一步</n-button> <n-button v-if="step !== 1" text type="primary" @click="prev">
</n-space> 上一步
</n-button>
<n-button v-if="step !== TOTAL" text type="primary" @click="next">
下一步
</n-button>
</n-space>
</n-scrollbar>
</n-gi> </n-gi>
<n-gi> <n-gi :span="14" class="relative">
<n-button type="primary" class="action" @click="run">运行</n-button>
<Monaco :value="code" @change="change" /> <Monaco :value="code" @change="change" />
<div></div>
</n-gi> </n-gi>
</n-grid> </n-grid>
<div v-else></div>
</template> </template>
<style> <style>
@@ -64,4 +104,15 @@ function change(value: string) {
text-align: right; text-align: right;
color: rgba(115, 138, 148, 0.4); color: rgba(115, 138, 148, 0.4);
} }
.relative {
position: relative;
}
.action {
position: absolute;
top: 10px;
right: 10px;
z-index: 10;
}
</style> </style>

View File

@@ -13,3 +13,99 @@ int main() {
``` ```
123 123
# 11
## 1
哈哈
```c {3-4}
#include<stdio.h>
int main() {
return 0;
}
```
123
# 11
## 1
哈哈
```c {3-4}
#include<stdio.h>
int main() {
return 0;
}
```
123
# 11
## 1
哈哈
```c {3-4}
#include<stdio.h>
int main() {
return 0;
}
```
123
# 11
## 1
哈哈
```c {3-4}
#include<stdio.h>
int main() {
return 0;
}
```
123
# 11
## 1
哈哈
```c {3-4}
#include<stdio.h>
int main() {
return 0;
}
```
123
# 11
## 1
哈哈
```c {3-4}
#include<stdio.h>
int main() {
return 0;
}
```
123

View File

@@ -0,0 +1 @@
asas

1
src/learn/step-2/main.c Normal file
View File

@@ -0,0 +1 @@
1212121212

View File

0
src/learn/step-3/main.c Normal file
View File

View File

@@ -8,7 +8,19 @@ import { routes } from "./routes"
import App from "./App.vue" import App from "./App.vue"
import { toggleLogin } from "./shared/composables/modal" import { toggleLogin } from "./shared/composables/modal"
import { init } from "./shared/composables/monaco" import { init as monacoInit } from "./shared/composables/monaco"
import {
Chart as ChartJS,
Title,
Colors,
Tooltip,
Legend,
BarElement,
ArcElement,
CategoryScale,
LinearScale,
} from "chart.js"
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
@@ -26,7 +38,19 @@ router.beforeEach((to, from, next) => {
next() next()
} }
}) })
init()
monacoInit()
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
ArcElement,
Colors,
Title,
Tooltip,
Legend
)
const pinia = createPinia() const pinia = createPinia()
const app = createApp(App) const app = createApp(App)
app.use(router) app.use(router)

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { DIFFICULTY } from "utils/constants" import { Pie } from "vue-chartjs"
import { DIFFICULTY, JUDGE_STATUS } from "utils/constants"
import { getACRate, getTagColor, parseTime } from "utils/functions" import { getACRate, getTagColor, parseTime } from "utils/functions"
import { isDesktop } from "~/shared/composables/breakpoints" import { isDesktop } from "~/shared/composables/breakpoints"
import { Problem } from "utils/types" import { Problem } from "utils/types"
@@ -7,7 +8,30 @@ import { Problem } from "utils/types"
interface Props { interface Props {
problem: Problem problem: Problem
} }
defineProps<Props>() const props = defineProps<Props>()
const data = computed(() => {
const status = props.problem.statistic_info
const labels = []
for (let i in status) {
if (status[i] === 0) {
delete status[i]
}
// @ts-ignore
labels.push(JUDGE_STATUS[i]["name"])
}
return {
labels,
datasets: [{ data: Object.values(status), hoverOffset: 4 }],
}
})
const options = ref({
responsive: true,
plugins: {
title: { text: "提交结果的比例", display: true, font: { size: 20 } },
},
})
</script> </script>
<template> <template>
@@ -49,4 +73,14 @@ defineProps<Props>()
</n-space> </n-space>
</n-descriptions-item> </n-descriptions-item>
</n-descriptions> </n-descriptions>
<div class="pie">
<Pie :data="data" :options="options" />
</div>
</template> </template>
<style scoped>
.pie {
width: 100%;
max-width: 500px;
margin: 24px auto;
}
</style>

View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import { Bar } from "vue-chartjs"
import { Rank } from "~/utils/types"
const props = defineProps<{ rankData: Rank[] }>()
const data = computed(() => ({
labels: props.rankData.map((rank) => rank.user.username),
datasets: [
{
label: "已解决",
data: props.rankData.map((rank) => rank.accepted_number),
backgroundColor: [
"rgba(255, 99, 132, 0.2)",
"rgba(255, 159, 64, 0.2)",
"rgba(55, 66, 250, 0.2)",
"rgba(75, 192, 192, 0.2)",
"rgba(54, 162, 235, 0.2)",
"rgba(153, 102, 255, 0.2)",
"rgba(48, 51, 107, 0.2)",
"rgba(249, 202, 36, 0.2)",
"rgba(106, 176, 76, 0.2)",
"rgba(119, 139, 235, 0.2)",
],
borderColor: [
"rgba(255, 99, 132, 0.6)",
"rgba(255, 159, 64, 0.6)",
"rgba(55, 66, 250, 0.6)",
"rgba(75, 192, 192, 0.6)",
"rgba(54, 162, 235, 0.6)",
"rgba(153, 102, 255, 0.6)",
"rgba(48, 51, 107, 0.6)",
"rgba(249, 202, 36, 0.6)",
"rgba(106, 176, 76, 0.6)",
"rgba(119, 139, 235, 0.6)",
],
hoverBackgroundColor: [
"rgba(255, 99, 132, 0.8)",
"rgba(255, 159, 64, 0.8)",
"rgba(55, 66, 250, 0.8)",
"rgba(75, 192, 192, 0.8)",
"rgba(54, 162, 235, 0.8)",
"rgba(153, 102, 255, 0.8)",
"rgba(48, 51, 107, 0.8)",
"rgba(249, 202, 36, 0.8)",
"rgba(106, 176, 76, 0.8)",
"rgba(119, 139, 235, 0.8)",
],
hoverBorderColor: [
"rgba(255, 99, 132, 1)",
"rgba(255, 159, 64, 1)",
"rgba(55, 66, 250, 1)",
"rgba(75, 192, 192, 1)",
"rgba(54, 162, 235, 1)",
"rgba(153, 102, 255, 1)",
"rgba(48, 51, 107, 1)",
"rgba(249, 202, 36, 1)",
"rgba(106, 176, 76, 1)",
"rgba(119, 139, 235, 1)",
],
borderWidth: 2,
},
{
label: "总提交数",
data: props.rankData.map((rank) => rank.submission_number),
hidden: true,
},
],
}))
const options = ref({
plugins: {
title: {
text: "全校前十名的提交者(不包括超管)",
display: true,
font: { size: 20 },
},
},
})
</script>
<template>
<Bar class="chart" :data="data" :options="options" />
</template>
<style scoped>
.chart {
margin-bottom: 24px;
}
</style>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { DataTableColumn, NButton, NButtonGroup } from "naive-ui" import { DataTableColumn, NButton } from "naive-ui"
import Chart from "./components/Chart.vue"
import Pagination from "~/shared/Pagination.vue" import Pagination from "~/shared/Pagination.vue"
import { Rank } from "utils/types" import { Rank } from "utils/types"
import { getRank } from "oj/api" import { getRank } from "oj/api"
@@ -11,13 +12,18 @@ const query = reactive({
limit: 10, limit: 10,
page: 1, page: 1,
}) })
const rankData = ref<Rank[]>([])
async function listRanks() { async function listRanks() {
const offset = (query.page - 1) * query.limit const offset = (query.page - 1) * query.limit
const res = await getRank(offset, query.limit) const res = await getRank(offset, query.limit)
data.value = res.data.results data.value = res.data.results
total.value = res.data.total total.value = res.data.total
if (query.page === 1) {
rankData.value = data.value
}
} }
const columns: DataTableColumn<Rank>[] = [ const columns: DataTableColumn<Rank>[] = [
{ {
title: "排名", title: "排名",
@@ -61,6 +67,7 @@ onMounted(listRanks)
</script> </script>
<template> <template>
<Chart v-if="!!rankData.length" :rankData="rankData" />
<n-data-table striped size="small" :data="data" :columns="columns" /> <n-data-table striped size="small" :data="data" :columns="columns" />
<Pagination <Pagination
:total="total" :total="total"

View File

@@ -1,4 +1,6 @@
export const routes = [ import { RouteRecordRaw } from "vue-router"
export const routes: RouteRecordRaw[] = [
{ {
path: "/", path: "/",
component: () => import("~/shared/layout/default.vue"), component: () => import("~/shared/layout/default.vue"),
@@ -41,7 +43,12 @@ export const routes = [
}, },
{ {
path: "learn", path: "learn",
redirect: "learn/step-1",
},
{
path: "learn/:step+",
component: () => import("learn/index.vue"), component: () => import("learn/index.vue"),
name: "learn",
}, },
], ],
}, },

View File

@@ -36,7 +36,7 @@ const defaultValue = computed(() => route.path.split("/")[1] || "problem")
const menus: MenuOption[] = [ const menus: MenuOption[] = [
{ {
label: () => label: () =>
h(RouterLink, { to: "/learn#step-1" }, { default: () => "自学" }), h(RouterLink, { to: "/learn/step-1" }, { default: () => "自学" }),
key: "learn", key: "learn",
}, },
{ {

View File

@@ -96,7 +96,7 @@ export interface Problem {
total_score: number total_score: number
submission_number: number submission_number: number
accepted_number: number accepted_number: number
statistic_info: {} statistic_info: { [key in string]: number }
share_submission: boolean share_submission: boolean
contest: null contest: null
my_status: number my_status: number