DCFronted/src/views/competition/Competition.vue

564 lines
12 KiB
Vue
Raw 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="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="prepare">筹备中</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 v-if="isLoading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在加载赛程数据...</p>
</div>
<!-- 赛程卡片网格 -->
<div v-else class="competition-grid">
<div
v-for="(competition, index) in filteredCompetitions"
:key="index"
class="competition-card"
@click="handleView(competition)"
>
<div class="card-header">
<div class="status-badge" :class="competition.status">
{{ getStatusText(competition.status) }}
</div>
<div class="format-badge">{{ competition.format === 'single' ? '单败' : competition.format === 'double' ? '双败' : '积分' }}</div>
</div>
<div class="card-body">
<h3 class="competition-title">{{ competition.name }}</h3>
<div class="info-row">
<i class="fas fa-user-tie"></i>
<span>主办方{{ competition.organizer }}</span>
</div>
<div class="info-row">
<i class="fab fa-qq"></i>
<span>QQ{{ competition.qq_code }}</span>
</div>
<div class="info-row time-row">
<i class="far fa-calendar-alt"></i>
<span>{{ formatDate(competition.start_time) }} - {{ formatDate(competition.end_time) }}</span>
</div>
</div>
<div class="card-footer">
<button
class="action-btn view"
@click.stop="handleSignUp(competition)"
:disabled="competition.status === 'finish'"
:class="{ 'disabled': competition.status === 'finish' }"
>
{{ competition.status === 'finish' ? '已结束' : '立即报名' }}
</button>
</div>
</div>
<!-- 空状态显示 -->
<div v-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.js'
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') {
if (filterStatus.value === 'ongoing') {
result = result.filter(comp => comp.status === 'starting')
} else if (filterStatus.value === 'finished') {
result = result.filter(comp => comp.status === 'finish')
} else if (filterStatus.value === 'prepare') {
result = result.filter(comp => comp.status === 'prepare')
}
}
return result
})
// 方法
const formatDate = (date) => {
if (!date) return ''
return date.replace(/\//g, '-')
}
const getStatusText = (status) => {
const map = {
'prepare': '筹备中',
'starting': '进行中',
'finish': '已结束'
}
return map[status] || status
}
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: 24px;
max-width: 1400px;
margin: 0 auto;
min-height: 100vh;
background-color: #f5f7fa;
}
.page-header {
margin-bottom: 24px;
text-align: center;
}
.page-header h1 {
font-size: 28px;
color: #1a237e;
margin: 0 0 8px 0;
font-weight: 600;
}
.header-subtitle {
color: #666;
font-size: 14px;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
background: white;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.left-actions,
.right-actions {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
position: relative;
width: 240px;
}
.search-box input {
width: 100%;
padding: 8px 12px 8px 32px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
transition: all 0.3s;
}
.search-box input:focus {
border-color: #409EFF;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
outline: none;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 14px;
}
.filter-select {
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
background: white;
min-width: 120px;
cursor: pointer;
transition: all 0.3s;
}
.filter-select:focus {
border-color: #409EFF;
outline: none;
}
.btn-common {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
border: none;
}
.btn-gradient {
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
color: white;
box-shadow: 0 4px 12px rgba(65, 107, 223, 0.2);
}
.btn-gradient:hover {
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
transform: translateY(-1px);
box-shadow: 0 6px 16px rgba(65, 107, 223, 0.3);
}
.btn-light {
background: #f0f7ff;
color: #2563eb;
border: 1px solid #dbeafe;
}
.btn-light:hover {
background: #dbeafe;
border-color: #2563eb;
}
/* 网格布局 */
.competition-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
}
.competition-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: all 0.3s ease;
cursor: pointer;
border: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
}
.competition-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
border-color: #e0e0e0;
}
.card-header {
padding: 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f5f5f5;
background: #fafafa;
}
.status-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.status-badge.prepare { background-color: #fff7e6; color: #fa8c16; border: 1px solid #ffd591; }
.status-badge.starting { background-color: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.status-badge.finish { background-color: #f5f5f5; color: #8c8c8c; border: 1px solid #d9d9d9; }
.format-badge {
font-size: 12px;
color: #666;
background: #e6f7ff;
padding: 2px 8px;
border-radius: 10px;
border: 1px solid #bae7ff;
}
.card-body {
padding: 20px;
flex: 1;
}
.competition-title {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: #1a237e;
line-height: 1.4;
}
.info-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
color: #666;
font-size: 14px;
}
.info-row i {
width: 16px;
text-align: center;
color: #909399;
}
.time-row {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #f0f0f0;
font-size: 13px;
}
.card-footer {
padding: 16px;
border-top: 1px solid #f5f5f5;
text-align: center;
}
.action-btn {
width: 100%;
padding: 10px;
border-radius: 6px;
background: linear-gradient(90deg, #71eaeb 0%, #416bdf 100%);
color: white;
font-size: 14px;
font-weight: 500;
border: none;
cursor: pointer;
transition: all 0.3s;
}
.action-btn:hover {
opacity: 0.9;
}
.action-btn.disabled {
background: #f5f5f5;
color: #999;
cursor: not-allowed;
background-image: none;
}
.empty-state {
grid-column: 1 / -1;
padding: 60px;
text-align: center;
color: #909399;
background: white;
border-radius: 12px;
}
.empty-state i {
font-size: 48px;
margin-bottom: 16px;
color: #d9d9d9;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px;
color: #409EFF;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background-color: #fef0f0;
color: #f56c6c;
padding: 12px 16px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
border: 1px solid #fde2e2;
}
.retry-btn {
margin-left: auto;
padding: 4px 12px;
font-size: 12px;
background: #f56c6c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
@media (max-width: 768px) {
.competition-page {
padding: 16px;
}
.action-bar {
flex-direction: column;
align-items: stretch;
}
.left-actions, .right-actions {
flex-direction: column;
width: 100%;
}
.search-box {
width: 100%;
}
.filter-select {
width: 100%;
}
}
</style>