重构赛事的树状图

This commit is contained in:
2025-07-29 16:37:03 +08:00
parent 1977b791e5
commit 2762a8b5f1
7 changed files with 598 additions and 56 deletions

View File

@@ -92,6 +92,18 @@
</div>
</div>
</div>
<!-- 对话框组件 -->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialog.visible"
:message="errorDialog.message"
@close="errorDialog.visible = false"
/>
</div>
</template>
@@ -99,6 +111,8 @@
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue';
import { getSignUpResultList, updateSignUpResult, getTournamentList } from '@/api/tournament';
import * as d3 from 'd3';
import SuccessDialog from '@/components/SuccessDialog.vue';
import ErrorDialog from '@/components/ErrorDialog.vue';
const props = defineProps({
tournamentId: {
@@ -121,6 +135,10 @@ const runnerUp = ref(null);
const thirdPlace = ref(null);
const loading = ref(false);
// Dialog state
const successDialog = ref({ visible: false, message: '' });
const errorDialog = ref({ visible: false, message: '' });
// Store zoom behaviors for both brackets
let winnersZoomBehavior = null;
let winnerssvgSelection = null;
@@ -218,7 +236,7 @@ const generateDoubleEliminationBracket = () => {
const totalPlayers = participants.value.length;
if (totalPlayers < 4) {
alert('双败淘汰赛至少需要4名参赛者');
errorDialog.value = { visible: true, message: '双败淘汰赛至少需要4名参赛者' };
return;
}
@@ -690,7 +708,10 @@ const drawD3Connections = (g, bracket, bracketType, matchWidth, matchHeight, rou
};
const confirmScore = async (match, bracketType, rIndex, mIndex) => {
if (match.score1 === match.score2) return alert('不能平局');
if (match.score1 === match.score2) {
errorDialog.value = { visible: true, message: '不能平局' };
return;
}
match.decided = true;
match.winner = match.score1 > match.score2 ? match.participant1 : match.participant2;
match.loser = match.score1 > match.score2 ? match.participant2 : match.participant1;
@@ -916,7 +937,10 @@ const resetZoomLosers = () => {
const confirmFinal = async () => {
const match = finalMatch.value;
if (!match || match.score1 === match.score2) return alert('决赛不能平局');
if (!match || match.score1 === match.score2) {
errorDialog.value = { visible: true, message: '决赛不能平局' };
return;
}
match.decided = true;
match.winner = match.score1 > match.score2 ? match.participant1 : match.participant2;
@@ -929,7 +953,7 @@ const confirmFinal = async () => {
// 如果败者组冠军participant2在决赛中获胜则需要重置比赛
if (match.winner === match.participant2) {
// Bracket Reset: 败者组冠军获胜,需要再打一场
alert('败者组冠军获胜!比赛重置,将进行最终决赛!');
successDialog.value = { visible: true, message: '败者组冠军获胜!比赛重置,将进行最终决赛!' };
finalMatch.value = {
participant1: match.participant2, // 原败者组冠军
participant2: match.participant1, // 原胜者组冠军
@@ -1067,7 +1091,7 @@ const updatePlayerStats = async (p1, p2, s1, s2, bracketType = 'winners') => {
await loadTournamentData();
} catch (err) {
console.error('更新选手数据失败:', err);
alert('更新选手数据失败');
errorDialog.value = { visible: true, message: '更新选手数据失败' };
}
};

View File

@@ -48,6 +48,18 @@
<svg id="d3-bracket" class="d3-bracket"></svg>
</div>
</div>
<!-- 对话框组件 -->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialog.visible"
:message="errorDialog.message"
@close="errorDialog.visible = false"
/>
</div>
</template>
@@ -55,6 +67,8 @@
import { ref, computed, onMounted, watch, nextTick, onUnmounted } from 'vue';
import { getSignUpResultList, updateSignUpResult, getTournamentList } from '@/api/tournament';
import * as d3 from 'd3';
import SuccessDialog from '@/components/SuccessDialog.vue';
import ErrorDialog from '@/components/ErrorDialog.vue';
const props = defineProps({
tournamentId: {
@@ -67,6 +81,10 @@ const participants = ref([]);
const loading = ref(false);
const finalRanking = ref(null);
// Dialog state
const successDialog = ref({ visible: false, message: '' });
const errorDialog = ref({ visible: false, message: '' });
// Store zoom behavior for external control
let zoomBehavior = null;
let svgSelection = null;
@@ -500,11 +518,11 @@ async function confirmScore(match) {
const s1 = Number(match.score1);
const s2 = Number(match.score2);
if (isNaN(s1) || isNaN(s2)) {
alert('请输入有效的比分');
errorDialog.value = { visible: true, message: '请输入有效的比分' };
return;
}
if (s1 === s2) {
alert('比分不能平局');
errorDialog.value = { visible: true, message: '比分不能平局' };
return;
}
match.score1 = s1;
@@ -558,7 +576,7 @@ async function confirmScore(match) {
tournament.value.participants = [...participants.value];
}
} catch (err) {
alert('更新数据失败');
errorDialog.value = { visible: true, message: '更新数据失败' };
console.error(err);
}
@@ -720,9 +738,29 @@ watch(() => props.tournamentId, (newId) => {
}
.tournament-bracket-root button {
background: #409EFF;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
width: 100%;
font-size: 14px;
font-weight: bold;
transition: all 0.3s;
outline: none;
}
.tournament-bracket-root button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.tournament-bracket-root button:hover:not(:disabled) {
background: #66b1ff;
}
.tournament-bracket-root input[type=number]::-webkit-inner-spin-button,
.tournament-bracket-root input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;

View File

@@ -100,6 +100,18 @@
<button type="button" class="cancel-btn" @click="handleCancel">取消</button>
</div>
</form>
<!-- 对话框组件 -->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialog.visible"
:message="errorDialog.message"
@close="errorDialog.visible = false"
/>
</div>
</template>
@@ -108,6 +120,8 @@ import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { addTournament, addSignUp, getTournamentList } from '@/api/tournament'
import * as XLSX from 'xlsx'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'
const router = useRouter()
const showExcelDialog = ref(false)
@@ -129,6 +143,10 @@ const formData = ref({
description: ''
})
// Dialog state
const successDialog = ref({ visible: false, message: '' })
const errorDialog = ref({ visible: false, message: '' })
// Excel赛事信息字段与表单字段的映射
const excelFieldMap = {
'赛事名称': 'name',
@@ -190,11 +208,11 @@ const handleSubmit = async () => {
// 调用API添加赛事
await addTournament(tournamentData)
alert('添加赛事成功!')
successDialog.value = { visible: true, message: '添加赛事成功!' }
router.push('/competition')
} catch (error) {
console.error('提交失败:', error)
alert(error.response?.data?.message || '添加赛事失败,请重试')
errorDialog.value = { visible: true, message: error.response?.data?.message || '添加赛事失败,请重试' }
}
}
@@ -310,7 +328,7 @@ const processExcelFile = (file) => {
}
}
} catch (error) {
alert('Excel文件格式错误请检查文件内容')
errorDialog.value = { visible: true, message: 'Excel文件格式错误请检查文件内容' }
}
}
reader.readAsArrayBuffer(file)
@@ -340,7 +358,7 @@ const confirmImport = async () => {
try {
await addTournament(tournamentData)
alert('赛事导入成功!')
successDialog.value = { visible: true, message: '赛事导入成功!' }
const tournamentList = await getTournamentList()
console.log('获取到的赛事列表:', tournamentList)
@@ -377,16 +395,16 @@ const confirmImport = async () => {
await addSignUp(signUpData)
} catch (error) {
console.error('报名失败:', error)
alert(`报名失败: ${row['队伍名称或者个人参赛名称']}${error.message}`)
errorDialog.value = { visible: true, message: `报名失败: ${row['队伍名称或者个人参赛名称']}${error.message}` }
}
}
alert('选手报名表导入完成!')
successDialog.value = { visible: true, message: '选手报名表导入完成!' }
}
router.push('/competition')
} catch (error) {
console.error('导入失败:', error)
alert(error.response?.data?.message || '赛事导入失败,请重试')
errorDialog.value = { visible: true, message: error.response?.data?.message || '赛事导入失败,请重试' }
}
closeExcelDialog()
}

View File

@@ -213,6 +213,18 @@
</div>
</div>
</div>
<!-- 对话框组件 -->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialog.visible"
:message="errorDialog.message"
@close="errorDialog.visible = false"
/>
</div>
</template>
@@ -231,6 +243,8 @@ import {
deleteSignUpResult
} from '@/api/tournament'
import { getStoredUser } from '@/utils/jwt'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'
const router = useRouter()
const route = useRoute()
@@ -295,6 +309,10 @@ const playerEditForm = ref({ // 玩家编辑表单
tournament_id: ''
})
// Dialog state
const successDialog = ref({ visible: false, message: '' })
const errorDialog = ref({ visible: false, message: '' })
// 格式化日期
const formatDate = (date) => {
if (!date) return ''
@@ -379,12 +397,12 @@ const handleUpdate = async () => {
}
await updateTournament(tournamentId, updateData)
alert('更新成功!')
successDialog.value = { visible: true, message: '更新成功!' }
closeEditDialog()
fetchTournamentDetail() // 刷新数据
} catch (error) {
console.error('更新失败:', error)
alert(error.response?.data?.message || '更新失败,请重试')
errorDialog.value = { visible: true, message: error.response?.data?.message || '更新失败,请重试' }
} finally {
isUpdating.value = false
}
@@ -396,11 +414,11 @@ const handleDelete = async () => {
isDeleting.value = true
const tournamentId = route.query.id
await deleteTournament(tournamentId)
alert('删除成功!')
successDialog.value = { visible: true, message: '删除成功!' }
router.push('/competition')
} catch (error) {
console.error('删除失败:', error)
alert(error.response?.data?.message || '删除失败,请重试')
errorDialog.value = { visible: true, message: error.response?.data?.message || '删除失败,请重试' }
} finally {
isDeleting.value = false
showDeleteConfirm.value = false
@@ -449,12 +467,12 @@ const fetchTournamentDetail = async () => {
}
} else {
console.log('未找到赛事')
alert('未找到赛事信息')
errorDialog.value = { visible: true, message: '未找到赛事信息' }
router.push('/competition')
}
} catch (error) {
console.error('获取赛事详情失败:', error)
alert('获取赛事详情失败,请重试')
errorDialog.value = { visible: true, message: '获取赛事详情失败,请重试' }
} finally {
isLoading.value = false
}
@@ -485,10 +503,10 @@ const handleRemovePlayer = async (playerId) => {
try {
await deleteSignUpResult(playerId)
await fetchRegisteredPlayers() // 刷新列表
alert('移除成功!')
successDialog.value = { visible: true, message: '移除成功!' }
} catch (error) {
console.error('移除玩家失败:', error)
alert(error.response?.data?.message || '移除失败,请重试')
errorDialog.value = { visible: true, message: error.response?.data?.message || '移除失败,请重试' }
}
}
@@ -530,7 +548,7 @@ const handleStatusUpdate = async () => {
console.log('更新赛事状态,发送数据:', updateData)
await updateTournament(tournamentId, updateData)
alert('状态更新成功!')
successDialog.value = { visible: true, message: '状态更新成功!' }
closeStatusDialog()
fetchTournamentDetail() // 刷新数据
} catch (error) {
@@ -540,7 +558,7 @@ const handleStatusUpdate = async () => {
response: error.response?.data,
status: error.response?.status
})
alert(error.message || '状态更新失败,请重试')
errorDialog.value = { visible: true, message: error.message || '状态更新失败,请重试' }
} finally {
isUpdating.value = false
}
@@ -591,7 +609,7 @@ const handlePlayerUpdate = async () => {
await updateSignUpResult(playerEditForm.value.id, updateData)
await fetchRegisteredPlayers() // 刷新列表
closePlayerEditDialog()
alert('更新成功!')
successDialog.value = { visible: true, message: '更新成功!' }
} catch (error) {
console.error('更新玩家信息失败:', error)
console.error('错误详情:', {
@@ -599,7 +617,7 @@ const handlePlayerUpdate = async () => {
response: error.response?.data,
status: error.response?.status
})
alert(error.response?.data?.detail || error.message || '更新失败,请重试')
errorDialog.value = { visible: true, message: error.response?.data?.detail || error.message || '更新失败,请重试' }
} finally {
isUpdating.value = false
}
@@ -611,11 +629,11 @@ const saveChanges = async () => {
isSaving.value = true
// TODO: 调用保存对阵图的API
// await saveTournamentBracket(route.query.id, tournamentRounds.value)
alert('保存成功!')
successDialog.value = { visible: true, message: '保存成功!' }
isEditMode.value = false
} catch (error) {
console.error('保存失败:', error)
alert('保存失败,请重试')
errorDialog.value = { visible: true, message: '保存失败,请重试' }
} finally {
isSaving.value = false
}

View File

@@ -124,6 +124,18 @@
<button class="btn-submit" @click="handleSubmit">提交报名</button>
</div>
</div>
<!-- 对话框组件 -->
<SuccessDialog
:visible="successDialog.visible"
:message="successDialog.message"
@close="successDialog.visible = false"
/>
<ErrorDialog
:visible="errorDialog.visible"
:message="errorDialog.message"
@close="errorDialog.visible = false"
/>
</div>
</template>
@@ -131,6 +143,8 @@
import { ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { addSignUp } from '@/api/tournament'
import SuccessDialog from '@/components/SuccessDialog.vue'
import ErrorDialog from '@/components/ErrorDialog.vue'
defineOptions({
name: 'CompetitionSignUp'
@@ -158,6 +172,10 @@ const signupForm = ref({
qq: ''
})
// Dialog state
const successDialog = ref({ visible: false, message: '' })
const errorDialog = ref({ visible: false, message: '' })
const formatDate = (date) => {
if (!date) return ''
// 将年/月/日格式转换为年-月-日格式显示
@@ -190,12 +208,12 @@ const handleSubmit = async () => {
// 根据报名类型验证必填字段
if (signupForm.value.type === 'teamname') {
if (!signupForm.value.teamName || !signupForm.value.username) {
alert('请填写完整的队伍信息')
errorDialog.value = { visible: true, message: '请填写完整的队伍信息' }
return
}
} else {
if (!signupForm.value.username) {
alert('请填写完整的个人信息')
errorDialog.value = { visible: true, message: '请填写完整的个人信息' }
return
}
}
@@ -203,37 +221,37 @@ const handleSubmit = async () => {
// 验证 sign_name
const signName = signupForm.value.username.trim()
if (!signName) {
alert('参赛人员名称不能为空')
errorDialog.value = { visible: true, message: '参赛人员名称不能为空' }
return
}
if (signName.length < 2) {
alert('参赛人员名称至少需要2个字符')
errorDialog.value = { visible: true, message: '参赛人员名称至少需要2个字符' }
return
}
if (signName.length > 20) {
alert('参赛人员名称不能超过20个字符')
errorDialog.value = { visible: true, message: '参赛人员名称不能超过20个字符' }
return
}
// 只允许中文、英文、数字和下划线
if (!/^[\u4e00-\u9fa5a-zA-Z0-9_]+$/.test(signName)) {
alert('参赛人员名称只能包含中文、英文、数字和下划线')
errorDialog.value = { visible: true, message: '参赛人员名称只能包含中文、英文、数字和下划线' }
return
}
// 不允许纯数字
if (/^\d+$/.test(signName)) {
alert('参赛人员名称不能为纯数字')
errorDialog.value = { visible: true, message: '参赛人员名称不能为纯数字' }
return
}
// 不允许纯下划线
if (/^_+$/.test(signName)) {
alert('参赛人员名称不能为纯下划线')
errorDialog.value = { visible: true, message: '参赛人员名称不能为纯下划线' }
return
}
try {
// 确保所有必需字段都存在
if (!competitionInfo.value.id || !competitionInfo.value.name) {
alert('比赛信息不完整,请返回重试')
errorDialog.value = { visible: true, message: '比赛信息不完整,请返回重试' }
return
}
@@ -252,7 +270,7 @@ const handleSubmit = async () => {
console.log('报名结果:', result)
if (result.signup && result.result) {
alert('报名成功!')
successDialog.value = { visible: true, message: '报名成功!' }
router.push('/competition')
} else {
console.error('报名结果不完整:', result)
@@ -268,13 +286,13 @@ const handleSubmit = async () => {
// 显示具体的错误信息
if (error.message.includes('返回数据为空')) {
alert('服务器返回数据为空,请稍后重试')
errorDialog.value = { visible: true, message: '服务器返回数据为空,请稍后重试' }
} else if (error.message.includes('数据不完整')) {
alert('报名数据不完整,请重试')
errorDialog.value = { visible: true, message: '报名数据不完整,请重试' }
} else if (error.message.includes('网络连接')) {
alert('网络连接失败,请检查网络后重试')
errorDialog.value = { visible: true, message: '网络连接失败,请检查网络后重试' }
} else {
alert(error.message || '报名失败,请稍后重试')
errorDialog.value = { visible: true, message: error.message || '报名失败,请稍后重试' }
}
}
}