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", "hash": "8d846fac",
"configHash": "a76fca65", "configHash": "2595757c",
"lockfileHash": "ab320c33", "lockfileHash": "13f0fece",
"browserHash": "dcab2661", "browserHash": "2a4b858f",
"optimized": { "optimized": {
"axios": { "axios": {
"src": "../../axios/index.js", "src": "../../axios/index.js",
"file": "axios.js", "file": "axios.js",
"fileHash": "b46b61da", "fileHash": "4a809c8a",
"needsInterop": false "needsInterop": false
}, },
"vue": { "vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js", "src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js", "file": "vue.js",
"fileHash": "a29ac7ab", "fileHash": "9723eef7",
"needsInterop": false "needsInterop": false
}, },
"vue-router": { "vue-router": {
"src": "../../vue-router/dist/vue-router.mjs", "src": "../../vue-router/dist/vue-router.mjs",
"file": "vue-router.js", "file": "vue-router.js",
"fileHash": "5cd170d9", "fileHash": "2ce656a9",
"needsInterop": false "needsInterop": false
}, },
"vue-tournament-bracket": { "vue-tournament-bracket": {
"src": "../../vue-tournament-bracket/dist/vue-tournament-bracket.common.js", "src": "../../vue-tournament-bracket/dist/vue-tournament-bracket.common.js",
"file": "vue-tournament-bracket.js", "file": "vue-tournament-bracket.js",
"fileHash": "e7817223", "fileHash": "b4b6c896",
"needsInterop": true "needsInterop": true
},
"xlsx": {
"src": "../../xlsx/xlsx.mjs",
"file": "xlsx.js",
"fileHash": "87bb22e0",
"needsInterop": false
} }
}, },
"chunks": { "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> <template v-else>
<div class="bracket-wrapper" ref="bracketWrapper" @mousedown="startDrag" @mousemove="onDrag" @mouseup="stopDrag" <p> {{ participants.length }} 位参赛者</p>
@mouseleave="stopDrag" :style="{ transform: `scale(${scale}) translate(${position.x}px, ${position.y}px)` }"> <ul>
<div class="round-titles"> <li v-for="participant in participants" :key="participant.id">
<div v-for="(round, index) in rounds" :key="index" class="round-info"> {{ participant.name }}
<div class="round-name">{{ getRoundName(index, rounds.length) }}</div> </li>
<div class="round-bo">{{ getRoundBo(index, rounds.length) }}</div> </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> </div>
<Bracket :rounds="formattedRounds" :options="bracketOptions" @onMatchClick="handleMatchClick" />
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed, watch } from 'vue' import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue';
import { Bracket } from 'vue-tournament-bracket' import { getSignUpResultList, updateSignUpResult, getTournamentList } from '@/api/tournament';
const props = defineProps({ const props = defineProps({
isEditMode: {
type: Boolean,
default: false
},
tournamentId: { tournamentId: {
type: Number, type: Number,
required: true required: true
},
tournamentName: {
type: String,
required: true
},
players: {
type: Array,
default: () => []
} }
}) });
const emit = defineEmits(['update:rounds']) const tournaments = ref([]);
const bracketContainer = ref(null) const selectedTournamentId = ref('');
const bracketWrapper = ref(null) const selectedTournamentName = ref('');
const scale = ref(1) const participants = ref([]);
const position = ref({ x: 0, y: 0 }) const loading = ref(false);
const isDragging = ref(false) const finalRanking = ref(null);
const dragStart = ref({ x: 0, y: 0 })
const startDrag = (e) => { const tournament = ref({
isDragging.value = true id: props.tournamentId,
dragStart.value = { name: '',
x: e.clientX - position.value.x, participants: [],
y: position.value.y rounds: 0,
} matches: []
} });
const onDrag = (e) => { const canGenerateBracket = computed(() => {
if (isDragging.value) { return selectedTournamentId.value && participants.value.length >= 2;
position.value = { });
x: e.clientX - dragStart.value.x,
y: position.value.y
}
}
}
const stopDrag = () => { const roundsMatches = computed(() => {
isDragging.value = false const arr = [];
for (let r = 1; r <= tournament.value.rounds; r++) {
arr.push(tournament.value.matches.filter(m => m.round === r));
} }
return arr;
});
onMounted(() => { onMounted(() => {
if (bracketContainer.value && bracketWrapper.value) { fetchTournamentList();
const containerRect = bracketContainer.value.getBoundingClientRect() });
const wrapperRect = bracketWrapper.value.getBoundingClientRect()
position.value = { const fetchTournamentList = async () => {
x: (containerRect.width - wrapperRect.width) / 2, try {
y: 0 const data = await getTournamentList();
tournaments.value = data;
} catch (err) {
console.error('获取比赛列表失败:', err);
} }
} };
})
// rounds const handleTournamentChange = async () => {
const rounds = ref([]) if (!selectedTournamentId.value) {
selectedTournamentName.value = '';
// participants.value = [];
const generateRounds = (players) => { tournament.value.id = null;
const totalPlayers = players.length tournament.value.name = '';
if (totalPlayers < 2) return [] tournament.value.participants = [];
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)
} }
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++) { for (let r = 2; r <= tournament.value.rounds; r++) {
const prevRound = rounds[round - 1] const matchCount = totalPlayers / Math.pow(2, r);
const currentRoundGames = [] for (let i = 0; i < matchCount; i++) {
tournament.value.matches.push({
// id: `${r}-${i + 1}`,
for (let i = 0; i < prevRound.games.length; i += 2) { round: r,
const game = { matchNumber: i + 1,
player1: null, participant1: null,
player2: null, participant2: null,
winnerId: null winner: null,
score1: null,
score2: null,
decided: false
});
} }
currentRoundGames.push(game) }
};
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();
} }
rounds.push({ games: currentRoundGames }) // 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);
} }
return rounds emit('refreshPlayers');
} }
// const updateNextRound = (match) => {
const initializeBracket = () => { if (!match.decided) return;
if (!props.players || props.players.length === 0) return
// if (match.round === tournament.value.rounds) {
rounds.value = generateRounds(props.players.map((p, idx) => ({ return;
...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 || '未知选手',
})))
} }
watch(() => props.players, () => { const nextRound = match.round + 1;
initializeBracket() const nextMatchIndex = Math.floor((match.matchNumber - 1) / 2);
}, { immediate: true }) const nextMatch = tournament.value.matches.find(m => m.round === nextRound && m.matchNumber === nextMatchIndex + 1);
// if (!nextMatch) return;
const getRoundName = (index, totalRounds) => {
if (totalRounds === 1) { if ((match.matchNumber - 1) % 2 === 0) {
return '决赛' nextMatch.participant1 = match.winner;
} else if (totalRounds === 2) {
return index === 0 ? '半决赛' : '决赛'
} else if (totalRounds === 3) {
return index === 0 ? '四分之一决赛' : index === 1 ? '半决赛' : '决赛'
} else { } else {
return `${index + 1}` nextMatch.participant2 = match.winner;
} }
}
const getRoundBo = (index, totalRounds) => { if ((nextMatch.participant1 && !nextMatch.participant2) || (!nextMatch.participant1 && nextMatch.participant2)) {
if (index === totalRounds - 1) { nextMatch.winner = nextMatch.participant1 || nextMatch.participant2;
return 'BO5' nextMatch.score1 = nextMatch.participant1 ? 1 : 0;
nextMatch.score2 = nextMatch.participant2 ? 1 : 0;
nextMatch.decided = true;
updateNextRound(nextMatch);
} else { } else {
return 'BO3' nextMatch.winner = null;
} nextMatch.score1 = null;
nextMatch.score2 = null;
nextMatch.decided = false;
} }
};
// vue-tournament-bracket const calculateFinalRanking = () => {
const formattedRounds = computed(() => { const finalMatch = tournament.value.matches.find(m => m.round === tournament.value.rounds);
return rounds.value.map((round, roundIndex) => ({ if (!finalMatch || !finalMatch.winner) return;
title: getRoundName(roundIndex, rounds.value.length),
matches: round.games.map((game, gameIndex) => {
const nextMatchId = roundIndex < rounds.value.length - 1
? Math.floor(gameIndex / 2)
: null
return { const champion = finalMatch.winner;
id: `${roundIndex}-${gameIndex}`, const runnerUp = finalMatch.participant1 && finalMatch.participant2
name: `比赛 ${gameIndex + 1}`, ? (finalMatch.winner.id === finalMatch.participant1.id ? finalMatch.participant2 : finalMatch.participant1)
nextMatchId: nextMatchId !== null ? `${roundIndex + 1}-${nextMatchId}` : null, : 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}`
}
]
}
})
}))
})
// let semiFinalMatches = tournament.value.matches.filter(m => m.round === tournament.value.rounds - 1);
const bracketOptions = { let semiFinalLosers = [];
style: { semiFinalMatches.forEach(m => {
roundHeader: { if (m.decided && m.winner) {
backgroundColor: '#1976d2', const loser = (m.participant1 && m.participant1.id !== m.winner.id) ? m.participant1 : m.participant2;
color: '#fff' if (loser) semiFinalLosers.push(loser);
},
connectorColor: '#1976d2',
connectorColorHighlight: '#4caf50'
},
matchHeight: 100,
roundHeaderHeight: 50,
roundHeaderMargin: 20,
roundHeaderFontSize: 16,
matchWidth: 300,
matchMargin: 20,
participantHeight: 40,
participantMargin: 5,
participantFontSize: 14,
participantPadding: 10
} }
});
// finalRanking.value = {
const handleMatchClick = (match) => { champion: champion.name,
if (!props.isEditMode) return runnerUp: runnerUp ? runnerUp.name : '未知',
const roundIndex = formattedRounds.value.findIndex(round => thirdPlace: semiFinalLosers.length > 0 ? semiFinalLosers[0].name : '未知'
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)
}
// // 线
const showWinnerDialog = ref(false) watch(roundsMatches, () => {
const editingGame = ref(null) nextTick(() => drawConnections());
});
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
}
</script> </script>
<style scoped> <style>
.tournament-bracket { .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; position: relative;
overflow-x: auto; overflow-x: auto;
overflow-y: auto; background: #fff;
background: #141414; padding: 15px;
color: #fff; border-radius: 6px;
height: auto; border: 1px solid #ccc;
min-height: 600px; box-shadow: 0 2px 12px rgba(0,0,0,0.08);
padding: 20px; min-height: 400px;
display: flex;
align-items: flex-start;
} }
.bracket-wrapper { .tournament-bracket-root .bracket {
transform-origin: center left; display: grid;
cursor: ew-resize; grid-auto-flow: column;
user-select: none; grid-auto-columns: 220px;
padding: 20px; gap: 40px 10px;
min-height: calc(100% - 40px);
position: relative; position: relative;
padding-top: 0; padding-bottom: 50px;
width: max-content; min-width: 600px;
min-width: 100%; min-height: 350px;
} }
.bracket-rounds { .tournament-bracket-root .round {
display: flex; display: grid;
gap: 40px; grid-auto-rows: 70px;
gap: 20px;
} }
.bracket-round { .tournament-bracket-root .round-title {
display: flex; text-align: center;
flex-direction: column; font-weight: bold;
gap: 24px; margin-bottom: 15px;
} }
.bracket-game { .tournament-bracket-root .match {
display: flex; background: #fafafa;
flex-direction: column; border: 1px solid #ddd;
gap: 8px; border-radius: 6px;
margin-bottom: 16px;
}
.player-info {
background: #1976d2;
color: #fff;
display: flex;
align-items: center;
gap: 8px;
width: 300px;
padding: 8px 12px; padding: 8px 12px;
border-radius: 4px; box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
margin: 4px 0; position: relative;
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;
font-size: 14px; font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
} }
.player-score { .tournament-bracket-root .participant {
display: flex; display: flex;
align-items: center; justify-content: space-between;
gap: 4px; margin-bottom: 6px;
font-size: 12px; cursor: default;
color: #fff; user-select: none;
background: rgba(255, 255, 255, 0.2);
padding: 2px 6px;
border-radius: 2px;
} }
.win-score { .tournament-bracket-root .participant.winner {
color: #4caf50; font-weight: bold;
font-weight: 500; color: #2a7f2a;
} }
.score-separator { .tournament-bracket-root .score-input {
color: rgba(255, 255, 255, 0.5); width: 40px;
font-size: 14px;
padding: 2px 4px;
margin-left: 6px;
border: 1px solid #aaa;
border-radius: 3px;
} }
.lose-score { .tournament-bracket-root .score-btn {
color: #f44336; margin-top: 4px;
font-weight: 500; width: 100%;
} background: #007acc;
.set-winner-btn {
margin-left: 12px;
padding: 2px 8px;
background: #ff9800;
color: #fff;
border: none; border: none;
color: white;
font-weight: bold;
padding: 6px 0;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 12px;
} }
.set-winner-btn:hover { .tournament-bracket-root .score-btn:disabled {
background: #e65100; background: #aaa;
cursor: not-allowed;
} }
.edit-dialog-overlay { .tournament-bracket-root #finalRanking {
position: fixed; margin-top: 15px;
top: 0; background: #fffbdb;
border: 1px solid #f0e68c;
padding: 12px;
border-radius: 6px;
}
.tournament-bracket-root svg.bracket-lines {
position: absolute;
top: 40px;
left: 0; left: 0;
right: 0; pointer-events: none;
bottom: 0; overflow: visible;
background: rgba(0, 0, 0, 0.5); height: 100%;
display: flex; width: 100%;
justify-content: center; }
align-items: center; .tournament-bracket-root svg.bracket-lines path {
z-index: 1000; stroke: #666;
fill: none;
stroke-width: 2;
} }
.edit-dialog { .tournament-bracket-root .tournament-info {
background: #1f1f1f; margin-bottom: 15px;
border-radius: 8px; padding: 10px;
padding: 24px; background: #e6f7ff;
width: 400px;
max-width: 90%;
color: #fff;
}
.dialog-buttons {
display: flex;
gap: 12px;
margin-top: 24px;
}
.confirm-btn {
background: #409EFF;
color: #fff;
border: none;
border-radius: 4px; border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-weight: 500;
} }
.confirm-btn:hover { .tournament-bracket-root .loading {
background: #66b1ff;
}
.cancel-btn {
background: #f5f5f5;
color: #666; color: #666;
border: none; font-style: italic;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-weight: 500;
} }
.cancel-btn:hover { .tournament-bracket-root select {
background: #e8e8e8; width: 100%;
padding: 8px;
margin-bottom: 10px;
border-radius: 4px;
border: 1px solid #ccc;
} }
</style> </style>

View File

@ -75,6 +75,11 @@
v-if="competition.status === 'finish'" v-if="competition.status === 'finish'"
:tournament-id="parseInt(route.query.id)" :tournament-id="parseInt(route.query.id)"
/> />
<tournament-bracket
v-if="competition.status === 'starting'"
:tournament-id="parseInt(route.query.id)"
@refreshPlayers="fetchRegisteredPlayers"
/>
</template> </template>
<div v-if="showEditForm" class="edit-dialog-overlay"> <div v-if="showEditForm" class="edit-dialog-overlay">