This commit is contained in:
510
package-lock.json
generated
510
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -24,7 +24,7 @@
|
|||||||
"@wangeditor-next/editor-for-vue": "^5.1.14",
|
"@wangeditor-next/editor-for-vue": "^5.1.14",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"copy-text-to-clipboard": "^3.2.2",
|
"copy-text-to-clipboard": "^3.2.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -39,20 +39,20 @@
|
|||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-chartjs": "^5.3.2",
|
"vue-chartjs": "^5.3.2",
|
||||||
"vue-codemirror": "^6.1.1",
|
"vue-codemirror": "^6.1.1",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.6.3",
|
||||||
"y-codemirror.next": "^0.3.5",
|
"y-codemirror.next": "^0.3.5",
|
||||||
"y-webrtc": "^10.3.0",
|
"y-webrtc": "^10.3.0",
|
||||||
"yjs": "^13.6.27"
|
"yjs": "^13.6.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
"@rsbuild/core": "^1.5.16",
|
"@rsbuild/core": "^1.5.17",
|
||||||
"@rsbuild/plugin-vue": "^1.1.2",
|
"@rsbuild/plugin-vue": "^1.2.0",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^24.7.1",
|
"@types/node": "^24.9.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"unplugin-auto-import": "^20.2.0",
|
"unplugin-auto-import": "^20.2.0",
|
||||||
"unplugin-vue-components": "^29.1.0"
|
"unplugin-vue-components": "^30.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
231
src/oj/contest/components/LineChart.vue
Normal file
231
src/oj/contest/components/LineChart.vue
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<template>
|
||||||
|
<n-card title="前10名题目通过时间" v-if="hasData">
|
||||||
|
<div class="chart">
|
||||||
|
<Line :data="chartData" :options="chartOptions" />
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Line } from "vue-chartjs"
|
||||||
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
} from "chart.js"
|
||||||
|
import { ContestRank } from "utils/types"
|
||||||
|
|
||||||
|
ChartJS.register(
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
Legend,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
ranks: ContestRank[]
|
||||||
|
problems?: Array<{ id: number; title: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
// 判断是否有数据
|
||||||
|
const hasData = computed(() => {
|
||||||
|
return props.ranks && props.ranks.length > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 预定义的颜色方案 - 更现代和可访问的颜色
|
||||||
|
const colorPalette = [
|
||||||
|
"#3B82F6", // 蓝色
|
||||||
|
"#EF4444", // 红色
|
||||||
|
"#10B981", // 绿色
|
||||||
|
"#F59E0B", // 黄色
|
||||||
|
"#8B5CF6", // 紫色
|
||||||
|
"#EC4899", // 粉色
|
||||||
|
"#06B6D4", // 青色
|
||||||
|
"#84CC16", // 青绿色
|
||||||
|
"#F97316", // 橙色
|
||||||
|
"#6366F1", // 靛蓝色
|
||||||
|
]
|
||||||
|
|
||||||
|
// 数据处理函数
|
||||||
|
const processChartData = () => {
|
||||||
|
if (!props.ranks || props.ranks.length === 0) {
|
||||||
|
return {
|
||||||
|
labels: [],
|
||||||
|
datasets: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取前10名用户的数据
|
||||||
|
const topUsers = props.ranks.slice(0, 10)
|
||||||
|
|
||||||
|
// 获取所有题目ID(从所有用户的submission_info中收集)
|
||||||
|
const allProblemIds = new Set<string>()
|
||||||
|
topUsers.forEach((rank) => {
|
||||||
|
Object.keys(rank.submission_info).forEach((problemId) => {
|
||||||
|
allProblemIds.add(problemId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 按题目ID排序
|
||||||
|
const problemIds = Array.from(allProblemIds).sort()
|
||||||
|
|
||||||
|
// 创建题目标签
|
||||||
|
const labels = problemIds.map((id) => {
|
||||||
|
if (props.problems) {
|
||||||
|
const problem = props.problems.find((p) => p.id.toString() === id)
|
||||||
|
return problem ? problem.title : `题目${id}`
|
||||||
|
}
|
||||||
|
return `题目${id}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 找到所有用户中最早的提交时间
|
||||||
|
let earliestTime = Infinity
|
||||||
|
topUsers.forEach((rank) => {
|
||||||
|
Object.values(rank.submission_info).forEach((submissionInfo) => {
|
||||||
|
if (submissionInfo.is_ac && submissionInfo.ac_time < earliestTime) {
|
||||||
|
earliestTime = submissionInfo.ac_time
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果没有找到任何通过记录,使用0作为基准
|
||||||
|
if (earliestTime === Infinity) {
|
||||||
|
earliestTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为每个用户创建数据集
|
||||||
|
const datasets = topUsers.map((rank, userIndex) => {
|
||||||
|
const userData = problemIds.map((problemId) => {
|
||||||
|
const submissionInfo = rank.submission_info[problemId]
|
||||||
|
if (!submissionInfo || !submissionInfo.is_ac) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return submissionInfo.ac_time - earliestTime
|
||||||
|
})
|
||||||
|
|
||||||
|
const actualRank = userIndex + 1
|
||||||
|
const colorIndex = userIndex % colorPalette.length
|
||||||
|
const color = colorPalette[colorIndex]
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: `第${actualRank}名: ${rank.user.username}`,
|
||||||
|
data: userData,
|
||||||
|
borderColor: color,
|
||||||
|
backgroundColor: color + "20",
|
||||||
|
tension: 0.3,
|
||||||
|
fill: false,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointHoverRadius: 8,
|
||||||
|
pointBackgroundColor: color,
|
||||||
|
pointBorderColor: "#fff",
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
spanGaps: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
datasets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听数据变化,重新处理
|
||||||
|
watch(
|
||||||
|
() => [props.ranks, props.problems],
|
||||||
|
() => {
|
||||||
|
if (props.ranks && props.ranks.length > 0) {
|
||||||
|
// 数据变化时重新处理
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const chartData = computed(() => {
|
||||||
|
return processChartData()
|
||||||
|
})
|
||||||
|
|
||||||
|
const chartOptions = computed(() => ({
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: "top" as const,
|
||||||
|
maxHeight: 80,
|
||||||
|
labels: {
|
||||||
|
boxWidth: 12,
|
||||||
|
boxHeight: 12,
|
||||||
|
padding: 8,
|
||||||
|
usePointStyle: true,
|
||||||
|
font: {
|
||||||
|
size: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: "index" as const,
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
title: function (context: any) {
|
||||||
|
return `题目: ${context[0].label}`
|
||||||
|
},
|
||||||
|
label: function (context: any) {
|
||||||
|
const value = context.parsed.y
|
||||||
|
const label = context.dataset.label
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return `${label}: 未通过`
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(value / 3600)
|
||||||
|
const minutes = Math.floor((value % 3600) / 60)
|
||||||
|
const seconds = Math.floor(value % 60)
|
||||||
|
|
||||||
|
let timeStr = ""
|
||||||
|
if (hours > 0) timeStr += `${hours}小时`
|
||||||
|
if (minutes > 0) timeStr += `${minutes}分钟`
|
||||||
|
if (seconds > 0 || timeStr === "") timeStr += `${seconds}秒`
|
||||||
|
|
||||||
|
return `${label}: +${timeStr}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: "相对通过时间",
|
||||||
|
},
|
||||||
|
min: 0,
|
||||||
|
ticks: {
|
||||||
|
callback: function (value: any) {
|
||||||
|
const hours = Math.floor(value / 3600)
|
||||||
|
const minutes = Math.floor((value % 3600) / 60)
|
||||||
|
const seconds = Math.floor(value % 60)
|
||||||
|
|
||||||
|
if (hours > 0) return `+${hours}h${minutes}m`
|
||||||
|
if (minutes > 0) return `+${minutes}m${seconds}s`
|
||||||
|
return `+${seconds}s`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
height: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,6 +9,7 @@ import { ContestStatus } from "utils/constants"
|
|||||||
import { renderTableTitle } from "utils/renders"
|
import { renderTableTitle } from "utils/renders"
|
||||||
import { ContestRank, ProblemFiltered } from "utils/types"
|
import { ContestRank, ProblemFiltered } from "utils/types"
|
||||||
import AcAndSubmission from "../components/AcAndSubmission.vue"
|
import AcAndSubmission from "../components/AcAndSubmission.vue"
|
||||||
|
import LineChart from "../components/LineChart.vue"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
contestID: string
|
contestID: string
|
||||||
@@ -82,13 +83,13 @@ const columns = ref<DataTableColumn<ContestRank>[]>([
|
|||||||
align: "center",
|
align: "center",
|
||||||
render: (row) => h(AcAndSubmission, { rank: row }),
|
render: (row) => h(AcAndSubmission, { rank: row }),
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// title: "总时间",
|
title: "总时间",
|
||||||
// key: "total_time",
|
key: "total_time",
|
||||||
// width: 120,
|
width: 120,
|
||||||
// align: "center",
|
align: "center",
|
||||||
// render: (row) => secondsToDuration(row.total_time),
|
render: (row) => secondsToDuration(row.total_time),
|
||||||
// },
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
async function listRanks() {
|
async function listRanks() {
|
||||||
@@ -207,6 +208,10 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- 排名变化图表 -->
|
||||||
|
<LineChart :ranks="chart" :problems="problems" v-if="chart.length > 0" />
|
||||||
|
|
||||||
|
<!-- 排名表格 -->
|
||||||
<n-data-table
|
<n-data-table
|
||||||
striped
|
striped
|
||||||
:single-line="false"
|
:single-line="false"
|
||||||
|
|||||||
Reference in New Issue
Block a user