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

1258 lines
29 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 v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
</div>
<div v-else class="detail-card">
<div class="nav-back">
<button class="back-btn" @click="handleBack">← 返回列表</button>
<div class="action-buttons" v-if="isOrganizer">
<button class="status-btn" @click="openStatusDialog">修改状态</button>
<button class="edit-btn" @click="openEditDialog">编辑赛事</button>
<button class="delete-btn" @click="confirmDelete">删除赛事</button>
</div>
</div>
<div class="page-header">
<h1>{{ competition.name }}</h1>
<div class="header-subtitle">
<span>赛制:{{ competition.format }}</span>
<span>|组织者:{{ competition.organizer }}</span>
<span>QQ{{ competition.qq_code }}</span>
<span>|开始时间:{{ formatDate(competition.start_time) }}</span>
<span>|结束时间:{{ formatDate(competition.end_time) }}</span>
<span :class="['status-tag', competition.status]">
{{ statusMap[competition.status] }}
</span>
</div>
<div class="edit-controls" v-if="isOrganizer">
<button class="edit-mode-btn" @click="toggleEditMode">
{{ isEditMode ? '退出编辑' : '编辑对阵图' }}
</button>
<button v-if="isEditMode" class="save-btn" @click="saveChanges" :disabled="isSaving">
{{ isSaving ? '保存中...' : '保存修改' }}
</button>
</div>
</div>
<!-- 报名玩家列表 -->
<div v-if="competition.status === 'prepare' || competition.status === 'starting'" class="registered-players">
<div class="section-title">报名玩家列表</div>
<div class="players-list">
<div v-if="registeredPlayers.length === 0" class="no-players">
暂无报名玩家
</div>
<table v-else class="players-table">
<thead>
<tr>
<th>玩家名称</th>
<th>队伍名称</th>
<th>胜场</th>
<th>负场</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="player in registeredPlayers" :key="player.id">
<td>{{ player.sign_name }}</td>
<td>{{ player.team_name || '个人' }}</td>
<td>{{ player.win }}</td>
<td>{{ player.lose }}</td>
<td>{{ player.status }}</td>
<td class="action-buttons">
<button class="edit-player-btn" @click="handleEditPlayer(player)" >修改</button>
<button class="remove-btn" @click="handleRemovePlayer(player.id)">移除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 非筹备中状态显示的内容 -->
<template v-if="competition.status !== 'prepare'">
<rank-contestant
v-if="competition.status === 'finish'"
:tournament-id="parseInt(route.query.id)"
/>
<tournament-bracket
v-if="competition.status === 'starting'"
:tournament-id="parseInt(route.query.id)"
@refreshPlayers="fetchRegisteredPlayers"
/>
</template>
<div v-if="showEditForm" class="edit-dialog-overlay">
<div class="edit-dialog">
<h3>编辑赛事</h3>
<div class="edit-form">
<div class="form-group">
<label>赛事名称:</label>
<input type="text" v-model="editForm.name" />
</div>
<div class="form-group">
<label>赛制类型:</label>
<select v-model="editForm.format">
<option value="single">单败淘汰</option>
<option value="double">双败淘汰</option>
<option value="count">积分赛</option>
</select>
</div>
<div class="form-group">
<label>组织者:</label>
<input type="text" v-model="editForm.organizer" />
</div>
<div class="form-group">
<label>QQ号</label>
<input type="text" v-model="editForm.qq_code" />
</div>
<div class="form-group">
<label>开始时间:</label>
<input type="date" v-model="editForm.start_time" />
</div>
<div class="form-group">
<label>结束时间:</label>
<input type="date" v-model="editForm.end_time" />
</div>
<div class="dialog-buttons">
<button class="cancel-btn" @click="closeEditDialog">取消</button>
<button class="confirm-btn" @click="handleUpdate" :disabled="isUpdating">
{{ isUpdating ? '保存中...' : '保存' }}
</button>
</div>
</div>
</div>
</div>
<div v-if="showDeleteConfirm" class="edit-dialog-overlay">
<div class="edit-dialog">
<h3>确认删除</h3>
<p class="confirm-message">确定要删除该赛事吗?此操作不可恢复。</p>
<div class="dialog-buttons">
<button class="cancel-btn" @click="showDeleteConfirm = false">取消</button>
<button class="delete-confirm-btn" @click="handleDelete" :disabled="isDeleting">
{{ isDeleting ? '删除中...' : '确认删除' }}
</button>
</div>
</div>
</div>
<div v-if="showStatusDialog" class="edit-dialog-overlay">
<div class="edit-dialog">
<h3>修改赛事状态</h3>
<div class="status-options">
<button v-for="(label, value) in statusMap" :key="value"
:class="['status-option', { active: selectedStatus === value }]" @click="selectedStatus = value">
{{ label }}
</button>
</div>
<div class="dialog-buttons">
<button class="cancel-btn" @click="closeStatusDialog">取消</button>
<button class="confirm-btn" @click="handleStatusUpdate" :disabled="isUpdating">
{{ isUpdating ? '保存中...' : '保存' }}
</button>
</div>
</div>
</div>
<!-- 编辑玩家弹窗 -->
<div v-if="showPlayerEditDialog" class="edit-dialog-overlay">
<div class="edit-dialog">
<h3>编辑参赛信息</h3>
<div class="edit-form">
<div class="form-group">
<label>参赛人员名称:</label>
<input type="text" v-model="playerEditForm.sign_name" />
</div>
<div class="form-group">
<label>队伍名称:</label>
<input type="text" v-model="playerEditForm.team_name" />
</div>
<div class="form-group">
<label>胜利局数:</label>
<input type="text" v-model="playerEditForm.win" />
</div>
<div class="form-group">
<label>失败局数:</label>
<input type="text" v-model="playerEditForm.lose" />
</div>
<div class="form-group">
<label>状态:</label>
<select v-model="playerEditForm.status">
<option value="win">胜利</option>
<option value="lose">失败</option>
<option value="tie">平局</option>
</select>
</div>
<div class="form-group">
<label>阵营:</label>
<select v-model="playerEditForm.faction">
<option value="allied">盟军</option>
<option value="soviet">苏联</option>
<option value="empire">帝国</option>
<option value="ob">OB</option>
<option value="voice">解说</option>
<option value="random">随机</option>
</select>
</div>
<div class="form-group">
<label>QQ</label>
<input type="text" v-model="playerEditForm.qq" />
</div>
<div class="dialog-buttons">
<button class="cancel-btn" @click="closePlayerEditDialog">取消</button>
<button class="confirm-btn" @click="handlePlayerUpdate" :disabled="isUpdating">
{{ isUpdating ? '保存中...' : '保存' }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import TournamentBracket from '@/components/TournamentBracket.vue'
import RankContestant from '@/components/RankContestant.vue'
import {
getTournamentList,
updateTournament,
deleteTournament,
getSignUpResultList,
updateSignUpResult,
deleteSignUpResult
} from '@/api/tournament'
import { getStoredUser } from '@/utils/jwt'
const router = useRouter()
const route = useRoute()
// 状态映射
const formatMap = {
'single': '单败淘汰',
'double': '双败淘汰',
'count': '积分赛'
}
const statusMap = {
'prepare': '筹备中',
'starting': '进行中',
'finish': '已结束'
}
// const factionMap = {
// 'allied': '盟军',
// 'soviet': '苏联',
// 'empire': '帝国',
// 'ob': 'OB',
// 'voice': '解说',
// 'random': '随机'
// }
// 状态管理
const competition = ref({
name: '',
format: '',
organizer: '',
qq_code: '',
start_time: '',
end_time: '',
status: ''
})
const finalResults = ref([])
const isEditMode = ref(false)
const isOrganizer = ref(false)
const isSaving = ref(false)
const showEditForm = ref(false)
const isUpdating = ref(false)
const editForm = ref({
name: '',
format: '',
organizer: '',
qq_code: '',
start_time: '',
end_time: ''
})
const showDeleteConfirm = ref(false)
const isDeleting = ref(false)
const showStatusDialog = ref(false)
const selectedStatus = ref('')
const registeredPlayers = ref([]) // 报名玩家列表
const isLoading = ref(true) // 添加加载状态
const showPlayerEditDialog = ref(false) // 玩家编辑弹窗
const playerEditForm = ref({ // 玩家编辑表单
id: '',
username: '',
type: '',
tournament_id: ''
})
// 格式化日期
const formatDate = (date) => {
if (!date) return ''
return date.replace(/\//g, '-')
}
// 格式化赛制
const formatType = (type) => {
return formatMap[type] || type
}
// // 格式化阵营
// const formatFaction = (faction) => {
// return factionMap[faction] || faction
// }
// 返回列表
const handleBack = () => {
router.push('/competition')
}
// 切换编辑模式
const toggleEditMode = () => {
isEditMode.value = !isEditMode.value
}
// 打开编辑弹窗
const openEditDialog = () => {
const getFormatValue = (format) => {
const formatEntries = Object.entries(formatMap)
const entry = formatEntries.find(([_, value]) => value === format)
return entry ? entry[0] : format
}
editForm.value = {
name: competition.value.name,
format: getFormatValue(competition.value.format),
organizer: competition.value.organizer,
qq_code: competition.value.qq_code,
start_time: competition.value.start_time.replace(/\//g, '-'),
end_time: competition.value.end_time.replace(/\//g, '-')
}
showEditForm.value = true
}
// 关闭编辑弹窗
const closeEditDialog = () => {
showEditForm.value = false
editForm.value = {
name: '',
format: '',
organizer: '',
qq_code: '',
start_time: '',
end_time: ''
}
}
// 确认删除
const confirmDelete = () => {
showDeleteConfirm.value = true
}
// 处理更新
const handleUpdate = async () => {
try {
isUpdating.value = true
const tournamentId = route.query.id
// 转换日期格式
const formatDate = (dateStr) => {
const [year, month, day] = dateStr.split('-')
return `${year}/${month}/${day}`
}
const updateData = {
...editForm.value,
start_time: formatDate(editForm.value.start_time),
end_time: formatDate(editForm.value.end_time),
status: competition.value.status // 保持原有状态
}
await updateTournament(tournamentId, updateData)
alert('更新成功!')
closeEditDialog()
fetchTournamentDetail() // 刷新数据
} catch (error) {
console.error('更新失败:', error)
alert(error.response?.data?.message || '更新失败,请重试')
} finally {
isUpdating.value = false
}
}
// 处理删除
const handleDelete = async () => {
try {
isDeleting.value = true
const tournamentId = route.query.id
await deleteTournament(tournamentId)
alert('删除成功!')
router.push('/competition')
} catch (error) {
console.error('删除失败:', error)
alert(error.response?.data?.message || '删除失败,请重试')
} finally {
isDeleting.value = false
showDeleteConfirm.value = false
}
}
// 获取赛事详情
const fetchTournamentDetail = async () => {
try {
isLoading.value = true
const tournamentId = parseInt(route.query.id)
console.log('获取到的赛事ID:', tournamentId)
const data = await getTournamentList()
console.log('获取到的赛事列表:', data)
// 根据ID查找对应的赛事
const tournament = data.find(item => item.id === tournamentId)
if (tournament) {
console.log('找到赛事:', tournament)
competition.value = {
...tournament,
format: formatType(tournament.format)
}
// 权限判断
const user = getStoredUser()
if (user && user.qq_code && tournament.qq_code) {
isOrganizer.value = String(user.qq_code) === String(tournament.qq_code)
} else {
isOrganizer.value = false
}
// 如果是筹备中状态,获取报名玩家列表
if (tournament.status === 'prepare') {
await fetchRegisteredPlayers()
}
// 如果是进行中状态,生成树状图
else if (tournament.status === 'starting') {
await fetchRegisteredPlayers()
}
// 如果是已结束状态,获取最终结果
else if (tournament.status === 'finish') {
await fetchFinalResults()
}
} else {
console.log('未找到赛事')
alert('未找到赛事信息')
router.push('/competition')
}
} catch (error) {
console.error('获取赛事详情失败:', error)
alert('获取赛事详情失败,请重试')
} finally {
isLoading.value = false
}
}
// 获取报名玩家列表
const fetchRegisteredPlayers = async () => {
try {
const tournamentId = parseInt(route.query.id)
const response = await getSignUpResultList()
// 调试日志
console.log('报名玩家原始数据:', response)
console.log('当前赛事ID:', tournamentId)
// 只保留 tournament_id 等于当前赛事 id 的玩家
registeredPlayers.value = response.filter(player =>
player.tournament_id === tournamentId
)
console.log('筛选后的玩家数据:', registeredPlayers.value)
} catch (error) {
console.error('获取报名玩家列表失败:', error)
}
}
// 移除玩家
const handleRemovePlayer = async (playerId) => {
if (!confirm('确定要移除该玩家吗?')) return
try {
await deleteSignUpResult(playerId)
await fetchRegisteredPlayers() // 刷新列表
alert('移除成功!')
} catch (error) {
console.error('移除玩家失败:', error)
alert(error.response?.data?.message || '移除失败,请重试')
}
}
// 打开状态修改弹窗
const openStatusDialog = () => {
selectedStatus.value = competition.value.status
showStatusDialog.value = true
}
// 关闭状态修改弹窗
const closeStatusDialog = () => {
showStatusDialog.value = false
selectedStatus.value = ''
}
// 处理状态更新
const handleStatusUpdate = async () => {
try {
isUpdating.value = true
const tournamentId = route.query.id
// 将中文赛制转换回英文值
const getFormatValue = (format) => {
const formatEntries = Object.entries(formatMap)
const entry = formatEntries.find(([_, value]) => value === format)
return entry ? entry[0] : format
}
// 构建更新数据
const updateData = {
name: competition.value.name,
format: getFormatValue(competition.value.format),
organizer: competition.value.organizer,
qq_code: competition.value.qq_code,
start_time: competition.value.start_time,
end_time: competition.value.end_time,
status: selectedStatus.value
}
console.log('更新赛事状态,发送数据:', updateData)
await updateTournament(tournamentId, updateData)
alert('状态更新成功!')
closeStatusDialog()
fetchTournamentDetail() // 刷新数据
} catch (error) {
console.error('状态更新失败:', error)
console.error('错误详情:', {
message: error.message,
response: error.response?.data,
status: error.response?.status
})
alert(error.message || '状态更新失败,请重试')
} finally {
isUpdating.value = false
}
}
// 打开玩家编辑弹窗
const handleEditPlayer = (player) => {
playerEditForm.value = {
id: player.id,
tournament_id: player.tournament_id,
tournament_name: competition.value.name,
team_name: player.team_name || '个人',
sign_name: player.sign_name,
win: player.win || '0',
lose: player.lose || '0',
status: player.status || 'tie',
faction: player.faction || 'random',
qq: player.qq || ''
}
showPlayerEditDialog.value = true
}
// 关闭玩家编辑弹窗
const closePlayerEditDialog = () => {
showPlayerEditDialog.value = false
}
// 处理玩家信息更新
const handlePlayerUpdate = async () => {
try {
isUpdating.value = true
console.log('更新前的数据:', playerEditForm.value)
// 构建更新数据
const updateData = {
tournament_id: parseInt(playerEditForm.value.tournament_id),
tournament_name: playerEditForm.value.tournament_name,
team_name: playerEditForm.value.team_name === '个人' ? null : playerEditForm.value.team_name,
sign_name: playerEditForm.value.sign_name,
win: playerEditForm.value.win.toString(),
lose: playerEditForm.value.lose.toString(),
status: playerEditForm.value.status,
faction: playerEditForm.value.faction,
qq: playerEditForm.value.qq
}
console.log('发送到API的数据:', updateData)
await updateSignUpResult(playerEditForm.value.id, updateData)
await fetchRegisteredPlayers() // 刷新列表
closePlayerEditDialog()
alert('更新成功!')
} catch (error) {
console.error('更新玩家信息失败:', error)
console.error('错误详情:', {
message: error.message,
response: error.response?.data,
status: error.response?.status
})
alert(error.response?.data?.detail || error.message || '更新失败,请重试')
} finally {
isUpdating.value = false
}
}
// 保存对阵图修改
const saveChanges = async () => {
try {
isSaving.value = true
// TODO: 调用保存对阵图的API
// await saveTournamentBracket(route.query.id, tournamentRounds.value)
alert('保存成功!')
isEditMode.value = false
} catch (error) {
console.error('保存失败:', error)
alert('保存失败,请重试')
} finally {
isSaving.value = false
}
}
// 获取最终结果
const fetchFinalResults = async () => {
try {
const tournamentId = parseInt(route.query.id)
const response = await getSignUpResultList()
// 筛选当前赛事的玩家并按胜场数排序
const results = response
.filter(player => player.tournament_id === tournamentId)
.map((player, index) => ({
rank: index + 1,
username: player.sign_name,
qq: player.qq,
faction: player.faction,
score: `${player.win}${player.lose}`
}))
.sort((a, b) => {
const aScore = parseInt(a.score.split('胜')[0])
const bScore = parseInt(b.score.split('胜')[0])
return bScore - aScore
})
finalResults.value = results
} catch (error) {
console.error('获取最终结果失败:', error)
}
}
// 保证每个玩家有 id 和 name 字段
const mappedPlayers = computed(() =>
registeredPlayers.value.map(p => ({
id: p.id,
name: p.sign_name || p.name || '未知选手',
team_name: p.team_name || '个人',
win: p.win || '0',
lose: p.lose || '0',
status: p.status || 'tie'
}))
)
// 初始化
onMounted(() => {
fetchTournamentDetail()
})
</script>
<style scoped>
.competition-page {
min-height: 100vh;
padding: 20px;
}
.detail-card {
background: white;
border-radius: 8px;
padding: 24px;
margin: 0 auto;
max-width: 1200px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.nav-back {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.back-btn {
background: #409EFF;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.back-btn:hover {
background: #66b1ff;
}
.action-buttons {
display: flex;
gap: 12px;
}
.status-btn,
.edit-btn,
.delete-btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: 500;
transition: all 0.3s;
}
.status-btn {
background: #67C23A;
color: white;
}
.status-btn:hover {
background: #85ce61;
}
.edit-btn {
background: #409EFF;
color: white;
}
.edit-btn:hover {
background: #66b1ff;
}
.delete-btn {
background: #F56C6C;
color: white;
}
.delete-btn:hover {
background: #f78989;
}
.page-header h1 {
font-size: 24px;
font-weight: bold;
margin-bottom: 12px;
color: #303133;
}
.header-subtitle {
color: #909399;
font-size: 14px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.status-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
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;
}
.final-results-modern {
margin-top: 30px;
}
.final-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 24px;
color: #303133;
}
.final-top3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.final-card {
background: white;
border-radius: 8px;
padding: 20px;
display: flex;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #EBEEF5;
}
.final-card:nth-child(1) {
background: #FFF9EB;
border: 1px solid #FFE4B5;
}
.final-card:nth-child(2) {
background: #F8F9FA;
border: 1px solid #E4E7ED;
}
.final-card:nth-child(3) {
background: #FDF6EC;
border: 1px solid #F3D19E;
}
.rank-number {
font-size: 24px;
font-weight: bold;
margin-right: 16px;
min-width: 50px;
text-align: center;
}
.final-card:nth-child(1) .rank-number {
color: #E6A23C;
}
.final-card:nth-child(2) .rank-number {
color: #909399;
}
.final-card:nth-child(3) .rank-number {
color: #F56C6C;
}
.player-info {
flex: 1;
}
.player-name {
font-size: 16px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.player-qq {
font-size: 14px;
color: #909399;
margin-bottom: 4px;
}
.player-faction {
font-size: 14px;
color: #409EFF;
margin-bottom: 4px;
}
.player-score {
font-size: 14px;
color: #67C23A;
font-weight: 500;
}
.final-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.final-item {
display: flex;
align-items: center;
padding: 16px;
background: white;
border-radius: 4px;
border: 1px solid #EBEEF5;
}
.rank {
width: 40px;
font-size: 16px;
font-weight: 600;
color: #909399;
text-align: center;
margin-right: 16px;
}
.tournament-section {
margin: 30px 0;
border-radius: 8px;
background: #f8f9fa;
padding: 20px;
}
.edit-controls {
margin-top: 16px;
display: flex;
gap: 12px;
}
.edit-mode-btn,
.save-btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: 500;
transition: all 0.3s;
}
.edit-mode-btn {
background: #4a90e2;
color: white;
}
.edit-mode-btn:hover {
background: #357abd;
}
.save-btn {
background: #42b983;
color: white;
}
.save-btn:hover {
background: #3aa876;
}
.save-btn:disabled {
background: #a8a8a8;
cursor: not-allowed;
}
.edit-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.edit-dialog {
background: white;
border-radius: 8px;
padding: 24px;
width: 500px;
max-width: 90%;
}
.edit-dialog h3 {
margin: 0 0 20px;
color: #333;
text-align: center;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #666;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-group input:focus,
.form-group select:focus {
border-color: #409EFF;
outline: none;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.cancel-btn,
.confirm-btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: 500;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
}
.confirm-btn {
background: #409EFF;
color: white;
}
.cancel-btn:hover {
background: #e8e8e8;
}
.confirm-btn:hover {
background: #66b1ff;
}
.confirm-btn:disabled {
background: #a0cfff;
cursor: not-allowed;
}
.confirm-message {
color: #666;
margin: 20px 0;
text-align: center;
}
.delete-confirm-btn {
background: #F56C6C;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: 500;
transition: all 0.3s;
}
.delete-confirm-btn:hover {
background: #f78989;
}
.delete-confirm-btn:disabled {
background: #fab6b6;
cursor: not-allowed;
}
.status-options {
display: flex;
flex-direction: column;
gap: 12px;
margin: 20px 0;
}
.status-option {
padding: 12px;
border: 1px solid #DCDFE6;
border-radius: 4px;
background: white;
cursor: pointer;
transition: all 0.3s;
text-align: left;
}
.status-option:hover {
border-color: #409EFF;
color: #409EFF;
}
.status-option.active {
background: #409EFF;
color: white;
border-color: #409EFF;
}
.status-option.active.prepare {
background: #e6a23c;
border-color: #e6a23c;
}
.status-option.active.starting {
background: #67c23a;
border-color: #67c23a;
}
.status-option.active.finish {
background: #909399;
border-color: #909399;
}
.registered-players {
margin-top: 30px;
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
}
.players-list {
min-height: 100px;
}
.no-players {
text-align: center;
color: #909399;
padding: 40px 0;
}
.players-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.players-table th,
.players-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #EBEEF5;
}
.players-table th {
background: #F5F7FA;
color: #606266;
font-weight: 500;
}
.players-table tr:last-child td {
border-bottom: none;
}
.players-table tr:hover {
background: #F5F7FA;
}
.action-buttons {
display: flex;
gap: 8px;
}
.edit-player-btn {
background: #409EFF;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.edit-player-btn:hover {
background: #66b1ff;
}
.remove-btn {
background: #F56C6C;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.remove-btn:hover {
background: #f78989;
}
.remove-btn:disabled {
background: #fab6b6;
cursor: not-allowed;
}
@media (max-width: 768px) {
.final-top3 {
grid-template-columns: 1fr;
}
.detail-card {
padding: 16px;
}
.header-subtitle {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.players-table {
display: block;
overflow-x: auto;
}
.action-buttons {
flex-direction: column;
}
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #409EFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>