Administrator 22f073d8e7 feat: 运动会记分板系统核心功能
- 前后端分离架构 (Nuxt 3 + Element Plus)
- SQLite 数据库 (better-sqlite3)
- 比赛项目管理 (田赛/径赛/团体赛)
- 队伍管理 (5 个组别)
- 成绩录入与积分统计
- 记分板展示 (排名/奖牌榜)
- 移动端响应式适配
- 侧边栏布局 + 抽屉菜单
- 自动生成初始化数据接口
2026-03-17 22:29:18 +08:00

222 lines
5.6 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>