news 2026/6/9 21:37:31

淘汰赛对阵图生成demo

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
淘汰赛对阵图生成demo

<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>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/5 10:40:10

HTTP 协议组成

一、概念HTTP 是一种应用层、基于请求–响应模型的无状态协议&#xff0c;由请求报文、响应报文以及传输规则组成。二、组成 HTTP 请求报文&#xff08;Request&#xff09; 请求行 请求头 空行 请求体&#xff08;1&#xff09;请求行 GET /index.html HTTP/1.1包含三部分&…

作者头像 李华
网站建设 2026/6/5 10:40:26

Part 12|模块要不要拆?新手也能用的一套判断方法

在做系统设计时&#xff0c;我们都会遇到一个问题&#xff1a;这个功能&#xff0c;要不要单独拆成一个模块&#xff1f;尤其是刚开始做项目的时候&#xff0c;很容易有一个想法&#xff1a;拆出来&#xff0c;看起来更专业一点。但实际情况往往是&#xff1a; 模块一多&#x…

作者头像 李华
网站建设 2026/6/9 19:43:38

英伟达调研600+从业者:AI医疗下一站从“影像领先”走向“虚拟助手+精准医疗”

【摘要】AI医疗正从影像诊断的单点突破&#xff0c;系统性地迈向以虚拟健康助手为入口、精准医疗为目标的生态整合新阶段。引言人工智能在医疗健康领域的渗透&#xff0c;早已不是新闻。从实验室的前沿探索到临床应用的逐步落地&#xff0c;这项技术正以前所未有的深度和广度重…

作者头像 李华
网站建设 2026/6/8 23:52:40

制砂机远程监控运维管理系统方案

制砂机作为矿山、建筑及道路工程中破碎与制砂作业的核心设备&#xff0c;其运行效率与稳定性直接关系到工程进度与产品质量。制砂机在工作过程中&#xff0c;涉及电机转速、进料量、出料粒度、振动强度及轴承温度等多个关键参数的实时监控与调节。 但传统运维方式依赖现场巡检与…

作者头像 李华
网站建设 2026/6/9 12:07:25

数字化航空的安全基石:测试人员如何应对DO-178C与网络安全双重要求

航空软件安全认证的重要性 航空软件安全认证是确保航空系统&#xff08;如飞行控制、导航和通信系统&#xff09;在极端环境下可靠运行的关键流程。对于软件测试从业者而言&#xff0c;理解认证标准&#xff08;如DO-178C&#xff09;不仅是合规要求&#xff0c;更是保障乘客安…

作者头像 李华