- 前后端分离架构 (Nuxt 3 + Element Plus) - SQLite 数据库 (better-sqlite3) - 比赛项目管理 (田赛/径赛/团体赛) - 队伍管理 (5 个组别) - 成绩录入与积分统计 - 记分板展示 (排名/奖牌榜) - 移动端响应式适配 - 侧边栏布局 + 抽屉菜单 - 自动生成初始化数据接口
222 lines
5.6 KiB
Vue
222 lines
5.6 KiB
Vue
<template>
|
||
<div class="scoreboard-container">
|
||
<el-card>
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span>记分板</span>
|
||
<el-button type="primary" @click="loadScoreboard">刷新</el-button>
|
||
</div>
|
||
</template>
|
||
|
||
<el-form :inline="true" class="filter-form">
|
||
<el-form-item label="组别">
|
||
<el-select v-model="filters.group" placeholder="全部" clearable @change="loadScoreboard">
|
||
<el-option v-for="g in groups" :key="g.value" :label="g.label" :value="g.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table :data="scoreboard" border stripe class="scoreboard-table">
|
||
<el-table-column label="排名" width="80" class-name="col-rank">
|
||
<template #default="{ $index }">
|
||
<div class="rank-cell">
|
||
<el-icon v-if="$index === 0" color="#FFD700" :size="24"><Trophy /></el-icon>
|
||
<el-icon v-else-if="$index === 1" color="#C0C0C0" :size="24"><Trophy /></el-icon>
|
||
<el-icon v-else-if="$index === 2" color="#CD7F32" :size="24"><Trophy /></el-icon>
|
||
<span v-else>{{ $index + 1 }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="name" label="队伍名称" />
|
||
<el-table-column prop="team_group" label="组别" width="150" class-name="col-group" />
|
||
<el-table-column prop="total_score" label="总分" width="100" sortable>
|
||
<template #default="{ row }">
|
||
<el-tag type="danger" size="large">{{ row.total_score }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="奖牌统计" width="200" class-name="col-medal">
|
||
<template #default="{ row }">
|
||
<div class="medals">
|
||
<el-tag type="warning">🥇 {{ row.gold_count }}</el-tag>
|
||
<el-tag type="success">🥈 {{ row.silver_count }}</el-tag>
|
||
<el-tag type="info">🥉 {{ row.bronze_count }}</el-tag>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<el-row :gutter="20" class="stats-section">
|
||
<el-col :xs="24" :sm="24" :md="8" :lg="8">
|
||
<el-card>
|
||
<template #header>
|
||
<span>积分规则</span>
|
||
</template>
|
||
<ul class="rules-list">
|
||
<li>第1名:7分 + 金牌</li>
|
||
<li>第2名:5分 + 银牌</li>
|
||
<li>第3名:3分 + 铜牌</li>
|
||
<li>其他名次:不计分</li>
|
||
</ul>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="24" :md="8" :lg="8">
|
||
<el-card>
|
||
<template #header>
|
||
<span>金牌榜前三</span>
|
||
</template>
|
||
<div v-for="(item, index) in topGold" :key="item.id" class="top-item">
|
||
<span>{{ index + 1 }}. {{ item.name }}</span>
|
||
<el-tag type="warning">{{ item.gold_count }} 枚</el-tag>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="24" :md="8" :lg="8">
|
||
<el-card>
|
||
<template #header>
|
||
<span>总分榜前三</span>
|
||
</template>
|
||
<div v-for="(item, index) in topScore" :key="item.id" class="top-item">
|
||
<span>{{ index + 1 }}. {{ item.name }}</span>
|
||
<el-tag type="danger">{{ item.total_score }} 分</el-tag>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ElMessage } from 'element-plus'
|
||
import { Trophy } from '@element-plus/icons-vue'
|
||
|
||
const scoreboard = ref([])
|
||
const groups = ref([])
|
||
const filters = ref({ group: '' })
|
||
|
||
const topGold = computed(() => {
|
||
return [...scoreboard.value]
|
||
.sort((a, b) => b.gold_count - a.gold_count)
|
||
.slice(0, 3)
|
||
})
|
||
|
||
const topScore = computed(() => {
|
||
return scoreboard.value.slice(0, 3)
|
||
})
|
||
|
||
const loadConfig = async () => {
|
||
try {
|
||
const res = await $fetch('/api/config')
|
||
groups.value = res.data.groups
|
||
} catch (error) {
|
||
ElMessage.error('加载配置失败')
|
||
}
|
||
}
|
||
|
||
const loadScoreboard = async () => {
|
||
try {
|
||
const params = new URLSearchParams()
|
||
if (filters.value.group) params.append('group', filters.value.group)
|
||
|
||
const res = await $fetch(`/api/scoreboard?${params}`)
|
||
scoreboard.value = res.data
|
||
} catch (error) {
|
||
ElMessage.error('加载记分板失败')
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadConfig()
|
||
loadScoreboard()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.scoreboard-container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-form {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.rank-cell {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.medals {
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.stats-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.rules-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.rules-list li {
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #ebeef5;
|
||
color: #606266;
|
||
}
|
||
|
||
.rules-list li:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.top-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.top-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.card-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
}
|
||
|
||
.filter-form :deep(.el-form-item) {
|
||
margin-right: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.filter-form :deep(.el-select) {
|
||
width: 100%;
|
||
}
|
||
|
||
.scoreboard-table :deep(.col-group),
|
||
.scoreboard-table :deep(.col-medal) {
|
||
display: none;
|
||
}
|
||
|
||
.stats-section :deep(.el-col) {
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
</style>
|