<template> <div class="page"> <section class="panel"> <h1>淘汰赛对阵图生成器</h1> <div class="controls"> <label class="field"> <span>队伍数量(2-64)</span> <input v-model.number="teamCount" type="number" min="0" max="64" /> </label> <div class="names"> <div class="names-header"> <span>队伍名称</span> <div class="actions"> <button type="button" @click="autoFillNames">自动填充</button> <button type="button" class="primary" @click="generateBracket">生成对阵</button> </div> </div> <div class="name-grid"> <div v-for="(name, idx) in teamInputs" :key="idx" class="name-item"> <label> <span>#{{ idx + 1 }}</span> <input v-model="teamInputs[idx]" type="text" :placeholder="`队伍${idx + 1}`" /> </label> </div> </div> </div> </div> </section> <section class="panel"> <div class="panel-head"> <h2>对阵图</h2> </div> <div v-if="rounds.length" class="bracket" :style="{ gridTemplateRows: `repeat(${gridRows}, 22px)` }"> <div v-for="(round, rIdx) in rounds" :key="rIdx" class="round"> <div v-for="(team, tIdx) in round" :key="`${rIdx}-${tIdx}`" class="match" :class="{ final: rIdx === rounds.length - 1, hasConnector: rIdx < rounds.length - 1, top: tIdx % 2 === 0, bottom: tIdx % 2 === 1 }" :style="teamGridStyle(rIdx, tIdx)"> <div class="team"> <span class="seed" v-if="rIdx === 0">{{ tIdx + 1 }}</span> <span class="name">{{ team }}</span> </div> </div> </div> </div> </section> </div> </template> <script setup> import { computed, ref, watch } from 'vue' const teamCount = ref(8) const teamInputs = ref(Array.from({ length: teamCount.value }, (_, i) => `队伍${i + 1}`)) const rounds = ref([]) const bracketSize = computed(() => (rounds.value.length ? rounds.value[0].length : 0)) const gridRows = computed(() => bracketSize.value * 2) watch(teamCount, (val) => { const safe = Math.min(64, Math.max(2, Number(val) || 2)) if (safe !== val) teamCount.value = safe if (teamInputs.value.length < safe) { const start = teamInputs.value.length for (let i = start; i < safe; i += 1) { teamInputs.value.push(`队伍${i + 1}`) } } else if (teamInputs.value.length > safe) { teamInputs.value.splice(safe) } }) const isPowerOfTwo = (n) => n > 0 && (n & (n - 1)) === 0 const makeRounds = (teams) => { const size = teams.length const result = [] let current = [] // 第一轮:每个队伍单独一个元素 for (let i = 0; i < size; i++) { current.push(teams[i]) } result.push(current) // 后续轮次:每轮队伍数量减半 while (current.length > 1) { const next = [] for (let i = 0; i < current.length / 2; i++) { next.push('上一场胜者') } result.push(next) current = next } return result } const generateBracket = () => { const names = teamInputs.value.map((t, i) => (t?.trim() ? t.trim() : `队伍${i + 1}`)) const valid = names.filter(Boolean) if (valid.length < 2) { alert('至少需要 2 支队伍') return } if (!isPowerOfTwo(valid.length)) { alert('队伍数量需为 2 的整数次幂,例如 2/4/8/16') return } rounds.value = makeRounds(valid) } const autoFillNames = () => { teamInputs.value = Array.from({ length: teamCount.value }, (_, i) => `队伍${i + 1}`) } const teamGridStyle = (roundIdx, teamIdx) => { // 所有div统一高度为50px,占1行 // 第一轮:队伍占据奇数行(1, 3, 5, 7...) // 后续轮次:胜者占据偶数行,位置在前一轮对应两队的中间 if (roundIdx === 0) { // 第一轮:每个队伍占1行,使用奇数行 const start = teamIdx * 2 + 1 return { gridRow: `${start} / span 1` } } else { // 后续轮次:递归计算前一轮对应两队的位置 // 前一轮的队伍索引是 teamIdx*2 和 teamIdx*2+1 const getRow = (rIdx, tIdx) => { if (rIdx === 0) { return tIdx * 2 + 1 } else { const prevTeam1Row = getRow(rIdx - 1, tIdx * 2) const prevTeam2Row = getRow(rIdx - 1, tIdx * 2 + 1) return Math.round((prevTeam1Row + prevTeam2Row) / 2) } } const centerRow = getRow(roundIdx, teamIdx) return { gridRow: `${centerRow} / span 1` } } } </script> <style scoped> :global(body) { margin: 0; background: #f7f8fb; font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif; color: #1f2933; } .page { max-width: 1200px; margin: 32px auto 64px; padding: 0 20px; display: flex; flex-direction: column; gap: 20px; } .panel { background: #fff; border-radius: 12px; box-shadow: 0 10px 30px rgba(31, 41, 51, 0.08); padding: 20px; } .panel h1, .panel h2 { margin: 0 0 12px; font-weight: 700; } .panel-head { display: flex; align-items: center; gap: 10px; } .hint { color: #5f6b7a; font-size: 14px; } .controls { display: flex; flex-direction: column; gap: 16px; } .field { display: flex; align-items: center; gap: 12px; font-weight: 600; } input[type='number'], input[type='text'] { padding: 8px 10px; border: 1px solid #d5dae1; border-radius: 8px; outline: none; transition: 0.15s ease; font-size: 14px; width: 110px; } input[type='text'] { width: 100%; } input:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); } .names { border: 1px solid #e3e7ef; border-radius: 10px; padding: 12px; } .names-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-weight: 600; } .actions { display: flex; gap: 10px; } button { border: 1px solid #cfd6e4; background: #fff; padding: 8px 12px; border-radius: 8px; cursor: pointer; font-weight: 600; transition: 0.15s ease; } button:hover { background: #f0f4ff; border-color: #94b3ff; } button.primary { background: #3b82f6; color: #fff; border-color: #3b82f6; } button.primary:hover { background: #2763c6; } .name-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; } .name-item label { display: flex; align-items: center; gap: 8px; font-weight: 600; color: #475364; } .name-item span { min-width: 36px; text-align: right; color: #6b7687; } .bracket { display: grid; grid-auto-flow: column; grid-auto-columns: 90px; column-gap: 10px; position: relative; padding: 4px 0 2px; overflow-x: auto; } .round { display: grid; grid-template-rows: subgrid; grid-row: 1 / -1; align-content: start; } .round-title { font-weight: 700; color: #334155; margin-bottom: 10px; } .match { position: relative; background: linear-gradient(180deg, #f9fbff 0%, #f0f4ff 100%); border: 1px solid #d8e2f3; border-radius: 5px; padding: 4px 6px; display: flex; align-items: center; height: 22px; box-shadow: 0 3px 6px rgba(59, 130, 246, 0.08); font-size: 11px; } .match.hasConnector::after { content: ''; position: absolute; right: -10px; top: 50%; width: 10px; height: 1.5px; background: #c4d4f5; } .match.hasConnector.top::before, .match.hasConnector.bottom::before { content: ''; position: absolute; right: -10px; width: 1.5px; background: #c4d4f5; } .match.hasConnector.top::before { top: 50%; height: calc(100% + 11px); /* 半场距 */ } .match.hasConnector.bottom::before { bottom: 50%; height: calc(100% + 11px); } .match.final::after { content: none; } .match.final::before, .match.final::after { display: none; } .team { display: flex; align-items: center; gap: 3px; font-weight: 600; color: #1f2937; width: 100%; font-size: 11px; } .seed { display: inline-flex; align-items: center; justify-content: center; min-width: 14px; height: 14px; background: #e6ecfb; color: #3b5ab8; border-radius: 3px; font-size: 9px; } .name { flex: 1; text-align: left; } .champion { position: absolute; top: 50%; right: -60px; transform: translateY(-50%); font-weight: 700; color: #f97316; } @media (max-width: 768px) { .page { margin: 8px auto 16px; padding: 0 8px; } .panel { padding: 10px; } .bracket { grid-auto-columns: 80px; column-gap: 8px; } .match { padding: 4px 5px; height: 20px; font-size: 10px; } .team { font-size: 10px; gap: 2px; } .seed { min-width: 12px; height: 12px; font-size: 8px; } .match.hasConnector::after { right: -10px; width: 10px; height: 1px; } } </style>淘汰赛对阵图生成demo
张小明
前端开发工程师
金仓数据库KingbaseES:从兼容到超越,打造企业级数据库新标杆
兼容是对企业历史投资的尊重是确保业务平稳过渡的基石然而这仅仅是故事的起点在数字化转型的深水区,企业对数据库的需求早已超越“语法兼容”的基础诉求。无论是核心业务系统的稳定运行,还是敏感数据的安全防护,亦或是复杂场景下的性能优化&a…
关于AI工具实战测评的技术
AI工具实战测评框架设计测评AI工具需要从多个维度展开,包括功能实用性、性能表现、易用性、适用场景等。以下为技术测评的核心框架和具体方法。功能覆盖与核心能力测试AI工具的核心功能是否与宣传一致。例如自然语言处理工具需验证文本生成、翻译、摘要等能力&#…
萤石开放平台 国标设备接入 | 三方品牌设备接入文档/宇视NVR对接文档
接入流程概览: 完整接入流程: 一. 绑定设备,获取接入参数(共3步) 1. 进入“国标控制台-设备管理”页,支持 绑定设备/批量绑定设备 申请国标设备所需的设备信息。 (以下操作以“绑定设备”为…
用了几年 Spring Boot,你真的知道请求是怎么进来的吗?—— JDK 原生实现 HTTP 服务
目录一、你有没有真正理解过:一个 HTTP 请求是怎么“飞”到你的代码里的?二、Spring Boot 为什么能监听和处理请求三、使用 JDK 自带的 HttpServer实现一个可运行的 HTTP 服务四、结语:但你有没有想过:HttpServer背后又是谁在监听…
Web Worker 处理图像:将 Canvas 像素处理移出主线程的实现
Web Worker 处理图像:将 Canvas 像素处理移出主线程的实现 大家好,今天我们来深入探讨一个在现代前端开发中越来越重要的技术主题——如何利用 Web Worker 将 Canvas 图像像素处理任务从主线程中剥离出来。这不仅能够显著提升用户体验,还能避…
垃圾回收压力(GC Pressure):频繁创建临时对象导致的 UI 掉帧分析
垃圾回收压力(GC Pressure):频繁创建临时对象导致的 UI 掉帧分析 各位开发者朋友,大家好!今天我们来深入探讨一个在移动端开发中非常常见、但又容易被忽视的问题——垃圾回收压力(GC Pressure)…