重置密码的60s冷却🐱🐱
This commit is contained in:
parent
0537bdb86e
commit
4c15c42ebc
@ -1,8 +1,8 @@
|
||||
import axios from 'axios';
|
||||
import { logoutUser } from '../utils/jwt'; // logoutUser会处理清除存储和重定向
|
||||
|
||||
const API_BASE_URL = 'https://api.zybdatasupport.online';
|
||||
//const API_BASE_URL = 'http://hk.zybdatasupport.online:8000/';
|
||||
//const API_BASE_URL = 'https://api.zybdatasupport.online';
|
||||
const API_BASE_URL = 'http://hk.zybdatasupport.online:8000/';
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
|
412
src/components/DoubleEliminationBracket.vue
Normal file
412
src/components/DoubleEliminationBracket.vue
Normal file
@ -0,0 +1,412 @@
|
||||
<template>
|
||||
<div class="tournament-bracket-root">
|
||||
<h1>双败淘汰赛赛程图 (BO3)</h1>
|
||||
<div class="container">
|
||||
<div class="bracket-container">
|
||||
<div ref="bracket" class="bracket"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="finalRanking"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const totalPlayers = 8
|
||||
const sampleNames = ["小明", "小红", "小刚", "小美", "阿强", "阿花", "老李", "老王", "张三", "李四", "王五", "赵六"]
|
||||
const shuffledPlayers = sampleNames.sort(() => Math.random() - 0.5).slice(0, totalPlayers)
|
||||
|
||||
let rounds = {
|
||||
winners: [], // 胜者组轮次数组
|
||||
losers: [], // 败者组轮次数组
|
||||
finals: [] // 决赛
|
||||
}
|
||||
|
||||
const bracket = ref(null)
|
||||
const finalRanking = ref(null)
|
||||
|
||||
function initWinners() {
|
||||
const firstRound = []
|
||||
for(let i=0; i < totalPlayers; i+=2) {
|
||||
firstRound.push({
|
||||
player1: shuffledPlayers[i],
|
||||
player2: shuffledPlayers[i+1],
|
||||
score1: 0,
|
||||
score2: 0,
|
||||
done: false,
|
||||
winner: null,
|
||||
loser: null,
|
||||
})
|
||||
}
|
||||
rounds.winners.push(firstRound)
|
||||
}
|
||||
|
||||
function getLosersRoundsCount(nPlayers) {
|
||||
return Math.ceil(Math.log2(nPlayers)) + 1
|
||||
}
|
||||
|
||||
function initLosers() {
|
||||
let roundsCount = getLosersRoundsCount(totalPlayers)
|
||||
for(let i=0; i < roundsCount; i++) {
|
||||
rounds.losers.push([])
|
||||
}
|
||||
}
|
||||
|
||||
function validScore(s1, s2) {
|
||||
if(s1 === s2) return false
|
||||
if(s1 < 0 || s2 < 0) return false
|
||||
if(s1 > 2 || s2 > 2) return false
|
||||
if(s1 !== 2 && s2 !== 2) return false
|
||||
return true
|
||||
}
|
||||
|
||||
function generateBracket() {
|
||||
if (!bracket.value) return
|
||||
bracket.value.innerHTML = ''
|
||||
|
||||
function createRound(title, matches, type, roundIndex) {
|
||||
const roundDiv = document.createElement('div')
|
||||
roundDiv.className = 'round'
|
||||
|
||||
const titleDiv = document.createElement('div')
|
||||
titleDiv.className = 'round-title'
|
||||
titleDiv.textContent = title
|
||||
roundDiv.appendChild(titleDiv)
|
||||
|
||||
matches.forEach((match, matchIndex) => {
|
||||
const matchDiv = document.createElement('div')
|
||||
matchDiv.className = 'match'
|
||||
|
||||
const p1 = match.player1 || '待定'
|
||||
const p2 = match.player2 || '待定'
|
||||
|
||||
let p1Class = ''
|
||||
let p2Class = ''
|
||||
if(match.done && match.winner) {
|
||||
if(match.winner === match.player1) p1Class = 'participant winner'
|
||||
if(match.winner === match.player2) p2Class = 'participant winner'
|
||||
}
|
||||
|
||||
const disabled = match.done ? 'disabled' : ''
|
||||
|
||||
matchDiv.innerHTML = `
|
||||
<div class="participant ${p1Class}">${p1}</div>
|
||||
<div class="participant ${p2Class}">${p2}</div>
|
||||
<div>
|
||||
<input type="number" min="0" max="2" class="score-input" id="${type}-${roundIndex}-${matchIndex}-score1" value="${match.score1}" ${disabled}>
|
||||
:
|
||||
<input type="number" min="0" max="2" class="score-input" id="${type}-${roundIndex}-${matchIndex}-score2" value="${match.score2}" ${disabled}>
|
||||
</div>
|
||||
<button class="score-btn" id="${type}-${roundIndex}-${matchIndex}-btn" ${disabled}>提交</button>
|
||||
`
|
||||
roundDiv.appendChild(matchDiv)
|
||||
|
||||
// 事件绑定
|
||||
if (!match.done) {
|
||||
setTimeout(() => {
|
||||
const btn = document.getElementById(`${type}-${roundIndex}-${matchIndex}-btn`)
|
||||
if (btn) {
|
||||
btn.onclick = () => submitMatch(type, roundIndex, matchIndex)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
})
|
||||
return roundDiv
|
||||
}
|
||||
|
||||
rounds.winners.forEach((r, i) => {
|
||||
bracket.value.appendChild(createRound(`胜者组第 ${i+1} 轮`, r, 'winners', i))
|
||||
})
|
||||
rounds.losers.forEach((r, i) => {
|
||||
bracket.value.appendChild(createRound(`败者组第 ${i+1} 轮`, r, 'losers', i))
|
||||
})
|
||||
if(rounds.finals.length > 0) {
|
||||
bracket.value.appendChild(createRound('总决赛', rounds.finals, 'finals', 0))
|
||||
}
|
||||
}
|
||||
|
||||
function submitMatch(type, roundIndex, matchIndex) {
|
||||
let match = rounds[type][roundIndex][matchIndex]
|
||||
if(match.done) return
|
||||
|
||||
const s1 = parseInt(document.getElementById(`${type}-${roundIndex}-${matchIndex}-score1`).value)
|
||||
const s2 = parseInt(document.getElementById(`${type}-${roundIndex}-${matchIndex}-score2`).value)
|
||||
|
||||
if(!validScore(s1, s2)) {
|
||||
alert('比分无效,请输入有效的 BO3 胜场数,且双方比分不能相同。')
|
||||
return
|
||||
}
|
||||
|
||||
match.score1 = s1
|
||||
match.score2 = s2
|
||||
match.done = true
|
||||
|
||||
if(s1 > s2) {
|
||||
match.winner = match.player1
|
||||
match.loser = match.player2
|
||||
} else {
|
||||
match.winner = match.player2
|
||||
match.loser = match.player1
|
||||
}
|
||||
|
||||
if(type === 'winners') {
|
||||
if(!rounds.winners[roundIndex+1]) rounds.winners[roundIndex+1] = []
|
||||
addPlayerToNextRound(rounds.winners[roundIndex+1], match.winner)
|
||||
if(!rounds.losers[roundIndex]) rounds.losers[roundIndex] = []
|
||||
addPlayerToNextRound(rounds.losers[roundIndex], match.loser)
|
||||
}
|
||||
else if(type === 'losers') {
|
||||
if(!rounds.losers[roundIndex+1]) rounds.losers[roundIndex+1] = []
|
||||
addPlayerToNextRound(rounds.losers[roundIndex+1], match.winner)
|
||||
}
|
||||
else if(type === 'finals') {
|
||||
handleFinalsResult(match)
|
||||
}
|
||||
|
||||
generateBracket()
|
||||
checkIfCanStartFinals()
|
||||
}
|
||||
|
||||
function addPlayerToNextRound(roundMatches, player) {
|
||||
if(!player) return
|
||||
for(let match of roundMatches) {
|
||||
if(!match.player1) {
|
||||
match.player1 = player
|
||||
match.done = false
|
||||
match.score1 = 0
|
||||
match.score2 = 0
|
||||
match.winner = null
|
||||
match.loser = null
|
||||
return
|
||||
}
|
||||
if(!match.player2) {
|
||||
match.player2 = player
|
||||
match.done = false
|
||||
match.score1 = 0
|
||||
match.score2 = 0
|
||||
match.winner = null
|
||||
match.loser = null
|
||||
return
|
||||
}
|
||||
}
|
||||
roundMatches.push({
|
||||
player1: player,
|
||||
player2: null,
|
||||
score1: 0,
|
||||
score2: 0,
|
||||
done: false,
|
||||
winner: null,
|
||||
loser: null,
|
||||
})
|
||||
}
|
||||
|
||||
function checkIfCanStartFinals() {
|
||||
const winnersLastRound = rounds.winners[rounds.winners.length-1]
|
||||
if(!winnersLastRound) return
|
||||
if(winnersLastRound.length !== 1) return
|
||||
if(!winnersLastRound[0].done) return
|
||||
|
||||
const losersLastRound = rounds.losers[rounds.losers.length-1]
|
||||
if(!losersLastRound) return
|
||||
if(losersLastRound.length !== 1) return
|
||||
if(!losersLastRound[0].done) return
|
||||
|
||||
if(rounds.finals.length === 0) {
|
||||
rounds.finals.push({
|
||||
player1: winnersLastRound[0].winner,
|
||||
player2: losersLastRound[0].winner,
|
||||
score1: 0,
|
||||
score2: 0,
|
||||
done: false,
|
||||
winner: null,
|
||||
loser: null,
|
||||
})
|
||||
generateBracket()
|
||||
}
|
||||
}
|
||||
|
||||
function handleFinalsResult(match) {
|
||||
if(!match.done) return
|
||||
const champion = match.winner
|
||||
const runnerUp = match.loser
|
||||
if (!finalRanking.value) return
|
||||
finalRanking.value.innerHTML = `
|
||||
<h3>比赛结束 - 最终排名</h3>
|
||||
<ol>
|
||||
<li>冠军:${champion}</li>
|
||||
<li>亚军:${runnerUp}</li>
|
||||
<li>季军:待定(可扩展实现季军赛)</li>
|
||||
</ol>
|
||||
`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initWinners()
|
||||
initLosers()
|
||||
generateBracket()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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>
|
@ -75,8 +75,9 @@
|
||||
v-if="competition.status === 'finish'"
|
||||
:tournament-id="parseInt(route.query.id)"
|
||||
/>
|
||||
<DoubleEliminationBracket v-if="competition.format === '双败淘汰' && competition.status === 'starting'" />
|
||||
<tournament-bracket
|
||||
v-if="competition.status === 'starting'"
|
||||
v-else-if="competition.status === 'starting'"
|
||||
:tournament-id="parseInt(route.query.id)"
|
||||
@refreshPlayers="fetchRegisteredPlayers"
|
||||
/>
|
||||
@ -217,6 +218,7 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import TournamentBracket from '@/components/TournamentBracket.vue'
|
||||
import RankContestant from '@/components/RankContestant.vue'
|
||||
import DoubleEliminationBracket from '@/components/DoubleEliminationBracket.vue'
|
||||
import {
|
||||
getTournamentList,
|
||||
updateTournament,
|
||||
|
Loading…
x
Reference in New Issue
Block a user