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",
|
||||
"axios": "^1.12.2",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"chart.js": "^4.5.0",
|
||||
"chart.js": "^4.5.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"copy-text-to-clipboard": "^3.2.2",
|
||||
"date-fns": "^4.1.0",
|
||||
@@ -39,20 +39,20 @@
|
||||
"vue": "^3.5.22",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-codemirror": "^6.1.1",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-router": "^4.6.3",
|
||||
"y-codemirror.next": "^0.3.5",
|
||||
"y-webrtc": "^10.3.0",
|
||||
"yjs": "^13.6.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@rsbuild/core": "^1.5.16",
|
||||
"@rsbuild/plugin-vue": "^1.1.2",
|
||||
"@rsbuild/core": "^1.5.17",
|
||||
"@rsbuild/plugin-vue": "^1.2.0",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "^24.7.1",
|
||||
"@types/node": "^24.9.1",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3",
|
||||
"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 { ContestRank, ProblemFiltered } from "utils/types"
|
||||
import AcAndSubmission from "../components/AcAndSubmission.vue"
|
||||
import LineChart from "../components/LineChart.vue"
|
||||
|
||||
interface Props {
|
||||
contestID: string
|
||||
@@ -82,13 +83,13 @@ const columns = ref<DataTableColumn<ContestRank>[]>([
|
||||
align: "center",
|
||||
render: (row) => h(AcAndSubmission, { rank: row }),
|
||||
},
|
||||
// {
|
||||
// title: "总时间",
|
||||
// key: "total_time",
|
||||
// width: 120,
|
||||
// align: "center",
|
||||
// render: (row) => secondsToDuration(row.total_time),
|
||||
// },
|
||||
{
|
||||
title: "总时间",
|
||||
key: "total_time",
|
||||
width: 120,
|
||||
align: "center",
|
||||
render: (row) => secondsToDuration(row.total_time),
|
||||
},
|
||||
])
|
||||
|
||||
async function listRanks() {
|
||||
@@ -207,6 +208,10 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 排名变化图表 -->
|
||||
<LineChart :ranks="chart" :problems="problems" v-if="chart.length > 0" />
|
||||
|
||||
<!-- 排名表格 -->
|
||||
<n-data-table
|
||||
striped
|
||||
:single-line="false"
|
||||
|
||||
Reference in New Issue
Block a user