重构赛事信息(修改树状图前)

This commit is contained in:
Kunagisa 2025-07-31 00:17:22 +08:00
parent 0274eb5407
commit 605d60ec7e
7 changed files with 777 additions and 991 deletions

View File

@ -3,239 +3,225 @@ import axiosInstance from './axiosConfig';
/**
* 添加赛事
* @param {Object} tournamentData - 赛事数据
* @param {string} tournamentData.name - 赛事名称
* @param {string} tournamentData.format - 赛事类型(single, double, count)
* @param {number} tournamentData.id - 数据库中id
* @param {string} tournamentData.name - 名称
* @param {string} tournamentData.format - 类型(single, double, count)
* @param {string} tournamentData.organizer - 组织者
* @param {string} tournamentData.qq_code - QQ号
* @param {string} tournamentData.status - 状态(prepare, finish, starting)
* @param {string} tournamentData.start_time - 开始时间(格式年//例2025/05/24)
* @param {string} tournamentData.end_time - 结束时间(格式年//例2025/05/24)
* @returns {Promise} 返回添加赛事的响应数据
* @returns {Promise<Object>} 返回添加赛事的响应数据
*/
export const addTournament = async (tournamentData) => {
try {
const response = await axiosInstance.post('/tournament/add', tournamentData)
return response.data
const response = await axiosInstance.post('/tournament/add', tournamentData);
return response.data;
} catch (error) {
console.error('添加赛事失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('添加赛事失败:', error);
throw error;
}
}
};
/**
* 获取赛事列表
* @returns {Promise} 返回赛事列表数据
* @returns {Promise<Array<Object>>} 返回赛事列表数据
*/
export const getTournamentList = async () => {
try {
const response = await axiosInstance.get('/tournament/getlist')
return response.data
const response = await axiosInstance.get('/tournament/getlist');
return response.data;
} catch (error) {
console.error('获取赛事列表失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('获取赛事列表失败:', error);
throw error;
}
}
};
// 更新赛事
export const updateTournament = async (id, data) => {
/**
* 更新赛事
* @param {number} id - 赛事ID
* @param {Object} tournamentData - 赛事数据
* @param {number} tournamentData.id - 数据库中id
* @param {string} tournamentData.name - 名称
* @param {string} tournamentData.format - 类型(single, double, count)
* @param {string} tournamentData.organizer - 组织者
* @param {string} tournamentData.qq_code - QQ号
* @param {string} tournamentData.status - 状态(prepare, finish, starting)
* @param {string} tournamentData.start_time - 开始时间(格式年//例2025/05/24)
* @param {string} tournamentData.end_time - 结束时间(格式年//例2025/05/24)
* @returns {Promise<Object>} 返回更新赛事的响应数据
*/
export const updateTournament = async (id, tournamentData) => {
try {
console.log('更新赛事,发送数据:', data)
const response = await axiosInstance.put(`/tournament/update/${id}`, {
name: data.name,
format: data.format,
organizer: data.organizer,
qq_code: data.qq_code,
start_time: data.start_time,
end_time: data.end_time,
status: data.status
})
return response.data
const response = await axiosInstance.put(`/tournament/update/${id}`, tournamentData);
return response.data;
} catch (error) {
console.error('更新赛事失败:', error)
if (error.response) {
console.error('错误详情:', {
status: error.response.status,
data: error.response.data,
headers: error.response.headers,
config: error.config
})
// 如果有详细的错误信息,抛出它
if (error.response.data?.detail) {
throw new Error(error.response.data.detail)
}
}
throw error
console.error('更新赛事失败:', error);
throw error;
}
}
};
// 删除赛事
/**
* 删除赛事
* @param {number} id - 赛事ID
* @returns {Promise<Object>} 返回删除赛事的响应数据
*/
export const deleteTournament = async (id) => {
try {
const response = await axiosInstance.delete(`/tournament/delete/${id}`)
return response.data
const response = await axiosInstance.delete(`/tournament/delete/${id}`);
return response.data;
} catch (error) {
console.error('删除赛事失败:', error)
throw error
console.error('删除赛事失败:', error);
throw error;
}
}
};
// 添加报名结果
export const addSignUpResult = async (data) => {
/**
* 添加玩家报名
* @param {Object} signupData - 报名数据
* @param {number} signupData.id - 数据库中id
* @param {string} signupData.type - 类型
* @param {string} signupData.teamname - 队伍名称
* @param {string} signupData.faction - 阵营(alliedsovietempireobvoicerandom)
* @param {string} signupData.username - 用户名
* @param {string} signupData.qq - QQ号
* @returns {Promise<Object>} 返回添加报名的响应数据
*/
export const addSignUp = async (signupData) => {
try {
const response = await axiosInstance.post('/tournament/signup_result/add', {
tournament_id: parseInt(data.tournament_id),
tournament_name: data.tournament_name,
team_name: data.team_name,
sign_name: data.sign_name.trim(),
win: '0',
lose: '0',
status: 'tie'
})
return response.data
const response = await axiosInstance.post('/tournament/signup/add', signupData);
return response.data;
} catch (error) {
console.error('请求错误:', error)
if (error.response?.data?.detail) {
throw new Error(error.response.data.detail)
}
throw error
console.error('添加报名失败:', error);
throw error;
}
}
};
// 获取参赛结果列表
/**
* 获取玩家报名列表
* @returns {Promise<Array<Object>>} 返回报名列表数据
*/
export const getSignUpList = async () => {
try {
const response = await axiosInstance.get('/tournament/signup/getlist');
return response.data;
} catch (error) {
console.error('获取报名列表失败:', error);
throw error;
}
};
/**
* 更新玩家报名
* @param {number} id - 报名ID
* @param {Object} signupData - 报名数据
* @param {number} signupData.id - 数据库中id
* @param {string} signupData.type - 类型
* @param {string} signupData.teamname - 队伍名称
* @param {string} signupData.faction - 阵营(alliedsovietempireobvoicerandom)
* @param {string} signupData.username - 用户名
* @param {string} signupData.qq - QQ号
* @returns {Promise<Object>} 返回更新报名的响应数据
*/
export const updateSignUp = async (id, signupData) => {
try {
const response = await axiosInstance.put(`/tournament/signup/update/${id}`, signupData);
return response.data;
} catch (error) {
console.error('更新报名失败:', error);
throw error;
}
};
/**
* 删除玩家报名
* @param {number} id - 报名ID
* @returns {Promise<Object>} 返回删除报名的响应数据
*/
export const deleteSignUp = async (id) => {
try {
const response = await axiosInstance.delete(`/tournament/signup/delete/${id}`);
return response.data;
} catch (error) {
console.error('删除报名失败:', error);
throw error;
}
};
/**
* 添加报名结果
* @param {Object} resultData - 结果数据
* @param {number} resultData.tournament_id - 赛事id(int)
* @param {string} resultData.tournament_name - 赛事名称
* @param {string} resultData.team_name - 队伍名称(可选)
* @param {string} resultData.sign_name - 参赛人员名称
* @param {string} resultData.win - 参赛人员胜利局数(str)
* @param {string} resultData.lose - 参赛人员失败局数(str)
* @param {string} resultData.status - 参赛人员对局状态(win,lose,tie)
* @param {string} resultData.round - 轮数(str)
* @param {string} resultData.rival_name - 对方name(str)
* @returns {Promise<Object>} 返回添加报名结果的响应数据
*/
export const addSignUpResult = async (resultData) => {
try {
const response = await axiosInstance.post('/tournament/signup_result/add', resultData);
return response.data;
} catch (error) {
console.error('添加报名结果失败:', error);
throw error;
}
};
/**
* 获取报名结果列表
* @returns {Promise<Array<Object>>} 返回报名结果列表数据
*/
export const getSignUpResultList = async () => {
try {
const response = await axiosInstance.get('/tournament/signup_result/getlist')
return response.data
const response = await axiosInstance.get('/tournament/signup_result/getlist');
return response.data;
} catch (error) {
console.error('获取参赛结果列表失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('获取报名结果列表失败:', error);
throw error;
}
}
};
// 更新参赛结果
export const updateSignUpResult = async (id, data) => {
/**
* 更新报名结果
* @param {number} id - 结果ID
* @param {Object} resultData - 结果数据
* @param {number} resultData.tournament_id - 赛事id(int)
* @param {string} resultData.tournament_name - 赛事名称
* @param {string} resultData.team_name - 队伍名称(可选)
* @param {string} resultData.sign_name - 参赛人员名称
* @param {string} resultData.win - 参赛人员胜利局数(str)
* @param {string} resultData.lose - 参赛人员失败局数(str)
* @param {string} resultData.status - 参赛人员对局状态(win,lose,tie)
* @param {string} resultData.round - 轮数(str)
* @param {string} resultData.rival_name - 对方name(str)
* @returns {Promise<Object>} 返回更新报名结果的响应数据
*/
export const updateSignUpResult = async (id, resultData) => {
try {
// // 更新报名信息 (这部分逻辑根据您的要求被注释掉)
// console.log('更新报名信息...')
// await axiosInstance.put(`/tournament/signup/update/${id}`, {
// tournament_id: parseInt(data.tournament_id),
// type: data.team_name ? 'teamname' : 'individual',
// teamname: data.team_name || '',
// faction: data.faction || 'random',
// username: data.sign_name,
// qq: data.qq || ''
// })
// console.log('报名信息更新成功')
// 更新报名结果
console.log('更新报名结果...')
await axiosInstance.put(`/tournament/signup_result/update/${id}`, {
tournament_id: parseInt(data.tournament_id),
tournament_name: data.tournament_name,
team_name: data.team_name || null,
sign_name: data.sign_name,
win: data.win || '0',
lose: data.lose || '0',
status: data.status || 'tie'
})
console.log('报名结果更新成功')
return { success: true }
const response = await axiosInstance.put(`/tournament/signup_result/update/${id}`, resultData);
return response.data;
} catch (error) {
console.error('更新参赛结果失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('更新报名结果失败:', error);
throw error;
}
}
};
// 删除参赛选手
/**
* 删除报名结果
* @param {number} id - 结果ID
* @returns {Promise<Object>} 返回删除报名结果的响应数据
*/
export const deleteSignUpResult = async (id) => {
try {
// 删除报名结果
console.log('删除报名结果...')
await axiosInstance.delete(`/tournament/signup_result/delete/${id}`)
console.log('报名结果删除成功')
// // 删除报名信息 (这部分逻辑根据您的要求被注释掉)
// console.log('删除报名信息...')
// await axiosInstance.delete(`/tournament/signup/delete/${id}`)
// console.log('报名信息删除成功')
return { success: true }
const response = await axiosInstance.delete(`/tournament/signup_result/delete/${id}`);
return response.data;
} catch (error) {
console.error('删除参赛选手失败:', {
status: error.response?.status,
data: error.response?.data,
message: error.message
})
throw error
console.error('删除报名结果失败:', error);
throw error;
}
}
// 添加报名
export const addSignUp = async (data) => {
try {
console.log('开始报名流程,数据:', data)
// 调用报名 API
console.log('调用报名 API...')
await axiosInstance.post('/tournament/signup/add', {
tournament_id: data.id,
type: data.type,
teamname: data.team_name || '',
faction: data.faction || 'random',
username: data.sign_name,
qq: data.qq || ''
})
console.log('报名 API 调用成功')
// 调用报名结果 API
console.log('调用报名结果 API...')
await axiosInstance.post('/tournament/signup_result/add', {
tournament_id: data.id,
tournament_name: data.tournament_name,
team_name: data.team_name || null,
sign_name: data.sign_name,
win: '0',
lose: '0',
status: 'tie'
})
console.log('报名结果 API 调用成功')
return {
signup: { success: true },
result: { success: true }
}
} catch (error) {
console.error('报名请求错误:', {
message: error.message,
response: error.response?.data,
status: error.response?.status,
config: error.config
})
// 如果是服务器返回的错误信息,直接使用
if (error.response?.data?.detail) {
throw new Error(error.response.data.detail)
}
// 其他错误,包装成更友好的错误信息
throw new Error('报名失败,请检查网络连接后重试')
}
}
};

