赛事
This commit is contained in:
221
src/components/RankContestant.vue
Normal file
221
src/components/RankContestant.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="rank-contestant">
|
||||
<div class="rank-contestant-header">
|
||||
<h2>选手排名</h2>
|
||||
</div>
|
||||
<div class="rank-content">
|
||||
<div class="top-three">
|
||||
<div v-for="(item, idx) in rankData.slice(0, 3)" :key="idx" class="rank-card">
|
||||
<div class="rank-number">
|
||||
{{ item.rank }}
|
||||
</div>
|
||||
<div class="player-info">
|
||||
<div class="player-name">{{ item.username }}</div>
|
||||
<div class="player-faction">{{ item.faction }}</div>
|
||||
<div class="player-score">{{ item.score }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rank-list">
|
||||
<div v-for="(item, idx) in rankData.slice(3)" :key="idx + 3" class="rank-item">
|
||||
<div class="rank">{{ item.rank }}</div>
|
||||
<div class="player-info">
|
||||
<div class="player-name">{{ item.username }}</div>
|
||||
<div class="player-faction">{{ item.faction }}</div>
|
||||
<div class="player-score">{{ item.score }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getSignUpResultList } from '@/api/tournament'
|
||||
|
||||
const props = defineProps({
|
||||
tournamentId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const rankData = ref([])
|
||||
|
||||
const fetchRankData = async () => {
|
||||
try {
|
||||
const response = await getSignUpResultList()
|
||||
// 筛选当前赛事的玩家并按胜场数排序
|
||||
const results = response
|
||||
.filter(player => player.tournament_id === props.tournamentId)
|
||||
.map((player, index) => ({
|
||||
rank: index + 1,
|
||||
username: player.sign_name,
|
||||
faction: player.faction,
|
||||
win: parseInt(player.win) || 0,
|
||||
lose: parseInt(player.lose) || 0,
|
||||
score: `${player.win}胜${player.lose}负`
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
// 首先按胜场数排序
|
||||
if (b.win !== a.win) {
|
||||
return b.win - a.win
|
||||
}
|
||||
// 如果胜场数相同,则按负场数排序(负场数少的排前面)
|
||||
return a.lose - b.lose
|
||||
})
|
||||
.map((player, index) => ({
|
||||
...player,
|
||||
rank: index + 1
|
||||
}))
|
||||
|
||||
rankData.value = results
|
||||
} catch (error) {
|
||||
console.error('获取排名数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRankData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rank-contestant {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.rank-contestant-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.rank-contestant-header h2 {
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rank-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.top-three {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.rank-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #EBEEF5;
|
||||
}
|
||||
|
||||
.rank-card:nth-child(1) {
|
||||
background: #FFF9EB;
|
||||
border: 1px solid #FFE4B5;
|
||||
}
|
||||
|
||||
.rank-card:nth-child(2) {
|
||||
background: #F8F9FA;
|
||||
border: 1px solid #E4E7ED;
|
||||
}
|
||||
|
||||
.rank-card:nth-child(3) {
|
||||
background: #FDF6EC;
|
||||
border: 1px solid #F3D19E;
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-right: 16px;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.rank-card:nth-child(1) .rank-number {
|
||||
color: #E6A23C;
|
||||
}
|
||||
|
||||
.rank-card:nth-child(2) .rank-number {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.rank-card:nth-child(3) .rank-number {
|
||||
color: #F56C6C;
|
||||
}
|
||||
|
||||
.player-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.player-qq {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.player-faction {
|
||||
font-size: 14px;
|
||||
color: #409EFF;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.player-score {
|
||||
font-size: 14px;
|
||||
color: #67C23A;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rank-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.rank-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #EBEEF5;
|
||||
}
|
||||
|
||||
.rank {
|
||||
width: 40px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.top-three {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.rank-contestant {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,204 +1,436 @@
|
||||
<template>
|
||||
<div class="tournament-container">
|
||||
<bracket :rounds="rounds">
|
||||
<template #player="{ player }">
|
||||
<div class="custom-node" :class="{ 'winner': player.winner }">
|
||||
<div class="node-content">
|
||||
<span class="team-name">{{ player.name }}</span>
|
||||
<span class="team-score">{{ player.score || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</bracket>
|
||||
<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 } from 'vue'
|
||||
import Bracket from 'vue-tournament-bracket'
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { Bracket } from 'vue-tournament-bracket'
|
||||
|
||||
// 示例数据
|
||||
const rounds = ref([
|
||||
// 半决赛
|
||||
{
|
||||
games: [
|
||||
{
|
||||
player1: { id: "1", name: "战队 A", score: 2, winner: true },
|
||||
player2: { id: "2", name: "战队 B", score: 1, winner: false }
|
||||
},
|
||||
{
|
||||
player1: { id: "3", name: "战队 C", score: 0, winner: false },
|
||||
player2: { id: "4", name: "战队 D", score: 2, winner: true }
|
||||
}
|
||||
]
|
||||
const props = defineProps({
|
||||
isEditMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 决赛
|
||||
{
|
||||
games: [
|
||||
{
|
||||
player1: { id: "1", name: "战队 A", score: 3, winner: true },
|
||||
player2: { id: "4", name: "战队 D", score: 1, winner: 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-container {
|
||||
min-height: 400px;
|
||||
.tournament-bracket {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
background: transparent;
|
||||
overflow-y: auto;
|
||||
background: #141414;
|
||||
color: #fff;
|
||||
height: auto;
|
||||
min-height: 600px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.vtb-wrapper),
|
||||
:deep(.vtb-item),
|
||||
:deep(.vtb-item-parent),
|
||||
:deep(.vtb-item-players),
|
||||
:deep(.vtb-item-children),
|
||||
:deep(.vtb-item-child),
|
||||
:deep(.custom-node),
|
||||
:deep(.custom-node.winner) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.vtb-wrapper) {
|
||||
display: flex;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
:deep(.vtb-item) {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
:deep(.vtb-item-parent) {
|
||||
.bracket-wrapper {
|
||||
transform-origin: center left;
|
||||
cursor: ew-resize;
|
||||
user-select: none;
|
||||
padding: 20px;
|
||||
min-height: calc(100% - 40px);
|
||||
position: relative;
|
||||
margin-left: 50px;
|
||||
padding-top: 0;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.bracket-rounds {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
:deep(.vtb-item-players) {
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.vtb-player) {
|
||||
padding: 10px 16px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(143, 143, 143, 0.3);
|
||||
color: #333;
|
||||
border-radius: 24px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
:deep(.vtb-player:hover) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border-color: rgba(143, 143, 143, 0.5);
|
||||
}
|
||||
|
||||
:deep(.vtb-player.winner) {
|
||||
border: 1px solid rgba(129, 199, 132, 0.5);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 2px 8px rgba(129, 199, 132, 0.15);
|
||||
}
|
||||
|
||||
:deep(.vtb-player.winner:hover) {
|
||||
border-color: rgba(129, 199, 132, 0.8);
|
||||
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.2);
|
||||
}
|
||||
|
||||
:deep(.vtb-item-children) {
|
||||
.bracket-round {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
:deep(.vtb-item-child) {
|
||||
.bracket-game {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
margin: 12px 0;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
.player-info {
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(143, 143, 143, 0.3);
|
||||
color: #333;
|
||||
padding: 10px 16px;
|
||||
border-radius: 24px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
gap: 8px;
|
||||
width: 300px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 4px 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.node-content:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border-color: rgba(143, 143, 143, 0.5);
|
||||
.player-info.winner {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.custom-node.winner .node-content {
|
||||
border: 1px solid rgba(129, 199, 132, 0.5);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 2px 8px rgba(129, 199, 132, 0.15);
|
||||
.player-info.defeated {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
.custom-node.winner .node-content:hover {
|
||||
border-color: rgba(129, 199, 132, 0.8);
|
||||
box-shadow: 0 4px 12px rgba(129, 199, 132, 0.2);
|
||||
.player-number {
|
||||
color: #bbdefb;
|
||||
font-size: 12px;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.team-name {
|
||||
.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;
|
||||
color: #333;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.team-score {
|
||||
font-weight: 600;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
margin-left: 16px;
|
||||
color: #333;
|
||||
.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: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 12px;
|
||||
background: #ff9800;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.vtb-player.winner) .team-score,
|
||||
.custom-node.winner .team-score {
|
||||
background: rgba(129, 199, 132, 0.1);
|
||||
color: #2e7d32;
|
||||
.set-winner-btn:hover {
|
||||
background: #e65100;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.vtb-wrapper) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
:deep(.vtb-player),
|
||||
.node-content {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.team-name {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.team-score {
|
||||
min-width: 20px;
|
||||
margin-left: 12px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
.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>
|
||||
@@ -191,7 +191,7 @@ const handleLogin = async () => {
|
||||
|
||||
// 登录成功,跳转到首页
|
||||
if (response.access_token) {
|
||||
router.push('/dashboard')
|
||||
router.push('/')
|
||||
} else {
|
||||
throw new Error('登录失败:未获取到访问令牌')
|
||||
}
|
||||
|
||||
@@ -4,38 +4,20 @@
|
||||
<form class="login-form-container" @submit.prevent="handleRegister">
|
||||
<div class="input-container">
|
||||
<label for="username">QQ号</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
v-model="username"
|
||||
placeholder="请输入QQ号"
|
||||
@blur="validateUsername"
|
||||
:class="{ 'error': usernameError }"
|
||||
/>
|
||||
<input type="text" id="username" v-model="username" placeholder="请输入QQ号" @blur="validateUsername"
|
||||
:class="{ 'error': usernameError }" />
|
||||
<span class="error-message" v-if="usernameError">{{ usernameError }}</span>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="password">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
v-model="password"
|
||||
placeholder="请输入密码"
|
||||
@blur="validatePassword"
|
||||
:class="{ 'error': passwordError }"
|
||||
/>
|
||||
<input type="password" id="password" v-model="password" placeholder="请输入密码" @blur="validatePassword"
|
||||
:class="{ 'error': passwordError }" />
|
||||
<span class="error-message" v-if="passwordError">{{ passwordError }}</span>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<label for="confirmPassword">再次输入密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
v-model="confirmPassword"
|
||||
placeholder="请输入密码"
|
||||
@blur="validateConfirmPassword"
|
||||
:class="{ 'error': confirmPasswordError }"
|
||||
/>
|
||||
<input type="password" id="confirmPassword" v-model="confirmPassword" placeholder="请输入密码"
|
||||
@blur="validateConfirmPassword" :class="{ 'error': confirmPasswordError }" />
|
||||
<span class="error-message" v-if="confirmPasswordError">{{ confirmPasswordError }}</span>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
@@ -94,6 +76,9 @@ const username = ref('')
|
||||
const password = ref('')
|
||||
const confirmPassword = ref('')
|
||||
|
||||
// 定义 emit
|
||||
const emit = defineEmits(['login'])
|
||||
|
||||
// 错误信息
|
||||
const usernameError = ref('')
|
||||
const passwordError = ref('')
|
||||
@@ -144,14 +129,14 @@ const validateConfirmPassword = () => {
|
||||
const isVaptchaVerified = ref(false)
|
||||
// 计算表单是否有效
|
||||
const isFormValid = computed(() => {
|
||||
return !usernameError.value &&
|
||||
!passwordError.value &&
|
||||
!confirmPasswordError.value &&
|
||||
username.value &&
|
||||
password.value &&
|
||||
confirmPassword.value &&
|
||||
VAPTCHAObj.value && // 确保人机验证已初始化
|
||||
isVaptchaVerified.value // 确保人机验证已通过
|
||||
return !usernameError.value &&
|
||||
!passwordError.value &&
|
||||
!confirmPasswordError.value &&
|
||||
username.value &&
|
||||
password.value &&
|
||||
confirmPassword.value &&
|
||||
VAPTCHAObj.value && // 确保人机验证已初始化
|
||||
isVaptchaVerified.value // 确保人机验证已通过
|
||||
})
|
||||
|
||||
|
||||
@@ -189,54 +174,44 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const handleRegister = async () => {
|
||||
try {
|
||||
// 验证表单
|
||||
if (!validateUsername() || !validatePassword() || !validateConfirmPassword()) {
|
||||
return
|
||||
}
|
||||
// 验证人机验证
|
||||
if (!isVaptchaVerified.value || !VAPTCHAObj.value) {
|
||||
alert('请完成人机验证')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取验证token
|
||||
try {
|
||||
const serverToken = await VAPTCHAObj.value.getServerToken()
|
||||
console.log('获取到的 serverToken:', serverToken)
|
||||
|
||||
if (!serverToken || !serverToken.token || !serverToken.server) {
|
||||
alert('获取验证token失败,请重新验证')
|
||||
return
|
||||
}
|
||||
|
||||
// 构造注册请求参数
|
||||
const registerData = {
|
||||
qq_code: username.value,
|
||||
password: password.value,
|
||||
server: serverToken.server,
|
||||
token: serverToken.token
|
||||
}
|
||||
console.log('注册请求参数:', registerData)
|
||||
|
||||
// 发送注册请求
|
||||
await userRegister(
|
||||
registerData.qq_code,
|
||||
registerData.password,
|
||||
registerData.server,
|
||||
registerData.token
|
||||
)
|
||||
alert('注册成功')
|
||||
// 切换到登录模块
|
||||
emit('login')
|
||||
} catch (tokenError) {
|
||||
console.error('获取验证token失败:', tokenError)
|
||||
alert('获取验证token失败,请重新验证')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error)
|
||||
alert(error.message || '注册失败')
|
||||
// 验证表单
|
||||
if (!validateUsername() || !validatePassword() || !validateConfirmPassword()) {
|
||||
return
|
||||
}
|
||||
// 验证人机验证
|
||||
if (!isVaptchaVerified.value || !VAPTCHAObj.value) {
|
||||
alert('请完成人机验证')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取验证token
|
||||
const serverToken = await VAPTCHAObj.value.getServerToken()
|
||||
console.log('获取到的 serverToken:', serverToken)
|
||||
|
||||
if (!serverToken || !serverToken.token || !serverToken.server) {
|
||||
alert('获取验证token失败,请重新验证')
|
||||
return
|
||||
}
|
||||
|
||||
// 构造注册请求参数
|
||||
const registerData = {
|
||||
qq_code: username.value,
|
||||
password: password.value,
|
||||
server: serverToken.server,
|
||||
token: serverToken.token
|
||||
}
|
||||
console.log('注册请求参数:', registerData)
|
||||
|
||||
// 发送注册请求
|
||||
await userRegister(
|
||||
registerData.qq_code,
|
||||
registerData.password,
|
||||
registerData.server,
|
||||
registerData.token
|
||||
)
|
||||
alert('注册成功')
|
||||
// 切换到登录模块
|
||||
emit('login')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user