mirror of
https://github.com/Coldsmiles/infstarweb.git
synced 2026-04-23 02:30:41 +08:00
init
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/stats
|
||||
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
1
fund_progress.txt
Normal file
1
fund_progress.txt
Normal file
@@ -0,0 +1 @@
|
||||
服务器升级M4 Mac Mini 32G,2620,5000
|
||||
214
index.html
Normal file
214
index.html
Normal 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">×</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>© 2025 白鹿原 Minecraft 服务器.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
717
pigwei.html
Normal file
717
pigwei.html
Normal 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
258
script.js
Normal 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
36
sponsors.txt
Normal 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
376
stats.html
Normal 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">×</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>© 2025 白鹿原 Minecraft 服务器.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="stats_script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
188
stats_script.js
Normal file
188
stats_script.js
Normal 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
180
statsprocess.py
Normal 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
707
style.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user