DCFronted/src/views/index/Competition.vue
2025-06-19 22:10:20 +08:00

488 lines
10 KiB
Vue

<template>
<div class="competition-page">
<div class="page-header">
<h1>赛程信息</h1>
<div class="header-subtitle">
<span class="date-range">点击即可查看和报名</span>
</div>
</div>
<div class="action-bar">
<div class="left-actions">
<button class="btn-common btn-gradient" @click="addNewCompetition">
<i class="fas fa-plus"></i>
添加赛程
</button>
<button
class="btn-common btn-light"
@click="refreshCompetitions"
:disabled="isLoading"
>
<i class="fas" :class="isLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt'"></i>
{{ isLoading ? '刷新中...' : '刷新赛程' }}
</button>
</div>
<div class="right-actions">
<div class="search-box">
<input
type="text"
v-model="searchQuery"
placeholder="搜索赛程..."
@input="handleSearch"
>
<i class="fas fa-search search-icon"></i>
</div>
<select v-model="filterStatus" @change="handleFilter" class="filter-select">
<option value="all">全部状态</option>
<option value="ongoing">进行中</option>
<option value="finished">已结束</option>
</select>
</div>
</div>
<!-- 错误提示 -->
<div v-if="errorMessage" class="error-message">
<i class="fas fa-exclamation-circle"></i>
{{ errorMessage }}
<button class="retry-btn" @click="refreshCompetitions">
重试
</button>
</div>
<div class="table-container" :class="{ 'loading': isLoading }">
<table class="competition-table">
<thead>
<tr>
<th>序号</th>
<th>赛程名称</th>
<th>开始时间</th>
<th>结束时间</th>
<th>状态</th>
<th>组织者</th>
<th>QQ号</th>
<th>赛制类型</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(competition, index) in filteredCompetitions"
:key="index"
class="competition-row"
@click="handleView(competition)">
<td>{{ index + 1 }}</td>
<td class="competition-name">{{ competition.name }}</td>
<td>{{ formatDate(competition.start_time) }}</td>
<td>{{ formatDate(competition.end_time) }}</td>
<td>
<span :class="['status-tag', competition.status]">
{{ competition.status === 'prepare' ? '筹备中' :
competition.status === 'starting' ? '进行中' : '已结束' }}
</span>
</td>
<td>{{ competition.organizer }}</td>
<td>{{ competition.qq_code }}</td>
<td>{{ competition.format === 'single' ? '单败淘汰' :
competition.format === 'double' ? '双败淘汰' : '积分赛' }}</td>
<td class="action-cell">
<button class="action-btn view" @click.stop="handleSignUp(competition)" :disabled="competition.status === 'finish'">
报名
</button>
</td>
</tr>
</tbody>
</table>
<!-- 加载状态 -->
<div v-if="isLoading" class="loading-overlay">
<i class="fas fa-spinner fa-spin"></i>
<span>加载中...</span>
</div>
<!-- 空状态显示 -->
<div v-else-if="filteredCompetitions.length === 0" class="empty-state">
<i class="fas fa-calendar-times"></i>
<p>暂无赛程信息</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { getTournamentList } from '@/api/tournament'
const router = useRouter()
// 状态管理
const competitions = ref([])
const searchQuery = ref('')
const filterStatus = ref('all')
const isLoading = ref(false)
const errorMessage = ref('')
// 计算属性
const filteredCompetitions = computed(() => {
let result = competitions.value
// 搜索过滤
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(comp =>
comp.name.toLowerCase().includes(query) ||
comp.organizer.toLowerCase().includes(query)
)
}
// 状态过滤
if (filterStatus.value !== 'all') {
result = result.filter(comp =>
filterStatus.value === 'ongoing' ? comp.status === 'starting' : comp.status === 'finish'
)
}
return result
})
// 方法
const formatDate = (date) => {
return date.replace(/\//g, '-')
}
const handleSearch = () => {
// 搜索时重置过滤
}
const handleFilter = () => {
// 过滤时重置搜索
}
const handleView = (competition) => {
router.push({
path: '/competition/detail',
query: {
id: competition.id,
name: competition.name,
start_time: competition.start_time,
end_time: competition.end_time,
organizer: competition.organizer,
qq_code: competition.qq_code,
format: competition.format,
status: competition.status
}
})
}
const handleSignUp = (competition) => {
router.push({
name: 'CompetitionSignUp',
query: {
id: competition.id,
name: competition.name,
start_time: competition.start_time,
end_time: competition.end_time,
organizer: competition.organizer,
qq_code: competition.qq_code,
format: competition.format,
status: competition.status
}
})
}
const addNewCompetition = () => {
router.push('/competition/add')
}
const refreshCompetitions = async () => {
try {
isLoading.value = true
errorMessage.value = ''
const data = await getTournamentList()
competitions.value = data
console.log('刷新赛程数据成功')
} catch (error) {
console.error('获取赛程数据失败:', error)
errorMessage.value = error.response?.data?.message || '获取赛程数据失败,请重试'
} finally {
isLoading.value = false
}
}
// 初始化
refreshCompetitions()
</script>
<style scoped>
.competition-page {
padding: 16px;
max-width: 1400px;
margin: 0 auto;
}
.page-header {
margin-bottom: 20px;
}
.page-header h1 {
font-size: 22px;
color: #1a237e;
margin: 0 0 6px 0;
}
.header-subtitle {
color: #666;
font-size: 13px;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.left-actions,
.right-actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
position: relative;
flex-grow: 1;
}
.search-box input {
padding: 6px 10px 6px 28px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
width: 100%;
max-width: 220px;
}
.search-icon {
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 14px;
}
.filter-select {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
background: white;
min-width: 100px;
cursor: pointer;
}
.btn-common {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
border-radius: 4px;
border: 1px solid #b6d2ff;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-gradient {
background: linear-gradient(90deg, #71eaeb 0%, #416bdf 100%);
color: white;
border: none;
}
.btn-gradient:hover {
background: linear-gradient(90deg, #416bdf 0%, #71eaeb 100%);
transform: translateY(-1px);
}
.btn-light {
background: white;
color: #2563eb;
}
.btn-light:hover {
background: #f5f7fa;
border-color: #2563eb;
}
.table-container {
background: white;
border-radius: 8px;
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.05);
overflow-x: auto;
margin-bottom: 20px;
position: relative;
min-height: 200px;
}
.competition-table {
width: 100%;
min-width: 800px;
border-collapse: collapse;
}
.competition-table th,
.competition-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #f0f0f0;
font-size: 13px;
}
.competition-table th {
background-color: #f8f9fa;
font-weight: 600;
color: #1a237e;
}
.competition-row {
cursor: pointer;
transition: all 0.3s ease;
}
.competition-row:hover {
background-color: #f0f7ff;
transform: translateY(-1px);
}
.competition-name {
font-weight: 500;
color: #1a237e;
}
.status-tag {
display: inline-block;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
}
.status-tag.prepare { background-color: #e6a23c; color: #fff; }
.status-tag.starting { background-color: #67c23a; color: #fff; }
.status-tag.finish { background-color: #909399; color: #fff; }
.action-cell {
display: flex;
gap: 6px;
}
.action-btn {
padding: 5px 10px;
border-radius: 4px;
background: linear-gradient(90deg, #71eaeb 0%, #416bdf 100%);
color: white;
font-size: 13px;
border: none;
cursor: pointer;
}
.empty-state {
padding: 30px;
text-align: center;
font-size: 14px;
color: #909399;
}
.error-message {
background-color: #fef0f0;
color: #f56c6c;
padding: 10px 14px;
border-radius: 4px;
display: flex;
gap: 8px;
font-size: 13px;
}
.retry-btn {
margin-left: auto;
padding: 4px 10px;
font-size: 12px;
background: #f56c6c;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.loading-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
color: #409EFF;
font-size: 14px;
}
.table-container.loading {
opacity: 0.6;
pointer-events: none;
}
.btn-common:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-btn:disabled {
background: #e0e0e0 !important;
color: #b0b0b0 !important;
cursor: not-allowed;
border: none;
opacity: 1;
}
@media (max-width: 768px) {
.competition-page {
padding: 12px;
}
.action-bar {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.left-actions, .right-actions {
flex-direction: column;
gap: 8px;
width: 100%;
}
.table-container {
margin: 0 -12px;
border-radius: 0;
}
.competition-table th, .competition-table td {
padding: 10px;
font-size: 12px;
}
.search-box input, .filter-select {
width: 100%;
max-width: 100%;
}
.status-tag {
font-size: 10px;
padding: 2px 5px;
}
}
</style>