This commit is contained in:
zhangyuheng
2026-02-10 13:01:46 +08:00
commit ba7137a64e
11 changed files with 2678 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/stats

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

1
fund_progress.txt Normal file
View File

@@ -0,0 +1 @@
服务器升级M4 Mac Mini 32G,2620,5000

214
index.html Normal file
View File

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>白鹿原 Minecraft 服务器</title>
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="style.css">
<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=Noto+Sans+SC:wght@400;500;700&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<!-- Navbar -->
<nav class="navbar">
<div class="nav-content">
<div class="logo">
<img src="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png" alt="白鹿原 Logo">
</div>
<div class="nav-links">
<a href="https://outline.lunadeer.cn/s/447e5db6-8af4-468e-b7c5-cdb7b48aa439">文档</a>
<a href="https://mcmap.lunadeer.cn/">地图</a>
<a href="https://mcphoto.lunadeer.cn/">相册</a>
<a href="https://qm.qq.com/q/9izlHDoef6">群聊</a>
<a href="/stats.html">数据</a>
<a href="https://outline.lunadeer.cn/s/447e5db6-8af4-468e-b7c5-cdb7b48aa439/doc/5yqg5ywl5pyn5yqh5zmo-WE4jkTxRmM" class="nav-cta">加入游戏</a>
</div>
</div>
</nav>
<!-- Hero Section -->
<header class="hero" style="background-image: url('https://img.lunadeer.cn/i/2025/11/26/69267755e14e3.png');">
<div class="hero-overlay"></div>
<div class="hero-content">
<h1 class="hero-title">白鹿原 Minecraft</h1>
<p class="hero-subtitle">永不换档的纯净原版服务器</p>
<div class="server-runtime">
已稳定运行 <span id="runtime-days">0</span><span id="runtime-hours">0</span> 小时 <span id="runtime-minutes">0</span><span id="runtime-seconds">0</span>
</div>
<div class="hero-actions">
<div class="server-ip-box" onclick="copyIp()">
<span id="server-ip">mcpure.lunadeer.cn</span>
<i class="fas fa-copy"></i>
<span class="tooltip" id="copy-tooltip">已复制</span>
</div>
<p class="ip-hint">点击复制服务器地址</p>
<!-- Online Status -->
<div class="online-status-box">
<div class="status-indicator">
<span class="status-dot"></span>
<span id="online-count">正在获取状态...</span>
</div>
<div class="players-tooltip" id="players-list">
<div class="player-item" style="justify-content: center;">加载中...</div>
</div>
</div>
</div>
</div>
</header>
<!-- Key Features (Bento Grid Style) -->
<section class="features-section">
<div class="container">
<!-- <h2 class="section-header">为什么选择白鹿原?</h2> -->
<div class="bento-grid">
<!-- Large Feature: Pure -->
<div class="bento-item large-item feature-pure" style="background-image: url('https://img.lunadeer.cn/i/2024/02/21/65d592eb4afad.jpg');">
<div class="bento-overlay"></div>
<div class="bento-content">
<i class="fas fa-leaf icon"></i>
<h3>纯净原版</h3>
<p>无纷繁复杂的 Mod无破坏平衡的插件。一切简单的就像是单机模式的共享一般。</p>
</div>
</div>
<!-- Medium Feature: Self-developed -->
<div class="bento-item medium-item feature-dev" style="background-image: url('https://img.lunadeer.cn/i/2025/11/26/6926982718ba8.png');">
<div class="bento-overlay"></div>
<div class="bento-content">
<i class="fas fa-code icon"></i>
<h3>深度自研</h3>
<p>全栈自研核心,拒绝卡脖子,保证可持续发展。</p>
</div>
</div>
<!-- Medium Feature: No Params Modified -->
<div class="bento-item medium-item feature-params" style="background-image: url('https://img.lunadeer.cn/i/2025/11/26/6926775006dea.jpg');">
<div class="bento-overlay"></div>
<div class="bento-content">
<i class="fas fa-sliders-h icon"></i>
<h3>原汁原味</h3>
<p>生物生成、红石参数与单机完全一致。</p>
</div>
</div>
<!-- Small Features Grid -->
<div class="bento-item small-item" style="background-image: url('https://img.lunadeer.cn/i/2024/02/21/65d592ea6faa1.jpg');">
<div class="bento-overlay"></div>
<div class="bento-content">
<i class="fas fa-home icon"></i>
<h4>免费圈地</h4>
<p>2048*2048 超大领地</p>
</div>
</div>
<div class="bento-item small-item" style="background-image: url('https://img.lunadeer.cn/i/2025/11/26/692677560db46.png');">
<div class="bento-overlay"></div>
<div class="bento-content">
<i class="fas fa-mobile-alt icon"></i>
<h4>基岩互通</h4>
<p>手机电脑随时畅玩</p>
</div>
</div>
<div class="bento-item small-item" style="background-image: url('https://img.lunadeer.cn/i/2024/02/21/65d592e248066.jpg');">
<div class="bento-overlay"></div>
<div class="bento-content">
<i class="fas fa-server icon"></i>
<h4>自有硬件</h4>
<p>物理工作站,永不跑路</p>
</div>
</div>
<div class="bento-item small-item" style="background-image: url('https://img.lunadeer.cn/i/2025/11/26/692677566b07b.png');">
<div class="bento-overlay"></div>
<div class="bento-content">
<i class="fas fa-gamepad icon"></i>
<h4>娱乐玩法</h4>
<p>空岛、跑酷、小游戏</p>
</div>
</div>
<!-- Medium Feature: Update -->
<div class="bento-item medium-item" style="background-image: url('https://img.lunadeer.cn/i/2025/11/26/692697b71431b.png');">
<div class="bento-content">
<i class="fas fa-sync-alt icon"></i>
<h3>紧跟新版</h3>
<p>紧跟 Paper 核心版本更新,始终保持在版本前列。第一时间体验 Minecraft 的最新内容。</p>
</div>
</div>
<!-- Medium Feature: Guide -->
<div class="bento-item medium-item" style="background-image: url('https://img.lunadeer.cn/i/2025/11/26/692697b7376c7.png');">
<div class="bento-content">
<i class="fas fa-book-open icon"></i>
<h3>新手指南</h3>
<p>完善的服务器文档与活跃的社区,帮助你快速上手,加入白鹿原大家庭。</p>
</div>
</div>
</div>
</div>
</section>
<!-- Sponsors Section -->
<section class="sponsors-section">
<div class="container">
<h2 class="section-header">特别鸣谢</h2>
<div id="top-sponsors" class="top-sponsors-grid">
<!-- Top 3 sponsors will be injected here by JS -->
</div>
<div class="sponsors-action">
<button id="view-sponsors-btn" class="view-sponsors-btn">查看赞助列表</button>
</div>
</div>
</section>
<!-- Crowdfunding Section -->
<section id="crowdfunding-section" class="crowdfunding-section" style="display: none;">
<div class="container">
<h2 class="section-header">众筹进度</h2>
<div id="crowdfunding-grid" class="crowdfunding-grid">
<!-- Crowdfunding items will be injected here -->
</div>
</div>
</section>
<!-- Sponsors Modal -->
<div id="sponsors-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h2>赞助列表</h2>
<div class="sponsors-list-container">
<table id="sponsors-table">
<thead>
<tr>
<th>用户</th>
<th>项目</th>
<th>金额</th>
<th>日期</th>
</tr>
</thead>
<tbody>
<!-- Full list will be injected here -->
</tbody>
</table>
</div>
</div>
</div>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-logo">白鹿原</div>
<p>&copy; 2025 白鹿原 Minecraft 服务器.</p>
</div>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>

717
pigwei.html Normal file
View File

