告别“分队黑幕”!我手搓了一个 LOL 内战分组神器

发布于 2026-01-08  33 次阅读


引言:为什么要做这个工具?

经常打《英雄联盟》的朋友都知道,凑齐 5 个人开黑是快乐,但凑齐 6 到 10 个人 往往就是烦恼的开始。

打灵活组排坐不下,打自定义内战(Custom Game)又面临一个千古难题:怎么分组?

  • 投骰子?太慢。
  • 队长选人?容易出现“强强抱团”或者“人情世故”。
  • 随机数?操作太麻烦。

为了维护“狗熊联盟”的内部团结,彻底杜绝“分队黑幕”,我利用周末时间手搓了一个在线分组神器。主打一个绝对公平视觉炫酷,并且内置了我们的固定车队名单

✨ 功能亮点

这个小工具虽然简单,但为了良好的体验,我加入了不少细节:

  1. 沉浸式海克斯 UI:深蓝配色 + 金色边框,还原 LOL 客户端的视觉体验。
  2. 内置花名册:我把咱们群里常玩的 ID(如“上单霸主”、“野区养猪”等)直接写死在代码里,一键点选即可参战,不用每次手动打字。
  3. 临时选手支持:有新朋友来?直接输入 ID 也能加入,并且会自动保存在浏览器缓存里,下次还在。
  4. Fisher-Yates 洗牌算法:采用专业的随机算法,保证每次分组结果都不可预测,谁是“大腿”谁是“混子”,全看天意。

🛠️ 技术实现 (Tech Stack)

对于对技术感兴趣的朋友,简单介绍下实现方案:

  • 前端:原生 HTML5 + CSS3 + JavaScript (ES6+),无任何第三方框架依赖,加载速度极快。
  • 数据存储:利用浏览器的 localStorage 实现临时数据的本地持久化。
  • 部署托管:项目直接部署在 Cloudflare Workers 上。
    • 为什么选 Cloudflare? 因为它全球节点覆盖,响应速度快,且对于这种静态单页应用(SPA)完全免费,无需购买服务器。

🎮 在线体验

废话不多说,直接上链接。大家下次内战可以直接用这个分组:

👉 点击进入:狗熊联盟 · 内战分组器

(建议大家收藏到浏览器书签,手机端访问体验同样丝滑)

👀 预览截图

👨‍💻 结语

这个工具虽然不大,但完美解决了我们开黑时的痛点。如果你也有类似的需求,或者想学习如何用 Cloudflare 托管静态网页,欢迎在评论区留言交流!

以后内战输了别怪分组,怪运气(或者怪我代码写得太好了)!😎

以下为代码部分:

