重置密码的60s冷却🐱🐱
This commit is contained in:
parent
0537bdb86e
commit
4c15c42ebc
@ -1,8 +1,8 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { logoutUser } from '../utils/jwt'; // logoutUser会处理清除存储和重定向
|
import { logoutUser } from '../utils/jwt'; // logoutUser会处理清除存储和重定向
|
||||||
|
|
||||||
const API_BASE_URL = 'https://api.zybdatasupport.online';
|
//const API_BASE_URL = 'https://api.zybdatasupport.online';
|
||||||
//const API_BASE_URL = 'http://hk.zybdatasupport.online:8000/';
|
const API_BASE_URL = 'http://hk.zybdatasupport.online:8000/';
|
||||||
|
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
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'"
|
v-if="competition.status === 'finish'"
|
||||||
:tournament-id="parseInt(route.query.id)"
|
:tournament-id="parseInt(route.query.id)"
|
||||||
/>
|
/>
|
||||||
|
<DoubleEliminationBracket v-if="competition.format === '双败淘汰' && competition.status === 'starting'" />
|
||||||
<tournament-bracket
|
<tournament-bracket
|
||||||
v-if="competition.status === 'starting'"
|
v-else-if="competition.status === 'starting'"
|
||||||
:tournament-id="parseInt(route.query.id)"
|
:tournament-id="parseInt(route.query.id)"
|
||||||
@refreshPlayers="fetchRegisteredPlayers"
|
@refreshPlayers="fetchRegisteredPlayers"
|
||||||
/>
|
/>
|
||||||
@ -217,6 +218,7 @@ import { ref, onMounted, computed } from 'vue'
|
|||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import TournamentBracket from '@/components/TournamentBracket.vue'
|
import TournamentBracket from '@/components/TournamentBracket.vue'
|
||||||
import RankContestant from '@/components/RankContestant.vue'
|
import RankContestant from '@/components/RankContestant.vue'
|
||||||
|
import DoubleEliminationBracket from '@/components/DoubleEliminationBracket.vue'
|
||||||
import {
|
import {
|
||||||
getTournamentList,
|
getTournamentList,
|
||||||
updateTournament,
|
updateTournament,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user