@@ -0,0 +1,717 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>薇薇变小猪倒计时</title>
<link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=Nunito:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #fff9f0;
--card-bg: #ffffff;
--primary-text: #5d4037;
--secondary-text: #8d6e63;
--accent-pink: #ffc4d6;
--accent-cream: #ffe4b5;
--accent-blue: #e0f7fa;
--shadow-soft: 0 10px 30px rgba(255, 196, 214, 0.3);
--shadow-inset: inset 2px 2px 5px rgba(209, 169, 169, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Nunito', 'ZCOOL KuaiLe', sans-serif;
background-color: var(--bg-color);
background-image:
radial-gradient(circle at 10% 20%, rgba(255, 196, 214, 0.2) 0%, transparent 20%),
radial-gradient(circle at 90% 80%, rgba(255, 228, 181, 0.3) 0%, transparent 20%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: var(--primary-text);
overflow: hidden;
}
.container {
position: relative;
background: var(--card-bg);
padding: 3rem 2rem;
border-radius: 30px;
box-shadow: var(--shadow-soft);
text-align: center;
max-width: 600px;
width: 90%;
border: 4px solid #fff;
animation: float 6s ease-in-out infinite;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: var(--primary-text);
text-shadow: 2px 2px 0px var(--accent-pink);
letter-spacing: 2px;
}
.subtitle {
font-size: 1.1rem;
color: var(--secondary-text);
margin-bottom: 2.5rem;
background: var(--accent-cream);
display: inline-block;
padding: 0.5rem 1rem;
border-radius: 20px;
font-weight: bold;
}
.countdown-wrapper {
display: flex;
justify-content: center;
gap: 1.5rem;
flex-wrap: wrap;
margin-bottom: 3rem;
}
.time-box {
background: linear-gradient(145deg, #fff0f5, #ffffff);
width: 90px;
height: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 20px;
box-shadow: 5px 5px 15px #f0dada, -5px -5px 15px #ffffff;
position: relative;
transition: transform 0.3s ease;
}
.time-box:hover {
transform: translateY(-5px);
}
.number {
font-size: 2.5rem;
font-weight: 700;
color: #ff8fa3;
line-height: 1;
}
.label {
font-size: 0.9rem;
color: var(--secondary-text);
margin-top: 5px;
}
.progress-container {
margin-top: 2rem;
width: 100%;
background-color: #fff0f0;
border-radius: 15px;
height: 20px;
position: relative;
overflow: hidden;
box-shadow: inset 0 2px 5px rgba(0,0,0,0.05);
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--accent-pink), #ff9eb5);
width: 0%;
border-radius: 15px;
transition: width 1s ease-in-out;
position: relative;
}
/* Pig Decoration */
.pig-mascot {
width: 120px;
height: 120px;
margin: -60px auto 10px;
position: relative;
z-index: 10;
}
.pig-svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 5px 10px rgba(255, 150, 170, 0.3));
animation: wiggle 3s ease-in-out infinite;
}
.footer-text {
margin-top: 2rem;
font-size: 0.9rem;
color: #bcaaa4;
}
/* Floating shapes background */
.shape {
position: absolute;
opacity: 0.3;
z-index: -1;
animation: floatBackground 10s infinite linear;
}
.shape-1 { top: 10%; left: 5%; font-size: 6rem; animation-duration: 15s; }
.shape-2 { bottom: 15%; right: 10%; font-size: 4rem; animation-duration: 20s; animation-delay: -5s; }
.shape-3 { top: 40%; right: 5%; font-size: 8rem; animation-duration: 12s; animation-delay: -2s; }
.shape-4 { bottom: 5%; left: 15%; font-size: 5.5rem; animation-duration: 18s; animation-delay: -7s; }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes wiggle {
0%, 100% { transform: rotate(-5deg); }
50% { transform: rotate(5deg); }
}
@keyframes floatBackground {
0% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-20px) rotate(10deg); }
100% { transform: translateY(0) rotate(0deg); }
}
@media (max-width: 480px) {
.time-box {
width: 70px;
height: 80px;
}
.number {
font-size: 1.8rem;
}
h1 {
font-size: 1.8rem;
}
}
/* 互动彩蛋样式 */
.speech-bubble {
position: absolute;
top: -50px;
left: 50%;
transform: translateX(-50%) scale(0);
background: #fff;
padding: 8px 15px;
border-radius: 15px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
font-size: 0.9rem;
color: var(--primary-text);
white-space: nowrap;
opacity: 0;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: none;
z-index: 20;
font-weight: bold;
border: 2px solid var(--accent-pink);
}
.speech-bubble::after {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
border-width: 8px 8px 0;
border-style: solid;
border-color: var(--accent-pink) transparent transparent transparent;
}
.speech-bubble.show {
transform: translateX(-50%) scale(1);
opacity: 1;
}
.click-particle {
position: absolute;
pointer-events: none;
animation: particleFloat 0.8s forwards;
font-size: 1.5rem;
z-index: 100;
user-select: none;
}
@keyframes particleFloat {
0% { transform: translateY(0) scale(0.5); opacity: 1; }
50% { transform: translateY(-30px) scale(1.2); opacity: 1; }
100% { transform: translateY(-60px) scale(1); opacity: 0; }
}
</style>
</head>
<body>
<!-- Background Decorations -->
<div class="shape shape-1">🍬</div>
<div class="shape shape-2">🧋</div>
<div class="shape shape-3">💖</div>
<div class="shape shape-4">🧁</div>
<div class="container">
<div class="pig-mascot">
<!-- Simple Cute Pig SVG -->
<svg class="pig-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="45" fill="#FFC4D6"/> <!-- Head -->
<circle cx="30" cy="35" r="5" fill="#5D4037"/> <!-- Left Eye -->
<circle cx="70" cy="35" r="5" fill="#5D4037"/> <!-- Right Eye -->
<!-- Snout -->
<ellipse cx="50" cy="55" rx="18" ry="12" fill="#FF9EB5"/>
<circle cx="44" cy="55" r="3" fill="#FFF"/>
<circle cx="56" cy="55" r="3" fill="#FFF"/>
<!-- Ears -->
<path d="M15 25 Q 5 5 25 15 Z" fill="#FF9EB5"/>
<path d="M85 25 Q 95 5 75 15 Z" fill="#FF9EB5"/>
<!-- Blush -->
<circle cx="20" cy="55" r="5" fill="#FF9EB5" opacity="0.6"/>
<circle cx="80" cy="55" r="5" fill="#FF9EB5" opacity="0.6"/>
</svg>
<div class="speech-bubble" id="speech-bubble">哼哼~</div>
</div>
<h1>薇薇变小猪倒计时</h1>
<div class="subtitle">距离变小猪还有...</div>
<div class="countdown-wrapper">
<div class="time-box">
<span class="number" id="days">00</span>
<span class="label">Days</span>
</div>
<div class="time-box">
<span class="number" id="hours">00</span>
<span class="label">Hours</span>
</div>
<div class="time-box">
<span class="number" id="minutes">00</span>
<span class="label">Mins</span>
</div>
<div class="time-box">
<span class="number" id="seconds">00</span>
<span class="label">Secs</span>
</div>
</div>
<div style="text-align: left; margin-bottom: 5px; padding-left: 5px; color: var(--secondary-text); font-size: 0.9rem; display: flex; justify-content: space-between;">
<span>Loading Happiness...</span>
<span id="percent-text">0.000%</span>
</div>
<div class="progress-container">
<div class="progress-bar" id="progress"></div>
</div>
</div>
<!-- Desktop Pet Pig (Top-Down) -->
<div id="pet-pig" style="position: absolute; left: 50px; top: 50px; width: 70px; height: 70px; z-index: 999; pointer-events: none; transition: transform 0.1s linear;">
<div class="pet-body" style="width: 100%; height: 100%; position: relative;">
<svg viewBox="0 0 100 100" style="width: 100%; height: 100%; filter: drop-shadow(0 4px 6px rgba(0,0,0,0.15));">
<!-- Feet (Animated) -->
<ellipse cx="25" cy="40" rx="7" ry="9" fill="#FF9EB5" class="pet-foot fl" />
<ellipse cx="75" cy="40" rx="7" ry="9" fill="#FF9EB5" class="pet-foot fr" />
<ellipse cx="25" cy="75" rx="7" ry="9" fill="#FF9EB5" class="pet-foot bl" />
<ellipse cx="75" cy="75" rx="7" ry="9" fill="#FF9EB5" class="pet-foot br" />
<!-- Tail -->
<path d="M50 82 Q 40 85 45 92 T 55 95" fill="none" stroke="#FF9EB5" stroke-width="4" stroke-linecap="round" />
<!-- Main Body -->
<ellipse cx="50" cy="60" rx="32" ry="34" fill="#FFC4D6" />
<!-- Head -->
<circle cx="50" cy="35" r="26" fill="#FFC4D6" />
<!-- Ears (Now Behind Head) -->
<path d="M 36 54 Q 10 42 24 38 Z" fill="#FF9EB5" stroke="#FF9EB5" stroke-width="2" stroke-linejoin="round"/>
<path d="M 64 54 Q 90 42 76 38 Z" fill="#FF9EB5" stroke="#FF9EB5" stroke-width="2" stroke-linejoin="round"/>
<!-- Snout -->
<ellipse cx="50" cy="20" rx="14" ry="10" fill="#FF9EB5" />
<circle cx="45" cy="18" r="2.5" fill="#FFF" />
<circle cx="55" cy="18" r="2.5" fill="#FFF" />
<!-- Eyes -->
<circle cx="36" cy="34" r="3" fill="#5D4037" class="pet-eye" />
<circle cx="64" cy="34" r="3" fill="#5D4037" class="pet-eye" />
</svg>
<div id="pet-status" style="position: absolute; top: -30px; left: 50%; transform: translateX(-50%); white-space: nowrap; font-size: 12px; background: rgba(255,255,255,0.9); padding: 4px 8px; border-radius: 10px; opacity: 0; transition: opacity 0.3s; pointer-events: none; font-weight: bold; color: #5d4037;">
Zzz...
</div>
</div>
</div>
<!-- Feed Button -->
<button id="feed-btn" style="position: fixed; bottom: 20px; right: 20px; z-index: 1000; background: #FFC4D6; border: none; padding: 10px 15px; border-radius: 20px; font-family: 'ZCOOL KuaiLe', sans-serif; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.1); font-size: 1rem; color: #5D4037;">
🍬 投喂
</button>
<script>
// Desktop Pet Logic
(function() {
const pet = document.getElementById('pet-pig');
const petBody = pet.querySelector('.pet-body');
const petStatus = document.getElementById('pet-status');
// Feet selectors
const footFL = pet.querySelector('.fl');
const footFR = pet.querySelector('.fr');
const footBL = pet.querySelector('.bl');
const footBR = pet.querySelector('.br');
let petX = 50;
let petY = 50;
let targetX = 50;
let targetY = 50;
let speed = 2.5;
let state = 'idle'; // idle, moving, sleeping, eating
let idleTimer = null;
let lastInteractionStr = Date.now();
let isSleeping = false;
function updatePetPosition() {
pet.style.left = (petX - 35) + 'px'; // Center anchor (70px width)
pet.style.top = (petY - 35) + 'px';
}
updatePetPosition();
function gameLoop() {
const now = Date.now();
if (state === 'moving') {
const dx = targetX - petX;
const dy = targetY - petY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > speed) {
const angle = Math.atan2(dy, dx);
petX += Math.cos(angle) * speed;
petY += Math.sin(angle) * speed;
// Rotate pet (Facing UP is 0 rotation in drawn SVG logic?
// Actually atan2(0,-1) is -PI/2. atan2(1,0) is 0.
// If we want movement direction to be UP (Top of screen), angle is -90deg.
// SVG is drawn facing UP. So -90deg should correspond to 0 rotation.
// So rotation = angle + 90deg.
let rotation = (angle * 180 / Math.PI) + 90;
petBody.style.transform = `rotate(${rotation}deg)`;
// Walk animation (Trot gait)
// FL and BR move together, FR and BL move together
const walkCycle = (now / 150) * Math.PI * 2;
const offset = Math.sin(walkCycle) * 6;
footFL.setAttribute('cy', 40 + offset);
footBR.setAttribute('cy', 75 + offset);
footFR.setAttribute('cy', 40 - offset);
footBL.setAttribute('cy', 75 - offset);
lastInteractionStr = now;
isSleeping = false;
petStatus.style.opacity = 0;
} else {
// Arrived at destination
if (window.currentFood) {
// Start eating
state = 'eating';
petStatus.innerText = "Yum!";
petStatus.style.opacity = 1;
resetFeet();
// Remove the food element
const food = window.currentFood;
window.currentFood = null;
// Eat effect
food.style.transition = 'all 0.3s';
food.style.transform = 'scale(0.5)';
food.style.opacity = 0;
setTimeout(() => food.remove(), 300);
// Wiggle effect for Eating
let startTime = Date.now();
// Capture the current rotation from the transform style
// Format is usually "rotate(Xdeg)" or similar
let currentTransform = petBody.style.transform || "";
let rotationPart = "";
if (currentTransform.includes("rotate")) {
rotationPart = currentTransform.match(/rotate\([^)]+\)/)[0];
} else {
rotationPart = "rotate(0deg)";
}
const eatAnim = setInterval(() => {
if (Date.now() - startTime > 2000) {
clearInterval(eatAnim);
state = 'idle';
petStatus.style.opacity = 0;
lastInteractionStr = Date.now();
// Reset scale but keep rotation logic for next move
petBody.style.transform = rotationPart;
}
// Combine rotation with scale wiggle
petBody.style.transform = `${rotationPart} scale(${1 + Math.sin(Date.now()/50)*0.1})`;
}, 16);
} else {
state = 'idle';
resetFeet();
}
}
} else if (state === 'idle') {
if (now - lastInteractionStr > 10000 && !isSleeping) { // 10s idle to sleep
startSleep();
}
if (!isSleeping && Math.random() < 0.008) {
wander();
}
}
updatePetPosition();
requestAnimationFrame(gameLoop);
}
function resetFeet() {
footFL.setAttribute('cy', 40);
footFR.setAttribute('cy', 40);
footBL.setAttribute('cy', 75);
footBR.setAttribute('cy', 75);
}
function startSleep() {
isSleeping = true;
state = 'sleeping';
petStatus.innerText = "Zzz...";
petStatus.style.opacity = 1;
// Sleep eyes
pet.querySelectorAll('.pet-eye').forEach(eye => {
eye.style.transformOrigin = 'center';
eye.style.transform = 'scaleY(0.1)';
});
}
function wakeUp() {
if (!isSleeping) return;
isSleeping = false;
state = 'idle';
lastInteractionStr = Date.now();
petStatus.style.opacity = 0;
pet.querySelectorAll('.pet-eye').forEach(eye => {
eye.style.transform = 'scaleY(1)';
});
}
function wander() {
const margin = 50;
const maxX = window.innerWidth - margin;
const maxY = window.innerHeight - margin;
targetX = margin + Math.random() * (maxX - margin * 2);
targetY = margin + Math.random() * (maxY - margin * 2);
state = 'moving';
}
window.movePetTo = function(x, y) {
if (state === 'eating') return;
wakeUp();
targetX = x;
targetY = y;
state = 'moving';
};
const feedBtn = document.getElementById('feed-btn');
feedBtn.addEventListener('click', (e) => {
e.stopPropagation();
// Prevent multiple feeds
if (window.currentFood || state === 'eating') return;
if(isSleeping) wakeUp();
// Random position for food
const margin = 50;
const maxX = window.innerWidth - margin;
const maxY = window.innerHeight - margin;
const foodX = margin + Math.random() * (maxX - margin * 2);
const foodY = margin + Math.random() * (maxY - margin * 2);
// Move pet to food
targetX = foodX;
targetY = foodY;
state = 'moving';
// Spawn food visual at target location
const food = document.createElement('div');
food.innerText = ['🍬','🍭','🍫'][Math.floor(Math.random()*3)];
food.style.position = 'absolute';
food.style.left = foodX + 'px';
food.style.top = foodY + 'px';
food.style.fontSize = '25px';
food.style.zIndex = 998; // Below pet so pet appears to cover it when eating
food.style.pointerEvents = 'none';
food.className = 'pet-food';
document.body.appendChild(food);
// Check distance to food in game loop to trigger eating
// We need a way to know we are moving to food.
// Let's attach the food element to the state or a variable
window.currentFood = food;
});
requestAnimationFrame(gameLoop);
})();
// Original script continues...
function updateCountdown() {
const targetDate = new Date('October 5, 2026 00:00:00').getTime();
const now = new Date().getTime();
const gap = targetDate - now;
if (gap < 0) {
// If the date has passed
document.getElementById('days').innerText = "00";
document.getElementById('hours').innerText = "00";
document.getElementById('minutes').innerText = "00";
document.getElementById('seconds').innerText = "00";
document.querySelector('.footer-text').innerText = "薇薇已经变成小猪啦!🎉";
return;
}
const second = 1000;
const minute = second * 60;
const hour = minute * 60;
const day = hour * 24;
const days = Math.floor(gap / day);
const hours = Math.floor((gap % day) / hour);
const minutes = Math.floor((gap % hour) / minute);
const seconds = Math.floor((gap % minute) / second);
document.getElementById('days').innerText = days < 10 ? '0' + days : days;
document.getElementById('hours').innerText = hours < 10 ? '0' + hours : hours;
document.getElementById('minutes').innerText = minutes < 10 ? '0' + minutes : minutes;
document.getElementById('seconds').innerText = seconds < 10 ? '0' + seconds : seconds;
// Calculate progress (Assuming start date is around now roughly for visual effect,
// or we can set a fixed start date like "engagement date".
// Let's set a hypothetical start date to make the bar look nice.
// e.g. Start of 2025 or today)
// For a meaningful progress bar, let's say the "countdown" started 1 year before?
// Or simple visual effect:
// Let's make it a "year progress" or just a visual filler.
// Actually, let's define a start date to calculate percentage completed.
// Let's assume the planning started today (Jan 7, 2026) for context, or earlier.
// Let's pick Jan 1, 2026 as start for the bar.
const startDate = new Date('January 1, 2026 00:00:00').getTime();
const totalDuration = targetDate - startDate;
const timeElapsed = now - startDate;
let percentage = (timeElapsed / totalDuration) * 100;
// Update text
let textPercent = percentage;
if (textPercent < 0) textPercent = 0;
if (textPercent > 100) textPercent = 100;
document.getElementById('percent-text').innerText = textPercent.toFixed(5) + '%';
// Clamp percentage between 0 and 100
if (percentage < 0) percentage = 5; // Minimal fill
if (percentage > 100) percentage = 100;
document.getElementById('progress').style.width = percentage + '%';
}
setInterval(updateCountdown, 1000);
updateCountdown(); // Initial call
// 互动功能:点击屏幕爆出可爱元素
document.addEventListener('click', function(e) {
// 定义一组可爱的 emoji
const emojis = ['❤️', '📱', '💻', '✨', '🌸', '💍', '🎀', '🔑'];
const particle = document.createElement('div');
particle.className = 'click-particle';
particle.innerText = emojis[Math.floor(Math.random() * emojis.length)];
// 设置位置
particle.style.left = e.pageX + 'px';
particle.style.top = e.pageY + 'px';
// 随机旋转一点角度
const rotate = (Math.random() - 0.5) * 60;
particle.style.transform = `rotate(${rotate}deg)`;
document.body.appendChild(particle);
// 动画结束后移除元素
setTimeout(() => {
particle.remove();
}, 800);
// 如果存在桌宠,则触发移动
if (window.movePetTo) {
window.movePetTo(e.pageX, e.pageY);
}
});
// 互动功能:戳戳小猪
const pig = document.querySelector('.pig-mascot');
const bubble = document.getElementById('speech-bubble');
const pigSvg = document.querySelector('.pig-svg');
const messages = [
"薇薇要喝奶茶 🧋!",
"薇薇今天也很可爱!",
"哼哼~ 给我火锅吃!",
"要一直幸福哦!",
"本猪猪在监督倒计时!",
"❤️ 爱你哟 ❤️",
"不要戳我啦!痒~"
];
let isTalking = false;
pig.addEventListener('click', function(e) {
e.stopPropagation(); // 防止触发背景点击特效
if (isTalking) return;
isTalking = true;
// 随机显示一句话
const msg = messages[Math.floor(Math.random() * messages.length)];
bubble.innerText = msg;
bubble.classList.add('show');
// 加速摇摆
const originalAnimation = getComputedStyle(pigSvg).animation;
pigSvg.style.animation = 'wiggle 0.2s ease-in-out infinite';
// 2秒后恢复
setTimeout(() => {
bubble.classList.remove('show');
pigSvg.style.animation = ''; // 恢复 CSS 中定义的动画
isTalking = false;
}, 2000);
// 同时在小猪头顶爆几个爱心
for(let i=0; i<3; i++) {
setTimeout(() => {
const rect = pig.getBoundingClientRect();
const particle = document.createElement('div');
particle.className = 'click-particle';
particle.innerText = '❤️';
particle.style.left = (rect.left + rect.width/2 + (Math.random()-0.5)*50) + 'px';
particle.style.top = (rect.top + rect.height/2 + (Math.random()-0.5)*50) + 'px';
// 修正绝对定位相对于文档的坐标
particle.style.left = (rect.left + window.scrollX + rect.width/2 + (Math.random()-0.5)*50) + 'px';
particle.style.top = (rect.top + window.scrollY + rect.height/2 - 30) + 'px';
document.body.appendChild(particle);
setTimeout(() => particle.remove(), 800);
}, i * 100);
}
});
</script>
</body>
</html>

