This commit is contained in:
Kunagisa 2025-05-27 23:02:39 +08:00
parent c8fff3e5c8
commit 479b6c38a8
3 changed files with 545 additions and 361 deletions

View File

@ -1,32 +1,38 @@
{
"hash": "840712f5",
"configHash": "a76fca65",
"lockfileHash": "ab320c33",
"browserHash": "dcab2661",
"hash": "8d846fac",
"configHash": "2595757c",
"lockfileHash": "13f0fece",
"browserHash": "2a4b858f",
"optimized": {
"axios": {
"src": "../../axios/index.js",
"file": "axios.js",
"fileHash": "b46b61da",
"fileHash": "4a809c8a",
"needsInterop": false
},
"vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "a29ac7ab",
"fileHash": "9723eef7",
"needsInterop": false
},
"vue-router": {
"src": "../../vue-router/dist/vue-router.mjs",
"file": "vue-router.js",
"fileHash": "5cd170d9",
"fileHash": "2ce656a9",
"needsInterop": false
},
"vue-tournament-bracket": {
"src": "../../vue-tournament-bracket/dist/vue-tournament-bracket.common.js",
"file": "vue-tournament-bracket.js",
"fileHash": "e7817223",
"fileHash": "b4b6c896",
"needsInterop": true
},
"xlsx": {
"src": "../../xlsx/xlsx.mjs",
"file": "xlsx.js",
"fileHash": "87bb22e0",
"needsInterop": false
}
},
"chunks": {

View File

@ -1,436 +1,609 @@
<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>
<div class="bracket-wrapper" ref="bracketWrapper" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag"
@mouseleave="stopDrag" :style="{ transform: `scale(${scale}) translate(${position.x}px, ${position.y}px)` }">
<div class="round-titles">
<div v-for="(round, index) in rounds" :key="index" class="round-info">
<div class="round-name">{{ getRoundName(index, rounds.length) }}</div>
<div class="round-bo">{{ getRoundBo(index, rounds.length) }}</div>
<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>
<Bracket :rounds="formattedRounds" :options="bracketOptions" @onMatchClick="handleMatchClick" />
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { Bracket } from 'vue-tournament-bracket'
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue';
import { getSignUpResultList, updateSignUpResult, getTournamentList } from '@/api/tournament';
const props = defineProps({
isEditMode: {
type: Boolean,
default: false
},
tournamentId: {
type: Number,
required: true
},
tournamentName: {
type: String,
required: true
},
players: {
type: Array,
default: () => []
}
})
});
const emit = defineEmits(['update:rounds'])
const bracketContainer = ref(null)
const bracketWrapper = ref(null)
const scale = ref(1)
const position = ref({ x: 0, y: 0 })
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })
const tournaments = ref([]);
const selectedTournamentId = ref('');
const selectedTournamentName = ref('');
const participants = ref([]);
const loading = ref(false);
const finalRanking = ref(null);
const startDrag = (e) => {
isDragging.value = true
dragStart.value = {
x: e.clientX - position.value.x,
y: position.value.y
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));
}
}
const onDrag = (e) => {
if (isDragging.value) {
position.value = {
x: e.clientX - dragStart.value.x,
y: position.value.y
}
}
}
const stopDrag = () => {
isDragging.value = false
}
return arr;
});
onMounted(() => {
if (bracketContainer.value && bracketWrapper.value) {
const containerRect = bracketContainer.value.getBoundingClientRect()
const wrapperRect = bracketWrapper.value.getBoundingClientRect()
position.value = {
x: (containerRect.width - wrapperRect.width) / 2,
y: 0
}
fetchTournamentList();
});
const fetchTournamentList = async () => {
try {
const data = await getTournamentList();
tournaments.value = data;
} catch (err) {
console.error('获取比赛列表失败:', err);
}
})
};
// rounds
const rounds = ref([])
//
const generateRounds = (players) => {
const totalPlayers = players.length
if (totalPlayers < 2) return []
//
const roundsCount = Math.ceil(Math.log2(totalPlayers))
//
const rounds = []
//
//
for (let i = 0; i < shuffledPlayers.length; i += 2) {
const game = {
player1: shuffledPlayers[i],
player2: shuffledPlayers[i + 1] || null,
winnerId: null
}
firstRoundGames.push(game)
const handleTournamentChange = async () => {
if (!selectedTournamentId.value) {
selectedTournamentName.value = '';
participants.value = [];
tournament.value.id = null;
tournament.value.name = '';
tournament.value.participants = [];
return;
}
rounds.push({ games: firstRoundGames })
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 firstRoundMatches = totalPlayers / 2;
for (let i = 0; i < firstRoundMatches; i++) {
const p1 = shuffled[i * 2];
const p2 = shuffled[i * 2 + 1];
let winner = null;
let decided = false;
if (p1 && !p2) {
winner = p1;
decided = true;
} else if (!p1 && p2) {
winner = p2;
decided = true;
}
//
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 && p1 && !p2 ? 1 : (p1Latest?.win || 0),
score2: decided && !p1 && p2 ? 1 : (p2Latest?.win || 0),
decided
});
}
//
for (let round = 1; round < roundsCount; round++) {
const prevRound = rounds[round - 1]
const currentRoundGames = []
//
for (let i = 0; i < prevRound.games.length; i += 2) {
const game = {
player1: null,
player2: null,
winnerId: null
}
currentRoundGames.push(game)
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,
participant2: null,
winner: null,
score1: null,
score2: null,
decided: false
});
}
}
};
rounds.push({ games: currentRoundGames })
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();
}
return rounds
// 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 initializeBracket = () => {
if (!props.players || props.players.length === 0) return
//
rounds.value = generateRounds(props.players.map((p, idx) => ({
...p,
number: idx + 1,
win: p.win || '0',
lose: p.lose || '0',
status: p.status || 'tie',
team_name: p.team_name || '个人',
name: p.sign_name || p.name || '未知选手',
})))
}
const updateNextRound = (match) => {
if (!match.decided) return;
watch(() => props.players, () => {
initializeBracket()
}, { immediate: true })
if (match.round === tournament.value.rounds) {
return;
}
//
const getRoundName = (index, totalRounds) => {
if (totalRounds === 1) {
return '决赛'
} else if (totalRounds === 2) {
return index === 0 ? '半决赛' : '决赛'
} else if (totalRounds === 3) {
return index === 0 ? '四分之一决赛' : index === 1 ? '半决赛' : '决赛'
const nextRound = match.round + 1;
const nextMatchIndex = Math.floor((match.matchNumber - 1) / 2);
const nextMatch = tournament.value.matches.find(m => m.round === nextRound && m.matchNumber === nextMatchIndex + 1);
if (!nextMatch) return;
if ((match.matchNumber - 1) % 2 === 0) {
nextMatch.participant1 = match.winner;
} else {
return `${index + 1}`
nextMatch.participant2 = match.winner;
}
}
const getRoundBo = (index, totalRounds) => {
if (index === totalRounds - 1) {
return 'BO5'
if ((nextMatch.participant1 && !nextMatch.participant2) || (!nextMatch.participant1 && nextMatch.participant2)) {
nextMatch.winner = nextMatch.participant1 || nextMatch.participant2;
nextMatch.score1 = nextMatch.participant1 ? 1 : 0;
nextMatch.score2 = nextMatch.participant2 ? 1 : 0;
nextMatch.decided = true;
updateNextRound(nextMatch);
} else {
return 'BO3'
nextMatch.winner = null;
nextMatch.score1 = null;
nextMatch.score2 = null;
nextMatch.decided = false;
}
}
};
// vue-tournament-bracket
const formattedRounds = computed(() => {
return rounds.value.map((round, roundIndex) => ({
title: getRoundName(roundIndex, rounds.value.length),
matches: round.games.map((game, gameIndex) => {
const nextMatchId = roundIndex < rounds.value.length - 1
? Math.floor(gameIndex / 2)
: null
const calculateFinalRanking = () => {
const finalMatch = tournament.value.matches.find(m => m.round === tournament.value.rounds);
if (!finalMatch || !finalMatch.winner) return;
return {
id: `${roundIndex}-${gameIndex}`,
name: `比赛 ${gameIndex + 1}`,
nextMatchId: nextMatchId !== null ? `${roundIndex + 1}-${nextMatchId}` : null,
tournamentRoundText: getRoundBo(roundIndex, rounds.value.length),
startTime: '',
state: 'SCHEDULED',
participants: [
{
id: game.player1?.id,
name: game.player1?.name || '待定',
status: game.winnerId === game.player1?.id ? 'WIN' : 'LOSE',
resultText: `${game.player1?.win || 0}-${game.player1?.lose || 0}`
},
{
id: game.player2?.id,
name: game.player2?.name || '待定',
status: game.winnerId === game.player2?.id ? 'WIN' : 'LOSE',
resultText: `${game.player2?.win || 0}-${game.player2?.lose || 0}`
}
]
}
})
}))
})
const champion = finalMatch.winner;
const runnerUp = finalMatch.participant1 && finalMatch.participant2
? (finalMatch.winner.id === finalMatch.participant1.id ? finalMatch.participant2 : finalMatch.participant1)
: null;
//
const bracketOptions = {
style: {
roundHeader: {
backgroundColor: '#1976d2',
color: '#fff'
},
connectorColor: '#1976d2',
connectorColorHighlight: '#4caf50'
},
matchHeight: 100,
roundHeaderHeight: 50,
roundHeaderMargin: 20,
roundHeaderFontSize: 16,
matchWidth: 300,
matchMargin: 20,
participantHeight: 40,
participantMargin: 5,
participantFontSize: 14,
participantPadding: 10
}
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);
}
});
//
const handleMatchClick = (match) => {
if (!props.isEditMode) return
const roundIndex = formattedRounds.value.findIndex(round =>
round.matches.some(m => m.id === match.id)
)
const gameIndex = formattedRounds.value[roundIndex].matches.findIndex(m => m.id === match.id)
const game = rounds.value[roundIndex].games[gameIndex]
openWinnerDialog(roundIndex, gameIndex, game)
}
finalRanking.value = {
champion: champion.name,
runnerUp: runnerUp ? runnerUp.name : '未知',
thirdPlace: semiFinalLosers.length > 0 ? semiFinalLosers[0].name : '未知'
};
};
//
const showWinnerDialog = ref(false)
const editingGame = ref(null)
const openWinnerDialog = (roundIndex, gameIndex, game) => {
editingGame.value = {
roundIndex,
gameIndex,
game
}
showWinnerDialog.value = true
}
const setWinner = (playerId) => {
if (editingGame.value) {
const roundIndex = editingGame.value.roundIndex
const gameIndex = editingGame.value.gameIndex
const game = editingGame.value.game
game.winnerId = playerId
showWinnerDialog.value = false
}
}
const closeWinnerDialog = () => {
showWinnerDialog.value = false
}
// 线
watch(roundsMatches, () => {
nextTick(() => drawConnections());
});
</script>
<style scoped>
.tournament-bracket {
<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;
overflow-y: auto;
background: #141414;
color: #fff;
height: auto;
min-height: 600px;
padding: 20px;
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;
}
.bracket-wrapper {
transform-origin: center left;
cursor: ew-resize;
user-select: none;
padding: 20px;
min-height: calc(100% - 40px);
.tournament-bracket-root .bracket {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 220px;
gap: 40px 10px;
position: relative;
padding-top: 0;
width: max-content;
min-width: 100%;
padding-bottom: 50px;
min-width: 600px;
min-height: 350px;
}
.bracket-rounds {
display: flex;
gap: 40px;
.tournament-bracket-root .round {
display: grid;
grid-auto-rows: 70px;
gap: 20px;
}
.bracket-round {
display: flex;
flex-direction: column;
gap: 24px;
.tournament-bracket-root .round-title {
text-align: center;
font-weight: bold;
margin-bottom: 15px;
}
.bracket-game {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.player-info {
background: #1976d2;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
width: 300px;
.tournament-bracket-root .match {
background: #fafafa;
border: 1px solid #ddd;
border-radius: 6px;
padding: 8px 12px;
border-radius: 4px;
margin: 4px 0;
transition: all 0.3s ease;
}
.player-info.winner {
background: #4caf50;
}
.player-info.defeated {
background: #f44336;
}
.player-number {
color: #bbdefb;
font-size: 12px;
min-width: 16px;
}
.player-name {
color: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
position: relative;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.player-score {
.tournament-bracket-root .participant {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #fff;
background: rgba(255, 255, 255, 0.2);
padding: 2px 6px;
border-radius: 2px;
justify-content: space-between;
margin-bottom: 6px;
cursor: default;
user-select: none;
}
.win-score {
color: #4caf50;
font-weight: 500;
.tournament-bracket-root .participant.winner {
font-weight: bold;
color: #2a7f2a;
}
.score-separator {
color: rgba(255, 255, 255, 0.5);
.tournament-bracket-root .score-input {
width: 40px;
font-size: 14px;
padding: 2px 4px;
margin-left: 6px;
border: 1px solid #aaa;
border-radius: 3px;
}
.lose-score {
color: #f44336;
font-weight: 500;
}
.set-winner-btn {
margin-left: 12px;
padding: 2px 8px;
background: #ff9800;
color: #fff;
.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;
font-size: 12px;
}
.set-winner-btn:hover {
background: #e65100;
.tournament-bracket-root .score-btn:disabled {
background: #aaa;
cursor: not-allowed;
}
.edit-dialog-overlay {
position: fixed;
top: 0;
.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;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
pointer-events: none;
overflow: visible;
height: 100%;
width: 100%;
}
.tournament-bracket-root svg.bracket-lines path {
stroke: #666;
fill: none;
stroke-width: 2;
}
.edit-dialog {
background: #1f1f1f;
border-radius: 8px;
padding: 24px;
width: 400px;
max-width: 90%;
color: #fff;
}
.dialog-buttons {
display: flex;
gap: 12px;
margin-top: 24px;
}
.confirm-btn {
background: #409EFF;
color: #fff;
border: none;
.tournament-bracket-root .tournament-info {
margin-bottom: 15px;
padding: 10px;
background: #e6f7ff;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-weight: 500;
}
.confirm-btn:hover {
background: #66b1ff;
}
.cancel-btn {
background: #f5f5f5;
.tournament-bracket-root .loading {
color: #666;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-weight: 500;
font-style: italic;
}
.cancel-btn:hover {
background: #e8e8e8;
.tournament-bracket-root select {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border-radius: 4px;
border: 1px solid #ccc;
}
</style>

View File

@ -75,6 +75,11 @@
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">