export default {
  async fetch(request) {
    const html = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>狗熊联盟 · 内战分组</title>
    <style>
        :root {
            --hex-gold: #C8AA6E;
            --hex-gold-dim: #785A28;
            --hex-blue: #0AC8B9;
            --hex-dark: #010A13;
            --team-blue: #1E88E5;
            --team-red: #E53935;
            --bg-active: rgba(10, 200, 185, 0.3);
            --bg-inactive: rgba(30, 35, 40, 0.8);
        }

        body {
            background-color: var(--hex-dark);
            background-image: radial-gradient(circle at center, #091428 0%, #010A13 100%);
            color: #F0E6D2;
            font-family: 'Microsoft YaHei', sans-serif;
            margin: 0;
            padding: 20px;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
            user-select: none;
        }

        h1 {
            color: var(--hex-gold);
            text-transform: uppercase;
            letter-spacing: 2px;
            text-shadow: 0 0 10px var(--hex-gold-dim);
            border-bottom: 2px solid var(--hex-gold);
            padding-bottom: 10px;
            margin-bottom: 30px;
            font-style: italic;
        }

        /* 核心控制区 */
        .control-panel {
            background: rgba(1, 10, 19, 0.85);
            border: 1px solid var(--hex-gold-dim);
            padding: 25px;
            border-radius: 4px;
            box-shadow: 0 0 20px rgba(0,0,0,0.5);
            width: 90%;
            max-width: 700px;
            position: relative;
            margin-bottom: 30px;
        }

        .control-panel::before {
            content: '';
            position: absolute;
            top: -2px; left: -2px; right: -2px; bottom: -2px;
            border: 1px solid transparent;
            border-image: linear-gradient(45deg, var(--hex-gold), transparent) 1;
            pointer-events: none;
            z-index: -1;
        }

        /* 选手库区域 */
        .roster-section {
            margin-bottom: 20px;
            border-bottom: 1px solid rgba(200, 170, 110, 0.2);
            padding-bottom: 20px;
        }

        .section-title {
            font-size: 14px;
            color: var(--hex-blue);
            margin-bottom: 10px;
            text-transform: uppercase;
            letter-spacing: 1px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .roster-grid {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }

        .roster-tag {
            padding: 8px 15px;
            border: 1px solid #444;
            border-radius: 2px;
            cursor: pointer;
            transition: all 0.2s;
            background: var(--bg-inactive);
            color: #888;
            font-size: 14px;
            position: relative;
        }

        .roster-tag:hover {
            border-color: var(--hex-gold);
            color: var(--hex-gold);
        }

        /* 激活状态(参战) */
        .roster-tag.active {
            background: var(--bg-active);
            border-color: var(--hex-blue);
            color: white;
            box-shadow: 0 0 8px rgba(10, 200, 185, 0.4);
            font-weight: bold;
        }
        
        /* 预设选手标识 */
        .roster-tag.preset::before {
            content: "★";
            color: var(--hex-gold);
            font-size: 10px;
            margin-right: 4px;
            opacity: 0.5;
        }

        /* 删除模式 */
        .roster-tag.delete-mode {
            border-color: var(--team-red);
            color: var(--team-red);
            background: rgba(229, 57, 53, 0.1);
        }
        .roster-tag.delete-mode::after {
            content: "×";
            margin-left: 5px;
        }

        /* 输入区域 */
        .input-row {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }

        input[type="text"] {
            background: #091428;
            border: 1px solid #333;
            color: white;
            padding: 12px;
            font-size: 16px;
            flex: 1;
            outline: none;
            transition: border 0.3s;
        }
        input[type="text"]:focus { border-color: var(--hex-gold); }

        .btn {
            border: 1px solid var(--hex-gold);
            color: var(--hex-gold);
            background: linear-gradient(180deg, #1E2328 0%, #1E282D 100%);
            padding: 0 20px;
            cursor: pointer;
            font-weight: bold;
            transition: all 0.2s;
            text-transform: uppercase;
        }
        .btn:hover { background: var(--hex-gold); color: black; }
        
        .btn-add { border-color: var(--hex-blue); color: var(--hex-blue); }
        .btn-add:hover { background: var(--hex-blue); color: black; }

        .btn-shuffle {
            width: 100%;
            margin-top: 20px;
            padding: 15px;
            font-size: 18px;
            background: linear-gradient(90deg, #091428, #1E2328);
            border: 1px solid var(--hex-blue);
            color: var(--hex-blue);
            letter-spacing: 3px;
        }
        .btn-shuffle:hover {
            box-shadow: 0 0 20px rgba(10, 200, 185, 0.2);
            text-shadow: 0 0 5px var(--hex-blue);
        }

        .status-bar {
            margin-top: 10px;
            font-size: 13px;
            color: #666;
            text-align: right;
        }
        .count-highlight { color: var(--hex-gold); font-weight: bold; font-size: 16px; }

        /* 结果区域 */
        .arena {
            display: flex;
            flex-wrap: wrap;
            width: 100%;
            max-width: 1000px;
            gap: 20px;
            justify-content: center;
            opacity: 0;
            transform: translateY(20px);
            transition: all 0.5s ease;
        }
        .arena.show { opacity: 1; transform: translateY(0); }

        .team-card {
            flex: 1;
            min-width: 300px;
            background: rgba(0, 0, 0, 0.6);
            border: 2px solid;
            position: relative;
        }
        .team-blue { border-color: var(--team-blue); }
        .team-red { border-color: var(--team-red); }

        .team-header {
            padding: 15px;
            text-align: center;
            font-size: 24px;
            font-weight: bold;
            color: white;
            text-transform: uppercase;
        }
        .team-blue .team-header { background: linear-gradient(90deg, rgba(30,136,229,0.8), transparent); }
        .team-red .team-header { background: linear-gradient(90deg, rgba(229,57,53,0.8), transparent); }

        .team-list { list-style: none; padding: 15px; margin: 0; }
        .team-list li {
            padding: 12px;
            margin-bottom: 5px;
            background: rgba(255, 255, 255, 0.05);
            border-left: 3px solid transparent;
            font-size: 18px;
        }
        .team-blue .team-list li { border-left-color: var(--team-blue); }
        .team-red .team-list li { border-left-color: var(--team-red); }

    </style>
</head>
<body>

    <h1>狗熊联盟 · 内战分组</h1>

    <div class="control-panel">
        
        <div class="roster-section">
            <div class="section-title">
                <span>🏆 选手库 (带★为预设固定选手)</span>
                <span style="font-size:12px; cursor:pointer; text-decoration:underline; color:#666;" onclick="toggleEditMode()" id="editBtn">管理临时选手</span>
            </div>
            <div class="roster-grid" id="rosterGrid">
                </div>
        </div>

        <div class="input-row">
            <input type="text" id="newPlayerInput" placeholder="输入临时选手ID" autocomplete="off">
            <button class="btn btn-add" onclick="addNewPlayer()">添加并参战</button>
        </div>

        <div class="status-bar">
            当前就位: <span id="activeCount" class="count-highlight">0</span> 人
        </div>

        <button class="btn btn-shuffle" onclick="generateTeams()">开始随机分组 (SHUFFLE)</button>
        <div style="text-align: center; margin-top: 10px;">
            <span style="font-size: 12px; color: #555; cursor: pointer;" onclick="clearActive()">[重置本局]</span>
        </div>
    </div>

    <div class="arena" id="arena">
        <div class="team-card team-blue">
            <div class="team-header">Blue Side</div>
            <ul class="team-list" id="blueTeamList"></ul>
        </div>
        <div class="team-card team-red">
            <div class="team-header">Red Side</div>
            <ul class="team-list" id="redTeamList"></ul>
        </div>
    </div>

    <script>
        // ============================================================
        // ⚙️ 【配置区域】请在下方 PRESET_PLAYERS 中修改你的固定选手
        // 使用双引号包裹名字,中间用逗号隔开
        // ============================================================
        
        const PRESET_PLAYERS = [
            "Dawn丶我不高兴", "Dawn丶命运", "极霜雪凌", "醉忆佳人", "IG丶AD接Q辣舞",
            "邮电部诗人", "请叫我李星美女", "君莫笑", "灬曾良", "Dawn丶教主", "Dawn丶三七"
        ];

        // ============================================================
        // ⚙️ 配置结束,下方代码请勿改动
        // ============================================================

        let customRoster = []; // 用户本地添加的临时选手
        let activePlayers = []; // 当前准备打游戏的选手
        let isEditMode = false;

        // 初始化
        window.onload = function() {
            // 读取本地添加的临时选手
            const savedCustom = localStorage.getItem('lol_custom_roster');
            if (savedCustom) {
                customRoster = JSON.parse(savedCustom);
            }
            renderRoster();
        };

        // 获取完整名单(预设 + 临时)
        function getFullRoster() {
            // 去重合并
            return Array.from(new Set([...PRESET_PLAYERS, ...customRoster]));
        }

        // 渲染选手库
        function renderRoster() {
            const grid = document.getElementById("rosterGrid");
            grid.innerHTML = "";
            const fullList = getFullRoster();

            fullList.forEach(player => {
                const tag = document.createElement("div");
                tag.className = "roster-tag";
                
                // 判断是否为预设选手
                const isPreset = PRESET_PLAYERS.includes(player);
                if(isPreset) tag.classList.add("preset");

                if (activePlayers.includes(player)) {
                    tag.classList.add("active");
                }
                
                if (isEditMode && !isPreset) {
                    // 编辑模式下,只有非预设的可以删除
                    tag.classList.add("delete-mode");
                    tag.onclick = () => deletePlayer(player);
                    tag.title = "点击删除";
                } else if (isEditMode && isPreset) {
                    tag.style.opacity = "0.5";
                    tag.style.cursor = "not-allowed";
                } else {
                    tag.onclick = () => togglePlayer(player);
                }
                
                tag.innerText = player;
                grid.appendChild(tag);
            });

            updateStatus();
        }

        // 切换选手参战状态
        function togglePlayer(name) {
            if (isEditMode) return;
            if (activePlayers.includes(name)) {
                activePlayers = activePlayers.filter(p => p !== name);
            } else {
                activePlayers.push(name);
            }
            renderRoster();
        }

        // 添加新选手(临时)
        function addNewPlayer() {
            const input = document.getElementById("newPlayerInput");
            const name = input.value.trim();
            
            if (!name) return;

            // 如果预设里有了,只选中,不加到临时库
            if (PRESET_PLAYERS.includes(name)) {
                if (!activePlayers.includes(name)) activePlayers.push(name);
                input.value = "";
                renderRoster();
                return;
            }

            // 如果临时库里没有,加进去
            if (!customRoster.includes(name)) {
                customRoster.push(name);
                saveCustomRoster();
            }

            if (!activePlayers.includes(name)) activePlayers.push(name);

            input.value = "";
            renderRoster();
        }

        function saveCustomRoster() {
            localStorage.setItem('lol_custom_roster', JSON.stringify(customRoster));
        }

        // 删除临时选手
        function deletePlayer(name) {
            if(confirm(\`确定删除临时选手 [\${name}] 吗?\`)) {
                customRoster = customRoster.filter(p => p !== name);
                activePlayers = activePlayers.filter(p => p !== name);
                saveCustomRoster();
                renderRoster();
            }
        }

        function toggleEditMode() {
            isEditMode = !isEditMode;
            const btn = document.getElementById("editBtn");
            btn.innerText = isEditMode ? "完成管理" : "管理临时选手";
            btn.style.color = isEditMode ? "#E53935" : "#666";
            renderRoster();
        }

        function updateStatus() {
            document.getElementById("activeCount").innerText = activePlayers.length;
        }

        function clearActive() {
            activePlayers = [];
            renderRoster();
            document.getElementById("arena").classList.remove("show");
        }

        function generateTeams() {
            if (activePlayers.length < 2) {
                alert("至少需要2名狗熊才能开始内战!");
                return;
            }

            const shuffled = [...activePlayers];
            for (let i = shuffled.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
            }

            const mid = Math.ceil(shuffled.length / 2);
            const teamBlue = shuffled.slice(0, mid);
            const teamRed = shuffled.slice(mid);

            const blueList = document.getElementById("blueTeamList");
            const redList = document.getElementById("redTeamList");
            const arena = document.getElementById("arena");

            blueList.innerHTML = "";
            redList.innerHTML = "";

            teamBlue.forEach(p => blueList.innerHTML += \`<li>\${p}</li>\`);
            teamRed.forEach(p => redList.innerHTML += \`<li>\${p}</li>\`);

            arena.classList.remove("show");
            void arena.offsetWidth; 
            arena.classList.add("show");
        }
        
        document.getElementById("newPlayerInput").addEventListener("keyup", function(event) {
            if (event.key === "Enter") addNewPlayer();
        });
    </script>
</body>
</html>`;

    return new Response(html, {
      headers: {
        'content-type': 'text/html;charset=UTF-8',
      },
    });
  },
};
届ける言葉を今は育ててる
最后更新于 2026-01-08