258
script.js Normal file
View File

@@ -0,0 +1,258 @@
function copyIp() {
const ipText = document.getElementById('server-ip').innerText;
const tooltip = document.getElementById('copy-tooltip');
navigator.clipboard.writeText(ipText).then(() => {
tooltip.innerText = "已复制!";
tooltip.classList.add('show');
setTimeout(() => {
tooltip.classList.remove('show');
setTimeout(() => {
tooltip.innerText = "点击复制 IP";
}, 200); // Wait for fade out
}, 2000);
}).catch(err => {
console.error('无法复制文本: ', err);
tooltip.innerText = "复制失败";
tooltip.classList.add('show');
setTimeout(() => {
tooltip.classList.remove('show');
}, 2000);
});
}
// Sponsors Logic
document.addEventListener('DOMContentLoaded', () => {
fetchSponsors();
fetchCrowdfunding();
setupModal();
fetchServerStatus();
startRuntimeTimer();
});
function startRuntimeTimer() {
const startTime = new Date("2021-09-14T09:57:59").getTime();
function update() {
const now = new Date().getTime();
const diff = now - startTime;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
const daysEl = document.getElementById("runtime-days");
const hoursEl = document.getElementById("runtime-hours");
const minutesEl = document.getElementById("runtime-minutes");
const secondsEl = document.getElementById("runtime-seconds");
if (daysEl) daysEl.innerText = days;
if (hoursEl) hoursEl.innerText = hours;
if (minutesEl) minutesEl.innerText = minutes;
if (secondsEl) secondsEl.innerText = seconds;
}
update();
setInterval(update, 1000);
}
async function fetchServerStatus() {
const countElement = document.getElementById('online-count');
const listElement = document.getElementById('players-list');
const dotElement = document.querySelector('.status-dot');
try {
const response = await fetch('https://api.mcstatus.io/v2/status/java/mcpure.lunadeer.cn');
const data = await response.json();
if (data.online) {
countElement.innerText = `在线人数: ${data.players.online} / ${data.players.max}`;
dotElement.classList.remove('offline');
if (data.players.list && data.players.list.length > 0) {
listElement.innerHTML = data.players.list.map(player => `
<div class="player-item">
<img src="https://minotar.net/avatar/${player.name_raw}/16" class="player-avatar" onerror="this.style.display='none'">
<span>${player.name_raw}</span>
</div>
`).join('');
} else {
listElement.innerHTML = '<div class="player-item" style="justify-content: center; color: #86868b;">暂无玩家在线</div>';
}
} else {
countElement.innerText = '服务器离线';
dotElement.classList.add('offline');
listElement.innerHTML = '<div class="player-item" style="justify-content: center; color: #ff3b30;">服务器离线</div>';
}
} catch (error) {
console.error('Error fetching server status:', error);
countElement.innerText = '无法获取状态';
dotElement.classList.add('offline');
listElement.innerHTML = '<div class="player-item" style="justify-content: center; color: #ff3b30;">获取失败</div>';
}
}
async function fetchCrowdfunding() {
try {
console.log('Fetching crowdfunding data...');
const response = await fetch('fund_progress.txt');
if (!response.ok) {
console.error('Failed to fetch fund_progress.txt:', response.status, response.statusText);
return;
}
const text = await response.text();
console.log('Crowdfunding data received:', text);
const lines = text.trim().split('\n');
const funds = [];
lines.forEach(line => {
// Replace Chinese comma with English comma just in case
const normalizedLine = line.replace(//g, ',');
const parts = normalizedLine.split(',');
if (parts.length >= 3) {
const name = parts[0].trim();
const current = parseFloat(parts[1].trim());
const target = parseFloat(parts[2].trim());
if (name && !isNaN(current) && !isNaN(target)) {
funds.push({ name, current, target });
}
}
});
console.log('Parsed funds:', funds);
if (funds.length > 0) {
renderCrowdfunding(funds);
const section = document.getElementById('crowdfunding-section');
if (section) {
section.style.display = 'block';
console.log('Crowdfunding section displayed');
} else {
console.error('Crowdfunding section element not found');
}
} else {
console.warn('No valid crowdfunding data found');
}
} catch (error) {
console.error('Error loading crowdfunding data:', error);
}
}
function renderCrowdfunding(funds) {
const container = document.getElementById('crowdfunding-grid');
container.innerHTML = funds.map(fund => {
const percentage = Math.min(100, Math.max(0, (fund.current / fund.target) * 100));
return `
<div class="fund-card">
<div class="fund-header">
<div class="fund-title">${fund.name}</div>
<div class="fund-stats">
<span>¥${fund.current}</span> / ¥${fund.target}
</div>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" style="width: ${percentage}%"></div>
</div>
<div class="fund-percentage">${percentage.toFixed(1)}%</div>
</div>
`;
}).join('');
}
async function fetchSponsors() {
try {
const response = await fetch('sponsors.txt');
const text = await response.text();
const lines = text.trim().split('\n');
const sponsors = [];
const userTotals = {};
lines.forEach(line => {
const parts = line.split(',');
if (parts.length >= 3) {
const name = parts[0].trim();
const project = parts[1].trim();
const amountStr = parts[2].trim().replace('¥', '');
const amount = parseFloat(amountStr);
const date = parts[3] ? parts[3].trim() : '';
if (!isNaN(amount)) {
sponsors.push({ name, project, amount, date });
if (userTotals[name]) {
userTotals[name] += amount;
} else {
userTotals[name] = amount;
}
}
}
});
// Sort users by total amount for Top 3
const sortedUsers = Object.keys(userTotals).map(name => ({
name,
total: userTotals[name]
})).sort((a, b) => b.total - a.total);
renderTopSponsors(sortedUsers.slice(0, 3));
renderSponsorsTable(sponsors);
} catch (error) {
console.error('Error loading sponsors:', error);
}
}
function renderTopSponsors(topUsers) {
const container = document.getElementById('top-sponsors');
const medals = ['🥇', '🥈', '🥉'];
container.innerHTML = topUsers.map((user, index) => `
<div class="sponsor-card">
<div class="sponsor-rank">${medals[index]}</div>
<div class="sponsor-name">${user.name}</div>
<div class="sponsor-amount">¥${user.total.toFixed(2)}</div>
</div>
`).join('');
}
function renderSponsorsTable(sponsors) {
const tbody = document.querySelector('#sponsors-table tbody');
// Sort by date descending (newest first) or keep original order?
// Usually original order in file is chronological. Let's reverse it to show newest first.
const reversedSponsors = [...sponsors].reverse();
tbody.innerHTML = reversedSponsors.map(s => `
<tr>
<td>${s.name}</td>
<td>${s.project}</td>
<td>¥${s.amount.toFixed(2)}</td>
<td>${s.date}</td>
</tr>
`).join('');
}
function setupModal() {
const modal = document.getElementById('sponsors-modal');
const btn = document.getElementById('view-sponsors-btn');
const span = document.getElementsByClassName('close-modal')[0];
btn.onclick = function() {
modal.style.display = "flex";
}
span.onclick = function() {
modal.style.display = "none";
}
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
}

36
sponsors.txt Normal file
View File

@@ -0,0 +1,36 @@
TowardsCommunism,服务器硬件升级,20¥,2024-11-22
Kun_Wu,服务器硬件升级,50¥,2024-11-22
ZhuTiZi_y,服务器硬件升级,20¥,2024-11-22
Moon_Bridge,服务器硬件升级,50¥,2024-11-22
Clincded_Xsa,服务器硬件升级,50¥,2024-11-22
Genera1314,服务器硬件升级,20¥,2024-11-22
Huakou,服务器硬件升级,50¥,2024-11-22
Director_HOU,服务器硬件升级,20¥,2024-11-23
Cinetank,服务器硬件升级,50¥,2024-11-23
33zi,服务器硬件升级,70¥,2024-11-23
obligationi,服务器硬件升级,100¥,2024-11-23
himscars,服务器硬件升级,50¥,2024-11-23
TowardsCommunism,服务器硬件升级,50¥,2024-11-23
dghyuiok,服务器硬件升级,20¥,2024-11-23
rainy_daily,服务器硬件升级,50¥,2024-11-23
OG Trouble,电费,5¥,2024-11-25
33zi,服务器硬件升级,20¥,2024-12-7
Kim,服务器硬件升级,100¥,2024-12-9
Su_,服务器硬件升级,50¥,2024-12-9
ZhuTiZi_y,服务器硬件升级,100¥,2024-12-17
M1AOYIN,服务器硬件升级,1000¥,2024-12-25
TowardsCommunism,服务器硬件升级,20¥,2024-12-25
流明,服务器硬件升级,50¥,2024-12-25
MISS_U,服务器硬件升级,100¥,2025-1-18
Treasure_yu,服务器硬件升级,30¥,2025-1-28
Huakou,服务器硬件升级,100¥,2025-1-31
Forever_Qi,服务器硬件升级,20¥,2025-2-21
C0ldWood,服务器硬件升级,20¥,2025-3-1
天气,服务器硬件升级,50¥,2025-3-23
天气,服务器硬件升级,20¥,2025-3-26
Bear_Brother,电费赞助,30¥,2025-4-20
Kun_Wu,服务器硬件升级,220¥,2025-4-21
CN_snowman,电费赞助,900¥,2025-5-2
BE.BackedKey9120,电费赞助,20¥,2025-5-19
wait_running,服务器硬件升级,100¥,2025-11-26
NthM7,服务器硬件升级,20¥,2025-12-2

376
stats.html Normal file
View File

@@ -0,0 +1,376 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>玩家数据 - 白鹿原 Minecraft 服务器</title>
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="style.css">
<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=Noto+Sans+SC:wght@400;500;700&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
/* Specific styles for stats page override/additions */
.stats-hero {
height: 40vh;
min-height: 300px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
margin-top: 30px;
}
.player-card {
background: var(--card-bg);
border-radius: var(--radius-medium);
padding: 20px;
text-align: center;
transition: var(--transition);
cursor: pointer;
box-shadow: 0 4px 6px rgba(0,0,0,0.02);
position: relative;
overflow: hidden;
}
.player-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0,0,0,0.05);
}
.player-card img {
width: 80px;
height: 80px;
border-radius: 10px;
margin-bottom: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.player-card h3 {
font-size: 16px;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.player-card .p-uuid {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 10px;
font-family: monospace;
}
/* Leaderboard Cards */
.leaderboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 60px;
}
.lb-card {
background: white;
border-radius: var(--radius-medium);
padding: 25px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
position: relative;
overflow: hidden;
}
.lb-card.gold { border-top: 4px solid #FFD700; }
.lb-card.silver { border-top: 4px solid #C0C0C0; }
.lb-card.bronze { border-top: 4px solid #CD7F32; }
.lb-card.red { border-top: 4px solid #ff3b30; }
.lb-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.lb-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
background: var(--bg-color);
color: var(--text-primary);
font-size: 20px;
}
.lb-title {
font-weight: 600;
font-size: 18px;
}
.lb-top-player {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.lb-top-player img {
width: 64px;
height: 64px;
border-radius: 8px;
margin-bottom: 10px;
}
.lb-top-data {
font-size: 24px;
font-weight: 700;
color: var(--accent-color);
}
.lb-list {
list-style: none;
}
.lb-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
font-size: 14px;
border-bottom: 1px dashed #eee;
}
.lb-item:last-child { border-bottom: none; }
.lb-rank {
width: 24px;
height: 24px;
background: #eee;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
margin-right: 8px;
}
/* Search Box */
.search-container {
max-width: 600px;
margin: 0 auto 40px;
position: relative;
}
.search-input {
width: 100%;
padding: 15px 20px 15px 45px;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 15px;
font-size: 16px;
outline: none;
transition: var(--transition);
background: white;
}
.search-input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.1);
}
.search-icon {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: #ccc;
}
/* Stats Modal Override */
.stat-row {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.stat-label { color: var(--text-secondary); }
.stat-value { font-weight: 600; }
.loading-text {
text-align: center;
padding: 40px;
color: var(--text-secondary);
font-size: 18px;
}
.load-more-container {
text-align: center;
margin-top: 40px;
}
.load-more-btn {
background: white;
border: 1px solid #ddd;
padding: 12px 30px;
border-radius: 20px;
cursor: pointer;
font-weight: 600;
transition: var(--transition);
}
.load-more-btn:hover {
background: #f9f9f9;
transform: translateY(-2px);
}
</style>
</head>
<body>
<!-- Navbar -->
<nav class="navbar">
<div class="nav-content">
<div class="logo">
<a href="/">
<img src="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png" alt="白鹿原 Logo">
</a>
</div>
<div class="nav-links">
<a href="https://outline.lunadeer.cn/s/447e5db6-8af4-468e-b7c5-cdb7b48aa439">文档</a>
<a href="https://mcmap.lunadeer.cn/">地图</a>
<a href="https://mcphoto.lunadeer.cn/">相册</a>
<a href="https://qm.qq.com/q/9izlHDoef6">群聊</a>
<a href="/stats.html" style="opacity: 1; font-weight: 600;">数据</a>
<a href="https://outline.lunadeer.cn/s/447e5db6-8af4-468e-b7c5-cdb7b48aa439/doc/5yqg5ywl5pyn5yqh5zmo-WE4jkTxRmM" class="nav-cta">加入游戏</a>
</div>
</div>
</nav>
<!-- Hero Section -->
<header class="hero stats-hero" style="background-image: url('https://img.lunadeer.cn/i/2025/11/26/69267755e14e3.png');">
<div class="hero-overlay"></div>
<div class="hero-content">
<h1 class="hero-title">数据中心</h1>
<p class="hero-subtitle">记录每一位冒险者的足迹</p>
</div>
</header>
<!-- Main Content -->
<div class="features-section" style="background: var(--bg-color);">
<div class="container">
<!-- Leaderboards -->
<h2 class="section-header">排行榜</h2>
<div class="leaderboard-grid" id="leaderboard-container">
<!-- 1. Walker -->
<div class="lb-card gold">
<div class="lb-header">
<div class="lb-icon"><i class="fas fa-walking"></i></div>
<div class="lb-title">旅行者</div>
</div>
<div class="lb-content" id="lb-walk">
<div class="lb-top-player">加载中...</div>
</div>
</div>
<!-- 2. Placer -->
<div class="lb-card silver">
<div class="lb-header">
<div class="lb-icon"><i class="fas fa-cube"></i></div>
<div class="lb-title">搬石大师</div>
</div>
<div class="lb-content" id="lb-placed">
<div class="lb-top-player">加载中...</div>
</div>
</div>
<!-- 3. Miner -->
<div class="lb-card bronze">
<div class="lb-header">
<div class="lb-icon"><i class="fas fa-hammer"></i></div>
<div class="lb-title">挖挖机</div>
</div>
<div class="lb-content" id="lb-mined">
<div class="lb-top-player">加载中...</div>
</div>
</div>
<!-- 4. Deaths -->
<div class="lb-card red">
<div class="lb-header">
<div class="lb-icon"><i class="fas fa-skull"></i></div>
<div class="lb-title">亡灵</div>
</div>
<div class="lb-content" id="lb-deaths">
<div class="lb-top-player">加载中...</div>
</div>
</div>
</div>
<!-- Search & Grid -->
<h2 class="section-header">玩家档案</h2>
<div class="search-container">
<i class="fas fa-search search-icon"></i>
<input type="text" id="player-search" class="search-input" placeholder="搜索玩家名称或 UUID...">
</div>
<div class="stats-grid" id="players-grid">
<!-- Players injected here -->
</div>
<div class="load-more-container">
<button id="load-more-btn" class="load-more-btn">加载更多</button>
</div>
<div id="loading-indicator" class="loading-text">正在从服务器获取数据...</div>
</div>
</div>
<!-- Player Details Modal -->
<div id="player-modal" class="modal">
<div class="modal-content" style="max-width: 400px;">
<span class="close-modal">&times;</span>
<div style="text-align: center; margin-bottom: 20px;">
<img id="modal-avatar" src="" alt="Avatar" style="width: 100px; height: 100px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<h2 id="modal-name" style="margin-top: 10px;">Player Name</h2>
<p id="modal-uuid" style="font-size: 12px; color: #999; font-family: monospace;">UUID</p>
</div>
<div class="stats-list-container">
<div class="stat-row">
<span class="stat-label"><i class="fas fa-walking"></i> 行走距离</span>
<span class="stat-value" id="modal-walk">0 m</span>
</div>
<div class="stat-row">
<span class="stat-label"><i class="fas fa-cube"></i> 放置方块</span>
<span class="stat-value" id="modal-placed">0</span>
</div>
<div class="stat-row">
<span class="stat-label"><i class="fas fa-hammer"></i> 挖掘方块</span>
<span class="stat-value" id="modal-mined">0</span>
</div>
<div class="stat-row">
<span class="stat-label"><i class="fas fa-skull"></i> 死亡次数</span>
<span class="stat-value" id="modal-deaths">0</span>
</div>
</div>
</div>
</div>
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-logo">白鹿原</div>
<p>&copy; 2025 白鹿原 Minecraft 服务器.</p>
</div>
</div>
</footer>
<script src="stats_script.js"></script>
</body>
</html>

188
stats_script.js Normal file
View File

@@ -0,0 +1,188 @@
document.addEventListener('DOMContentLoaded', () => {
fetchStats();
setupModal();
setupSearch();
setupLoadMore();
});
let allPlayers = [];
let displayedPlayers = [];
let currentPage = 1;
const pageSize = 24;
async function fetchStats() {
try {
const response = await fetch('stats/summary.json');
if (!response.ok) throw new Error('Failed to load stats');
const data = await response.json();
allPlayers = data.players;
// Hide loading
document.getElementById('loading-indicator').style.display = 'none';
// Render things
renderLeaderboards();
// Initial Grid Render
displayedPlayers = allPlayers; // Start with all
renderPlayerGrid(true); // reset
} catch (error) {
console.error('Error:', error);
document.getElementById('loading-indicator').innerText = "加载数据失败,请稍后重试。";
}
}
function renderLeaderboards() {
// Helper to sort and slice
const getTop = (key, subKey) => {
return [...allPlayers]
.sort((a, b) => {
let valA = subKey ? a.stats[key] : a.stats[key]; // if structure allows direct access
let valB = subKey ? b.stats[key] : b.stats[key];
// Special case for walk which has raw sorting value
if (key === 'walk_fmt') valA = a.stats.walk_raw;
if (key === 'walk_fmt') valB = b.stats.walk_raw;
return valB - valA;
})
.slice(0, 4); // Top 4
};
const renderCard = (elementId, players, valueFormatter) => {
const container = document.getElementById(elementId);
if (!players || players.length === 0) {
container.innerHTML = '<div class="lb-top-player">暂无数据</div>';
return;
}
const top1 = players[0];
let html = `
<div class="lb-top-player">
<img src="${top1.avatar}" onerror="this.src='https://crafatar.com/avatars/${top1.uuid}?size=64&overlay'">
<div style="font-weight:700; margin-bottom:4px;">${top1.name}</div>
<div class="lb-top-data">${valueFormatter(top1)}</div>
</div>
<div class="lb-list">
`;
for (let i = 1; i < players.length; i++) {
const p = players[i];
html += `
<div class="lb-item">
<div style="display:flex; alignItems:center;">
<span class="lb-rank">${i+1}</span>
<span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100px;">${p.name}</span>
</div>
<span>${valueFormatter(p)}</span>
</div>
`;
}
html += '</div>';
container.innerHTML = html;
};
// 1. Walk (stats.walk_raw)
const topWalkers = getTop('walk_fmt'); // uses walk_raw internally
renderCard('lb-walk', topWalkers, p => p.stats.walk_fmt);
// 2. Placed (stats.placed)
const topPlacers = getTop('placed');
renderCard('lb-placed', topPlacers, p => p.stats.placed.toLocaleString());
// 3. Mined (stats.mined)
const topMiners = getTop('mined');
renderCard('lb-mined', topMiners, p => p.stats.mined.toLocaleString());
// 4. Deaths (stats.deaths)
const topDeaths = getTop('deaths');
renderCard('lb-deaths', topDeaths, p => p.stats.deaths);
}
function renderPlayerGrid(reset = false) {
const grid = document.getElementById('players-grid');
const loadMoreBtn = document.getElementById('load-more-btn');
if (reset) {
grid.innerHTML = '';
currentPage = 1;
}
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
const items = displayedPlayers.slice(start, end);
items.forEach(p => {
const card = document.createElement('div');
card.className = 'player-card';
card.onclick = () => openModal(p);
card.innerHTML = `
<img src="${p.avatar}" onerror="this.src='https://crafatar.com/avatars/${p.uuid}?size=64&overlay'">
<h3>${p.name}</h3>
<!-- <div class="p-uuid">${p.uuid.substring(0,8)}...</div> -->
`;
grid.appendChild(card);
});
if (end >= displayedPlayers.length) {
loadMoreBtn.style.display = 'none';
} else {
loadMoreBtn.style.display = 'inline-block';
}
}
function setupLoadMore() {
document.getElementById('load-more-btn').addEventListener('click', () => {
currentPage++;
renderPlayerGrid(false);
});
}
function setupSearch() {
const input = document.getElementById('player-search');
input.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase().trim();
if (!term) {
displayedPlayers = allPlayers;
} else {
displayedPlayers = allPlayers.filter(p =>
p.name.toLowerCase().includes(term) ||
p.uuid.toLowerCase().includes(term)
);
}
renderPlayerGrid(true);
});
}
// Modal Logic
const modal = document.getElementById("player-modal");
const span = document.getElementsByClassName("close-modal")[0];
function setupModal() {
span.onclick = function() {
modal.style.display = "none";
}
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
}
}
}
function openModal(player) {
document.getElementById('modal-name').innerText = player.name;
document.getElementById('modal-uuid').innerText = player.uuid;
const avatar = document.getElementById('modal-avatar');
avatar.src = player.avatar;
avatar.onerror = () => { avatar.src = `https://crafatar.com/avatars/${player.uuid}?size=64&overlay`; };
document.getElementById('modal-walk').innerText = player.stats.walk_fmt;
document.getElementById('modal-placed').innerText = player.stats.placed.toLocaleString();
document.getElementById('modal-mined').innerText = player.stats.mined.toLocaleString();
document.getElementById('modal-deaths').innerText = player.stats.deaths;
modal.style.display = "block";
}

