<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>天空公會 | 全方位抽籤系統</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;500;700&family=Noto+Sans+TC:wght@300;500;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
<style>
:root {
--bg-dark: #090c15;
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--glass-surface: rgba(255, 255, 255, 0.08);
--glass-border: rgba(255, 255, 255, 0.15);
--glass-highlight: rgba(255, 255, 255, 0.3);
--text-primary: #ffffff;
--text-secondary: #a0aec0;
--accent-glow: #4fd1c5;
--tab-active-bg: rgba(79, 209, 197, 0.2);
--card-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5);
--transition-speed: 0.3s;
}
* { box-sizing: border-box; margin: 0; padding: 0; outline: none; }
body {
font-family: 'Inter', 'Noto Sans TC', sans-serif;
background-color: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px 20px;
overflow-x: hidden;
}
.ambient-light {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; overflow: hidden;
background: radial-gradient(circle at 50% -20%, #1a202c, #090c15);
}
.orb { position: absolute; border-radius: 50%; filter: blur(100px); opacity: 0.6; animation: breathe 10s infinite ease-in-out; }
.orb-1 { width: 500px; height: 500px; background: #4c1d95; top: -10%; left: -10%; animation-duration: 12s; }
.orb-2 { width: 400px; height: 400px; background: #2563eb; bottom: -5%; right: -5%; animation-duration: 15s; animation-delay: -2s; }
@keyframes breathe { 0%, 100% { transform: scale(1) translate(0, 0); } 50% { transform: scale(1.1) translate(20px, -20px); } }
.main-card {
width: 100%; max-width: 1100px;
background: var(--glass-surface);
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border); border-top: 1px solid var(--glass-highlight);
border-radius: 24px; padding: 40px;
box-shadow: var(--card-shadow); margin-top: 20px;
position: relative; z-index: 10;
}
header { text-align: center; margin-bottom: 30px; }
h1 {
font-size: 2.2rem; font-weight: 700; margin-bottom: 10px;
background: linear-gradient(to right, #fff, #b3e5fc);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.badge {
display: inline-block; padding: 4px 12px;
background: rgba(79, 209, 197, 0.1); border: 1px solid rgba(79, 209, 197, 0.3);
border-radius: 50px; font-size: 0.8rem; color: var(--accent-glow); margin-top: 5px;
}
.tabs {
display: flex; justify-content: center; gap: 10px; margin-bottom: 30px;
border-bottom: 1px solid var(--glass-border); padding-bottom: 20px;
}
.tab-btn {
background: transparent; border: none; color: var(--text-secondary);
padding: 10px 20px; border-radius: 12px; cursor: pointer;
font-size: 1rem; font-weight: 500; transition: all 0.3s ease;
}
.tab-btn:hover { color: #fff; background: rgba(255,255,255,0.05); }
.tab-btn.active {
background: var(--tab-active-bg); color: var(--accent-glow);
font-weight: 700; box-shadow: 0 0 15px rgba(79, 209, 197, 0.1);
}
.tab-content { display: none; animation: fadeIn 0.4s ease; }
.tab-content.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.panel-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-bottom: 30px; }
.full-width { grid-column: 1 / -1; }
.input-group { display: flex; flex-direction: column; gap: 10px; }
label { font-size: 0.95rem; font-weight: 500; color: var(--text-primary); display: flex; align-items: center; gap: 8px; }
label svg { color: var(--accent-glow); }
textarea, input[type="number"] {
width: 100%; background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border); border-radius: 12px; padding: 15px;
color: #fff; font-size: 0.95rem; font-family: monospace; line-height: 1.6;
resize: vertical; transition: all 0.3s;
}
textarea:focus, input:focus {
background: rgba(0, 0, 0, 0.4); border-color: var(--accent-glow);
}
textarea.tall { height: 200px; }
textarea.short { height: 120px; }
.note { font-size: 0.8rem; color: var(--text-secondary); margin-top: -5px; line-height: 1.4; }
.controls { display: flex; justify-content: center; gap: 15px; margin-top: 20px; border-top: 1px solid var(--glass-border); padding-top: 25px; }
.btn {
padding: 12px 28px; border-radius: 10px; font-size: 1rem; font-weight: 600;
cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px; border: none;
}
.btn-ghost { background: transparent; color: var(--text-secondary); border: 1px solid var(--glass-border); }
.btn-ghost:hover { background: rgba(255, 255, 255, 0.05); color: #fff; }
.btn-primary { background: var(--primary-gradient); color: #fff; box-shadow: 0 4px 12px rgba(118, 75, 162, 0.4); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(118, 75, 162, 0.6); }
.btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
.results-section { margin-top: 30px; min-height: 100px; }
.status-text { text-align: center; font-size: 1.1rem; color: var(--accent-glow); margin-bottom: 20px; height: 25px; font-weight: 500; }
.results-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }
.result-card {
background: linear-gradient(145deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%);
border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 15px;
display: flex; align-items: center; justify-content: space-between;
opacity: 0; transform: translateY(15px); animation: slideUp 0.5s forwards;
}
.result-main { font-weight: 700; color: var(--accent-glow); font-size: 1.1rem; }
.result-sub { font-size: 0.9rem; color: rgba(255,255,255,0.6); }
.arrow { color: rgba(255,255,255,0.2); }
@keyframes slideUp { to { opacity: 1; transform: translateY(0); } }
@media (max-width: 768px) {
.panel-grid { grid-template-columns: 1fr; }
.tabs { flex-wrap: wrap; }
}
</style>
</head>
<body>
<div class="ambient-light">
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
</div>
<div class="main-card">
<header>
<h1>天空公會抽籤系統</h1>
<div class="badge">─╪幻想天空╭Midgard╗</div>
</header>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('tab-blind')">1. 雙盲配對</button>
<button class="tab-btn" onclick="switchTab('tab-preference')">2. 志願序分發</button>
<button class="tab-btn" onclick="switchTab('tab-single')">3. 單一抽選</button>
</div>
<div id="tab-blind" class="tab-content active">
<div class="panel-grid">
<div class="input-group">
<label>分配項目 / 獎項 (一行一個)</label>
<textarea id="blindPrizes" class="tall" placeholder="範例: 會長職位 副會長 掃廁所"></textarea>
</div>
<div class="input-group">
<label>參與人員 (一行一個)</label>
<textarea id="blindMembers" class="tall" placeholder="範例: Player_A Player_B Player_C"></textarea>
</div>
</div>
<div class="controls">
<button class="btn btn-ghost" onclick="clearInput('blindPrizes', 'blindMembers')">清空</button>
<button class="btn btn-primary" onclick="runBlindDraw()">開始雙盲配對</button>
</div>
</div>
<div id="tab-preference" class="tab-content">
<div class="panel-grid">
<div class="input-group full-width">
<label>
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
獎項庫存 (一行一個)
</label>
<div class="note">系統將依據此庫存進行扣除。若同一獎項有多個名額,請重複輸入或在後面加編號。</div>
<textarea id="prefPrizes" class="short" placeholder="範例: S級武器 A級防具 金幣100萬"></textarea>
</div>
<div class="input-group full-width">
<label>
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg>
人員與志願序
</label>
<div class="note">格式:會員名稱 : 第一志願, 第二志願, 第三志願... (使用冒號與逗號分隔)</div>
<textarea id="prefMembers" class="tall" placeholder="範例: 會員A : S級武器, 金幣100萬 會員B : S級武器, A級防具 會員C : 金幣100萬"></textarea>
</div>
</div>
<div class="controls">
<button class="btn btn-ghost" onclick="clearInput('prefPrizes', 'prefMembers')">清空</button>
<button class="btn btn-primary" onclick="runPreferenceDraw()">開始志願序分發</button>
</div>
</div>
<div id="tab-single" class="tab-content">
<div class="panel-grid">
<div class="input-group">
<label>抽籤名單 (一行一個)</label>
<textarea id="singleMembers" class="tall" placeholder="範例: 會員01 會員02 會員03 會員04..."></textarea>
</div>
<div class="input-group">
<label>設定中獎人數</label>
<input type="number" id="singleCount" value="1" min="1" style="height: 50px;">
<div class="note" style="margin-top: 10px">系統將從左側名單中隨機抽出指定人數。</div>
</div>
</div>
<div class="controls">
<button class="btn btn-ghost" onclick="clearInput('singleMembers', 'singleCount')">清空</button>
<button class="btn btn-primary" onclick="runSingleDraw()">開始抽選</button>
</div>
</div>
<div class="results-section">
<div class="status-text" id="statusMsg"></div>
<div class="results-grid" id="resultGrid"></div>
</div>
</div>
<script>
function getLines(id) {
return document.getElementById(id).value.split('\n').map(s => s.trim()).filter(s => s);
}
function shuffle(array) {
let cur = array.length, rand;
while (cur != 0) {
rand = Math.floor(Math.random() * cur);
cur--;
[array[cur], array[rand]] = [array[rand], array[cur]];
}
return array;
}
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
document.getElementById('resultGrid').innerHTML = '';
document.getElementById('statusMsg').textContent = '';
}
function clearInput(...ids) {
if(confirm("確定要清空輸入資料嗎?")) {
ids.forEach(id => document.getElementById(id).value = '');
document.getElementById('resultGrid').innerHTML = '';
document.getElementById('statusMsg').textContent = '';
}
}
async function playAnimation(msg) {
const status = document.getElementById('statusMsg');
const grid = document.getElementById('resultGrid');
grid.innerHTML = '';
status.textContent = '🔒 數據加密中...';
await new Promise(r => setTimeout(r, 400));
status.textContent = '🎲 正在進行隨機運算...';
await new Promise(r => setTimeout(r, 600));
status.textContent = msg;
confetti({ particleCount: 150, spread: 70, origin: { y: 0.6 }, colors: ['#4fd1c5', '#9f7aea'] });
}
function renderCard(title, value, subtitle = '', delay = 0) {
const grid = document.getElementById('resultGrid');
const card = document.createElement('div');
card.className = 'result-card';
card.style.animationDelay = `${delay}s`;
card.innerHTML = `
<div>
<div class="result-sub">${title}</div>
<div class="result-main">${value}</div>
</div>
${subtitle ? `<div class="arrow">➜</div><div><div class="result-sub">獲得</div><div class="result-main">${subtitle}</div></div>` : ''}
`;
grid.appendChild(card);
}
async function runBlindDraw() {
const prizes = getLines('blindPrizes');
const members = getLines('blindMembers');
if (prizes.length === 0 || members.length === 0) return alert('請輸入完整的獎項與人員名單!');
await playAnimation('✨ 雙盲配對完成');
const sPrizes = shuffle([...prizes]);
const sMembers = shuffle([...members]);
const count = Math.max(sPrizes.length, sMembers.length);
for(let i=0; i<count; i++) {
if(i >= sPrizes.length) break;
const p = sPrizes[i];
const m = sMembers[i] || '從缺';
renderCard('獎項', p, m, i * 0.1);
}
}
async function runPreferenceDraw() {
const prizePoolRaw = getLines('prefPrizes');
const memberLines = getLines('prefMembers');
if (prizePoolRaw.length === 0 || memberLines.length === 0) return alert('請輸入獎項庫存與人員志願!');
let participants = [];
try {
participants = memberLines.map(line => {
const parts = line.split(/[::]/);
if(parts.length < 2) throw new Error("格式錯誤");
const name = parts[0].trim();
const prefs = parts[1].split(/[,,]/).map(p => p.trim()).filter(p => p);
return { name, prefs };
});
} catch(e) {
return alert('人員志願格式錯誤,請確保使用 "名稱 : 志願1, 志願2" 的格式');
}
await playAnimation('🎯 志願序分發完成');
let inventory = {};
prizePoolRaw.forEach(p => inventory[p] = (inventory[p] || 0) + 1);
const shuffledParticipants = shuffle([...participants]);
const grid = document.getElementById('resultGrid');
shuffledParticipants.forEach((p, index) => {
let assigned = "無 (志願落空)";
for (let wish of p.prefs) {
if (inventory[wish] && inventory[wish] > 0) {
assigned = wish;
inventory[wish]--;
break;
}
}
renderCard('會員', p.name, assigned, index * 0.1);
});
}
async function runSingleDraw() {
const members = getLines('singleMembers');
const count = parseInt(document.getElementById('singleCount').value);
if (members.length === 0) return alert('請輸入抽籤名單!');
if (count < 1) return alert('抽出人數至少為 1 人');
await playAnimation(`🎉 已抽出 ${Math.min(count, members.length)} 位幸運兒`);
const shuffled = shuffle([...members]);
const winners = shuffled.slice(0, count);
winners.forEach((w, i) => {
renderCard('中獎者', w, '', i * 0.1);
});
}
</script>
</body>
</html>