DCFronted/src/components/TournamentBracket.vue
2025-05-26 20:35:34 +08:00

436 lines
9.2 KiB
Vue

<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>
</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'
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 startDrag = (e) => {
isDragging.value = true
dragStart.value = {
x: e.clientX - position.value.x,
y: position.value.y
}
}
const onDrag = (e) => {
if (isDragging.value) {
position.value = {
x: e.clientX - dragStart.value.x,
y: position.value.y
}
}
}
const stopDrag = () => {
isDragging.value = false
}
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
}
}
})
// 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)
}
rounds.push({ games: firstRoundGames })
// 生成后续轮次
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)
}
rounds.push({ games: currentRoundGames })
}
return rounds
}
// 初始化对阵图
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 || '未知选手',
})))
}
watch(() => props.players, () => {
initializeBracket()
}, { immediate: true })
// 轮次名称
const getRoundName = (index, totalRounds) => {
if (totalRounds === 1) {
return '决赛'
} else if (totalRounds === 2) {
return index === 0 ? '半决赛' : '决赛'
} else if (totalRounds === 3) {
return index === 0 ? '四分之一决赛' : index === 1 ? '半决赛' : '决赛'
} else {
return `${index + 1}`
}
}
const getRoundBo = (index, totalRounds) => {
if (index === totalRounds - 1) {
return 'BO5'
} else {
return 'BO3'
}
}
// 格式化轮次数据为 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
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 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
}
// 处理比赛点击事件
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)
}
// 选择胜者弹窗
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
}
</script>
<style scoped>
.tournament-bracket {
position: relative;
overflow-x: auto;
overflow-y: auto;
background: #141414;
color: #fff;
height: auto;
min-height: 600px;
padding: 20px;
}
.bracket-wrapper {
transform-origin: center left;
cursor: ew-resize;
user-select: none;
padding: 20px;
min-height: calc(100% - 40px);
position: relative;
padding-top: 0;
width: max-content;
min-width: 100%;
}
.bracket-rounds {
display: flex;
gap: 40px;
}
.bracket-round {
display: flex;
flex-direction: column;
gap: 24px;
}
.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;
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;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.player-score {
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;
}
.win-score {
color: #4caf50;
font-weight: 500;
}
.score-separator {
color: rgba(255, 255, 255, 0.5);
}
.lose-score {
color: #f44336;
font-weight: 500;
}
.set-winner-btn {
margin-left: 12px;
padding: 2px 8px;
background: #ff9800;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.set-winner-btn:hover {
background: #e65100;
}
.edit-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.edit-dialog {
background: #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;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-weight: 500;
}
.confirm-btn:hover {
background: #66b1ff;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-weight: 500;
}
.cancel-btn:hover {
background: #e8e8e8;
}
</style>