180
statsprocess.py Normal file
View File

@@ -0,0 +1,180 @@
import os
import json
import requests
import re
import time
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
BASE_URL = "http://x2.sjcmc.cn:15960/stats/"
STATS_DIR = "stats"
IMAGE_DIR = os.path.join(STATS_DIR, "images")
# Ensure directories exist
os.makedirs(STATS_DIR, exist_ok=True)
os.makedirs(IMAGE_DIR, exist_ok=True)
print("Fetching file list...")
try:
response = requests.get(BASE_URL, timeout=10)
response.raise_for_status()
content = response.text
# Regex for UUID.json
files = re.findall(r'href="([0-9a-f-]{36}\.json)"', content)
files = list(set(files))
print(f"Found {len(files)} player stats files.")
except Exception as e:
print(f"Error fetching file list: {e}")
files = []
def get_player_name(uuid):
# Try Ashcon first
try:
r = requests.get(f"https://api.ashcon.app/mojang/v2/user/{uuid}", timeout=5)
if r.status_code == 200:
return r.json().get('username')
except:
pass
# Try Mojang Session
try:
r = requests.get(f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}", timeout=5)
if r.status_code == 200:
return r.json().get('name')
except:
pass
return "Unknown"
def process_player(filename):
uuid = filename.replace(".json", "")
json_path = os.path.join(STATS_DIR, filename)
# img_path = os.path.join(IMAGE_DIR, f"{uuid}.png") # No longer used
print(f"Processing {uuid}...")
# 1. Download/Load JSON
data = None
try:
# Check if we already have it locally and it's valid, maybe skip download?
# User implies fetching updates, so we download.
r = requests.get(BASE_URL + filename, timeout=10)
if r.status_code == 200:
data = r.json()
else:
print(f"Failed to download {filename}")
return None
except Exception as e:
print(f"Error downloading {filename}: {e}")
return None
if not data:
return None
# 2. Get Name
# We can check if name is already in the processing file to avoid API calls if scraping repeatedly?
# For this task, we assume we need to fetch it.
# To save API calls, we could check if we have a saved version with a name.
player_name = "Unknown"
# Check if 'extra' exists in downloaded data (unlikely if strictly from server)
# But checking if we have a local cache of this file with a name is smart
if os.path.exists(json_path):
try:
with open(json_path, 'r', encoding='utf-8') as f:
local_data = json.load(f)
if 'extra' in local_data and local_data['extra'].get('player_name') != "Unknown":
player_name = local_data['extra']['player_name']
except:
pass
if player_name == "Unknown":
player_name = get_player_name(uuid)
# Sleep slightly to be nice to APIs if meaningful massive parallel
time.sleep(0.1)
# 3. Download Avatar - SKIPPED to avoid rate limits
# The frontend will handle dynamic loading of avatars using Minotar/Crafatar URLs.
# 4. Calculate Stats
stats = data.get('stats', {})
# Walk
# Handle both modern ':' and potentially flattened or different versions if necessary,
# but usually proper JSON has "minecraft:custom"
# "minecraft:walk_one_cm"
custom = stats.get('minecraft:custom', {})
walk_cm = custom.get('minecraft:walk_one_cm', 0)
def format_dist(cm):
m = cm / 100
if m < 1000:
return f"{m:.1f} m"
else:
return f"{m/1000:.2f} km"
walk_fmt = format_dist(walk_cm)
# Mined
mined = stats.get('minecraft:mined', {})
total_mined = sum(mined.values())
# Placed (Used)
used = stats.get('minecraft:used', {})
total_placed = sum(used.values())
# Deaths (Killed By)
killed_by = stats.get('minecraft:killed_by', {})
total_deaths = sum(killed_by.values())
# Inject into JSON
data['extra'] = {
'player_name': player_name,
'formatted_walk': walk_fmt,
'walk_cm': walk_cm,
'total_mined': total_mined,
'total_placed': total_placed,
'total_deaths': total_deaths
}
# Save
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
return {
'uuid': uuid,
'name': player_name,
'avatar': f"https://minotar.net/avatar/{player_name}/64" if player_name != "Unknown" else f"https://minotar.net/avatar/{uuid}/64",
'stats': {
'walk_fmt': walk_fmt,
'walk_raw': walk_cm,
'mined': total_mined,
'placed': total_placed,
'deaths': total_deaths
}
}
# Process in parallel
# Reduce max_workers to avoid hitting API limits too hard locally
results = []
with ThreadPoolExecutor(max_workers=4) as executor:
# Process only first 50 for testing? No, user wants all.
# But for a script I am writing for them, I should let them run it.
# I will process ALL found files.
results = list(executor.map(process_player, files))
results = [r for r in results if r is not None]
# Sort by name perhaps? Or just raw list.
results.sort(key=lambda x: x['name'])
summary = {
'updated_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'players': results
}
with open(os.path.join(STATS_DIR, 'summary.json'), 'w', encoding='utf-8') as f:
json.dump(summary, f, ensure_ascii=False, indent=4)
print("Processing complete. Summary saved to stats/summary.json")

