引言:为什么要做这个工具?
经常打《英雄联盟》的朋友都知道,凑齐 5 个人开黑是快乐,但凑齐 6 到 10 个人 往往就是烦恼的开始。
打灵活组排坐不下,打自定义内战(Custom Game)又面临一个千古难题:怎么分组?
- 投骰子?太慢。
- 队长选人?容易出现“强强抱团”或者“人情世故”。
- 随机数?操作太麻烦。
为了维护“狗熊联盟”的内部团结,彻底杜绝“分队黑幕”,我利用周末时间手搓了一个在线分组神器。主打一个绝对公平、视觉炫酷,并且内置了我们的固定车队名单。
✨ 功能亮点
这个小工具虽然简单,但为了良好的体验,我加入了不少细节:
- 沉浸式海克斯 UI:深蓝配色 + 金色边框,还原 LOL 客户端的视觉体验。
- 内置花名册:我把咱们群里常玩的 ID(如“上单霸主”、“野区养猪”等)直接写死在代码里,一键点选即可参战,不用每次手动打字。
- 临时选手支持:有新朋友来?直接输入 ID 也能加入,并且会自动保存在浏览器缓存里,下次还在。
- 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',
},
});
},
};








Comments NOTHING