658 lines
20 KiB
Vue
658 lines
20 KiB
Vue
<template>
|
||
<div class="tournament-bracket-root">
|
||
<h1>单败淘汰赛赛程树状图</h1>
|
||
<div class="container">
|
||
<div class="control-panel">
|
||
<h2>选择比赛</h2>
|
||
<div class="tournament-info">
|
||
<select v-model="selectedTournamentId" @change="handleTournamentChange">
|
||
<option value="">-- 请选择比赛 --</option>
|
||
<option v-for="tournament in tournaments" :key="tournament.id" :value="tournament.id">
|
||
{{ tournament.name }}
|
||
</option>
|
||
</select>
|
||
<div v-if="selectedTournamentId">
|
||
<strong>已选择:</strong> {{ selectedTournamentName }}
|
||
</div>
|
||
</div>
|
||
|
||
<h2>参赛者列表</h2>
|
||
<div id="player-list">
|
||
<div v-if="!selectedTournamentId" class="loading">请先选择比赛</div>
|
||
<div v-else-if="loading" class="loading">加载中...</div>
|
||
<div v-else>
|
||
<p v-if="participants.length === 0">暂无参赛者</p>
|
||
<template v-else>
|
||
<p>共 {{ participants.length }} 位参赛者</p>
|
||
<ul>
|
||
<li v-for="participant in participants" :key="participant.id">
|
||
{{ participant.name }}
|
||
</li>
|
||
</ul>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>生成赛程</h2>
|
||
<button @click="generateBracket" :disabled="!canGenerateBracket">生成单败淘汰赛程</button>
|
||
|
||
<div v-if="finalRanking" id="finalRanking">
|
||
<h3>最终排名</h3>
|
||
<p>冠军: {{ finalRanking.champion }}</p>
|
||
<p>亚军: {{ finalRanking.runnerUp }}</p>
|
||
<p>季军: {{ finalRanking.thirdPlace }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bracket-container" id="bracketContainer">
|
||
<div class="bracket" id="bracket">
|
||
<template v-for="(roundMatches, rIdx) in roundsMatches" :key="rIdx">
|
||
<div class="round">
|
||
<div class="round-title">第 {{ rIdx + 1 }} 轮</div>
|
||
<template v-for="match in roundMatches" :key="match.id">
|
||
<div class="match" :id="'match-' + match.id">
|
||
<!-- 参赛者1 -->
|
||
<div class="participant" :class="{ winner: match.winner && match.participant1 && match.winner.id === match.participant1.id }">
|
||
{{ match.participant1 ? match.participant1.name : '轮空' }}
|
||
<input type="number" min="0" class="score-input"
|
||
v-model.number="match.score1"
|
||
:disabled="match.decided || !match.participant1 || !match.participant2"
|
||
:placeholder="match.participant1 ? (match.participant1.win || '0') : '0'"
|
||
>
|
||
</div>
|
||
<!-- 参赛者2 -->
|
||
<div class="participant" :class="{ winner: match.winner && match.participant2 && match.winner.id === match.participant2.id }">
|
||
{{ match.participant2 ? match.participant2.name : '轮空' }}
|
||
<input type="number" min="0" class="score-input"
|
||
v-model.number="match.score2"
|
||
:disabled="match.decided || !match.participant1 || !match.participant2"
|
||
:placeholder="match.participant2 ? (match.participant2.win || '0') : '0'"
|
||
>
|
||
</div>
|
||
<!-- 确认比分按钮 -->
|
||
<button class="score-btn"
|
||
:disabled="match.decided || !match.participant1 || !match.participant2"
|
||
@click="confirmScore(match)">
|
||
确认比分
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<svg class="bracket-lines" id="bracketLines"></svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue';
|
||
import { getSignUpResultList, updateSignUpResult, getTournamentList } from '@/api/tournament';
|
||
|
||
const props = defineProps({
|
||
tournamentId: {
|
||
type: Number,
|
||
required: true
|
||
}
|
||
});
|
||
|
||
const tournaments = ref([]);
|
||
const selectedTournamentId = ref('');
|
||
const selectedTournamentName = ref('');
|
||
const participants = ref([]);
|
||
const loading = ref(false);
|
||
const finalRanking = ref(null);
|
||
|
||
const tournament = ref({
|
||
id: props.tournamentId,
|
||
name: '',
|
||
participants: [],
|
||
rounds: 0,
|
||
matches: []
|
||
});
|
||
|
||
const canGenerateBracket = computed(() => {
|
||
return selectedTournamentId.value && participants.value.length >= 2;
|
||
});
|
||
|
||
const roundsMatches = computed(() => {
|
||
const arr = [];
|
||
for (let r = 1; r <= tournament.value.rounds; r++) {
|
||
arr.push(tournament.value.matches.filter(m => m.round === r));
|
||
}
|
||
return arr;
|
||
});
|
||
|
||
onMounted(() => {
|
||
fetchTournamentList();
|
||
});
|
||
|
||
const fetchTournamentList = async () => {
|
||
try {
|
||
const data = await getTournamentList();
|
||
tournaments.value = data;
|
||
} catch (err) {
|
||
console.error('获取比赛列表失败:', err);
|
||
}
|
||
};
|
||
|
||
const handleTournamentChange = async () => {
|
||
if (!selectedTournamentId.value) {
|
||
selectedTournamentName.value = '';
|
||
participants.value = [];
|
||
tournament.value.id = null;
|
||
tournament.value.name = '';
|
||
tournament.value.participants = [];
|
||
return;
|
||
}
|
||
|
||
const selectedTournament = tournaments.value.find(t => t.id === selectedTournamentId.value);
|
||
if (selectedTournament) {
|
||
selectedTournamentName.value = selectedTournament.name;
|
||
tournament.value.id = selectedTournamentId.value;
|
||
tournament.value.name = selectedTournament.name;
|
||
loading.value = true;
|
||
|
||
try {
|
||
const data = await getSignUpResultList();
|
||
const filtered = data.filter(item => item.tournament_id == selectedTournamentId.value);
|
||
participants.value = filtered.map(item => ({ id: item.id, name: item.sign_name }));
|
||
tournament.value.participants = participants.value;
|
||
} catch (err) {
|
||
console.error('获取参赛者失败:', err);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
const generateBracket = () => {
|
||
if (!canGenerateBracket.value) return;
|
||
finalRanking.value = null;
|
||
generateSingleEliminationBracket();
|
||
nextTick(() => drawConnections());
|
||
};
|
||
|
||
const generateSingleEliminationBracket = async () => {
|
||
tournament.value.matches = [];
|
||
tournament.value.rounds = Math.ceil(Math.log2(tournament.value.participants.length));
|
||
const totalPlayers = Math.pow(2, tournament.value.rounds);
|
||
const shuffled = [...tournament.value.participants].sort(() => Math.random() - 0.5);
|
||
|
||
const latestData = await getSignUpResultList();
|
||
const filteredData = latestData.filter(item => item.tournament_id == tournament.value.id);
|
||
|
||
while (shuffled.length < totalPlayers) {
|
||
shuffled.push(null);
|
||
}
|
||
|
||
// 生成第一轮比赛
|
||
const firstRoundMatchesCount = totalPlayers / 2;
|
||
for (let i = 0; i < firstRoundMatchesCount; i++) {
|
||
const p1 = shuffled[i * 2];
|
||
const p2 = shuffled[i * 2 + 1];
|
||
let winner = null;
|
||
let decided = false;
|
||
|
||
if (p1 && !p2) { // p1 gets a bye
|
||
winner = p1;
|
||
decided = true;
|
||
} else if (!p1 && p2) { // p2 gets a bye
|
||
winner = p2;
|
||
decided = true;
|
||
} else if (!p1 && !p2) { // double bye
|
||
winner = null; // Winner is null for a double bye
|
||
decided = true;
|
||
}
|
||
// If p1 && p2, winner is null, decided is false (normal match)
|
||
|
||
const p1Latest = p1 ? filteredData.find(item => item.id === p1.id) : null;
|
||
const p2Latest = p2 ? filteredData.find(item => item.id === p2.id) : null;
|
||
|
||
tournament.value.matches.push({
|
||
id: `${1}-${i + 1}`,
|
||
round: 1,
|
||
matchNumber: i + 1,
|
||
participant1: p1,
|
||
participant2: p2,
|
||
winner,
|
||
score1: decided && winner === p1 ? 1 : (p1Latest?.win || 0), // Score 1 for bye winner, else existing or 0
|
||
score2: decided && winner === p2 ? 1 : (p2Latest?.win || 0), // Score 1 for bye winner, else existing or 0
|
||
decided,
|
||
// participantXResolved flags are not needed for round 1 matches as they are resolved by definition or play
|
||
});
|
||
}
|
||
|
||
// 生成后续轮次
|
||
for (let r = 2; r <= tournament.value.rounds; r++) {
|
||
const matchCount = totalPlayers / Math.pow(2, r);
|
||
for (let i = 0; i < matchCount; i++) {
|
||
tournament.value.matches.push({
|
||
id: `${r}-${i + 1}`,
|
||
round: r,
|
||
matchNumber: i + 1,
|
||
participant1: null, // Placeholder, will be filled by previous round's winner
|
||
participant2: null, // Placeholder
|
||
participant1Resolved: false, // New flag
|
||
participant2Resolved: false, // New flag
|
||
winner: null,
|
||
score1: null,
|
||
score2: null,
|
||
decided: false
|
||
});
|
||
}
|
||
}
|
||
|
||
// 处理第一轮中的轮空晋级
|
||
const firstRoundActualMatches = tournament.value.matches.filter(m => m.round === 1);
|
||
for (const match of firstRoundActualMatches) {
|
||
if (match.decided) { // If the match was decided (bye, double bye)
|
||
updateNextRound(match); // Propagate its result (winner could be player or null)
|
||
}
|
||
}
|
||
};
|
||
|
||
const drawConnections = () => {
|
||
try {
|
||
const bracketLinesSVG = document.getElementById('bracketLines');
|
||
const bracketDiv = document.getElementById('bracket');
|
||
if (!bracketDiv || !bracketLinesSVG) return;
|
||
bracketLinesSVG.innerHTML = '';
|
||
const bracketRect = bracketDiv.getBoundingClientRect();
|
||
const matchElements = {};
|
||
document.querySelectorAll('.match').forEach(el => {
|
||
const id = el.id.replace('match-', '');
|
||
matchElements[id] = el;
|
||
});
|
||
tournament.value.matches.forEach(match => {
|
||
if (match.round === tournament.value.rounds) return;
|
||
if (!match.decided) return;
|
||
const nextRound = match.round + 1;
|
||
const nextMatchNumber = Math.floor((match.matchNumber - 1) / 2) + 1;
|
||
const nextMatchId = `${nextRound}-${nextMatchNumber}`;
|
||
const nextMatchEl = matchElements[nextMatchId];
|
||
const currentMatchEl = matchElements[match.id];
|
||
if (!nextMatchEl || !currentMatchEl) return;
|
||
const winnerIndex = (match.winner.id === (match.participant1 && match.participant1.id)) ? 1 : 2;
|
||
const curRect = currentMatchEl.getBoundingClientRect();
|
||
const nextRect = nextMatchEl.getBoundingClientRect();
|
||
const startX = curRect.right - bracketRect.left;
|
||
const startY = curRect.top - bracketRect.top + (winnerIndex === 1 ? 20 : 50);
|
||
const endX = nextRect.left - bracketRect.left;
|
||
const endY = nextRect.top - bracketRect.top + (match.matchNumber % 2 === 1 ? 20 : 50);
|
||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||
path.setAttribute('stroke', '#666');
|
||
path.setAttribute('fill', 'none');
|
||
path.setAttribute('stroke-width', '2');
|
||
const midX = (startX + endX) / 2;
|
||
const d = `M${startX},${startY} L${midX},${startY} L${midX},${endY} L${endX},${endY}`;
|
||
path.setAttribute('d', d);
|
||
bracketLinesSVG.appendChild(path);
|
||
});
|
||
} catch (e) {
|
||
console.warn('drawConnections error:', e);
|
||
}
|
||
};
|
||
|
||
onUnmounted(() => {
|
||
const bracketLinesSVG = document.getElementById('bracketLines');
|
||
if (bracketLinesSVG) bracketLinesSVG.innerHTML = '';
|
||
});
|
||
|
||
const emit = defineEmits(['refreshPlayers']);
|
||
|
||
async function confirmScore(match) {
|
||
const s1 = Number(match.score1);
|
||
const s2 = Number(match.score2);
|
||
if (isNaN(s1) || isNaN(s2)) {
|
||
alert('请输入有效的比分');
|
||
return;
|
||
}
|
||
if (s1 === s2) {
|
||
alert('比分不能平局');
|
||
return;
|
||
}
|
||
match.score1 = s1;
|
||
match.score2 = s2;
|
||
match.winner = s1 > s2 ? match.participant1 : match.participant2;
|
||
match.decided = true;
|
||
updateNextRound(match);
|
||
// 重新绘制连线
|
||
nextTick(() => drawConnections());
|
||
if (match.round === tournament.value.rounds) {
|
||
calculateFinalRanking();
|
||
}
|
||
|
||
// 新增:同步胜负到API
|
||
const p1 = match.participant1;
|
||
const p2 = match.participant2;
|
||
try {
|
||
if (p1 && p2) {
|
||
// 先获取最新的报名数据
|
||
const latestData = await getSignUpResultList();
|
||
const p1Latest = latestData.find(item => item.id === p1.id);
|
||
const p2Latest = latestData.find(item => item.id === p2.id);
|
||
|
||
// 补全字段,使用最新的API数据
|
||
const p1Data = {
|
||
tournament_id: tournament.value.id,
|
||
tournament_name: tournament.value.name,
|
||
team_name: p1.team_name || '',
|
||
sign_name: p1.name || p1.sign_name || '',
|
||
win: String(Number(p1Latest?.win || 0) + s1),
|
||
lose: String(Number(p1Latest?.lose || 0) + s2),
|
||
status: s1 > s2 ? 'win' : 'lose',
|
||
qq_code: p1.qq_code || p1.qq || ''
|
||
};
|
||
const p2Data = {
|
||
tournament_id: tournament.value.id,
|
||
tournament_name: tournament.value.name,
|
||
team_name: p2.team_name || '',
|
||
sign_name: p2.name || p2.sign_name || '',
|
||
win: String(Number(p2Latest?.win || 0) + s2),
|
||
lose: String(Number(p2Latest?.lose || 0) + s1),
|
||
status: s2 > s1 ? 'win' : 'lose',
|
||
qq_code: p2.qq_code || p2.qq || ''
|
||
};
|
||
await updateSignUpResult(p1.id, p1Data);
|
||
await updateSignUpResult(p2.id, p2Data);
|
||
// 刷新报名数据和赛程
|
||
await handleTournamentChange();
|
||
// 强制同步 participants 和 tournament.value.participants
|
||
participants.value = [...participants.value];
|
||
tournament.value.participants = [...participants.value];
|
||
}
|
||
} catch (err) {
|
||
alert('更新数据失败');
|
||
console.error(err);
|
||
}
|
||
|
||
emit('refreshPlayers');
|
||
}
|
||
|
||
const updateNextRound = (currentDecidedMatch) => {
|
||
if (!currentDecidedMatch.decided) { // Winner can be null (e.g. double bye)
|
||
console.warn("updateNextRound called on non-decided match", currentDecidedMatch);
|
||
return;
|
||
}
|
||
|
||
if (currentDecidedMatch.round === tournament.value.rounds) {
|
||
return; // This was the final match or a match in the final round
|
||
}
|
||
|
||
const nextRound = currentDecidedMatch.round + 1;
|
||
const nextMatchIndexInRound = Math.floor((currentDecidedMatch.matchNumber - 1) / 2);
|
||
const nextMatch = tournament.value.matches.find(m => m.round === nextRound && m.matchNumber === nextMatchIndexInRound + 1);
|
||
|
||
if (!nextMatch) {
|
||
console.error("Could not find next match for", currentDecidedMatch);
|
||
return;
|
||
}
|
||
|
||
const isFeederForParticipant1 = (currentDecidedMatch.matchNumber - 1) % 2 === 0;
|
||
|
||
if (isFeederForParticipant1) {
|
||
nextMatch.participant1 = currentDecidedMatch.winner; // Winner might be null
|
||
nextMatch.participant1Resolved = true;
|
||
} else {
|
||
nextMatch.participant2 = currentDecidedMatch.winner; // Winner might be null
|
||
nextMatch.participant2Resolved = true;
|
||
}
|
||
|
||
// Only proceed if both participant slots for nextMatch are resolved
|
||
if (nextMatch.participant1Resolved && nextMatch.participant2Resolved) {
|
||
const p1 = nextMatch.participant1;
|
||
const p2 = nextMatch.participant2;
|
||
|
||
if (p1 && !p2) { // p1 advances due to p2 being a bye (null)
|
||
nextMatch.winner = p1;
|
||
nextMatch.score1 = 1; // Convention for bye win
|
||
nextMatch.score2 = 0;
|
||
nextMatch.decided = true;
|
||
} else if (!p1 && p2) { // p2 advances due to p1 being a bye (null)
|
||
nextMatch.winner = p2;
|
||
nextMatch.score1 = 0;
|
||
nextMatch.score2 = 1; // Convention for bye win
|
||
nextMatch.decided = true;
|
||
} else if (!p1 && !p2) { // Double bye in this nextMatch
|
||
nextMatch.winner = null; // No winner advances
|
||
nextMatch.score1 = 0;
|
||
nextMatch.score2 = 0;
|
||
nextMatch.decided = true; // This "match" is decided as a double bye
|
||
} else if (p1 && p2) { // Both participants are actual players, match is ready to be played
|
||
nextMatch.winner = null;
|
||
nextMatch.score1 = null; // Or 0, depending on display preference for undecided matches
|
||
nextMatch.score2 = null; // Or 0
|
||
nextMatch.decided = false;
|
||
} else {
|
||
// This case should ideally not be reached if logic is correct
|
||
console.warn("Unexpected state in updateNextRound for nextMatch:", nextMatch);
|
||
nextMatch.decided = false; // Default to not decided to prevent erroneous propagation
|
||
}
|
||
|
||
// If nextMatch itself was decided (due to byes), propagate its result further
|
||
if (nextMatch.decided) {
|
||
updateNextRound(nextMatch);
|
||
}
|
||
}
|
||
// If only one participant is resolved, wait for the other feeder match to complete.
|
||
};
|
||
|
||
const calculateFinalRanking = () => {
|
||
const finalMatch = tournament.value.matches.find(m => m.round === tournament.value.rounds);
|
||
if (!finalMatch || !finalMatch.winner) return;
|
||
|
||
const champion = finalMatch.winner;
|
||
const runnerUp = finalMatch.participant1 && finalMatch.participant2
|
||
? (finalMatch.winner.id === finalMatch.participant1.id ? finalMatch.participant2 : finalMatch.participant1)
|
||
: null;
|
||
|
||
let semiFinalMatches = tournament.value.matches.filter(m => m.round === tournament.value.rounds - 1);
|
||
let semiFinalLosers = [];
|
||
semiFinalMatches.forEach(m => {
|
||
if (m.decided && m.winner) {
|
||
const loser = (m.participant1 && m.participant1.id !== m.winner.id) ? m.participant1 : m.participant2;
|
||
if (loser) semiFinalLosers.push(loser);
|
||
}
|
||
});
|
||
|
||
finalRanking.value = {
|
||
champion: champion.name,
|
||
runnerUp: runnerUp ? runnerUp.name : '未知',
|
||
thirdPlace: semiFinalLosers.length > 0 ? semiFinalLosers[0].name : '未知'
|
||
};
|
||
};
|
||
|
||
// 监听赛程变化自动重绘连线
|
||
watch(roundsMatches, () => {
|
||
nextTick(() => drawConnections());
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.tournament-bracket-root {
|
||
font-family: Arial, sans-serif;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
.tournament-bracket-root * {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.tournament-bracket-root ul {
|
||
padding-left: 20px;
|
||
margin: 0 0 10px 0;
|
||
}
|
||
|
||
.tournament-bracket-root li {
|
||
margin-bottom: 4px;
|
||
list-style: disc;
|
||
}
|
||
|
||
.tournament-bracket-root button {
|
||
outline: none;
|
||
}
|
||
|
||
.tournament-bracket-root input[type=number]::-webkit-inner-spin-button,
|
||
.tournament-bracket-root input[type=number]::-webkit-outer-spin-button {
|
||
-webkit-appearance: none;
|
||
margin: 0;
|
||
}
|
||
|
||
.tournament-bracket-root input[type=number] {
|
||
-moz-appearance: textfield;
|
||
}
|
||
|
||
.tournament-bracket-root h1 {
|
||
text-align: center;
|
||
}
|
||
|
||
.tournament-bracket-root .container {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: flex-start;
|
||
min-height: 500px;
|
||
}
|
||
|
||
.tournament-bracket-root .control-panel {
|
||
flex: 1;
|
||
min-width: 280px;
|
||
background: #f0f0f0;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.tournament-bracket-root .bracket-container {
|
||
flex: 3;
|
||
position: relative;
|
||
overflow-x: auto;
|
||
background: #fff;
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
border: 1px solid #ccc;
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||
min-height: 400px;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.tournament-bracket-root .bracket {
|
||
display: grid;
|
||
grid-auto-flow: column;
|
||
grid-auto-columns: 220px;
|
||
gap: 40px 10px;
|
||
position: relative;
|
||
padding-bottom: 50px;
|
||
min-width: 600px;
|
||
min-height: 350px;
|
||
}
|
||
|
||
.tournament-bracket-root .round {
|
||
display: grid;
|
||
grid-auto-rows: 70px;
|
||
gap: 20px;
|
||
}
|
||
|
||
.tournament-bracket-root .round-title {
|
||
text-align: center;
|
||
font-weight: bold;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.tournament-bracket-root .match {
|
||
background: #fafafa;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
padding: 8px 12px;
|
||
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
|
||
position: relative;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tournament-bracket-root .participant {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 6px;
|
||
cursor: default;
|
||
user-select: none;
|
||
}
|
||
|
||
.tournament-bracket-root .participant.winner {
|
||
font-weight: bold;
|
||
color: #2a7f2a;
|
||
}
|
||
|
||
.tournament-bracket-root .score-input {
|
||
width: 40px;
|
||
font-size: 14px;
|
||
padding: 2px 4px;
|
||
margin-left: 6px;
|
||
border: 1px solid #aaa;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.tournament-bracket-root .score-btn {
|
||
margin-top: 4px;
|
||
width: 100%;
|
||
background: #007acc;
|
||
border: none;
|
||
color: white;
|
||
font-weight: bold;
|
||
padding: 6px 0;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.tournament-bracket-root .score-btn:disabled {
|
||
background: #aaa;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.tournament-bracket-root #finalRanking {
|
||
margin-top: 15px;
|
||
background: #fffbdb;
|
||
border: 1px solid #f0e68c;
|
||
padding: 12px;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.tournament-bracket-root svg.bracket-lines {
|
||
position: absolute;
|
||
top: 40px;
|
||
left: 0;
|
||
pointer-events: none;
|
||
overflow: visible;
|
||
height: 100%;
|
||
width: 100%;
|
||
}
|
||
.tournament-bracket-root svg.bracket-lines path {
|
||
stroke: #666;
|
||
fill: none;
|
||
stroke-width: 2;
|
||
}
|
||
|
||
.tournament-bracket-root .tournament-info {
|
||
margin-bottom: 15px;
|
||
padding: 10px;
|
||
background: #e6f7ff;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.tournament-bracket-root .loading {
|
||
color: #666;
|
||
font-style: italic;
|
||
}
|
||
|
||
.tournament-bracket-root select {
|
||
width: 100%;
|
||
padding: 8px;
|
||
margin-bottom: 10px;
|
||
border-radius: 4px;
|
||
border: 1px solid #ccc;
|
||
}
|
||
</style> |