707
style.css Normal file
View File

@@ -0,0 +1,707 @@
:root {
--bg-color: #f5f5f7;
--card-bg: #ffffff;
--text-primary: #1d1d1f;
--text-secondary: #86868b;
--accent-color: #0071e3; /* Apple Blue, but we might want Green */
--brand-green: #34c759; /* Apple Green */
--radius-large: 30px;
--radius-medium: 20px;
--transition: all 0.4s cubic-bezier(0.25, 1, 0.5, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", "Noto Sans SC", -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
}
/* Navbar */
.navbar {
position: fixed;
top: 0;
width: 100%;
height: 44px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
z-index: 1000;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
justify-content: center;
}
.nav-content {
width: 100%;
max-width: 1000px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
font-size: 12px;
}
.logo img {
height: 32px;
width: auto;
display: block;
}
.nav-links a {
text-decoration: none;
color: var(--text-primary);
margin-left: 24px;
opacity: 0.8;
transition: opacity 0.2s;
}
.nav-links a:hover {
opacity: 1;
}
.nav-cta {
background: #0071e3;
color: white !important;
padding: 6px 14px;
border-radius: 980px;
font-size: 12px;
font-weight: 500;
opacity: 1 !important;
transition: all 0.2s;
}
.nav-cta:hover {
background: #0077ed;
transform: scale(1.05);
}
/* Hero */
.hero {
height: 80vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding-top: 44px; /* Navbar height */
background-color: #000; /* Fallback */
background-size: cover;
background-position: center;
position: relative;
color: white;
}
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4); /* Dark overlay for text readability */
z-index: 1;
}
.hero-content {
position: relative;
z-index: 2;
}
.hero-title {
font-size: 56px;
line-height: 1.07143;
font-weight: 700;
letter-spacing: -0.005em;
margin-bottom: 10px;
color: white;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
/* Remove gradient text for image background */
background: none;
-webkit-text-fill-color: white;
}
.hero-subtitle {
font-size: 28px;
line-height: 1.10722;
font-weight: 400;
letter-spacing: .004em;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 15px;
text-shadow: 0 2px 5px rgba(0,0,0,0.3);
}
.server-runtime {
font-size: 18px;
color: rgba(255, 255, 255, 0.85);
margin-bottom: 40px;
font-weight: 500;
text-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.hero-actions {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.server-ip-box {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 12px 24px;
border-radius: 980px;
font-size: 17px;
font-weight: 400;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 8px;
position: relative;
}
.server-ip-box:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.02);
}
.ip-hint {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
.tooltip {
position: absolute;
top: -30px;
background: #333;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
opacity: 0;
transition: opacity 0.2s;
}
.tooltip.show { opacity: 1; }
/* Online Status */
.online-status-box {
margin-top: 15px;
position: relative;
cursor: default;
}
.status-indicator {
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
padding: 8px 16px;
border-radius: 20px;
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
transition: var(--transition);
}
.status-dot {
width: 8px;
height: 8px;
background-color: #34c759; /* Green */
border-radius: 50%;
box-shadow: 0 0 8px rgba(52, 199, 89, 0.6);
}
.status-dot.offline {
background-color: #ff3b30; /* Red */
box-shadow: 0 0 8px rgba(255, 59, 48, 0.6);
}
.players-tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 10px;
background: rgba(255, 255, 255, 0.95);
color: #1d1d1f;
padding: 10px;
border-radius: 12px;
width: 200px;
max-height: 300px;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
z-index: 10;
text-align: left;
}
.online-status-box:hover .players-tooltip {
opacity: 1;
visibility: visible;
margin-top: 15px;
}
.players-tooltip::before {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
border-width: 0 6px 6px 6px;
border-style: solid;
border-color: transparent transparent rgba(255, 255, 255, 0.95) transparent;
}
.player-item {
padding: 6px 8px;
font-size: 13px;
border-bottom: 1px solid rgba(0,0,0,0.05);
display: flex;
align-items: center;
gap: 8px;
}
.player-item:last-child {
border-bottom: none;
}
.player-avatar {
width: 16px;
height: 16px;
border-radius: 2px;
}
/* Features Section */
.features-section {
padding: 100px 0;
background: var(--bg-color);
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 0 20px;
}
.section-header {
font-size: 40px;
font-weight: 700;
text-align: center;
margin-bottom: 60px;
}
/* Bento Grid */
.bento-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 180px;
gap: 20px;
}
.bento-item {
background: var(--card-bg);
border-radius: var(--radius-large);
padding: 30px;
display: flex;
flex-direction: column;
justify-content: flex-end; /* Align content to bottom for image cards */
align-items: flex-start; /* Align left */
text-align: left;
transition: var(--transition);
box-shadow: 2px 4px 12px rgba(0,0,0,0.02);
overflow: hidden;
position: relative;
background-size: cover;
background-position: center;
}
.bento-item:hover {
transform: scale(1.02);
box-shadow: 2px 8px 24px rgba(0,0,0,0.06);
}
/* Overlay for Bento Items with Images */
.bento-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,0.6) 100%);
z-index: 1;
}
.bento-content {
position: relative;
z-index: 2;
width: 100%;
}
/* Grid Spans */
.large-item {
grid-column: span 2;
grid-row: span 2;
}
.medium-item {
grid-column: span 2;
grid-row: span 1;
}
.small-item {
grid-column: span 1;
grid-row: span 1;
}
/* Hardware Card (No Image) */
.hardware-card {
background: linear-gradient(135deg, #2c3e50 0%, #000000 100%);
justify-content: center;
align-items: center;
text-align: center;
position: relative;
}
/* Add a subtle tech grid pattern overlay */
.hardware-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
z-index: 1;
}
.hardware-card .bento-content {
color: white;
z-index: 2;
}
.hardware-card .icon {
color: white;
font-size: 32px;
margin-bottom: 10px;
}
.hardware-card h4 {
color: white;
font-size: 17px;
margin: 10px 0 5px;
}
.hardware-card p {
color: rgba(255, 255, 255, 0.8);
font-size: 13px;
}
/* Content Styling for Image Cards */
.bento-item:not(.hardware-card) .icon {
color: white;
font-size: 32px;
margin-bottom: 10px;
}
.bento-item:not(.hardware-card) h3,
.bento-item:not(.hardware-card) h4 {
color: white;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.bento-item:not(.hardware-card) p {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.bento-item h3 {
font-size: 24px;
margin-bottom: 10px;
}
.small-item h4 {
font-size: 17px;
margin: 10px 0 5px;
}
.small-item p {
font-size: 13px;
}
/* Details Section */
.details-section {
padding: 100px 0;
background: #fff;
}
.detail-row {
margin-bottom: 80px;
text-align: center;
}
.detail-text h3 {
font-size: 32px;
margin-bottom: 15px;
}
.detail-text p {
font-size: 19px;
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
}
/* Footer */
footer {
background: var(--bg-color);
padding: 40px 0;
border-top: 1px solid #e5e5e5;
font-size: 12px;
color: var(--text-secondary);
}
/* Responsive */
@media (max-width: 900px) {
.bento-grid {
grid-template-columns: 1fr;
grid-auto-rows: auto;
}
.large-item, .medium-item, .small-item {
grid-column: span 1;
grid-row: auto;
min-height: 250px;
}
.hero-title {
font-size: 40px;
}
}
/* Sponsors Section */
.sponsors-section {
padding: 80px 0;
background: #fff;
text-align: center;
}
.top-sponsors-grid {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 40px;
flex-wrap: wrap;
}
.sponsor-card {
background: var(--bg-color);
border-radius: var(--radius-medium);
padding: 30px;
width: 250px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: var(--transition);
display: flex;
flex-direction: column;
align-items: center;
}
.sponsor-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
.sponsor-rank {
font-size: 48px;
margin-bottom: 10px;
}
.sponsor-name {
font-size: 20px;
font-weight: 600;
margin-bottom: 5px;
color: var(--text-primary);
}
.sponsor-amount {
font-size: 16px;
color: var(--accent-color);
font-weight: 500;
}
.view-sponsors-btn {
background: var(--text-primary);
color: white;
border: none;
padding: 12px 30px;
border-radius: 980px;
font-size: 16px;
cursor: pointer;
transition: var(--transition);
}
.view-sponsors-btn:hover {
background: #000;
transform: scale(1.05);
}
/* Crowdfunding Section */
.crowdfunding-section {
padding: 80px 0;
background: var(--bg-color); /* Or white, depending on contrast preference */
}
.crowdfunding-grid {
display: flex;
flex-direction: column;
gap: 30px;
max-width: 800px;
margin: 0 auto;
}
.fund-card {
background: #fff;
border-radius: var(--radius-medium);
padding: 30px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: var(--transition);
}
.fund-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
.fund-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 15px;
}
.fund-title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.fund-stats {
font-size: 14px;
color: var(--text-secondary);
}
.fund-stats span {
font-weight: 600;
color: var(--text-primary);
}
.progress-bar-bg {
width: 100%;
height: 12px;
background-color: #f0f0f0;
border-radius: 6px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #0071e3, #34c759);
border-radius: 6px;
width: 0%; /* Will be set by JS */
transition: width 1s ease-out;
}
.fund-percentage {
text-align: right;
font-size: 12px;
color: var(--text-secondary);
margin-top: 8px;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
backdrop-filter: blur(5px);
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #fff;
padding: 40px;
border-radius: var(--radius-large);
width: 90%;
max-width: 800px;
max-height: 80vh;
position: relative;
display: flex;
flex-direction: column;
}
.close-modal {
position: absolute;
top: 20px;
right: 30px;
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
}
.close-modal:hover {
color: #000;
}
.modal-content h2 {
margin-bottom: 20px;
text-align: center;
}
.sponsors-list-container {
overflow-y: auto;
flex-grow: 1;
}
#sponsors-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
#sponsors-table th, #sponsors-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
#sponsors-table th {
background-color: #f9f9f9;
font-weight: 600;
position: sticky;
top: 0;
}
#sponsors-table tr:hover {
background-color: #f5f5f7;
}