View File

@ -60,26 +60,26 @@ const routes = [
{
path: 'competition',
name: 'Competition',
component: () => import('@/views/index/Competition.vue'),
component: () => import('@/views/competition/Competition.vue'),
// meta: { requiresAuth: true, requiredPrivilege: ['lv-admin','lv-competitor'] }
meta: { requiresAuth: true}
},
{
path: 'competition/add',
name: 'AddCompetition',
component: () => import('@/views/index/AddContestant.vue'),
component: () => import('@/views/competition/AddContestant.vue'),
meta: { requiresAuth: true }
},
{
path: 'competition/detail',
name: 'CompetitionDetail',
component: () => import('@/views/index/CompetitionDetail.vue'),
component: () => import('@/views/competition/CompetitionDetail.vue'),
meta: { requiresAuth: true }
},
{
path: 'competition/signup',
name: 'CompetitionSignUp',
component: () => import('@/views/index/CompetitionSignUp.vue'),
component: () => import('@/views/competition/CompetitionSignUp.vue'),
meta: { requiresAuth: true }
},
// {

View File

@ -3,62 +3,62 @@
<div class="page-header">
<h1>添加新赛事</h1>
<div class="header-actions">
<button class="btn-excel" @click="handleExcelImport">
<i class="fas fa-file-excel"></i>
通过表格添加
</button>
<!-- <button class="btn-excel" @click="handleExcelImport">-->
<!-- <i class="fas fa-file-excel"></i>-->
<!-- 通过表格添加-->
<!-- </button>-->
</div>
</div>
<!-- Excel导入弹窗 -->
<div v-if="showExcelDialog" class="excel-dialog-overlay">
<div class="excel-dialog">
<h3>通过Excel导入赛事信息</h3>
<div class="excel-upload-area" @click="triggerFileInput" @dragover.prevent @drop.prevent="handleFileDrop"
:class="{ 'is-dragover': isDragover }">
<input type="file" ref="fileInput" accept=".xlsx" @change="handleFileSelect" style="display: none">
<div class="upload-content">
<i class="fas fa-file-excel"></i>
<p>点击或拖拽Excel文件到此处</p>
</div>
</div>
<div v-if="eventPreviewData.length > 0" class="preview-table">
<h4>赛事信息表预览</h4>
<table>
<thead>
<tr>
<th v-for="h in eventPreviewHeaders" :key="h">{{ h }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in eventPreviewData" :key="index">
<td v-for="h in eventPreviewHeaders" :key="h">{{ item[h] }}</td>
</tr>
</tbody>
</table>
</div>
<div v-if="playerPreviewData.length > 0" class="preview-table">
<h4>选手报名表预览</h4>
<table>
<thead>
<tr>
<th v-for="h in playerPreviewHeaders" :key="h">{{ h }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in playerPreviewData" :key="index">
<td v-for="h in playerPreviewHeaders" :key="h">{{ item[h] }}</td>
</tr>
</tbody>
</table>
</div>
<div class="dialog-actions">
<button class="confirm-btn" @click="confirmImport" :disabled="eventPreviewData.length === 0">确认导入</button>
<button class="cancel-btn" @click="closeExcelDialog">取消</button>
</div>
</div>
</div>
<!-- &lt;!&ndash; Excel导入弹窗 &ndash;&gt;-->
<!-- <div v-if="showExcelDialog" class="excel-dialog-overlay">-->
<!-- <div class="excel-dialog">-->
<!-- <h3>通过Excel导入赛事信息</h3>-->
<!-- <div class="excel-upload-area" @click="triggerFileInput" @dragover.prevent @drop.prevent="handleFileDrop"-->
<!-- :class="{ 'is-dragover': isDragover }">-->
<!-- <input type="file" ref="fileInput" accept=".xlsx" @change="handleFileSelect" style="display: none">-->
<!-- <div class="upload-content">-->
<!-- <i class="fas fa-file-excel"></i>-->
<!-- <p>点击或拖拽Excel文件到此处</p>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div v-if="eventPreviewData.length > 0" class="preview-table">-->
<!-- <h4>赛事信息表预览</h4>-->
<!-- <table>-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th v-for="h in eventPreviewHeaders" :key="h">{{ h }}</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr v-for="(item, index) in eventPreviewData" :key="index">-->
<!-- <td v-for="h in eventPreviewHeaders" :key="h">{{ item[h] }}</td>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- <div v-if="playerPreviewData.length > 0" class="preview-table">-->
<!-- <h4>选手报名表预览</h4>-->
<!-- <table>-->
<!-- <thead>-->
<!-- <tr>-->
<!-- <th v-for="h in playerPreviewHeaders" :key="h">{{ h }}</th>-->
<!-- </tr>-->
<!-- </thead>-->
<!-- <tbody>-->
<!-- <tr v-for="(item, index) in playerPreviewData" :key="index">-->
<!-- <td v-for="h in playerPreviewHeaders" :key="h">{{ item[h] }}</td>-->
<!-- </tr>-->
<!-- </tbody>-->
<!-- </table>-->
<!-- </div>-->
<!-- <div class="dialog-actions">-->
<!-- <button class="confirm-btn" @click="confirmImport" :disabled="eventPreviewData.length === 0">确认导入</button>-->
<!-- <button class="cancel-btn" @click="closeExcelDialog">取消</button>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<form @submit.prevent="handleSubmit" class="contest-form">
<div class="form-group">
@ -119,7 +119,7 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { addTournament, addSignUp, getTournamentList } from '@/api/tournament'
import { addTournament, addSignUp, getTournamentList } from '@/api/tournament.js'
import * as XLSX from 'xlsx'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'

View File

@ -1,271 +1,488 @@
<script setup>
import { ref, onMounted } from 'vue';
import TournamentBracket from '@/components/TournamentBracket.vue';
//
const activeTab = ref('upcoming'); // upcoming, ongoing, completed
const tournaments = ref([]);
const loading = ref(false);
// ID
const selectedTournamentId = ref(null);
//
onMounted(() => {
fetchTournaments();
});
//
const fetchTournaments = async () => {
loading.value = true;
try {
// API
//
tournaments.value = [
{ id: 1, name: '2023年夏季赛', status: 'completed', startDate: '2023-06-01', endDate: '2023-08-30' },
{ id: 2, name: '2023年秋季赛', status: 'ongoing', startDate: '2023-09-01', endDate: '2023-11-30' },
{ id: 3, name: '2024年春季赛', status: 'upcoming', startDate: '2024-03-01', endDate: '2024-05-30' }
];
} catch (error) {
console.error('获取比赛列表失败:', error);
} finally {
loading.value = false;
}
};
//
const selectTournament = (tournamentId) => {
selectedTournamentId.value = tournamentId;
};
//
const refreshParticipants = () => {
//
console.log('刷新参赛者列表');
};
//
const filteredTournaments = () => {
return tournaments.value.filter(tournament => {
if (activeTab.value === 'upcoming') return tournament.status === 'upcoming';
if (activeTab.value === 'ongoing') return tournament.status === 'ongoing';
if (activeTab.value === 'completed') return tournament.status === 'completed';
return true;
});
};
</script>
<template>
<div class="competition-container">
<h1 class="page-title">比赛中心</h1>
<!-- 比赛类型选项卡 -->
<div class="tabs">
<div
class="tab"
:class="{ active: activeTab === 'upcoming' }"
@click="activeTab = 'upcoming'"
>
即将开始
</div>
<div
class="tab"
:class="{ active: activeTab === 'ongoing' }"
@click="activeTab = 'ongoing'"
>
正在进行
</div>
<div
class="tab"
:class="{ active: activeTab === 'completed' }"
@click="activeTab = 'completed'"
>
已结束
<div class="competition-page">
<div class="page-header">
<h1>赛程信息</h1>
<div class="header-subtitle">
<span class="date-range">点击即可查看和报名</span>
</div>
</div>
<!-- 比赛列表 -->
<div class="tournament-list">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="filteredTournaments().length === 0" class="no-data">
暂无{{ activeTab === 'upcoming' ? '即将开始' : activeTab === 'ongoing' ? '正在进行' : '已结束' }}的比赛
<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
v-else
v-for="tournament in filteredTournaments()"
:key="tournament.id"
class="tournament-card"
:class="{ active: selectedTournamentId === tournament.id }"
@click="selectTournament(tournament.id)"
>
<div class="tournament-header">
<h3>{{ tournament.name }}</h3>
<span class="status-badge" :class="tournament.status">
{{ tournament.status === 'upcoming' ? '即将开始' :
tournament.status === 'ongoing' ? '正在进行' : '已结束' }}
</span>
</div>
<div class="tournament-dates">
<span>开始日期: {{ tournament.startDate }}</span>
<span>结束日期: {{ tournament.endDate }}</span>
<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="selectedTournamentId" class="tournament-detail">
<TournamentBracket
:tournamentId="selectedTournamentId"
@refreshPlayers="refreshParticipants"
/>
<!-- 错误提示 -->
<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-else class="select-prompt">
请从左侧选择一个比赛查看详情
<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.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') {
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-container {
display: flex;
flex-direction: column;
max-width: 1200px;
.competition-page {
padding: 16px;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.page-title {
font-size: 24px;
.page-header {
margin-bottom: 20px;
color: #333;
}
.tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid #ddd;
.page-header h1 {
font-size: 22px;
color: #1a237e;
margin: 0 0 6px 0;
}
.tab {
padding: 10px 20px;
cursor: pointer;
font-weight: 500;
.header-subtitle {
color: #666;
border-bottom: 2px solid transparent;
transition: all 0.3s ease;
font-size: 13px;
}
.tab:hover {
color: #007acc;
}
.tab.active {
color: #007acc;
border-bottom: 2px solid #007acc;
}
.tournament-list {
display: flex;
flex-direction: column;
gap: 15px;
margin-bottom: 30px;
}
.tournament-card {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.tournament-card:hover {
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.tournament-card.active {
border-color: #007acc;
box-shadow: 0 0 0 2px rgba(0, 122, 204, 0.2);
}
.tournament-header {
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.tournament-header h3 {
margin: 0;
font-size: 18px;
.left-actions,
.right-actions {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.status-badge {
padding: 4px 8px;
.search-box {
position: relative;
flex-grow: 1;
}
.search-box input {
padding: 6px 10px 6px 28px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
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-badge.upcoming {
background-color: #e6f7ff;
color: #1890ff;
}
.status-tag.prepare { background-color: #e6a23c; color: #fff; }
.status-tag.starting { background-color: #67c23a; color: #fff; }
.status-tag.finish { background-color: #909399; color: #fff; }
.status-badge.ongoing {
background-color: #f6ffed;
color: #52c41a;
}
.status-badge.completed {
background-color: #f5f5f5;
color: #666;
}
.tournament-dates {
.action-cell {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #666;
gap: 6px;
}
.loading, .no-data, .select-prompt {
padding: 20px;
.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;
color: #666;
background: #f9f9f9;
border-radius: 8px;
font-style: italic;
font-size: 14px;
color: #909399;
}
.tournament-detail {
margin-top: 20px;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
.error-message {
background-color: #fef0f0;
color: #f56c6c;
padding: 10px 14px;
border-radius: 4px;
display: flex;
gap: 8px;
font-size: 13px;
}
@media (min-width: 768px) {
.competition-container {
flex-direction: row;
flex-wrap: wrap;
.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;
}
.page-title, .tabs {
.action-bar {
flex-direction: column;
gap: 10px;
align-items: stretch;
}
.left-actions, .right-actions {
flex-direction: column;
gap: 8px;
width: 100%;
}
.tournament-list {
width: 30%;
margin-right: 2%;
.table-container {
margin: 0 -12px;
border-radius: 0;
}
.tournament-detail, .select-prompt {
width: 68%;
.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>

View File

@ -24,14 +24,14 @@
{{ statusMap[competition.status] }}
</span>
</div>
<div class="edit-controls" v-if="isOrganizer">
<!-- <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>
<!-- 报名玩家列表 -->
@ -46,6 +46,8 @@
<tr>
<th>玩家名称</th>
<th>队伍名称</th>
<th>阵营</th>
<th>QQ</th>
<th>胜场</th>
<th>负场</th>
<th>状态</th>
@ -55,12 +57,14 @@
<tbody>
<tr v-for="player in registeredPlayers" :key="player.id">
<td>{{ player.sign_name }}</td>
<td>{{ player.team_name || '个人' }}</td>
<td>{{ player.team_name === ' ' ? '个人' : player.team_name}}</td>
<td>{{ formatFaction(player.faction) }}</td>
<td>{{ player.qq || '-' }}</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="edit-player-btn" @click="handleEditPlayer(player)" >修改</button>-->
<button class="remove-btn" @click="handleRemovePlayer(player.id)">移除</button>
</td>
</tr>
@ -238,11 +242,13 @@ import {
getTournamentList,
updateTournament,
deleteTournament,
getSignUpList,
getSignUpResultList,
updateSignUpResult,
deleteSignUpResult
} from '@/api/tournament'
import { getStoredUser } from '@/utils/jwt'
deleteSignUpResult,
deleteSignUp
} from '@/api/tournament.js'
import { getStoredUser } from '@/utils/jwt.js'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'
@ -324,10 +330,18 @@ const formatType = (type) => {
return formatMap[type] || type
}
// //
// const formatFaction = (faction) => {
// return factionMap[faction] || faction
// }
//
const formatFaction = (faction) => {
const factionMap = {
'allied': '盟军',
'soviet': '苏联',
'empire': '帝国',
'ob': 'OB',
'voice': '解说',
'random': '随机'
}
return factionMap[faction] || faction
}
//
const handleBack = () => {
@ -482,15 +496,40 @@ const fetchTournamentDetail = async () => {
const fetchRegisteredPlayers = async () => {
try {
const tournamentId = parseInt(route.query.id)
const response = await getSignUpResultList()
//
console.log('报名玩家原始数据:', response)
//
const [signupList, resultList] = await Promise.all([
getSignUpList(),
getSignUpResultList()
])
console.log('报名信息原始数据:', signupList)
console.log('结果信息原始数据:', resultList)
console.log('当前赛事ID:', tournamentId)
// tournament_id id
registeredPlayers.value = response.filter(player =>
player.tournament_id === tournamentId
//
const tournamentSignups = signupList.filter(signup =>
signup.tournament_id === tournamentId
)
console.log('筛选后的玩家数据:', registeredPlayers.value)
//
const tournamentResults = resultList.filter(result =>
result.tournament_id === tournamentId
)
//
registeredPlayers.value = tournamentResults.map(result => {
//
const signup = tournamentSignups.find(s => s.username === result.sign_name)
return {
...result,
faction: signup?.faction || 'random', //
qq: signup?.qq || '' // QQ
}
})
console.log('合并后的玩家数据:', registeredPlayers.value)
} catch (error) {
console.error('获取报名玩家列表失败:', error)
}
@ -501,7 +540,33 @@ const handleRemovePlayer = async (playerId) => {
if (!confirm('确定要移除该玩家吗?')) return
try {
await deleteSignUpResult(playerId)
//
const player = registeredPlayers.value.find(p => p.id === playerId)
if (!player) {
throw new Error('未找到玩家信息')
}
//
const signupList = await getSignUpList()
const signupRecord = signupList.find(s =>
s.tournament_id === parseInt(route.query.id) &&
s.username === player.sign_name
)
//
const deletePromises = []
//
deletePromises.push(deleteSignUpResult(playerId))
//
if (signupRecord) {
deletePromises.push(deleteSignUp(signupRecord.id))
}
//
await Promise.all(deletePromises)
await fetchRegisteredPlayers() //
successDialog.value = { visible: true, message: '移除成功!' }
} catch (error) {

View File

@ -142,7 +142,7 @@
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { addSignUp } from '@/api/tournament'
import {addSignUp, addSignUpResult} from '@/api/tournament.js'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'
@ -256,26 +256,32 @@ const handleSubmit = async () => {
}
const submitData = {
id: parseInt(competitionInfo.value.id),
tournament_name: competitionInfo.value.name,
tournament_id: parseInt(competitionInfo.value.id),
type: signupForm.value.type,
team_name: signupForm.value.type === 'teamname' ? signupForm.value.teamName : '',
sign_name: signName,
teamname: signupForm.value.type === 'teamname' ? signupForm.value.teamName : ' ',
username: signupForm.value.username,
faction: signupForm.value.faction,
qq_code: String(competitionInfo.value.qq_code) // qq_code
qq: signupForm.value.qq
}
const signupResultData = {
tournament_id: parseInt(competitionInfo.value.id),
tournament_name: competitionInfo.value.name,
team_name: signupForm.value.type === 'teamname' ? signupForm.value.teamName : ' ',
sign_name: signupForm.value.username,
win: '0',
lose: '0',
status: 'tie',
round: '0',
rival_name: ''
}
console.log('提交的报名数据:', submitData)
const result = await addSignUp(submitData)
console.log('报名结果:', result)
if (result.signup && result.result) {
successDialog.value = { visible: true, message: '报名成功!' }
const resultSignup = await addSignUpResult(signupResultData)
console.log('提交的报名数据:', submitData, signupResultData)
console.log('报名结果:', result, resultSignup)
successDialog.value = { visible: true, message: '报名成功!' }
setTimeout(() => {
router.push('/competition')
} else {
console.error('报名结果不完整:', result)
throw new Error('报名数据不完整,请重试')
}
}, 1500)
} catch (error) {
console.error('报名失败:', error)
console.error('错误详情:', {

View File

@ -1,488 +0,0 @@
<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>