feat: initialize Vue application with main components and styles

- Added App.vue as the main application component with a structured layout.
- Created main.js to bootstrap the Vue application and mount it to the DOM.
- Introduced styles.css for global styling, including responsive design and theming.
- Removed outdated HTML files (stats.html, towns.html) and Python script (statsprocess.py) as part of the migration to a new Vue-based architecture.
- Added Vite configuration (vite.config.js) for building the Vue application.
This commit is contained in:
zhangyuheng
2026-03-18 10:07:03 +08:00
parent 3c65860619
commit 124b545ee2
51 changed files with 1671 additions and 14130 deletions

View File

@@ -1,68 +0,0 @@
# Project Guidelines
## Code Style
- This repo is framework-free: plain HTML + CSS + vanilla JavaScript — no frameworks, modules, or build tooling.
- Keep existing naming style: `camelCase` for JS functions/variables and descriptive DOM ids/classes (for example `fetchCrowdfunding`, `setupMobileMenu`, `#players-grid`).
- Preserve current formatting patterns: 4-space indentation in HTML/CSS, simple function-based JS.
- Shared JS utilities use the global-object pattern (e.g. `DataUtils` in `js/data_utils.js`) — do not convert to ES modules.
- Reuse shared tokens in `css/style.css` (`:root` variables such as `--bg-color`, `--accent-color`) instead of introducing new ad-hoc styles.
- Keep page-local style overrides scoped to their page blocks; avoid broad visual refactors unless explicitly requested.
- Keep user-facing copy in Chinese unless the surrounding section is already English.
## Architecture
- Public pages are static entry points:
- `index.html` + `js/script.js`: landing page, sponsors, fundraising progress, live server status.
- `sponsor.html` + `js/sponsor_script.js`: donation total, sponsor list, search/filter, donation modal.
- `stats.html` + `js/stats_script.js`: player leaderboard + searchable player cards + modal details.
- `join.html` + `js/join_script.js`: 4-step wizard (convention → agree → device → tutorial). Uses `js/marked.min.js` to render `data/convention.md` as HTML.
- `facilities.html` + `js/facilities_script.js`: searchable/filterable catalog of server facilities from `data/facilities.json`. Supports Bilibili video embeds (BV IDs) in notes.
- `doc.html`, `map.html`, `photo.html`: iframe wrappers (navbar + fullscreen iframe to external hosts). No page-specific JS beyond `js/components.js`.
- Shared utilities:
- `js/components.js`: injected into `#navbar-component` / `#footer-component`, handles mobile menu & current-link highlighting.
- `js/data_utils.js`: `DataUtils.parseSponsorsText()` and `DataUtils.buildSponsorTotals()` — used by both `index.html` and `sponsor.html`.
- Shared visual system lives in `css/style.css`; page-specific styles live in `css/pages/` (`join.css`, `facilities.css`, `sponsor.css`, `stats.css`).
- Every page includes `<script type="application/ld+json">` Schema.org metadata and full OG/Twitter Card meta tags.
- Data flow for stats:
1. `statsprocess.py` fetches raw player JSON files and writes normalized outputs to `stats/`.
2. It generates `stats/summary.json` consumed by `stats_script.js` via `fetch('stats/summary.json')`.
3. `stats_script.js` lazy-loads `stats/{uuid}.json` when opening player details.
## Build and Test
- No package manager/build step exists in this workspace.
- Local static preview:
- `python3 -m http.server 8000`
- open `http://localhost:8000/`
- Regenerate player summary data:
- `python3 statsprocess.py` (requires `STATS_BASE_URL`, `STATS_USER`, `STATS_PASS` env vars for remote authentication).
- Python script dependencies are runtime imports in `statsprocess.py` (not pinned): `requests`, `tqdm`.
- CI/CD: `.github/workflows/deploy.yml` runs `statsprocess.py` then deploys to GitHub Pages on push to `main`, daily cron, or manual dispatch. Secrets live in GitHub Actions — never commit credentials.
## Data Contracts
- `data/sponsors.txt`: comma-separated fields (`name, project, amount, [date]`), parsed by `DataUtils.parseSponsorsText()`.
- `data/fund_progress.txt`: line-parsed by frontend scripts.
- `data/facilities.json`: array of `{title, intro, type, dimension, status, coordinates:{x,y,z}, contributors:[], instructions:[{type,content}], notes:[{type,content}]}`. Notes with `type:"video"` use Bilibili BV IDs.
- `data/convention.md`: Markdown server rules, rendered via marked.js in join wizard.
- `stats/summary.json` and `stats/{uuid}.json`: generated by `statsprocess.py` — do not hand-edit.
## Project Conventions
- Prefer progressive enhancement with `DOMContentLoaded` initializers (all page scripts and `components.js`).
- Keep network fetch paths relative for local assets.
- For unavailable remote APIs, follow existing behavior: log errors and render fallback text instead of throwing.
- Iframe wrapper pages (`doc.html`, `map.html`, `photo.html`) share an identical pattern: navbar + inline-styled fullscreen iframe. Follow this pattern for new external-embed pages.
- Do not introduce bundlers/framework migrations unless explicitly requested.
## Integration Points
- External APIs/services:
- Server status: `https://api.mcstatus.io/v2/status/java/mcpure.lunadeer.cn`
- Avatars: `https://minotar.net/...` and `https://crafatar.com/...`
- Player name resolution in pipeline: Ashcon + Mojang Session APIs (`statsprocess.py`).
- Stats source endpoint: authenticated, URL in `STATS_BASE_URL` secret.
- External iframe hosts: `schema.lunadeer.cn` (docs), `mcmap.lunadeer.cn` (map), `mcphoto.lunadeer.cn` (photo).
- Navbar external links are centralized in `js/components.js`.
- External assets: Google Fonts, Font Awesome (CDN), Bilibili embeds (facilities notes).
## Security
- Treat player UUID/name datasets in `stats/` as production content; avoid destructive bulk edits.
- Preserve SEO/verification metadata in page `<head>` blocks unless a task explicitly targets SEO.
- Avoid adding secrets/tokens to repository files; keep any future credentials out of static HTML/JS.
- Frontend rendering uses `innerHTML` with fetched txt/json content in several views; keep those data sources trusted or sanitize before accepting untrusted input.

View File

@@ -4,8 +4,7 @@ on:
push:
branches: [main]
schedule:
# Every days at 04:00 UTC
- cron: "0 4 */1 * *"
- cron: "0 4 * * *"
workflow_dispatch:
permissions:
@@ -24,35 +23,37 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install frontend dependencies
run: npm ci
- name: Install Python dependencies
run: pip install requests tqdm
- name: Run statsprocess.py
- name: Update player stats
env:
STATS_BASE_URL: ${{ secrets.STATS_BASE_URL }}
STATS_USER: ${{ secrets.STATS_USER }}
STATS_PASS: ${{ secrets.STATS_PASS }}
run: python statsprocess.py
run: python scripts/statsprocess.py
- name: Prepare Pages artifact
run: |
mkdir -p _site
rsync -a --delete \
--exclude '.git/' \
--exclude '.github/' \
--exclude 'README.md' \
--exclude 'statsprocess.py' \
./ ./_site/
- name: Build site
run: npm run build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./_site
path: ./dist
deploy:
needs: build
@@ -71,7 +72,6 @@ jobs:
if: success()
steps:
- name: Submit URLs to IndexNow
if: ${{ env.INDEXNOW_KEY != '' }}
env:
INDEXNOW_KEY: ${{ secrets.INDEXNOW_KEY }}
run: |
@@ -79,6 +79,7 @@ jobs:
echo "INDEXNOW_KEY not set, skipping."
exit 0
fi
HOST="bailuyuan.lunadeer.cn"
curl -s -X POST "https://api.indexnow.org/indexnow" \
-H "Content-Type: application/json" \
@@ -87,14 +88,7 @@ jobs:
\"key\": \"${INDEXNOW_KEY}\",
\"keyLocation\": \"https://${HOST}/${INDEXNOW_KEY}.txt\",
\"urlList\": [
\"https://${HOST}/\",
\"https://${HOST}/sponsor.html\",
\"https://${HOST}/stats.html\",
\"https://${HOST}/join.html\",
\"https://${HOST}/facilities.html\",
\"https://${HOST}/doc.html\",
\"https://${HOST}/map.html\",
\"https://${HOST}/photo.html\"
\"https://${HOST}/\"
]
}"
echo ""

16
.gitignore vendored
View File

@@ -1 +1,15 @@
/stats
node_modules/
dist/
.vite/
coverage/
old-html-ver/
public/stats/*.json
*.log
*.local
__pycache__/
*.py[cod]
.DS_Store
Thumbs.db

206
README.md
View File

@@ -1,206 +0,0 @@
# 白鹿原 Minecraft 服务器官网
这是白鹿原 Minecraft 服务器的静态官网仓库,使用原生 HTML、CSS 和 JavaScript 构建,无前端框架、无打包流程。
站点主要用于展示服务器介绍、加入指引、共享设施、赞助信息、玩家统计等内容,并通过 GitHub Pages 自动部署。
## 在线预览
- 正式站点https://bailuyuan.lunadeer.cn/
## 功能概览
- 首页:服务器介绍、运行时长、在线状态、众筹进度、赞助榜
- 加入指引:分步骤展示服务器公约、设备选择、启动教程
- 共享设施:展示全服公共设施,支持搜索、筛选和详情弹窗
- 赞助页:展示赞助名单、累计金额、筛选和赞助弹窗
- 数据中心:展示玩家排行榜、玩家卡片、详细统计弹窗
- 其他页面:文档、地图、相册等导航入口
## 技术栈
- HTML5
- CSS3
- Vanilla JavaScript
- Python 3用于生成玩家统计数据
- GitHub Actions + GitHub Pages自动部署
## 目录结构
```text
.
├── index.html # 首页
├── join.html # 加入游戏指引
├── facilities.html # 共享设施页
├── sponsor.html # 赞助页
├── stats.html # 玩家数据页
├── doc.html # 文档入口
├── map.html # 地图入口
├── photo.html # 相册入口
├── css/
│ ├── style.css # 全站公共样式
│ └── pages/ # 页面级样式
├── js/
│ ├── components.js # 导航栏、页脚、移动端菜单
│ ├── data_utils.js # 文本数据解析工具
│ ├── script.js # 首页逻辑
│ ├── join_script.js # 加入指引逻辑
│ ├── facilities_script.js# 设施页逻辑
│ ├── sponsor_script.js # 赞助页逻辑
│ └── stats_script.js # 数据页逻辑
├── data/
│ ├── convention.md # 服务器公约
│ ├── facilities.json # 公共设施数据
│ ├── fund_progress.txt # 众筹进度数据
│ └── sponsors.txt # 赞助数据
├── stats/ # 玩家统计明细与 summary.json
├── statsprocess.py # 拉取并生成统计数据
└── .github/workflows/
└── deploy.yml # GitHub Pages 自动部署工作流
```
## 页面与脚本对应关系
| 页面 | 入口文件 | 脚本 | 说明 |
| --- | --- | --- | --- |
| 首页 | `index.html` | `js/script.js` | 服务器介绍、在线状态、赞助榜、众筹 |
| 加入指引 | `join.html` | `js/join_script.js` | 加入流程、公约加载、设备指引 |
| 共享设施 | `facilities.html` | `js/facilities_script.js` | 设施检索、筛选、详情弹窗 |
| 赞助页 | `sponsor.html` | `js/sponsor_script.js` | 赞助统计、搜索筛选、赞助弹窗 |
| 数据中心 | `stats.html` | `js/stats_script.js` | 排行榜、玩家搜索、详情数据加载 |
公共导航和页脚由 `js/components.js` 注入到 `#navbar-component``#footer-component`
## 本地开发
这个项目没有构建步骤,直接启动静态文件服务即可。
### 1. 启动本地预览
```bash
python3 -m http.server 8000
```
然后访问:
```text
http://localhost:8000/
```
### 2. 开发时的注意事项
- 不要直接双击打开 HTML 文件,部分 `fetch()` 读取本地数据会被浏览器拦截
- 页面资源路径均为相对路径,建议始终从仓库根目录启动静态服务
- 站点是纯静态结构,修改后刷新浏览器即可验证效果
## 数据文件说明
### `data/convention.md`
加入游戏页面会读取这份 Markdown 并渲染为服务器公约内容。
### `data/fund_progress.txt`
首页读取众筹进度数据,按行解析。每行格式为:
```text
项目名称,当前金额,目标金额
```
示例:
```text
新机器升级,1200,3000
```
### `data/sponsors.txt`
首页和赞助页都会读取赞助文本数据,格式为逗号分隔:
```text
赞助者,项目,金额,日期
```
其中日期字段为可选。
### `data/facilities.json`
共享设施页面的数据源,用于渲染设施卡片、筛选项和详情弹窗。
## 玩家统计数据流程
玩家统计不是前端实时计算,而是由 `statsprocess.py` 预生成静态 JSON 文件。
### 数据来源
- 源统计接口:将服务器玩家的存档数据目录通过 HTTP 暴露,路径一般为 `your-server/world/stats`
- 玩家名解析Ashcon API、Mojang Session API
- 头像展示Minotar / Crafatar
### 生成结果
脚本会完成以下工作:
1. 拉取远端玩家 JSON 数据
2. 解析玩家名
3. 计算行走距离、游玩时长、挖掘数、放置数、死亡数、击杀数
4. 将单个玩家明细写入 `stats/{uuid}.json`
5. 汇总生成 `stats/summary.json`
### 本地更新统计数据
先安装依赖:
```bash
pip install requests tqdm
```
源接口需要配置认证,再设置环境变量:
```bash
export STATS_BASE_URL=http://your-server/stats
export STATS_USER=your_username
export STATS_PASS=your_password
```
执行更新:
```bash
python3 statsprocess.py
```
## 自动部署
仓库包含 GitHub Pages 自动部署工作流:`.github/workflows/deploy.yml`
触发条件:
- 推送到 `main`
- 每 2 天自动执行一次UTC 04:00
- 手动触发 `workflow_dispatch`
工作流会执行以下步骤:
1. 检出仓库
2. 安装 Python
3. 安装 `requests``tqdm`
4. 运行 `statsprocess.py`
5. 上传站点文件到 GitHub Pages
如果统计接口需要认证,需要在 GitHub 仓库 Secrets 中配置:
- `STATS_USER`
- `STATS_PASS`
## 维护建议
- 修改导航结构时,优先更新 `js/components.js`
- 修改公共视觉变量时,优先调整 `css/style.css`
- 新增页面样式时,放到 `css/pages/` 下单独维护
- 前端数据读取依赖固定格式,修改 `data/` 下文本文件时不要破坏既有约定
- `stats/` 目录是生成产物,同时也是站点线上数据的一部分,批量处理前先确认影响
## 许可证
[GPL-3.0 License](LICENSE)

View File

@@ -1,189 +0,0 @@
<!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>
<meta name="description" content="白鹿原Minecraft服务器活动公告了解最新的服务器活动、维护通知和重要公告信息。">
<meta name="keywords" content="Minecraft公告,MC活动,白鹿原公告,服务器活动,维护通知">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/announcements.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/announcements.html">
<meta property="og:title" content="活动公告 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="白鹿原Minecraft服务器活动公告了解最新的服务器活动、维护通知和重要公告信息。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/announcements.html">
<meta property="twitter:title" content="活动公告 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="白鹿原Minecraft服务器活动公告了解最新的服务器活动、维护通知和重要公告信息。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/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">
<link rel="stylesheet" href="css/pages/announcements.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "活动公告",
"description": "白鹿原Minecraft服务器活动公告",
"url": "https://mcpure.lunadeer.cn/announcements.html",
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
</head>
<body>
<div id="navbar-component"></div>
<!-- Hero Section -->
<header id="hero-component" data-title="活动公告" data-subtitle="了解服务器最新动态、活动安排与维护通知。" data-class="announcements-hero-bg"></header>
<div class="announcements-container">
<!-- Controls -->
<div class="controls-section">
<div class="controls-header-row">
<div class="title-with-action">
<h2 class="section-title">公告列表</h2>
<button class="btn-add-announcement edit-hidden" id="btn-add-announcement">
<i class="fas fa-plus"></i> 新增公告
</button>
</div>
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="announcement-search" placeholder="搜索公告标题或简介...">
</div>
</div>
<div class="filters-wrapper">
<div class="filter-group">
<div class="filter-label"><i class="fas fa-tag"></i> 类别</div>
<div class="filter-tags" id="category-filters">
<button class="filter-tag active" data-filter="all">全部</button>
<button class="filter-tag" data-filter="activity"><i class="fas fa-calendar-check"></i> 活动</button>
<button class="filter-tag" data-filter="maintenance"><i class="fas fa-wrench"></i> 维护</button>
<button class="filter-tag" data-filter="other"><i class="fas fa-info-circle"></i> 其他</button>
</div>
</div>
</div>
</div>
<!-- Timeline -->
<div class="timeline" id="announcements-timeline">
<!-- JS will inject timeline items here -->
</div>
<div id="no-results" class="no-results-message is-hidden">
没有找到匹配的公告
</div>
</div>
<!-- Editor Modal -->
<div id="editor-modal" class="modal">
<div class="modal-content editor-modal-content">
<span class="close-editor-modal">&times;</span>
<div class="editor-modal-header">
<h3><i class="fas fa-bullhorn"></i> 公告编辑器</h3>
</div>
<div class="editor-layout">
<!-- Left: Preview -->
<div class="editor-preview">
<div class="editor-panel-title"><i class="fas fa-eye"></i> 实时预览</div>
<div class="editor-preview-content" id="editor-preview-area"></div>
</div>
<!-- Right: Editor Form -->
<div class="editor-form">
<div class="editor-panel-title"><i class="fas fa-edit"></i> 编辑内容</div>
<div class="editor-form-scroll">
<div class="form-group">
<label for="editor-title">公告标题</label>
<input type="text" id="editor-title" placeholder="输入公告标题...">
</div>
<div class="form-group">
<label for="editor-intro">简介</label>
<textarea id="editor-intro" placeholder="输入公告简介..." rows="2"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="editor-time">时间</label>
<input type="date" id="editor-time">
</div>
<div class="form-group">
<label>类别</label>
<div class="custom-select">
<input type="hidden" id="editor-category" value="activity">
<div class="custom-select-trigger">
<span class="custom-select-text">活动</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="custom-select-options">
<div class="custom-option selected" data-value="activity">活动</div>
<div class="custom-option" data-value="maintenance">维护</div>
<div class="custom-option" data-value="other">其他</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label>正文内容</label>
<div class="sortable-list" id="editor-content-list"></div>
<div class="add-item-row">
<button type="button" class="add-item-btn" data-type="text">
<i class="fas fa-plus"></i> 添加文字
</button>
<button type="button" class="add-item-btn" data-type="image">
<i class="fas fa-image"></i> 添加图片
</button>
<button type="button" class="add-item-btn" data-type="video">
<i class="fas fa-video"></i> 添加视频
</button>
</div>
</div>
<div class="editor-actions">
<button type="button" class="btn-save-announcement" id="btn-save-announcement">
<i class="fas fa-save"></i> 生成 JSON
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JSON Output Modal -->
<div id="json-output-modal" class="modal">
<div class="modal-content json-output-content">
<span class="close-json-modal">&times;</span>
<h3><i class="fas fa-code"></i> 生成完成</h3>
<p class="json-output-hint">请复制以下 JSON 内容,更新到 data/announcements.json 文件中。</p>
<textarea id="json-output" readonly></textarea>
<button type="button" class="btn-copy-json" id="btn-copy-json">
<i class="fas fa-copy"></i> 复制到剪贴板
</button>
</div>
</div>
<div id="footer-component"></div>
<script src="js/components.js"></script>
<script src="js/announcements_script.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,599 +0,0 @@
.sponsor-hero {
padding: 140px 20px 50px;
text-align: center;
background: radial-gradient(circle at center, rgba(0,113,227,0.08) 0%, rgba(255,255,255,0) 70%);
position: relative;
overflow: hidden;
}
.sponsor-hero h1 {
font-size: 56px;
font-weight: 800;
margin-bottom: 24px;
background: linear-gradient(135deg, #1d1d1f 0%, #434344 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: 18px;
color: var(--text-secondary);
margin-top: 16px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.total-donations {
display: inline-flex;
flex-direction: column;
align-items: center;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
padding: 20px 40px;
border-radius: var(--radius-large);
box-shadow: 0 10px 40px rgba(0,0,0,0.06);
border: 1px solid rgba(255,255,255,0.6);
transform: translateY(0);
transition: transform 0.3s ease;
}
.total-donations:hover {
transform: translateY(-5px);
}
.counter-label {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary);
margin-bottom: 8px;
font-weight: 600;
}
.counter-value {
font-size: 42px;
font-weight: 800;
color: var(--brand-green);
font-feature-settings: "tnum";
font-variant-numeric: tabular-nums;
}
.sponsor-container {
max-width: 1100px;
margin: 0 auto;
padding: 40px 20px;
}
.section-title {
font-size: 28px;
font-weight: 700;
margin-bottom: 30px;
text-align: center;
}
.sponsor-list-title {
margin-bottom: 10px;
}
/* Top Donors Podium */
.top-donors-section {
margin-bottom: 60px;
}
.podium-container {
display: flex;
justify-content: center;
align-items: flex-end;
gap: 20px;
padding: 40px 0;
min-height: 300px;
}
.podium-spot {
background: white;
border-radius: var(--radius-medium);
padding: 20px;
width: 220px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
position: relative;
transition: transform 0.3s ease;
text-align: center;
}
.podium-spot:hover {
transform: translateY(-10px);
z-index: 2;
}
.podium-rank {
width: 40px;
height: 40px;
background: #f0f0f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
margin-bottom: 12px;
position: absolute;
top: -20px;
border: 4px solid var(--bg-color);
}
/* Gold */
.podium-spot.rank-1 {
height: 260px;
border: 2px solid rgba(255, 215, 0, 0.3);
background: linear-gradient(180deg, rgba(255,215,0,0.05) 0%, rgba(255,255,255,1) 100%);
order: 2;
}
.podium-spot.rank-1 .podium-rank {
background: #FFD700;
color: #fff;
box-shadow: 0 4px 10px rgba(255, 215, 0, 0.4);
}
.podium-spot.rank-1 .donor-avatar {
border-color: #FFD700;
width: 80px;
height: 80px;
}
/* Silver */
.podium-spot.rank-2 {
height: 220px;
border: 2px solid rgba(192, 192, 192, 0.3);
order: 1;
}
.podium-spot.rank-2 .podium-rank {
background: #C0C0C0;
color: #fff;
}
/* Bronze */
.podium-spot.rank-3 {
height: 200px;
border: 2px solid rgba(205, 127, 50, 0.3);
order: 3;
}
.podium-spot.rank-3 .podium-rank {
background: #CD7F32;
color: #fff;
}
.donor-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
border: 3px solid transparent;
margin-bottom: 12px;
background-color: #f0f0f0;
object-fit: cover;
}
.podium-name {
font-weight: 700;
font-size: 18px;
margin-bottom: 4px;
word-break: break-all;
}
.podium-total {
color: var(--brand-green);
font-weight: 600;
font-size: 16px;
}
/* Controls */
.controls-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
margin-bottom: 40px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.controls-header {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 24px;
}
/* Mobile adjustment for header */
@media (max-width: 600px) {
.controls-header {
flex-direction: column;
}
}
.cta-button {
height: 48px;
padding: 0 24px;
background-color: var(--text-primary);
color: white;
border-radius: 99px;
text-decoration: none;
font-weight: 600;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
cursor: pointer;
border: 1px solid transparent;
white-space: nowrap;
box-sizing: border-box;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.cta-button.outline {
background-color: transparent;
color: var(--text-primary);
border: 1px solid rgba(0,0,0,0.1);
}
.cta-button.outline:hover {
border-color: var(--text-primary);
background-color: white;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
backdrop-filter: blur(5px);
opacity: 0;
transition: opacity 0.3s ease;
}
.modal.show {
display: flex;
opacity: 1;
align-items: center;
justify-content: center;
}
.modal-content {
background-color: white;
margin: auto;
padding: 40px;
border-radius: 24px;
width: 90%;
max-width: 400px;
position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
transform: scale(0.9);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
text-align: center;
}
.modal.show .modal-content {
transform: scale(1);
}
.close-modal {
position: absolute;
top: 20px;
right: 20px;
color: #aaa;
font-size: 24px;
cursor: pointer;
width: 32px;
height: 32px;
background: #f5f5f7;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.close-modal:hover {
color: #000;
background: #e5e5e7;
}
.modal-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.modal-gift-icon {
width: 50px;
height: 50px;
background: rgba(52, 199, 89, 0.1);
border-radius: 50%;
color: var(--brand-green);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin: 0 auto 20px;
}
.modal-subtitle {
color: var(--text-secondary);
font-size: 14px;
margin-bottom: 30px;
line-height: 1.5;
}
.qr-placeholder {
background: #f9f9f9;
padding: 20px;
border-radius: 16px;
display: inline-block;
margin-bottom: 20px;
}
.qr-img {
width: 180px;
height: 180px;
display: block;
}
.desktop-qr-hint {
font-size: 13px;
color: #999;
}
.alipay-btn {
background: #1677FF;
color: white;
display: inline-block;
width: 100%;
padding: 14px;
border-radius: 12px;
font-weight: 600;
text-decoration: none;
margin-top: 10px;
}
#mobile-btn-view {
display: none;
}
.mobile-pay-hint {
font-size: 12px;
color: #999;
margin-top: 16px;
}
.search-box {
position: relative;
flex-grow: 0;
width: 100%;
max-width: 320px;
}
.search-box input {
width: 100%;
height: 48px;
padding: 0 20px 0 44px;
border-radius: 99px;
border: 1px solid rgba(0,0,0,0.1);
background: white;
font-size: 15px;
outline: none;
transition: all 0.2s;
}
.search-box input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0,113,227,0.1);
}
.search-box i {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: var(--text-secondary);
}
.filter-tags {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.filter-tag {
padding: 8px 16px;
border-radius: 99px;
border: none;
background: white;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
}
.filter-tag:hover {
transform: translateY(-1px);
background: #fafafa;
}
.filter-tag.active {
background: var(--text-primary);
color: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.donation-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.no-results-message {
text-align: center;
color: var(--text-secondary);
padding: 40px;
}
.sponsor-load-error {
text-align: center;
padding: 40px;
color: var(--text-secondary);
grid-column: 1 / -1;
}
.donation-card-body {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.donation-date-icon {
margin-right: 4px;
}
.is-hidden {
display: none;
}
.donation-card {
background: white;
padding: 24px;
border-radius: var(--radius-medium);
transition: var(--transition);
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
overflow: hidden;
border: 1px solid rgba(0,0,0,0.03);
animation: fadeInUp 0.5s ease backwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.donation-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
border-color: transparent;
}
.donation-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.donor-info {
display: flex;
align-items: center;
gap: 12px;
}
.mini-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #f0f0f0;
}
.donor-name {
font-weight: 700;
font-size: 16px;
line-height: 1.2;
}
.donation-amount {
color: var(--brand-green);
font-weight: 800;
font-size: 18px;
background: rgba(52, 199, 89, 0.1);
padding: 4px 10px;
border-radius: 8px;
}
.donation-purpose {
font-size: 13px;
color: var(--text-primary);
background: var(--bg-color);
padding: 6px 12px;
border-radius: 6px;
display: inline-block;
margin-bottom: 12px;
align-self: flex-start;
}
.donation-date {
font-size: 12px;
color: #999;
text-align: right;
margin-top: auto;
border-top: 1px solid rgba(0,0,0,0.05);
padding-top: 12px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.sponsor-hero h1 {
font-size: 32px;
}
.counter-value {
font-size: 32px;
}
.podium-container {
flex-direction: column;
align-items: center;
gap: 30px;
}
.podium-spot {
width: 100%;
max-width: 300px;
order: initial !important;
height: auto !important;
}
.podium-rank {
position: relative;
top: 0;
margin-top: -30px;
}
}

View File

@@ -1,568 +0,0 @@
/* Specific styles for stats page override/additions */
.stats-hero-bg {
background-image: url('https://img.lunadeer.cn/i/2025/11/26/69267755e14e3.png');
}
.stats-main-section {
background: var(--bg-color);
}
.stats-updated-at {
text-align: center;
color: var(--text-secondary);
font-size: 14px;
margin-top: -45px;
margin-bottom: 40px;
}
.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-card.purple {
border-top: 4px solid #9b59b6;
}
.lb-card.kill-red {
border-top: 4px solid #e74c3c;
}
.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-top-name {
font-weight: 700;
margin-bottom: 4px;
}
.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-main {
display: flex;
align-items: center;
}
.lb-item-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100px;
}
.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;
}
.loading-details-text {
text-align: center;
padding: 20px;
color: #888;
}
.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);
}
/* Modal Redesign */
.modal-content.expanded-modal {
max-width: 800px;
width: 90%;
display: flex;
flex-direction: column;
max-height: 90vh;
overflow-y: auto;
}
/* Custom Scrollbar for Modal */
.modal-content.expanded-modal::-webkit-scrollbar {
width: 6px;
}
.modal-content.expanded-modal::-webkit-scrollbar-track {
background: transparent;
margin: 10px 0;
}
.modal-content.expanded-modal::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
border-radius: 10px;
}
.modal-content.expanded-modal::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
}
.modal-top-section {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.modal-identity {
flex: 1;
min-width: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.modal-identity img {
width: 100px;
height: 100px;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
margin-bottom: 15px;
}
#modal-name {
margin: 5px 0;
font-size: 24px;
}
#modal-uuid {
font-size: 12px;
color: #999;
font-family: monospace;
word-break: break-all;
}
.stats-list-container.compact-stats {
flex: 1;
min-width: 250px;
display: flex;
flex-direction: column;
justify-content: center;
background: #f9f9f9;
padding: 15px;
border-radius: 12px;
}
.modal-divider {
border: 0;
border-top: 1px solid #eee;
margin: 10px 0 20px;
}
/* Accordion Styles */
.accordion {
display: flex;
flex-direction: column;
gap: 10px;
}
.accordion-item {
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
}
.accordion-header {
background: #fdfdfd;
padding: 12px 15px;
cursor: pointer;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
transition: background 0.2s;
}
.accordion-header:hover {
background: #f0f0f0;
}
.accordion-header .icon {
margin-right: 8px;
width: 20px;
text-align: center;
color: var(--text-secondary);
}
.accordion-header .arrow {
transition: transform 0.3s;
}
.accordion-header.active .arrow {
transform: rotate(180deg);
}
.accordion-header.active {
background: #f0f0f0;
color: var(--accent-color);
}
.accordion-content {
display: none;
padding: 15px;
background: white;
border-top: 1px solid #eee;
}
.accordion-content.show {
display: block;
}
/* Item count badge in accordion header */
.accordion-header .item-count {
font-size: 11px;
font-weight: 500;
color: var(--text-secondary);
background: var(--bg-color);
padding: 2px 8px;
border-radius: 10px;
margin-left: 8px;
}
.accordion-header.active .item-count {
background: rgba(0, 113, 227, 0.08);
color: var(--accent-color);
}
/* Search filter inside accordion content */
.detail-search-wrapper {
position: relative;
margin-bottom: 12px;
}
.detail-search-wrapper i {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: #c0c0c0;
font-size: 12px;
pointer-events: none;
}
.detail-search {
width: 100%;
padding: 8px 12px 8px 30px;
border: 1px solid #eee;
border-radius: 8px;
font-size: 13px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
background: #fafafa;
}
.detail-search:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(0, 113, 227, 0.08);
background: white;
}
.detail-no-results {
text-align: center;
color: var(--text-secondary);
font-size: 13px;
padding: 20px 0;
display: none;
}
/* Grid for stats inside accordion */
.detail-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
}
.detail-stat-item {
display: flex;
flex-direction: column;
background: #fafafa;
border-radius: 8px;
padding: 10px 12px;
border-left: 3px solid transparent;
transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
cursor: default;
}
.detail-stat-item:hover {
background: #f0f4ff;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
/* Top 3 ranking indicators */
.detail-stat-item.rank-1 {
border-left-color: #FFD700;
background: linear-gradient(135deg, #fffdf0, #fafafa);
}
.detail-stat-item.rank-2 {
border-left-color: #C0C0C0;
background: linear-gradient(135deg, #fafafa, #f8f8f8);
}
.detail-stat-item.rank-3 {
border-left-color: #CD7F32;
background: linear-gradient(135deg, #fdf8f4, #fafafa);
}
.detail-stat-item.rank-1:hover,
.detail-stat-item.rank-2:hover,
.detail-stat-item.rank-3:hover {
background: #f0f4ff;
}
.detail-stat-value {
font-size: 15px;
font-weight: 700;
font-family: 'Inter', monospace;
color: var(--text-primary, #1d1d1f);
line-height: 1.3;
}
.detail-stat-label {
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-stat-item.hidden {
display: none;
}
/* Mobile Adjustments */
@media (max-width: 600px) {
.modal-top-section {
flex-direction: column;
}
.modal-identity,
.stats-list-container.compact-stats {
width: 100%;
}
.detail-stats-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 6px;
}
.detail-stat-item {
padding: 8px 10px;
}
.detail-stat-value {
font-size: 14px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,968 +0,0 @@
: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;
}
.skip-to-main {
position: absolute;
left: -9999px;
top: 0;
z-index: 999;
}
.home-hidden {
display: none;
}
/* 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 a {
display: block;
}
.logo img {
height: 32px;
width: auto;
display: block;
}
.nav-links {
display: flex;
align-items: center;
}
.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-container {
display: flex;
align-items: center;
margin-left: 24px;
}
.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;
text-decoration: none;
}
.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;
}
.page-hero {
height: 35vh;
min-height: 300px;
}
.home-hero {
background-image: url('https://img.lunadeer.cn/i/2025/11/26/69267755e14e3.png');
}
.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);
}
.hero-subtitle-container {
margin-bottom: 15px;
}
.subtitle-prefix, .subtitle-suffix {
white-space: nowrap;
}
.subtitle-dynamic {
white-space: nowrap;
margin: 0 8px;
position: relative;
display: inline-block;
min-width: 40px;
text-align: center;
}
/* Fade animation for dynamic subtitle */
.fade-enter {
opacity: 0;
transform: translateY(10px);
}
.fade-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.fade-exit {
opacity: 1;
transform: translateY(0);
}
.fade-exit-active {
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.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-center {
justify-content: center;
}
.player-item-muted {
justify-content: center;
color: #86868b;
}
.player-item-error {
justify-content: center;
color: #ff3b30;
}
.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;
}
.feature-pure {
background-image: url('https://img.lunadeer.cn/i/2024/02/21/65d592eb4afad.jpg');
}
.feature-dev {
background-image: url('https://img.lunadeer.cn/i/2025/11/26/6926982718ba8.png');
}
.feature-params {
background-image: url('https://img.lunadeer.cn/i/2025/11/26/6926775006dea.jpg');
}
.feature-land {
background-image: url('https://img.lunadeer.cn/i/2024/02/21/65d592ea6faa1.jpg');
}
.feature-bedrock {
background-image: url('https://img.lunadeer.cn/i/2025/11/26/692677560db46.png');
}
.feature-hardware {
background-image: url('https://img.lunadeer.cn/i/2024/02/21/65d592e248066.jpg');
}
.feature-fun {
background-image: url('https://img.lunadeer.cn/i/2025/11/26/692677566b07b.png');
}
.feature-update {
background-image: url('https://img.lunadeer.cn/i/2025/11/26/692697b71431b.png');
}
.feature-guide {
background-image: url('https://img.lunadeer.cn/i/2025/11/26/692697b7376c7.png');
}
/* 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;
}
/* Mobile Navbar Optimization */
.mobile-toggle {
display: none;
background: none;
border: none;
color: var(--text-primary);
font-size: 20px;
cursor: pointer;
padding: 0;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background 0.2s;
}
.mobile-toggle:hover {
background: rgba(0,0,0,0.05);
}
.mobile-menu {
position: fixed;
top: 44px;
left: 0;
width: 100%;
height: 0;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
overflow: hidden;
transition: height 0.5s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
opacity: 0;
visibility: hidden;
z-index: 998;
}
.mobile-menu.active {
height: calc(100vh - 44px);
opacity: 1;
visibility: visible;
}
.mobile-menu-links {
padding: 24px 40px;
display: flex;
flex-direction: column;
gap: 0;
max-width: 600px;
margin: 0 auto;
}
.mobile-menu-links a {
display: block;
font-size: 24px;
font-weight: 600;
text-decoration: none;
color: var(--text-primary);
padding: 16px 0;
border-bottom: 1px solid rgba(0,0,0,0.05);
opacity: 0;
transform: translateY(-20px);
transition: all 0.4s ease;
}
.mobile-menu.active .mobile-menu-links a {
opacity: 1;
transform: translateY(0);
}
/* Stagger animation */
.mobile-menu.active .mobile-menu-links a:nth-child(1) { transition-delay: 0.1s; }
.mobile-menu.active .mobile-menu-links a:nth-child(2) { transition-delay: 0.15s; }
.mobile-menu.active .mobile-menu-links a:nth-child(3) { transition-delay: 0.2s; }
.mobile-menu.active .mobile-menu-links a:nth-child(4) { transition-delay: 0.25s; }
.mobile-menu.active .mobile-menu-links a:nth-child(5) { transition-delay: 0.3s; }
/* Desktop Adjustment to keep alignment with split items */
@media (min-width: 769px) {
.nav-links.desktop-only {
margin-left: auto; /* Push links and CTA to right */
}
.nav-cta-container {
margin-left: 24px;
}
}
/* Responsive Navbar Adjustments */
@media (max-width: 768px) {
.nav-content {
justify-content: space-between;
padding: 0 15px;
}
.desktop-only {
display: none !important;
}
.mobile-toggle {
display: flex;
order: 1; /* Left */
}
.logo {
position: absolute;
left: 50%;
transform: translateX(-50%);
order: 2; /* Center */
margin: 0;
}
.nav-cta-container {
order: 3; /* Right */
margin: 0;
}
.nav-cta {
padding: 6px 12px;
font-size: 11px;
}
/* Prevent body scroll when menu is open */
body.menu-open {
overflow: hidden;
}
}
/* Active State for Navbar Links */
.nav-links a.active,
.mobile-menu-links a.active {
opacity: 1;
font-weight: 600;
color: var(--text-primary);
}

View File

@@ -1,79 +0,0 @@
<!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>
<meta name="description" content="白鹿原Minecraft服务器文档中心提供全面的服务器使用指南与参考资料。包含服务器规则详解、特色玩法说明、自研插件使用教程等详细文档帮助新老玩家快速了解服务器功能与机制轻松上手白鹿原纯净原版Minecraft生存体验。">
<meta name="keywords" content="白鹿原文档,Minecraft服务器文档,MC服务器规则,白鹿原指南,服务器帮助">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/doc.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/doc.html">
<meta property="og:title" content="文档 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="白鹿原Minecraft服务器文档中心提供服务器规则详解、玩法说明、自研插件使用指南等完整文档资料帮助新老玩家快速上手纯净原版生存。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/doc.html">
<meta property="twitter:title" content="文档 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="白鹿原Minecraft服务器文档中心提供服务器规则详解、玩法说明、自研插件使用指南等完整文档资料帮助新老玩家快速上手纯净原版生存。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/style.css">
<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">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "白鹿原服务器文档中心",
"description": "白鹿原Minecraft服务器文档中心包含服务器规则、玩法说明和插件使用指南",
"url": "https://mcpure.lunadeer.cn/doc.html",
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
<style>
body, html {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.iframe-container {
position: absolute;
top: 44px;
left: 0;
width: 100%;
height: calc(100% - 44px);
border: none;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<div id="navbar-component"></div>
<div class="iframe-container">
<iframe src="https://schema.lunadeer.cn/public/libraries/wco40gb6blucloqv" title="白鹿原Minecraft服务器文档中心" loading="lazy"></iframe>
</div>
<script src="js/components.js"></script>
</body>
</html>

View File

@@ -1,320 +0,0 @@
<!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>
<meta name="description" content="白鹿原Minecraft服务器全服共享公共设施资源一览包含各类自动化农场、刷怪塔、交易所等实用设施。支持按类型筛选和关键词搜索查看设施坐标位置、详细使用说明与视频教程。共同建设共同分享让纯净原版生存更加便捷轻松。">
<meta name="keywords" content="Minecraft共享资源,MC公共设施,白鹿原设施,Minecraft农场,服务器公共资源">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/facilities.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/facilities.html">
<meta property="og:title" content="共享资源 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="白鹿原Minecraft服务器全服共享资源一览包含各类农场、刷怪塔等公共设施。支持筛选搜索查看坐标与使用说明共同建设共同分享让生存更轻松。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/facilities.html">
<meta property="twitter:title" content="共享资源 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="白鹿原Minecraft服务器全服共享资源一览包含各类农场、刷怪塔等公共设施。支持筛选搜索查看坐标与使用说明共同建设共同分享让生存更轻松。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/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">
<link rel="stylesheet" href="css/pages/facilities.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "全服共享资源",
"description": "白鹿原Minecraft服务器全服共享资源一览",
"url": "https://mcpure.lunadeer.cn/facilities.html",
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
</head>
<body>
<div id="navbar-component"></div>
<!-- Hero Section -->
<header id="hero-component" data-title="全服共享资源" data-subtitle="共同建设,共同分享,让生存更轻松。" data-class="facilities-hero-bg"></header>
<div class="facilities-container">
<!-- Controls -->
<div class="controls-section">
<div class="controls-header-row">
<div class="title-with-action">
<h2 class="section-title">设施列表</h2>
<button class="btn-add-facility" id="btn-add-facility">
<i class="fas fa-plus"></i> 新增设施
</button>
</div>
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="facility-search" placeholder="搜索设施名称...">
</div>
</div>
<div class="filters-wrapper">
<div class="filter-group">
<div class="filter-label"><i class="fas fa-layer-group"></i> 类型</div>
<div class="filter-tags" id="type-filters">
<button class="filter-tag active" data-filter="all">全部</button>
<button class="filter-tag" data-filter="resource"><i class="fas fa-cube"></i> 资源</button>
<button class="filter-tag" data-filter="xp"><i class="fas fa-star"></i> 经验</button>
<button class="filter-tag" data-filter="infrastructure"><i class="fas fa-road"></i> 基建</button>
</div>
</div>
<div class="filter-group">
<div class="filter-label"><i class="fas fa-globe"></i> 维度</div>
<div class="filter-tags" id="dimension-filters">
<button class="filter-tag active" data-filter="all">全部</button>
<button class="filter-tag" data-filter="overworld">主世界</button>
<button class="filter-tag" data-filter="nether">下界</button>
<button class="filter-tag" data-filter="end">末地</button>
</div>
</div>
</div>
</div>
<!-- Facilities Grid -->
<div class="facilities-grid" id="facilities-list">
<!-- JS will inject cards here -->
</div>
<div id="no-results" class="no-results-message is-hidden">
没有找到匹配的设施
</div>
</div>
<!-- Facility Modal -->
<div id="facility-modal" class="modal">
<div class="modal-content facility-modal-content">
<span class="close-modal">&times;</span>
<div class="modal-header">
<h3 class="modal-title" id="modal-title">设施名称</h3>
<div class="modal-badges-row">
<div class="modal-badges" id="modal-badges">
<!-- Badges injected by JS -->
</div>
<div class="modal-actions">
<button class="btn-share-facility" id="btn-share-facility" title="分享此设施">
<i class="fas fa-share-alt"></i> 分享
</button>
<button class="btn-edit-facility" id="btn-edit-facility" title="编辑此设施">
<i class="fas fa-pen"></i> 编辑
</button>
</div>
</div>
</div>
<div class="modal-body">
<p class="modal-intro" id="modal-intro">设施简介...</p>
<div class="modal-section">
<h4 class="modal-section-title"><i class="fas fa-map-marker-alt"></i> 位置信息</h4>
<p>
<span id="modal-dimension"></span>:
<span id="modal-coords"></span>
<a href="#" target="_blank" id="modal-map-link" class="map-link">
<i class="fas fa-map-marked-alt"></i> 查看地图
</a>
</p>
</div>
<div class="modal-section">
<h4 class="modal-section-title"><i class="fas fa-users-cog"></i> 贡献/维护人员</h4>
<div class="contributors-list" id="modal-contributors">
<!-- Contributors injected by JS -->
</div>
</div>
<div class="modal-section">
<h4 class="modal-section-title"><i class="fas fa-book-open"></i> 使用说明</h4>
<div class="instruction-content" id="modal-instructions">
<!-- Instructions injected by JS -->
</div>
</div>
<div class="modal-section">
<h4 class="modal-section-title"><i class="fas fa-exclamation-triangle"></i> 注意事项</h4>
<div class="notes-content" id="modal-notes">
<!-- Notes injected by JS -->
</div>
</div>
</div>
</div>
</div>
<!-- Editor Modal -->
<div id="editor-modal" class="modal">
<div class="modal-content editor-modal-content">
<span class="close-editor-modal">&times;</span>
<div class="editor-modal-header">
<h3><i class="fas fa-tools"></i> 设施编辑器</h3>
</div>
<div class="editor-layout">
<!-- Left: Preview -->
<div class="editor-preview">
<div class="editor-panel-title"><i class="fas fa-eye"></i> 实时预览</div>
<div class="editor-preview-content" id="editor-preview-area"></div>
</div>
<!-- Right: Editor Form -->
<div class="editor-form">
<div class="editor-panel-title"><i class="fas fa-edit"></i> 编辑内容</div>
<div class="editor-form-scroll">
<div class="form-group">
<label for="editor-title">设施名称</label>
<input type="text" id="editor-title" placeholder="输入设施名称...">
</div>
<div class="form-group">
<label for="editor-intro">设施简介</label>
<textarea id="editor-intro" placeholder="输入设施简介..." rows="3"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>类型</label>
<div class="custom-select">
<input type="hidden" id="editor-type" value="resource">
<div class="custom-select-trigger">
<span class="custom-select-text">资源类</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="custom-select-options">
<div class="custom-option selected" data-value="resource">资源类</div>
<div class="custom-option" data-value="xp">经验类</div>
<div class="custom-option" data-value="infrastructure">基础设施</div>
</div>
</div>
</div>
<div class="form-group">
<label>状态</label>
<div class="custom-select">
<input type="hidden" id="editor-status" value="online">
<div class="custom-select-trigger">
<span class="custom-select-text">正常运行</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="custom-select-options">
<div class="custom-option selected" data-value="online">正常运行</div>
<div class="custom-option" data-value="maintenance">维护中</div>
<div class="custom-option" data-value="offline">暂时失效</div>
</div>
</div>
</div>
<div class="form-group">
<label>维度</label>
<div class="custom-select">
<input type="hidden" id="editor-dimension" value="overworld">
<div class="custom-select-trigger">
<span class="custom-select-text">主世界</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="custom-select-options">
<div class="custom-option selected" data-value="overworld">主世界</div>
<div class="custom-option" data-value="nether">下界</div>
<div class="custom-option" data-value="end">末地</div>
</div>
</div>
</div>
</div>
<div class="form-row coords-row">
<div class="form-group">
<label for="editor-x">X 坐标</label>
<input type="number" id="editor-x" placeholder="0">
</div>
<div class="form-group">
<label for="editor-y">Y 坐标</label>
<input type="number" id="editor-y" placeholder="64">
</div>
<div class="form-group">
<label for="editor-z">Z 坐标</label>
<input type="number" id="editor-z" placeholder="0">
</div>
</div>
<div class="form-group">
<label>贡献/维护人员</label>
<div class="tags-input-wrapper" id="editor-contributors-wrapper">
<div class="tags-list" id="editor-contributors-tags"></div>
<input type="text" id="editor-contributor-input" placeholder="输入名称后按回车或空格添加...">
</div>
</div>
<div class="form-group">
<label>使用说明</label>
<div class="sortable-list" id="editor-instructions-list"></div>
<div class="add-item-row">
<button type="button" class="add-item-btn" data-target="instructions" data-type="text">
<i class="fas fa-plus"></i> 添加文字
</button>
<button type="button" class="add-item-btn" data-target="instructions" data-type="image">
<i class="fas fa-image"></i> 添加图片
</button>
<button type="button" class="add-item-btn" data-target="instructions" data-type="video">
<i class="fas fa-video"></i> 添加视频
</button>
</div>
</div>
<div class="form-group">
<label>注意事项</label>
<div class="sortable-list" id="editor-notes-list"></div>
<div class="add-item-row">
<button type="button" class="add-item-btn" data-target="notes" data-type="text">
<i class="fas fa-plus"></i> 添加文字
</button>
<button type="button" class="add-item-btn" data-target="notes" data-type="image">
<i class="fas fa-image"></i> 添加图片
</button>
<button type="button" class="add-item-btn" data-target="notes" data-type="video">
<i class="fas fa-video"></i> 添加视频
</button>
</div>
</div>
<div class="editor-actions">
<button type="button" class="btn-save-facility" id="btn-save-facility">
<i class="fas fa-save"></i> 生成 JSON
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JSON Output Modal -->
<div id="json-output-modal" class="modal">
<div class="modal-content json-output-content">
<span class="close-json-modal">&times;</span>
<h3><i class="fas fa-code"></i> 生成完成</h3>
<p class="json-output-hint">请复制以下 JSON 内容,发送给服主以更新到网站上。</p>
<textarea id="json-output" readonly></textarea>
<button type="button" class="btn-copy-json" id="btn-copy-json">
<i class="fas fa-copy"></i> 复制到剪贴板
</button>
</div>
</div>
<div id="footer-component"></div>
<script src="js/components.js"></script>
<script src="js/facilities_script.js"></script>
</body>
</html>

View File

@@ -1,258 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>白鹿原 Minecraft 服务器 - 永不换档的纯净原版生存Minecraft服务器</title>
<meta name="description" content="白鹿原是一个永不换档的纯净原版生存Minecraft我的世界服务器支持Java版与基岩版互通。提供免费圈地保护、自研管理插件紧跟最新游戏版本更新。物理工作站保障7×24小时稳定运行实时查看服务器在线状态与众筹进展。立即加入白鹿原开启纯净原版生存冒险之旅服务器地址mcpure.lunadeer.cn">
<meta name="keywords" content="白鹿原Minecraft,白鹿原我的世界,白鹿原mc,Minecraft服务器,我的世界,我的世界服务器,纯净服务器,原版服务器,纯净生存,基岩互通,白鹿原,MC服务器,永不换档,免费圈地,Minecraft中国">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/">
<!-- Google Site Verification -->
<meta name="google-site-verification" content="ZMGHsJuJU3soEw09Xa0lfKTxhxEBKN-h-goxg5lhCRw" />
<!-- Bing Site Verification -->
<meta name="msvalidate.01" content="A46E723A4AEF6D9EEB1D9AB9DC1267FD" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/">
<meta property="og:title" content="白鹿原 Minecraft 服务器 - 永不换档的纯净原版生存Minecraft服务器">
<meta property="og:description" content="白鹿原——永不换档的纯净原版生存Minecraft服务器支持Java版与基岩版互通。免费圈地保护、自研管理插件、紧跟最新版本物理工作站保障全天候稳定运行。立即加入纯净生存冒险">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/">
<meta property="twitter:title" content="白鹿原 Minecraft 服务器 - 永不换档的纯净原版生存Minecraft服务器">
<meta property="twitter:description" content="白鹿原——永不换档的纯净原版生存Minecraft服务器支持Java版与基岩版互通。免费圈地保护、自研管理插件、紧跟最新版本物理工作站保障全天候稳定运行。立即加入纯净生存冒险">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://img.lunadeer.cn">
<link rel="dns-prefetch" href="https://outline.lunadeer.cn">
<link rel="dns-prefetch" href="https://mcmap.lunadeer.cn">
<link rel="dns-prefetch" href="https://mcphoto.lunadeer.cn">
<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">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "GameServer",
"name": "白鹿原 Minecraft 服务器",
"description": "永不换档的纯净原版生存Minecraft服务器支持Java版和基岩版互通",
"url": "https://mcpure.lunadeer.cn/",
"logo": "https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png",
"image": "https://img.lunadeer.cn/i/2025/11/26/69267755e14e3.png",
"game": {
"@type": "VideoGame",
"name": "Minecraft",
"gamePlatform": ["Java Edition", "Bedrock Edition"]
},
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "CNY",
"availability": "https://schema.org/InStock"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "5",
"ratingCount": "100"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://mcpure.lunadeer.cn/stats.html?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
</head>
<body>
<!-- Skip to main content for accessibility -->
<a href="#main-content" class="skip-to-main">跳转到主内容</a>
<!-- Navbar Component -->
<div id="navbar-component"></div>
<!-- Hero Section -->
<header id="main-content" class="hero home-hero" role="banner">
<div class="hero-overlay"></div>
<div class="hero-content">
<h1 class="hero-title">白鹿原</h1>
<!-- Dynamic Subtitle with Rotating Descriptors -->
<div class="hero-subtitle-container">
<p class="hero-subtitle">
<span class="subtitle-prefix">永不换档的</span>
<span class="subtitle-dynamic" id="dynamic-subtitle"></span>
<span class="subtitle-suffix">Minecraft 服务器</span>
</p>
</div>
<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 player-item-center">加载中...</div>
</div>
</div>
</div>
</div>
</header>
<!-- Key Features (Bento Grid Style) -->
<main>
<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" role="article" aria-label="纯净原版特性">
<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" role="article" aria-label="深度自研特性">
<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" role="article" aria-label="原汁原味特性">
<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 feature-land">
<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 feature-bedrock">
<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 feature-hardware">
<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 feature-fun">
<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 feature-update">
<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 feature-guide">
<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">
<a href="/sponsor.html" class="view-sponsors-btn">查看赞助列表</a>
</div>
</div>
</section>
<!-- Crowdfunding Section -->
<section id="crowdfunding-section" class="crowdfunding-section home-hidden">
<div class="container">
<h2 class="section-header">众筹进度</h2>
<div id="crowdfunding-grid" class="crowdfunding-grid">
<!-- Crowdfunding items will be injected here -->
</div>
</div>
</section>
</main>
<div id="footer-component"></div>
<script src="js/components.js"></script>
<script src="js/data_utils.js"></script>
<script src="js/script.js"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="白鹿原 Minecraft 服务器官网 Vue 重构工程。" />
<title>白鹿原 Minecraft 服务器</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

320
join.html
View File

@@ -1,320 +0,0 @@
<!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>
<meta name="description" content="白鹿原Minecraft服务器新手加入指南——支持Java版与基岩版互通四步轻松入服阅读服务器公约、同意规则条款、选择游戏设备、跟随配置教程完成设置。无论您使用电脑还是手机平板都能快速加入白鹿原开启纯净原版生存冒险之旅。">
<meta name="keywords" content="Minecraft加入服务器,MC怎么进服,白鹿原加入,Minecraft教程,基岩版加入,Java版加入">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/join.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/join.html">
<meta property="og:title" content="加入游戏指引 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="白鹿原Minecraft服务器加入指南支持Java版与基岩版互通。四步轻松入服阅读公约、同意条款、选择设备、跟随教程配置快速开启纯净原版冒险之旅。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/join.html">
<meta property="twitter:title" content="加入游戏指引 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="白鹿原Minecraft服务器加入指南支持Java版与基岩版互通。四步轻松入服阅读公约、同意条款、选择设备、跟随教程配置快速开启纯净原版冒险之旅。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<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="css/style.css">
<link rel="stylesheet" href="css/pages/join.css">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Marked.js for Markdown parsing -->
<script src="js/marked.min.js"></script>
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HowTo",
"name": "加入白鹿原Minecraft服务器",
"description": "白鹿原Minecraft服务器加入指南支持Java版与基岩版互通",
"step": [
{
"@type": "HowToStep",
"name": "服务器公约",
"text": "阅读并同意服务器公约"
},
{
"@type": "HowToStep",
"name": "选择设备",
"text": "选择您所使用的设备和版本"
},
{
"@type": "HowToStep",
"name": "配置启动",
"text": "配置服务器地址并启动游戏"
},
{
"@type": "HowToStep",
"name": "选择玩法",
"text": "选择适合您的玩法开始冒险"
}
],
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
</head>
<body>
<!-- Navigation Bar -->
<div id="navbar-component"></div>
<div class="join-page-wrapper">
<div class="join-header-minimal">
<h1>开启您的冒险</h1>
</div>
<main class="wizard-container-modern">
<div class="wizard-sidebar">
<div class="wizard-progress-vertical">
<div class="progress-step active" data-step="1">
<div class="step-dot"></div>
<div class="step-text">
<span class="step-title">公约</span>
<span class="step-desc">阅读须知</span>
</div>
</div>
<div class="progress-step" data-step="2">
<div class="step-dot"></div>
<div class="step-text">
<span class="step-title">设备</span>
<span class="step-desc">选择平台</span>
</div>
</div>
<div class="progress-step" data-step="3">
<div class="step-dot"></div>
<div class="step-text">
<span class="step-title">启动</span>
<span class="step-desc">配置教程</span>
</div>
</div>
<div class="progress-step" data-step="4">
<div class="step-dot"></div>
<div class="step-text">
<span class="step-title">出发</span>
<span class="step-desc">选择玩法</span>
</div>
</div>
</div>
</div>
<div class="wizard-content-area">
<!-- Step 1: Convention -->
<div id="step-1" class="step-content">
<div class="content-header">
<h2>服务器公约</h2>
<p>为了维护良好的游戏环境,请务必阅读并遵守以下规则。</p>
</div>
<div class="convention-wrapper">
<div class="markdown-content" id="convention-content">
<!-- Content will be loaded from data/convention.md -->
<div class="loading-state">
<i class="fas fa-circle-notch fa-spin"></i>
<span>正在加载公约内容...</span>
</div>
</div>
</div>
<div class="agreement-area" onclick="document.getElementById('agree-checkbox').click()">
<div class="custom-checkbox">
<input type="checkbox" id="agree-checkbox">
<span class="checkmark"><i class="fas fa-check"></i></span>
</div>
<label for="agree-checkbox" class="agreement-label">我已认真阅读并承诺遵守《服务器公约》</label>
</div>
</div>
<!-- Step 2: Device Selection -->
<div id="step-2" class="step-content">
<div class="content-header">
<h2>选择您的设备</h2>
<p>工欲善其事,必先利其器。</p>
</div>
<div class="card-content">
<div class="device-selection-grid">
<div class="device-card" data-device="pc">
<div class="icon-circle"><i class="fas fa-desktop"></i></div>
<div class="device-info">
<h3>电脑版</h3>
<p>Win / Mac / Linux</p>
</div>
<div class="check-mark"><i class="fas fa-check"></i></div>
</div>
<div class="device-card" data-device="ios">
<div class="icon-circle"><i class="fab fa-apple"></i></div>
<div class="device-info">
<h3>iOS 设备</h3>
<p>iPhone / iPad</p>
</div>
<div class="check-mark"><i class="fas fa-check"></i></div>
</div>
<div class="device-card" data-device="android">
<div class="icon-circle"><i class="fab fa-android"></i></div>
<div class="device-info">
<h3>安卓设备</h3>
<p>Android 手机 / 平板</p>
</div>
<div class="check-mark"><i class="fas fa-check"></i></div>
</div>
</div>
<div id="edition-selector" class="edition-selector hidden">
<div class="edition-toggle">
<button class="edition-btn active" data-edition="java">
<i class="fab fa-java"></i> Java 版
</button>
<button class="edition-btn" data-edition="bedrock">
<i class="fas fa-cubes"></i> 基岩版
</button>
</div>
</div>
<div id="launcher-recommendation" class="launcher-box hidden">
<div id="recommendation-content"></div>
</div>
</div>
</div>
<!-- Step 3: Tutorial -->
<div id="step-3" class="step-content">
<div class="content-header">
<h2>配置与启动</h2>
<p>只需几步简单操作即可进入游戏。</p>
</div>
<div class="card-content" id="tutorial-content">
<!-- Content will be injected based on device selection -->
</div>
</div>
<!-- Step 4: Gameplay Guide -->
<div id="step-4" class="step-content">
<div class="content-header">
<h2>选择您的玩法</h2>
<p>这里有无限可能,你想成为什么样的玩家?(点击卡片查看详情)</p>
</div>
<div class="card-content">
<div class="playstyle-grid">
<!-- Style 1: Large Towns -->
<div class="playstyle-card" data-style="large-town">
<div class="playstyle-visual town-bg">
<i class="fas fa-city"></i>
</div>
<div class="playstyle-text">
<h3>融入大型城镇</h3>
<p>快速启航,共建繁华</p>
</div>
</div>
<!-- Style 2: Small Towns -->
<div class="playstyle-card" data-style="small-town">
<div class="playstyle-visual village-bg">
<i class="fas fa-home"></i>
</div>
<div class="playstyle-text">
<h3>加入小型城镇</h3>
<p>共同成长,见证历史</p>
</div>
</div>
<!-- Style 3: Friends -->
<div class="playstyle-card" data-style="friends">
<div class="playstyle-visual friends-bg">
<i class="fas fa-users"></i>
</div>
<div class="playstyle-text">
<h3>与朋友共建</h3>
<p>白手起家,开创时代</p>
</div>
</div>
<!-- Style 4: Solo -->
<div class="playstyle-card" data-style="solo">
<div class="playstyle-visual solo-bg">
<i class="fas fa-user-ninja"></i>
</div>
<div class="playstyle-text">
<h3>独狼求生</h3>
<p>自力更生,隐于山林</p>
</div>
</div>
</div>
<!-- Details Section (Initially Hidden) -->
<div id="playstyle-details" class="playstyle-details-container">
<div class="details-card">
<div class="details-header">
<h3 id="detail-title">玩法标题</h3>
<span id="detail-subtitle" class="badge-subtitle">副标题</span>
</div>
<div class="details-body">
<div class="detail-section">
<h4><i class="fas fa-user-tag"></i> 适合人群</h4>
<p id="detail-target">...</p>
</div>
<div class="detail-grid-row">
<div class="detail-section check-list">
<h4><i class="fas fa-thumbs-up"></i> 优势</h4>
<ul id="detail-pros"></ul>
</div>
<div class="detail-section warn-list">
<h4><i class="fas fa-exclamation-circle"></i> 注意事项</h4>
<ul id="detail-cons"></ul>
</div>
</div>
</div>
</div>
</div>
<div class="final-message">
<p>无论你选择哪条路,都别忘了多探索世界,结识他人。我们期待看到你的故事在这里展开!</p>
</div>
</div>
</div>
<div class="step-footer">
<button id="btn-prev" class="btn-clean" disabled>Back</button>
<div class="step-action-group">
<button id="btn-next" class="btn-primary-large">
下一步 <i class="fas fa-arrow-right"></i>
</button>
<div id="step4-buttons" class="hidden final-actions">
<a href="doc.html" class="btn-clean"><i class="fas fa-book"></i> 查看文档</a>
<a href="index.html" class="btn-primary-large"><i class="fas fa-home"></i> 回到首页</a>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Footer -->
<div id="footer-component"></div>
<script src="js/components.js"></script>
<script src="js/join_script.js"></script>
</body>
</html>

View File

@@ -1,605 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
let announcementsData = [];
const timeline = document.getElementById('announcements-timeline');
const noResults = document.getElementById('no-results');
const categoryFilters = document.getElementById('category-filters');
const searchInput = document.getElementById('announcement-search');
let currentFilters = {
category: 'all',
search: ''
};
let editModeEnabled = false;
let currentEditItem = null;
// ========== Secret "edit" keyboard shortcut ==========
let secretBuffer = '';
document.addEventListener('keydown', (e) => {
// Ignore if user is typing in an input/textarea
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
secretBuffer += e.key.toLowerCase();
// Keep only last 4 characters
if (secretBuffer.length > 4) {
secretBuffer = secretBuffer.slice(-4);
}
if (secretBuffer === 'edit') {
editModeEnabled = !editModeEnabled;
secretBuffer = '';
toggleEditButtons(editModeEnabled);
if (editModeEnabled) {
console.log('%c[公告管理] 编辑模式已启用', 'color: #34c759; font-weight: bold; font-size: 14px;');
console.log('%c再次输入 "edit" 可隐藏编辑按钮', 'color: #86868b;');
} else {
console.log('%c[公告管理] 编辑模式已隐藏', 'color: #f59e0b; font-weight: bold; font-size: 14px;');
}
}
});
// Log hint on page load
console.log('%c[公告管理] 提示:在页面中键入 "edit" 可显示编辑按钮', 'color: #0071e3; font-weight: bold; font-size: 13px;');
function toggleEditButtons(show) {
document.querySelectorAll('.edit-hidden').forEach(el => {
if (show) {
el.classList.remove('edit-hidden');
el.classList.add('edit-visible');
} else {
el.classList.remove('edit-visible');
el.classList.add('edit-hidden');
}
});
}
// ========== Fetch Data ==========
fetch('data/announcements.json')
.then(response => response.json())
.then(data => {
announcementsData = data;
// Sort by time descending (newest first)
announcementsData.sort((a, b) => new Date(b.time) - new Date(a.time));
renderTimeline();
handleHashNavigation();
})
.catch(err => {
console.error('Error loading announcements:', err);
timeline.innerHTML = '<p class="error" style="text-align:center;color:var(--text-secondary);padding:40px;">无法加载公告数据。</p>';
});
// ========== Event Listeners ==========
categoryFilters.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
Array.from(categoryFilters.children).forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
currentFilters.category = e.target.dataset.filter;
renderTimeline();
}
});
searchInput.addEventListener('input', (e) => {
currentFilters.search = e.target.value.toLowerCase().trim();
renderTimeline();
});
// ========== Render ==========
function renderTimeline() {
timeline.innerHTML = '';
const filtered = announcementsData.filter(item => {
const matchCategory = currentFilters.category === 'all' || item.category === currentFilters.category;
const matchSearch = !currentFilters.search ||
item.title.toLowerCase().includes(currentFilters.search) ||
item.intro.toLowerCase().includes(currentFilters.search);
return matchCategory && matchSearch;
});
if (filtered.length === 0) {
noResults.classList.remove('is-hidden');
return;
} else {
noResults.classList.add('is-hidden');
}
filtered.forEach((item, index) => {
const anchorId = generateAnchorId(item);
const timelineItem = document.createElement('div');
timelineItem.className = 'timeline-item category-' + item.category;
timelineItem.id = anchorId;
const card = document.createElement('div');
card.className = 'announcement-card';
// Expand the first (newest) item by default
if (index === 0) {
card.classList.add('expanded');
}
const categoryBadgeClass = getCategoryBadgeClass(item.category);
const categoryText = getCategoryText(item.category);
const categoryIcon = getCategoryIcon(item.category);
// Summary
const summary = document.createElement('div');
summary.className = 'card-summary';
summary.innerHTML = `
<div class="card-summary-main">
<div class="card-summary-top">
<span class="category-badge ${categoryBadgeClass}">
<i class="fas ${categoryIcon}"></i> ${categoryText}
</span>
<h3 class="announcement-title">${escapeHtml(item.title)}</h3>
</div>
<p class="announcement-intro">${escapeHtml(item.intro)}</p>
</div>
<span class="card-summary-time"><i class="far fa-clock"></i> ${escapeHtml(item.time)}</span>
<i class="fas fa-chevron-down expand-icon"></i>
`;
summary.addEventListener('click', () => {
const wasExpanded = card.classList.contains('expanded');
// Collapse all
timeline.querySelectorAll('.announcement-card.expanded').forEach(c => c.classList.remove('expanded'));
// Toggle current
if (!wasExpanded) {
card.classList.add('expanded');
}
});
// Detail
const detail = document.createElement('div');
detail.className = 'card-detail';
const detailInner = document.createElement('div');
detailInner.className = 'detail-content';
renderContentBlocks(detailInner, item.content);
// Action buttons row inside detail
const actionRow = document.createElement('div');
actionRow.className = 'detail-action-btn-row';
// Share button
const shareBtn = document.createElement('button');
shareBtn.className = 'btn-share-announcement';
shareBtn.innerHTML = '<i class="fas fa-share-alt"></i> 分享';
shareBtn.addEventListener('click', (e) => {
e.stopPropagation();
var url = location.origin + location.pathname + '#' + anchorId;
navigator.clipboard.writeText(url).then(() => {
shareBtn.innerHTML = '<i class="fas fa-check"></i> 已复制链接';
shareBtn.classList.add('shared');
setTimeout(() => {
shareBtn.innerHTML = '<i class="fas fa-share-alt"></i> 分享';
shareBtn.classList.remove('shared');
}, 2000);
}).catch(() => {
// Fallback: use a temporary input
var tmp = document.createElement('input');
tmp.value = url;
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
shareBtn.innerHTML = '<i class="fas fa-check"></i> 已复制链接';
setTimeout(() => {
shareBtn.innerHTML = '<i class="fas fa-share-alt"></i> 分享';
}, 2000);
});
});
actionRow.appendChild(shareBtn);
// Edit button (hidden by default)
const editBtn = document.createElement('button');
editBtn.className = 'btn-edit-announcement ' + (editModeEnabled ? 'edit-visible' : 'edit-hidden');
editBtn.innerHTML = '<i class="fas fa-pen"></i> 编辑';
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
openEditor(item);
});
actionRow.appendChild(editBtn);
detail.appendChild(detailInner);
detail.appendChild(actionRow);
card.appendChild(summary);
card.appendChild(detail);
timelineItem.appendChild(card);
timeline.appendChild(timelineItem);
});
}
function renderContentBlocks(container, blocks) {
container.innerHTML = '';
if (!blocks || blocks.length === 0) {
container.innerHTML = '<p style="color:var(--text-secondary);">暂无内容</p>';
return;
}
blocks.forEach(block => {
if (block.type === 'text') {
const p = document.createElement('p');
p.innerText = block.content;
container.appendChild(p);
} else if (block.type === 'image') {
const img = document.createElement('img');
img.src = block.content;
img.loading = 'lazy';
container.appendChild(img);
} else if (block.type === 'video') {
const bv = parseBVNumber(block.content);
if (bv) {
const wrapper = document.createElement('div');
wrapper.className = 'video-embed-wrapper';
const iframe = document.createElement('iframe');
iframe.src = 'https://player.bilibili.com/player.html?bvid=' + bv + '&autoplay=0&high_quality=1';
iframe.allowFullscreen = true;
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups');
iframe.loading = 'lazy';
wrapper.appendChild(iframe);
container.appendChild(wrapper);
} else {
const p = document.createElement('p');
p.className = 'text-secondary';
p.innerText = '无效的视频 BV 号';
container.appendChild(p);
}
}
});
}
// ========== Generate stable ID for announcement ==========
function generateAnchorId(item) {
// Use title + time to create a stable hash-friendly ID
var raw = (item.time || '') + '_' + (item.title || '');
var hash = 0;
for (var i = 0; i < raw.length; i++) {
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
hash |= 0;
}
return 'a' + Math.abs(hash).toString(36);
}
// ========== Handle URL hash on load ==========
function handleHashNavigation() {
var hash = location.hash.replace('#', '');
if (!hash) return;
var target = document.getElementById(hash);
if (!target) return;
// Expand this card
var card = target.querySelector('.announcement-card');
if (card) {
timeline.querySelectorAll('.announcement-card.expanded').forEach(function(c) { c.classList.remove('expanded'); });
card.classList.add('expanded');
}
// Scroll into view with a small delay for layout
setTimeout(function() {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
}
// ========== Helpers ==========
function getCategoryText(cat) {
const map = { 'activity': '活动', 'maintenance': '维护', 'other': '其他' };
return map[cat] || cat;
}
function getCategoryIcon(cat) {
const map = { 'activity': 'fa-calendar-check', 'maintenance': 'fa-wrench', 'other': 'fa-info-circle' };
return map[cat] || 'fa-info-circle';
}
function getCategoryBadgeClass(cat) {
const map = { 'activity': 'badge-activity', 'maintenance': 'badge-maintenance', 'other': 'badge-other' };
return map[cat] || 'badge-other';
}
function parseBVNumber(input) {
if (!input) return null;
input = input.trim();
var bvPattern = /^(BV[A-Za-z0-9]+)$/;
var directMatch = input.match(bvPattern);
if (directMatch) return directMatch[1];
var urlPattern = /bilibili\.com\/video\/(BV[A-Za-z0-9]+)/;
var urlMatch = input.match(urlPattern);
if (urlMatch) return urlMatch[1];
var generalPattern = /(BV[A-Za-z0-9]{10,})/;
var generalMatch = input.match(generalPattern);
if (generalMatch) return generalMatch[1];
return null;
}
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
// ========== Editor Modal ==========
const editorModal = document.getElementById('editor-modal');
const jsonOutputModal = document.getElementById('json-output-modal');
const closeEditorModal = document.querySelector('.close-editor-modal');
const closeJsonModal = document.querySelector('.close-json-modal');
document.getElementById('btn-add-announcement').addEventListener('click', () => {
openEditor(null);
});
closeEditorModal.addEventListener('click', () => {
editorModal.style.display = 'none';
document.body.style.overflow = 'auto';
});
window.addEventListener('click', (e) => {
if (e.target === editorModal) {
editorModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
if (e.target === jsonOutputModal) {
jsonOutputModal.style.display = 'none';
}
});
closeJsonModal.addEventListener('click', () => {
jsonOutputModal.style.display = 'none';
});
let editorContentBlocks = [];
// Custom select init
document.querySelectorAll('.custom-select').forEach(select => {
const trigger = select.querySelector('.custom-select-trigger');
const options = select.querySelectorAll('.custom-option');
const input = select.querySelector('input[type="hidden"]');
const text = select.querySelector('.custom-select-text');
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = select.classList.contains('open');
document.querySelectorAll('.custom-select').forEach(s => s.classList.remove('open'));
if (!isOpen) {
select.classList.add('open');
}
});
options.forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
options.forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
text.innerText = option.innerText;
input.value = option.dataset.value;
input.dispatchEvent(new Event('change'));
select.classList.remove('open');
});
});
});
document.addEventListener('click', () => {
document.querySelectorAll('.custom-select').forEach(s => s.classList.remove('open'));
});
function setCustomSelectValue(id, value) {
var input = document.getElementById(id);
if (!input) return;
var select = input.closest('.custom-select');
var option = select.querySelector('.custom-option[data-value="' + value + '"]');
if (option) {
input.value = value;
select.querySelector('.custom-select-text').innerText = option.innerText;
select.querySelectorAll('.custom-option').forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
}
}
function openEditor(item) {
currentEditItem = item;
editorContentBlocks = item ? item.content.map(c => ({...c})) : [];
document.getElementById('editor-title').value = item ? item.title : '';
document.getElementById('editor-intro').value = item ? item.intro : '';
document.getElementById('editor-time').value = item ? item.time : new Date().toISOString().slice(0, 10);
setCustomSelectValue('editor-category', item ? item.category : 'activity');
renderSortableList('editor-content-list', editorContentBlocks);
updatePreview();
editorModal.style.display = 'block';
document.body.style.overflow = 'hidden';
}
// ========== Sortable List (drag-and-drop) ==========
let dragState = { listId: null, fromIdx: null };
function renderSortableList(listId, items) {
var container = document.getElementById(listId);
container.innerHTML = '';
items.forEach((item, idx) => {
var div = document.createElement('div');
div.className = 'sortable-item';
div.draggable = true;
div.dataset.idx = idx;
div.dataset.listId = listId;
var typeBadgeClass = item.type === 'text' ? 'badge-text' : item.type === 'image' ? 'badge-image' : 'badge-video';
var typeBadgeLabel = item.type === 'text' ? '文字' : item.type === 'image' ? '图片' : '视频';
var contentHtml;
if (item.type === 'text') {
contentHtml = '<textarea class="item-content" rows="2" placeholder="输入文字内容...">' + escapeHtml(item.content) + '</textarea>';
} else if (item.type === 'image') {
contentHtml = '<input type="text" class="item-content" placeholder="输入图片URL..." value="' + escapeHtml(item.content) + '">';
} else {
contentHtml = '<input type="text" class="item-content" placeholder="BV1xxxxxxxxxx 或 bilibili 视频地址" value="' + escapeHtml(item.content) + '">';
}
div.innerHTML =
'<span class="drag-handle"><i class="fas fa-grip-vertical"></i></span>' +
'<span class="item-type-badge ' + typeBadgeClass + '">' + typeBadgeLabel + '</span>' +
contentHtml +
'<button type="button" class="remove-item-btn" title="删除"><i class="fas fa-trash-alt"></i></button>';
container.appendChild(div);
div.addEventListener('dragstart', onDragStart);
div.addEventListener('dragover', onDragOver);
div.addEventListener('dragenter', onDragEnter);
div.addEventListener('dragleave', onDragLeave);
div.addEventListener('drop', onDrop);
div.addEventListener('dragend', onDragEnd);
var contentEl = div.querySelector('.item-content');
contentEl.addEventListener('input', () => {
items[idx].content = contentEl.value;
updatePreview();
});
div.querySelector('.remove-item-btn').addEventListener('click', () => {
items.splice(idx, 1);
renderSortableList(listId, items);
updatePreview();
});
});
}
function onDragStart(e) {
var item = e.target.closest('.sortable-item');
if (!item) return;
dragState.listId = item.dataset.listId;
dragState.fromIdx = parseInt(item.dataset.idx);
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', '');
}
function onDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }
function onDragEnter(e) {
var item = e.target.closest('.sortable-item');
if (item && item.dataset.listId === dragState.listId) item.classList.add('drag-over');
}
function onDragLeave(e) {
var item = e.target.closest('.sortable-item');
if (item) item.classList.remove('drag-over');
}
function onDrop(e) {
e.preventDefault();
var item = e.target.closest('.sortable-item');
if (!item || item.dataset.listId !== dragState.listId) return;
var toIdx = parseInt(item.dataset.idx);
var fromIdx = dragState.fromIdx;
if (fromIdx === toIdx) return;
var moved = editorContentBlocks.splice(fromIdx, 1)[0];
editorContentBlocks.splice(toIdx, 0, moved);
renderSortableList('editor-content-list', editorContentBlocks);
updatePreview();
}
function onDragEnd() {
document.querySelectorAll('.sortable-item').forEach(el => el.classList.remove('dragging', 'drag-over'));
dragState = { listId: null, fromIdx: null };
}
// Add content buttons
document.querySelectorAll('.add-item-btn').forEach(btn => {
btn.addEventListener('click', () => {
var type = btn.dataset.type;
editorContentBlocks.push({ type: type, content: '' });
renderSortableList('editor-content-list', editorContentBlocks);
updatePreview();
});
});
// Live Preview
['editor-title', 'editor-intro', 'editor-time', 'editor-category'].forEach(id => {
document.getElementById(id).addEventListener('input', updatePreview);
document.getElementById(id).addEventListener('change', updatePreview);
});
function updatePreview() {
var preview = document.getElementById('editor-preview-area');
var title = document.getElementById('editor-title').value || '未命名公告';
var intro = document.getElementById('editor-intro').value || '暂无简介';
var time = document.getElementById('editor-time').value || '未设定';
var category = document.getElementById('editor-category').value;
var categoryText = getCategoryText(category);
var categoryIcon = getCategoryIcon(category);
var badgeClass = getCategoryBadgeClass(category);
var html = '<div class="preview-announcement">';
html += '<div class="preview-header">';
html += '<div class="preview-title">' + escapeHtml(title) + '</div>';
html += '<div class="preview-meta">';
html += '<span class="category-badge ' + badgeClass + '"><i class="fas ' + categoryIcon + '"></i> ' + categoryText + '</span>';
html += '<span class="card-summary-time"><i class="far fa-clock"></i> ' + escapeHtml(time) + '</span>';
html += '</div>';
html += '</div>';
html += '<div class="preview-body">';
html += '<p class="preview-intro-text">' + escapeHtml(intro) + '</p>';
html += '<div class="detail-content">';
if (editorContentBlocks.length > 0) {
editorContentBlocks.forEach(block => {
if (block.type === 'text') {
html += '<p>' + (escapeHtml(block.content) || '<span class="text-secondary">空文字</span>') + '</p>';
} else if (block.type === 'image') {
html += block.content ? '<img src="' + escapeHtml(block.content) + '" loading="lazy">' : '<p class="text-secondary">空图片</p>';
} else if (block.type === 'video') {
html += renderVideoPreviewHtml(block.content);
}
});
} else {
html += '<p class="text-secondary">暂无正文内容</p>';
}
html += '</div></div></div>';
preview.innerHTML = html;
}
function renderVideoPreviewHtml(content) {
var bv = parseBVNumber(content);
if (bv) {
return '<div class="video-embed-wrapper"><iframe src="https://player.bilibili.com/player.html?bvid=' + bv + '&autoplay=0&high_quality=1" allowfullscreen sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe></div>';
}
return '<p class="text-secondary">请输入有效的 BV 号或 bilibili 视频地址</p>';
}
// ========== Save / Generate JSON ==========
document.getElementById('btn-save-announcement').addEventListener('click', () => {
var title = document.getElementById('editor-title').value.trim();
if (!title) {
alert('请填写公告标题');
document.getElementById('editor-title').focus();
return;
}
var announcementObj = {
title: title,
intro: document.getElementById('editor-intro').value.trim(),
time: document.getElementById('editor-time').value,
category: document.getElementById('editor-category').value,
content: editorContentBlocks.filter(i => i.content.trim() !== '').map(i => {
if (i.type === 'video') {
return { type: 'video', content: parseBVNumber(i.content) || i.content };
}
return { ...i };
})
};
var jsonStr = JSON.stringify(announcementObj, null, 4);
document.getElementById('json-output').value = jsonStr;
jsonOutputModal.style.display = 'block';
});
// Copy JSON
document.getElementById('btn-copy-json').addEventListener('click', () => {
var textArea = document.getElementById('json-output');
textArea.select();
textArea.setSelectionRange(0, 99999);
navigator.clipboard.writeText(textArea.value).then(() => {
var btn = document.getElementById('btn-copy-json');
var originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> 已复制!';
btn.style.background = '#34c759';
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.style.background = '';
}, 2000);
}).catch(() => {
document.execCommand('copy');
alert('已复制到剪贴板');
});
});
});

View File

@@ -1,151 +0,0 @@
const Components = {
navbarHTML: `
<nav class="navbar">
<div class="nav-content">
<button class="mobile-toggle" id="mobile-toggle" aria-label="菜单">
<i class="fas fa-bars"></i>
</button>
<div class="logo">
<a href="/">
<img src="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png" alt="白鹿原 Minecraft 服务器 Logo">
</a>
</div>
<div class="nav-links desktop-only">
<a href="/doc.html">文档</a>
<a href="/map.html">地图</a>
<a href="/facilities.html">设施</a>
<a href="/towns.html">城镇</a>
<a href="/announcements.html">公告</a>
<a href="/photo.html">相册</a>
<a href="/stats.html">数据</a>
<a href="/sponsor.html">赞助</a>
<a href="https://qm.qq.com/q/9izlHDoef6" target="_blank" rel="noopener noreferrer">群聊</a>
</div>
<div class="nav-cta-container">
<a href="join.html" class="nav-cta">加入游戏</a>
</div>
</div>
</nav>
<!-- Mobile Menu -->
<div class="mobile-menu" id="mobile-menu">
<div class="mobile-menu-links">
<a href="/doc.html">文档</a>
<a href="/map.html">地图</a>
<a href="/facilities.html">设施</a>
<a href="/towns.html">城镇</a>
<a href="/announcements.html">公告</a>
<a href="/photo.html">相册</a>
<a href="/stats.html">数据</a>
<a href="/sponsor.html">赞助</a>
<a href="https://qm.qq.com/q/9izlHDoef6" target="_blank" rel="noopener noreferrer">群聊</a>
<a href="join.html">加入游戏</a>
</div>
</div>
`,
footerHTML: `
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-logo">白鹿原</div>
<p>&copy; 2026 白鹿原 Minecraft 服务器.</p>
</div>
</div>
</footer>
`,
initPageHero: function() {
const heroContainer = document.getElementById('hero-component');
if (heroContainer) {
const title = heroContainer.dataset.title || '';
const subtitle = heroContainer.dataset.subtitle || '';
const extraClass = heroContainer.dataset.class || '';
heroContainer.className = `hero page-hero ${extraClass}`;
heroContainer.innerHTML = `
<div class="hero-overlay"></div>
<div class="hero-content">
<h1 class="hero-title">${title}</h1>
<p class="hero-subtitle">${subtitle}</p>
</div>`;
}
},
init: function() {
// Inject Navbar
const navContainer = document.getElementById('navbar-component');
if (navContainer) {
navContainer.innerHTML = this.navbarHTML;
}
// Inject Page Hero
this.initPageHero();
// Inject Footer
const footerContainer = document.getElementById('footer-component');
if (footerContainer) {
footerContainer.innerHTML = this.footerHTML;
}
// Setup Mobile Menu Logic
this.setupMobileMenu();
// Highlight current page
this.highlightCurrentPage();
},
setupMobileMenu: function() {
const toggle = document.getElementById('mobile-toggle');
const menu = document.getElementById('mobile-menu');
if (toggle && menu) {
const icon = toggle.querySelector('i');
// Remove old listeners if any to avoid duplicates?
// Since we just injected the HTML, there are no listeners.
toggle.addEventListener('click', () => {
menu.classList.toggle('active');
document.body.classList.toggle('menu-open');
if (menu.classList.contains('active')) {
if(icon) {
icon.classList.remove('fa-bars');
icon.classList.add('fa-times');
}
} else {
if(icon) {
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
}
}
});
menu.querySelectorAll('a').forEach(link => {
link.addEventListener('click', () => {
menu.classList.remove('active');
document.body.classList.remove('menu-open');
if(icon) {
icon.classList.remove('fa-times');
icon.classList.add('fa-bars');
}
});
});
}
},
highlightCurrentPage: function() {
const currentPath = window.location.pathname;
const links = document.querySelectorAll('.nav-links a, .mobile-menu-links a');
links.forEach(link => {
if (link.getAttribute('href') === currentPath) {
link.classList.add('active'); // You might need to add CSS for .active
}
});
}
};
document.addEventListener('DOMContentLoaded', () => {
Components.init();
});

View File

@@ -1,42 +0,0 @@
const DataUtils = {
parseSponsorsText: function(text) {
const sponsors = [];
if (!text) {
return sponsors;
}
const lines = text.trim().split('\n');
lines.forEach(line => {
const parts = line.split(',');
if (parts.length < 3) {
return;
}
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 });
}
});
return sponsors;
},
buildSponsorTotals: function(sponsors) {
const totals = {};
sponsors.forEach(item => {
if (!totals[item.name]) {
totals[item.name] = 0;
}
totals[item.name] += item.amount;
});
return totals;
}
};

View File

@@ -1,825 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
let facilitiesData = [];
const grid = document.getElementById('facilities-list');
const noResults = document.getElementById('no-results');
const statusFilters = document.getElementById('type-filters'); // Wait, I named it type-filters in HTML
const dimensionFilters = document.getElementById('dimension-filters');
const searchInput = document.getElementById('facility-search');
// Modal Elements
const modal = document.getElementById('facility-modal');
const closeModal = document.querySelector('.close-modal');
// Initial State
let currentFilters = {
type: 'all',
dimension: 'all',
search: ''
};
let currentDetailItem = null;
// Generate stable anchor ID for a facility
function generateFacilityId(item) {
var raw = (item.title || '');
var hash = 0;
for (var i = 0; i < raw.length; i++) {
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
hash |= 0;
}
return 'f' + Math.abs(hash).toString(36);
}
// Handle URL hash: auto-open facility modal
function handleHashNavigation() {
var hash = location.hash.replace('#', '');
if (!hash) return;
for (var i = 0; i < facilitiesData.length; i++) {
if (generateFacilityId(facilitiesData[i]) === hash) {
openModal(facilitiesData[i]);
return;
}
}
}
// 1. Fetch Data
fetch('data/facilities.json')
.then(response => response.json())
.then(data => {
facilitiesData = data;
renderGrid();
handleHashNavigation();
})
.catch(err => {
console.error('Error loading facilities:', err);
grid.innerHTML = '<p class="error">无法加载设施数据。</p>';
});
// 2. Event Listeners
// Type Filter
statusFilters.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
// Remove active class from siblings
Array.from(statusFilters.children).forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
currentFilters.type = e.target.dataset.filter;
renderGrid();
}
});
// Dimension Filter
dimensionFilters.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
Array.from(dimensionFilters.children).forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
currentFilters.dimension = e.target.dataset.filter;
renderGrid();
}
});
// Search
searchInput.addEventListener('input', (e) => {
currentFilters.search = e.target.value.toLowerCase().trim();
renderGrid();
});
// Modal Close
closeModal.addEventListener('click', () => {
modal.style.display = 'none';
document.body.style.overflow = 'auto'; // Enable scrolling
history.replaceState(null, '', location.pathname + location.search);
});
window.addEventListener('click', (e) => {
if (e.target === modal) {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
history.replaceState(null, '', location.pathname + location.search);
}
});
// 3. Render Functions
function renderGrid() {
grid.innerHTML = '';
const filtered = facilitiesData.filter(item => {
const matchType = currentFilters.type === 'all' || item.type === currentFilters.type;
const matchDim = currentFilters.dimension === 'all' || item.dimension === currentFilters.dimension;
const matchSearch = !currentFilters.search ||
item.title.toLowerCase().includes(currentFilters.search) ||
item.intro.toLowerCase().includes(currentFilters.search);
return matchType && matchDim && matchSearch;
});
if (filtered.length === 0) {
noResults.classList.remove('is-hidden');
return;
} else {
noResults.classList.add('is-hidden');
}
filtered.forEach(item => {
const card = document.createElement('div');
card.className = 'facility-card';
card.onclick = () => openModal(item);
const statusColor = getStatusColor(item.status);
const statusText = getStatusText(item.status);
card.innerHTML = `
<div class="card-header">
<h3 class="card-title">${item.title}</h3>
<div class="status-indicator-badge status-${item.status}">
<div class="status-dot"></div>
<span>${statusText}</span>
</div>
</div>
<p class="card-intro">${item.intro}</p>
<div class="card-meta">
<span class="meta-tag">${getTypeText(item.type)}</span>
<span class="meta-tag">${getDimensionText(item.dimension)}</span>
</div>
`;
grid.appendChild(card);
});
}
function openModal(item) {
currentDetailItem = item;
// Populate specific fields
document.getElementById('modal-title').innerText = item.title;
document.getElementById('modal-intro').innerText = item.intro;
// Badges
const badgesContainer = document.getElementById('modal-badges');
badgesContainer.innerHTML = '';
// Status Badge
const statusBadge = document.createElement('span');
statusBadge.className = `badge badge-status-${item.status} large-badge`;
statusBadge.innerHTML = `<i class="fas ${getStatusIcon(item.status)}"></i> ${getStatusText(item.status)}`;
badgesContainer.appendChild(statusBadge);
// Type Badge
const typeBadge = document.createElement('span');
typeBadge.className = 'badge badge-type large-badge';
typeBadge.innerHTML = `<i class="fas fa-cube"></i> ${getTypeText(item.type)}`;
badgesContainer.appendChild(typeBadge);
// Location
document.getElementById('modal-dimension').innerText = getDimensionText(item.dimension);
const coords = item.coordinates;
document.getElementById('modal-coords').innerText = `X: ${coords.x}, Y: ${coords.y}, Z: ${coords.z}`;
// Map Link
const mapLink = document.getElementById('modal-map-link');
const worldName = getMapWorldName(item.dimension);
// Format: #world:X:Y:Z:88:0:0:0:1:flat
mapLink.href = `https://mcmap.lunadeer.cn/#${worldName}:${coords.x}:${coords.y}:${coords.z}:500:0:0:0:1:flat`;
// Contributors
const contribList = document.getElementById('modal-contributors');
contribList.innerHTML = '';
if (item.contributors && item.contributors.length > 0) {
item.contributors.forEach(name => {
const tag = document.createElement('div');
tag.className = 'contributor-tag';
// Using minotar for avatar
tag.innerHTML = `<img src="https://minotar.net/avatar/${name}/20" alt="${name}">${name}`;
contribList.appendChild(tag);
});
} else {
contribList.innerHTML = '<span class="text-secondary">暂无记录</span>';
}
// Instructions
renderContentList(document.getElementById('modal-instructions'), item.instructions);
// Notes
renderContentList(document.getElementById('modal-notes'), item.notes);
modal.style.display = 'block';
document.body.style.overflow = 'hidden'; // Prevent scrolling background
// Update URL hash
var anchorId = generateFacilityId(item);
history.replaceState(null, '', '#' + anchorId);
}
function renderContentList(container, list) {
container.innerHTML = '';
if (!list || list.length === 0) {
container.innerHTML = '<p>无</p>';
return;
}
list.forEach(block => {
if (block.type === 'text') {
const p = document.createElement('p');
p.innerText = block.content;
container.appendChild(p);
} else if (block.type === 'image') {
const img = document.createElement('img');
img.src = block.content;
img.loading = 'lazy';
container.appendChild(img);
} else if (block.type === 'video') {
const bv = parseBVNumber(block.content);
if (bv) {
const wrapper = document.createElement('div');
wrapper.className = 'video-embed-wrapper';
const iframe = document.createElement('iframe');
iframe.src = `https://player.bilibili.com/player.html?bvid=${bv}&autoplay=0&high_quality=1`;
iframe.allowFullscreen = true;
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups');
iframe.loading = 'lazy';
wrapper.appendChild(iframe);
container.appendChild(wrapper);
} else {
const p = document.createElement('p');
p.className = 'text-secondary';
p.innerText = '无效的视频 BV 号';
container.appendChild(p);
}
}
});
}
function parseBVNumber(input) {
if (!input) return null;
input = input.trim();
// Match BV number directly (e.g. BV1qPhWzdEwU)
const bvPattern = /^(BV[A-Za-z0-9]+)$/;
const directMatch = input.match(bvPattern);
if (directMatch) return directMatch[1];
// Match from bilibili URL (e.g. https://www.bilibili.com/video/BV1qPhWzdEwU/...)
const urlPattern = /bilibili\.com\/video\/(BV[A-Za-z0-9]+)/;
const urlMatch = input.match(urlPattern);
if (urlMatch) return urlMatch[1];
// Match from b23.tv short URL or other formats containing BV
const generalPattern = /(BV[A-Za-z0-9]{10,})/;
const generalMatch = input.match(generalPattern);
if (generalMatch) return generalMatch[1];
return null;
}
// Helpers
function getStatusText(status) {
const map = {
'online': '正常运行',
'maintenance': '维护中',
'offline': '暂时失效'
};
return map[status] || status;
}
function getStatusColor(status) {
const map = {
'online': 'status-online',
'maintenance': 'status-maintenance',
'offline': 'status-offline'
};
return map[status] || '';
}
function getStatusIcon(status) {
const map = {
'online': 'fa-check-circle',
'maintenance': 'fa-wrench',
'offline': 'fa-times-circle'
};
return map[status] || 'fa-info-circle';
}
function getTypeText(type) {
const map = {
'resource': '资源类',
'xp': '经验类',
'infrastructure': '基础设施'
};
return map[type] || type;
}
function getDimensionText(dim) {
const map = {
'overworld': '主世界',
'nether': '下界',
'end': '末地'
};
return map[dim] || dim;
}
function getMapWorldName(dim) {
const map = {
'overworld': 'world',
'nether': 'world_nether',
'end': 'world_the_end'
};
return map[dim] || 'world';
}
// ========== Editor Modal Logic ==========
const editorModal = document.getElementById('editor-modal');
const jsonOutputModal = document.getElementById('json-output-modal');
const closeEditorModal = document.querySelector('.close-editor-modal');
const closeJsonModal = document.querySelector('.close-json-modal');
// Open empty editor for new facility
document.getElementById('btn-add-facility').addEventListener('click', () => {
openEditor(null);
});
// Share facility link
document.getElementById('btn-share-facility').addEventListener('click', () => {
if (!currentDetailItem) return;
var anchorId = generateFacilityId(currentDetailItem);
var url = location.origin + location.pathname + '#' + anchorId;
var btn = document.getElementById('btn-share-facility');
navigator.clipboard.writeText(url).then(() => {
btn.innerHTML = '<i class="fas fa-check"></i> 已复制链接';
btn.classList.add('shared');
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-share-alt"></i> 分享';
btn.classList.remove('shared');
}, 2000);
}).catch(() => {
var tmp = document.createElement('input');
tmp.value = url;
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
btn.innerHTML = '<i class="fas fa-check"></i> 已复制链接';
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-share-alt"></i> 分享';
}, 2000);
});
});
// Open editor from detail modal
document.getElementById('btn-edit-facility').addEventListener('click', () => {
if (currentDetailItem) {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
openEditor(currentDetailItem);
}
});
// Close editor modal
closeEditorModal.addEventListener('click', () => {
editorModal.style.display = 'none';
document.body.style.overflow = 'auto';
});
window.addEventListener('click', (e) => {
if (e.target === editorModal) {
editorModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
if (e.target === jsonOutputModal) {
jsonOutputModal.style.display = 'none';
}
});
closeJsonModal.addEventListener('click', () => {
jsonOutputModal.style.display = 'none';
});
// State for editor
let editorContributors = [];
let editorInstructions = [];
let editorNotes = [];
// Initialize custom selects
document.querySelectorAll('.custom-select').forEach(select => {
const trigger = select.querySelector('.custom-select-trigger');
const options = select.querySelectorAll('.custom-option');
const input = select.querySelector('input[type="hidden"]');
const text = select.querySelector('.custom-select-text');
trigger.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = select.classList.contains('open');
// Close all others
document.querySelectorAll('.custom-select').forEach(s => s.classList.remove('open'));
if (!isOpen) {
select.classList.add('open');
}
});
options.forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
// Update selection visually
options.forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
text.innerText = option.innerText;
// Update hidden input and trigger change
input.value = option.dataset.value;
input.dispatchEvent(new Event('change'));
// Close dropdown
select.classList.remove('open');
});
});
});
// Close custom selects on outside click
document.addEventListener('click', () => {
document.querySelectorAll('.custom-select').forEach(s => s.classList.remove('open'));
});
function setCustomSelectValue(id, value) {
const input = document.getElementById(id);
if (!input) return;
const select = input.closest('.custom-select');
const option = select.querySelector(`.custom-option[data-value="${value}"]`);
if (option) {
input.value = value;
select.querySelector('.custom-select-text').innerText = option.innerText;
select.querySelectorAll('.custom-option').forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
}
}
function openEditor(item) {
// Reset state
editorContributors = item ? [...item.contributors] : [];
editorInstructions = item ? item.instructions.map(i => ({...i})) : [];
editorNotes = item ? item.notes.map(n => ({...n})) : [];
// Populate form fields
document.getElementById('editor-title').value = item ? item.title : '';
document.getElementById('editor-intro').value = item ? item.intro : '';
setCustomSelectValue('editor-type', item ? item.type : 'resource');
setCustomSelectValue('editor-status', item ? item.status : 'online');
setCustomSelectValue('editor-dimension', item ? item.dimension : 'overworld');
document.getElementById('editor-x').value = item ? item.coordinates.x : '';
document.getElementById('editor-y').value = item ? item.coordinates.y : '';
document.getElementById('editor-z').value = item ? item.coordinates.z : '';
renderContributorTags();
renderSortableList('editor-instructions-list', editorInstructions);
renderSortableList('editor-notes-list', editorNotes);
updatePreview();
editorModal.style.display = 'block';
document.body.style.overflow = 'hidden';
}
// --- Contributors tags ---
function renderContributorTags() {
const container = document.getElementById('editor-contributors-tags');
container.innerHTML = '';
editorContributors.forEach((name, idx) => {
const tag = document.createElement('span');
tag.className = 'editor-tag';
tag.innerHTML = `${name} <span class="editor-tag-remove" data-idx="${idx}"><i class="fas fa-times"></i></span>`;
container.appendChild(tag);
});
}
function commitContributorInput() {
const contributorInput = document.getElementById('editor-contributor-input');
const value = contributorInput.value.trim();
if (value && !editorContributors.includes(value)) {
editorContributors.push(value);
renderContributorTags();
updatePreview();
}
contributorInput.value = '';
}
document.getElementById('editor-contributors-tags').addEventListener('click', (e) => {
const removeBtn = e.target.closest('.editor-tag-remove');
if (removeBtn) {
const idx = parseInt(removeBtn.dataset.idx);
editorContributors.splice(idx, 1);
renderContributorTags();
updatePreview();
}
});
document.getElementById('editor-contributor-input').addEventListener('keydown', (e) => {
if (e.isComposing) {
return;
}
if (e.key === 'Enter' || e.key === ' ' || e.code === 'Space') {
e.preventDefault();
commitContributorInput();
}
});
document.getElementById('editor-contributor-input').addEventListener('blur', () => {
commitContributorInput();
});
// Click on wrapper focuses input
document.getElementById('editor-contributors-wrapper').addEventListener('click', () => {
document.getElementById('editor-contributor-input').focus();
});
// --- Sortable Lists (drag-and-drop) ---
let dragState = { listId: null, fromIdx: null };
function renderSortableList(listId, items) {
const container = document.getElementById(listId);
container.innerHTML = '';
items.forEach((item, idx) => {
const div = document.createElement('div');
div.className = 'sortable-item';
div.draggable = true;
div.dataset.idx = idx;
div.dataset.listId = listId;
const typeBadgeClass = item.type === 'text' ? 'badge-text' : item.type === 'image' ? 'badge-image' : 'badge-video';
const typeBadgeLabel = item.type === 'text' ? '文字' : item.type === 'image' ? '图片' : '视频';
let contentHtml;
if (item.type === 'text') {
contentHtml = `<textarea class="item-content" rows="2" placeholder="输入文字内容...">${escapeHtml(item.content)}</textarea>`;
} else if (item.type === 'image') {
contentHtml = `<input type="text" class="item-content" placeholder="输入图片URL..." value="${escapeHtml(item.content)}">`;
} else {
contentHtml = `<input type="text" class="item-content" placeholder="BV1xxxxxxxxxx 或 bilibili 视频地址" value="${escapeHtml(item.content)}">`;
}
div.innerHTML = `
<span class="drag-handle"><i class="fas fa-grip-vertical"></i></span>
<span class="item-type-badge ${typeBadgeClass}">${typeBadgeLabel}</span>
${contentHtml}
<button type="button" class="remove-item-btn" title="删除"><i class="fas fa-trash-alt"></i></button>
`;
container.appendChild(div);
// Drag events
div.addEventListener('dragstart', onDragStart);
div.addEventListener('dragover', onDragOver);
div.addEventListener('dragenter', onDragEnter);
div.addEventListener('dragleave', onDragLeave);
div.addEventListener('drop', onDrop);
div.addEventListener('dragend', onDragEnd);
// Content change
const contentEl = div.querySelector('.item-content');
contentEl.addEventListener('input', () => {
items[idx].content = contentEl.value;
updatePreview();
});
// Remove
div.querySelector('.remove-item-btn').addEventListener('click', () => {
items.splice(idx, 1);
renderSortableList(listId, items);
updatePreview();
});
});
}
function onDragStart(e) {
const item = e.target.closest('.sortable-item');
if (!item) return;
dragState.listId = item.dataset.listId;
dragState.fromIdx = parseInt(item.dataset.idx);
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', ''); // required for Firefox
}
function onDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function onDragEnter(e) {
const item = e.target.closest('.sortable-item');
if (item && item.dataset.listId === dragState.listId) {
item.classList.add('drag-over');
}
}
function onDragLeave(e) {
const item = e.target.closest('.sortable-item');
if (item) {
item.classList.remove('drag-over');
}
}
function onDrop(e) {
e.preventDefault();
const item = e.target.closest('.sortable-item');
if (!item || item.dataset.listId !== dragState.listId) return;
const toIdx = parseInt(item.dataset.idx);
const fromIdx = dragState.fromIdx;
if (fromIdx === toIdx) return;
const listId = dragState.listId;
const items = listId === 'editor-instructions-list' ? editorInstructions : editorNotes;
// Reorder
const [moved] = items.splice(fromIdx, 1);
items.splice(toIdx, 0, moved);
renderSortableList(listId, items);
updatePreview();
}
function onDragEnd(e) {
document.querySelectorAll('.sortable-item').forEach(el => {
el.classList.remove('dragging', 'drag-over');
});
dragState = { listId: null, fromIdx: null };
}
// --- Add item buttons ---
document.querySelectorAll('.add-item-btn').forEach(btn => {
btn.addEventListener('click', () => {
const target = btn.dataset.target; // 'instructions' or 'notes'
const type = btn.dataset.type; // 'text' or 'image'
const newItem = { type: type, content: '' };
if (target === 'instructions') {
editorInstructions.push(newItem);
renderSortableList('editor-instructions-list', editorInstructions);
} else {
editorNotes.push(newItem);
renderSortableList('editor-notes-list', editorNotes);
}
updatePreview();
});
});
// --- Live Preview ---
// Listen for form field changes to update preview
['editor-title', 'editor-intro', 'editor-type', 'editor-status',
'editor-dimension', 'editor-x', 'editor-y', 'editor-z'].forEach(id => {
document.getElementById(id).addEventListener('input', updatePreview);
document.getElementById(id).addEventListener('change', updatePreview);
});
function updatePreview() {
const preview = document.getElementById('editor-preview-area');
const title = document.getElementById('editor-title').value || '未命名设施';
const intro = document.getElementById('editor-intro').value || '暂无简介';
const type = document.getElementById('editor-type').value;
const status = document.getElementById('editor-status').value;
const dimension = document.getElementById('editor-dimension').value;
const x = document.getElementById('editor-x').value || '0';
const y = document.getElementById('editor-y').value || '64';
const z = document.getElementById('editor-z').value || '0';
const statusText = getStatusText(status);
const statusIcon = getStatusIcon(status);
const typeText = getTypeText(type);
const dimensionText = getDimensionText(dimension);
let html = `<div class="preview-facility">`;
html += `<div class="preview-header">`;
html += `<div class="preview-title">${escapeHtml(title)}</div>`;
html += `<div class="modal-badges">`;
html += `<span class="badge badge-status-${status} large-badge"><i class="fas ${statusIcon}"></i> ${statusText}</span>`;
html += `<span class="badge badge-type large-badge"><i class="fas fa-cube"></i> ${typeText}</span>`;
html += `</div>`;
html += `</div>`;
html += `<div class="preview-body">`;
html += `<p class="preview-intro">${escapeHtml(intro)}</p>`;
// Location
html += `<div class="modal-section">`;
html += `<h4 class="modal-section-title"><i class="fas fa-map-marker-alt"></i> 位置信息</h4>`;
html += `<p>${dimensionText}: X: ${escapeHtml(x)}, Y: ${escapeHtml(y)}, Z: ${escapeHtml(z)}</p>`;
html += `</div>`;
// Contributors
html += `<div class="modal-section">`;
html += `<h4 class="modal-section-title"><i class="fas fa-users-cog"></i> 贡献/维护人员</h4>`;
if (editorContributors.length > 0) {
html += `<div class="contributors-list">`;
editorContributors.forEach(name => {
html += `<div class="contributor-tag"><img src="https://minotar.net/avatar/${encodeURIComponent(name)}/20" alt="${escapeHtml(name)}">${escapeHtml(name)}</div>`;
});
html += `</div>`;
} else {
html += `<span class="text-secondary">暂无记录</span>`;
}
html += `</div>`;
// Instructions
html += `<div class="modal-section">`;
html += `<h4 class="modal-section-title"><i class="fas fa-book-open"></i> 使用说明</h4>`;
html += `<div class="instruction-content">`;
if (editorInstructions.length > 0) {
editorInstructions.forEach(block => {
if (block.type === 'text') {
html += `<p>${escapeHtml(block.content) || '<span class=\"text-secondary\">空文字</span>'}</p>`;
} else if (block.type === 'image') {
html += block.content ? `<img src="${escapeHtml(block.content)}" loading="lazy">` : '<p class="text-secondary">空图片</p>';
} else if (block.type === 'video') {
html += renderVideoPreviewHtml(block.content);
}
});
} else {
html += `<p>无</p>`;
}
html += `</div></div>`;
// Notes
html += `<div class="modal-section">`;
html += `<h4 class="modal-section-title"><i class="fas fa-exclamation-triangle"></i> 注意事项</h4>`;
html += `<div class="notes-content">`;
if (editorNotes.length > 0) {
editorNotes.forEach(block => {
if (block.type === 'text') {
html += `<p>${escapeHtml(block.content) || '<span class=\"text-secondary\">空文字</span>'}</p>`;
} else if (block.type === 'image') {
html += block.content ? `<img src="${escapeHtml(block.content)}" loading="lazy">` : '<p class="text-secondary">空图片</p>';
} else if (block.type === 'video') {
html += renderVideoPreviewHtml(block.content);
}
});
} else {
html += `<p>无</p>`;
}
html += `</div></div>`;
html += `</div></div>`;
preview.innerHTML = html;
}
// --- Save / Generate JSON ---
document.getElementById('btn-save-facility').addEventListener('click', () => {
const title = document.getElementById('editor-title').value.trim();
if (!title) {
alert('请填写设施名称');
document.getElementById('editor-title').focus();
return;
}
const facilityObj = {
title: title,
intro: document.getElementById('editor-intro').value.trim(),
type: document.getElementById('editor-type').value,
dimension: document.getElementById('editor-dimension').value,
status: document.getElementById('editor-status').value,
coordinates: {
x: parseInt(document.getElementById('editor-x').value) || 0,
y: parseInt(document.getElementById('editor-y').value) || 64,
z: parseInt(document.getElementById('editor-z').value) || 0
},
contributors: [...editorContributors],
instructions: editorInstructions.filter(i => i.content.trim() !== '').map(i => i.type === 'video' ? { type: 'video', content: parseBVNumber(i.content) || i.content } : {...i}),
notes: editorNotes.filter(n => n.content.trim() !== '').map(n => n.type === 'video' ? { type: 'video', content: parseBVNumber(n.content) || n.content } : {...n})
};
const jsonStr = JSON.stringify(facilityObj, null, 4);
document.getElementById('json-output').value = jsonStr;
jsonOutputModal.style.display = 'block';
});
// --- Copy JSON ---
document.getElementById('btn-copy-json').addEventListener('click', () => {
const textArea = document.getElementById('json-output');
textArea.select();
textArea.setSelectionRange(0, 99999);
navigator.clipboard.writeText(textArea.value).then(() => {
const btn = document.getElementById('btn-copy-json');
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> 已复制!';
btn.style.background = '#34c759';
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.style.background = '';
}, 2000);
}).catch(() => {
// Fallback
document.execCommand('copy');
alert('已复制到剪贴板');
});
});
function renderVideoPreviewHtml(content) {
const bv = parseBVNumber(content);
if (bv) {
return `<div class="video-embed-wrapper"><iframe src="https://player.bilibili.com/player.html?bvid=${bv}&autoplay=0&high_quality=1" allowfullscreen sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe></div>`;
}
return '<p class="text-secondary">请输入有效的 BV 号或 bilibili 视频地址</p>';
}
// --- Utility ---
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
});

View File

@@ -1,634 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
// State
let currentStep = 1;
let selectedDevice = null;
let selectedEdition = 'java';
const totalSteps = 4;
// Elements
const prevBtn = document.getElementById('btn-prev');
const nextBtn = document.getElementById('btn-next');
// Updated selector for new sidebar structure
const stepIndicators = document.querySelectorAll('.progress-step');
const stepContents = document.querySelectorAll('.step-content');
const conventionContent = document.getElementById('convention-content');
if (!conventionContent) {
console.error('Critical Error: Element #convention-content not found in DOM');
return;
}
const agreeCheckbox = document.getElementById('agree-checkbox');
const deviceCards = document.querySelectorAll('.device-card');
const recommendationSection = document.getElementById('launcher-recommendation');
const recommendationContent = document.getElementById('recommendation-content');
const tutorialContent = document.getElementById('tutorial-content');
const step4Buttons = document.getElementById('step4-buttons');
const mainWizardActions = document.querySelector('.wizard-actions');
const editionSelector = document.getElementById('edition-selector');
const editionBtns = document.querySelectorAll('.edition-btn');
console.log('DOM Elements loaded. Step contents found:', stepContents.length);
// Fallback if marked is not loading correctly
if (typeof marked === 'undefined') {
console.warn("Marked not defined globally, checking for window.marked");
if (window.marked) {
console.log("Found window.marked");
// Assign to local variable if needed or just use window.marked
}
}
// --- Step 1: Convention Loading ---
console.log('Fetching convention from data/convention.md ...');
fetch('data/convention.md')
.then(response => {
console.log('Response status:', response.status);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.text();
})
.then(markdown => {
console.log('Convention loaded, length:', markdown.length);
let parser = null;
if (typeof marked !== 'undefined') {
if (typeof marked.parse === 'function') parser = marked.parse;
else if (typeof marked === 'function') parser = marked;
} else if (window.marked) {
if (typeof window.marked.parse === 'function') parser = window.marked.parse;
else if (typeof window.marked === 'function') parser = window.marked;
}
if (parser) {
try {
const result = parser(markdown);
if (result instanceof Promise) {
result.then(html => conventionContent.innerHTML = html);
} else {
conventionContent.innerHTML = result;
}
} catch (e) {
console.error('Parse error:', e);
conventionContent.innerHTML = '<pre>' + markdown + '</pre>';
}
} else {
console.error('No markdown parser found');
conventionContent.innerHTML = '<pre>' + markdown + '</pre>';
}
})
.catch(error => {
console.error('Convention fetch error:', error);
conventionContent.innerHTML = `<p style="color:red">无法加载公约内容: ${error.message}</p>`;
});
// --- Navigation Logic ---
function updateWizard() {
console.log('UpdateWizard called, step:', currentStep);
// Update Indicators
stepIndicators.forEach(indicator => {
const step = parseInt(indicator.dataset.step);
if (step === currentStep) {
indicator.classList.add('active');
indicator.classList.remove('completed');
// Scroll into view on mobile
if (window.innerWidth <= 800) {
indicator.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}
} else if (step < currentStep) {
indicator.classList.add('completed');
indicator.classList.remove('active');
} else {
indicator.classList.remove('active', 'completed');
}
});
// Update Progress Bar Fill
const progressFill = document.getElementById('progress-fill');
if (progressFill) {
const progress = ((currentStep - 1) / (totalSteps - 1)) * 100;
progressFill.style.width = `${progress}%`;
}
// Update Content visibility with Animation timeout
stepContents.forEach(content => {
if (content.id === `step-${currentStep}`) {
content.classList.add('active');
// Optional: ensure display block if handled by CSS alone or JS
} else {
content.classList.remove('active');
}
});
// Buttons State
updateButtons();
}
function updateButtons() {
// Prev Button
if (prevBtn) prevBtn.disabled = currentStep === 1;
// Next Button logic
if (currentStep === 1) {
// Step 1: Checkbox required
if (nextBtn) nextBtn.disabled = !agreeCheckbox.checked;
} else if (currentStep === 2) {
// Step 2: Device selection required
if (nextBtn) nextBtn.disabled = !selectedDevice;
} else {
if (nextBtn) nextBtn.disabled = false;
}
// Step 4 special buttons visibility
if (currentStep === totalSteps) {
if (nextBtn) nextBtn.style.display = 'none';
if (step4Buttons) step4Buttons.classList.remove('hidden');
} else {
if (nextBtn) nextBtn.style.display = 'inline-flex';
if (step4Buttons) step4Buttons.classList.add('hidden');
}
}
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (currentStep > 1) {
currentStep--;
updateWizard();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', () => {
if (currentStep < totalSteps) {
if (currentStep === 2) {
renderTutorial(); // Generate step 3 content before showing
// Also scroll to top for better ux
window.scrollTo({ top: 0, behavior: 'smooth' });
}
currentStep++;
updateWizard();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
});
}
// Step 1 Checkbox
if (agreeCheckbox) {
agreeCheckbox.addEventListener('change', updateButtons);
}
// --- Step 2: Device Selection ---
const deviceData = {
pc: {
title: "电脑版 (Java Edition)",
recommendations: [
{
name: "PCL2",
icon: "fas fa-cube",
desc: "界面精美功能强大的现代化启动器仅Win",
url: "https://afdian.net/p/0164034c016c11ebafcb52540025c377",
primary: true
},
{
name: "HMCL",
icon: "fas fa-horse-head",
desc: "历史悠久,跨平台支持好 (Win/Mac/Linux)",
url: "https://hmcl.huangyuhui.net/",
primary: false
}
],
note: "推荐使用 PCL2 或 HMCL均支持极大改善游戏体验。"
},
ios: {
title: "iOS 设备",
recommendations: [
{
name: "PojavLauncher",
icon: "fab fa-app-store-ios",
desc: "iOS 上运行 Java 版的唯一选择",
url: "https://apps.apple.com/us/app/pojavlauncher/id6443526546",
primary: true
}
],
note: "需要 iOS 14.0 或更高版本。若未越狱,请保持 JIT 开启以获得最佳性能(部分版本可能需要)。"
},
android: {
title: "安卓设备",
recommendations: [
{
name: "FCL 启动器",
icon: "fab fa-android",
desc: "基于 FoldCraft 的高性能启动器",
url: "https://github.com/FoldCraftLauncher/FoldCraftLauncher/releases",
primary: true
},
{
name: "PojavLauncher",
icon: "fas fa-gamepad",
desc: "经典的移动端 Java 版启动器",
url: "https://play.google.com/store/apps/details?id=net.kdt.pojavlaunch",
primary: false
}
],
note: "建议设备拥有至少 4GB 运存以流畅运行 1.21 版本。"
}
};
const bedrockDeviceData = {
pc: {
title: "电脑版 (Bedrock Edition)",
recommendations: [
{
name: "Minecraft 基岩版",
icon: "fas fa-cube",
desc: "从 Microsoft Store 获取 Minecraft需 Windows 10/11",
url: "https://www.xbox.com/games/store/minecraft/9NBLGGH2JHXJ",
primary: true
}
],
note: "基岩版通过 Microsoft Store 购买,使用 Xbox / Microsoft 账号登录即可游玩。"
},
ios: {
title: "iOS 基岩版",
recommendations: [
{
name: "Minecraft",
icon: "fas fa-cube",
desc: "从 App Store 购买并下载 Minecraft",
url: "https://apps.apple.com/app/minecraft/id479516143",
primary: true
}
],
note: "基岩版是 iOS 上的原生 Minecraft性能最佳、操作体验最好。"
},
android: {
title: "安卓基岩版",
recommendations: [
{
name: "Minecraft",
icon: "fas fa-cube",
desc: "从 Google Play 购买并下载 Minecraft",
url: "https://play.google.com/store/apps/details?id=com.mojang.minecraftpe",
primary: true
}
],
note: "基岩版是安卓上的原生 Minecraft性能最佳、操作体验最好。"
}
};
deviceCards.forEach(card => {
card.addEventListener('click', () => {
// UI Update
deviceCards.forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
const deviceType = card.dataset.device;
selectedDevice = deviceType;
selectedEdition = 'java';
// Show edition selector and reset to Java
if (editionSelector) {
editionSelector.classList.remove('hidden');
editionBtns.forEach(btn => {
btn.classList.toggle('active', btn.dataset.edition === 'java');
});
}
// Show Recommendation
showRecommendation(deviceType);
// Re-enable next button
updateButtons();
});
});
// Edition toggle handlers
editionBtns.forEach(btn => {
btn.addEventListener('click', () => {
editionBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedEdition = btn.dataset.edition;
if (selectedDevice) {
showRecommendation(selectedDevice);
}
});
});
function showRecommendation(device) {
const data = selectedEdition === 'bedrock' ? bedrockDeviceData[device] : deviceData[device];
if (!data) return;
if (recommendationSection) {
recommendationSection.classList.remove('hidden');
// Little fade-in effect
recommendationSection.style.opacity = '0';
setTimeout(() => recommendationSection.style.opacity = '1', 50);
}
let cardsHtml = data.recommendations.map(req => `
<a href="${req.url}" target="_blank" class="launcher-card ${req.primary ? 'primary' : ''}">
<div class="launcher-icon">
<i class="${req.icon}"></i>
</div>
<div class="launcher-details">
<h4>${req.name} ${req.primary ? '<span class="badge-rec">推荐</span>' : ''}</h4>
<p>${req.desc}</p>
</div>
<div class="launcher-action">
<i class="fas fa-download"></i>
</div>
</a>
`).join('');
const html = `
<div class="recommendation-header">
<h3>为 ${data.title} 准备启动器</h3>
<p>${data.note}</p>
</div>
<div class="launcher-grid">
${cardsHtml}
</div>
`;
if (recommendationContent) recommendationContent.innerHTML = html;
}
// --- Step 3: Tutorial Rendering ---
const deviceTutorials = {
pc: [
{
title: '登录账号',
desc: '打开启动器PCL2/HMCL选择“添加账号”。推荐使用 Microsoft 账号登录拥有正版 Minecraft 的账户。'
},
{
title: '安装游戏',
desc: '在启动器中创建一个新游戏配置,选择游戏版本 <strong>1.21.x</strong>。强烈建议安装 <a href="https://fabricmc.net/" target="_blank">Fabric</a> 加载器以获得更好的模组支持和性能优化。'
},
{
title: '启动游戏',
desc: '等待游戏资源文件下载完成,点击启动游戏直到看到 Minecraft 主界面。'
},
{
title: '加入服务器',
desc: `点击“多人游戏” -> “添加服务器”。<br>
服务器名称:<span class="highlight-text">白鹿原</span><br>
输入以下服务器地址,点击“完成”并双击服务器即可加入。
<div class="server-address-box">
<code id="server-address">mcpure.lunadeer.cn</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('mcpure.lunadeer.cn').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i> 复制
</button>
</div>
`
}
],
ios: [
{
title: '准备环境',
desc: '打开 PojavLauncher。若您的设备未越狱请确保已启用 JITJust-In-Time以获得可玩的帧率。'
},
{
title: '登录账号',
desc: '点击“添加账户”选择“Microsoft 账户”并完成登录流程。'
},
{
title: '下载并启动',
desc: '点击“创建新配置”,选择版本 <strong>1.21.x</strong>。建议调整内存分配至设备总内存的 50% 左右,然后点击“启动”。'
},
{
title: '加入服务器',
desc: `进入主界面后,选择 Multiplayer多人游戏 -> Add Server添加服务器。<br>
Address地址填写以下内容点击 Done 并加入。
<div class="server-address-box">
<code id="server-address-ios">mcpure.lunadeer.cn</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('mcpure.lunadeer.cn').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i>
</button>
</div>
`
}
],
android: [
{
title: '配置启动器',
desc: '打开 FCL 或 PojavLauncher。给予必要的存储权限。'
},
{
title: '登录账号',
desc: '在账户设置中添加 Microsoft 账户。'
},
{
title: '安装版本',
desc: '下载 <strong>1.21.x</strong> 游戏核心。FCL 用户可直接使用内置下载源加速下载。建议安装 OptiFine 或 Fabric+Sodium 以提升帧率。'
},
{
title: '加入服务器',
desc: `启动游戏后,点击 Multiplayer多人游戏 -> Add Server添加服务器。<br>
Address地址填写以下内容点击 Done 并加入。
<div class="server-address-box">
<code id="server-address-android">mcpure.lunadeer.cn</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('mcpure.lunadeer.cn').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i> 复制
</button>
</div>
`
}
]
};
const bedrockTutorials = {
pc: [
{
title: '获取游戏',
desc: '从 <a href="https://www.xbox.com/games/store/minecraft/9NBLGGH2JHXJ" target="_blank">Microsoft Store</a> 购买并下载 Minecraft基岩版/Bedrock Edition需要 Windows 10 或 Windows 11。'
},
{
title: '登录账号',
desc: '打开 Minecraft使用 Microsoft / Xbox 账号登录。'
},
{
title: '加入服务器',
desc: `点击"游戏" → 切换到"服务器"标签页 → 滚动到底部点击"添加服务器"。<br>
服务器名称:<span class="highlight-text">白鹿原</span><br>
填写以下服务器信息:
<div class="server-address-box">
<span>地址:</span><code>mcbe.lunadeer.cn</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('mcbe.lunadeer.cn').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i> 复制
</button>
</div>
<div class="server-address-box">
<span>端口:</span><code>15337</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('15337').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i> 复制
</button>
</div>
填写完成后点击"保存",然后选中该服务器加入即可。`
}
],
ios: [
{
title: '获取游戏',
desc: '从 <a href="https://apps.apple.com/app/minecraft/id479516143" target="_blank">App Store</a> 购买并下载 Minecraft。'
},
{
title: '登录账号',
desc: '打开 Minecraft使用 Microsoft / Xbox 账号登录。'
},
{
title: '加入服务器',
desc: `点击"游戏" → 切换到"服务器"标签页 → 滚动到底部点击"添加服务器"。<br>
服务器名称:<span class="highlight-text">白鹿原</span><br>
填写以下服务器信息:
<div class="server-address-box">
<span>地址:</span><code>mcbe.lunadeer.cn</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('mcbe.lunadeer.cn').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i> 复制
</button>
</div>
<div class="server-address-box">
<span>端口:</span><code>15337</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('15337').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i> 复制
</button>
</div>
填写完成后点击"保存",然后选中该服务器加入即可。`
}
],
android: [
{
title: '获取游戏',
desc: '从 <a href="https://play.google.com/store/apps/details?id=com.mojang.minecraftpe" target="_blank">Google Play</a> 购买并下载 Minecraft。'
},
{
title: '登录账号',
desc: '打开 Minecraft使用 Microsoft / Xbox 账号登录。'
},
{
title: '加入服务器',
desc: `点击"游戏" → 切换到"服务器"标签页 → 滚动到底部点击"添加服务器"。<br>
服务器名称:<span class="highlight-text">白鹿原</span><br>
填写以下服务器信息:
<div class="server-address-box">
<span>地址:</span><code>mcbe.lunadeer.cn</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('mcbe.lunadeer.cn').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i> 复制
</button>
</div>
<div class="server-address-box">
<span>端口:</span><code>15337</code>
<button class="btn-copy" onclick="navigator.clipboard.writeText('15337').then(() => { this.innerHTML = '<i class=\\'fas fa-check\\'></i> 已复制'; setTimeout(() => this.innerHTML = '<i class=\\'fas fa-copy\\'></i> 复制', 2000); })">
<i class="fas fa-copy"></i> 复制
</button>
</div>
填写完成后点击"保存",然后选中该服务器加入即可。`
}
]
};
function renderTutorial() {
const device = selectedDevice || 'pc';
let steps;
if (selectedEdition === 'bedrock') {
steps = bedrockTutorials[device] || bedrockTutorials['pc'];
} else {
steps = deviceTutorials[device] || deviceTutorials['pc'];
}
let content = '<div class="tutorial-steps">';
steps.forEach((step, index) => {
content += `
<div class="tutorial-step">
<div class="step-badge">${index + 1}</div>
<div class="step-text">
<h4>${step.title}</h4>
<p>${step.desc}</p>
</div>
</div>
`;
});
content += '</div>';
if (tutorialContent) {
tutorialContent.innerHTML = content;
}
}
// --- Step 4: Playstyle Selection ---
const playstyleCards = document.querySelectorAll('.playstyle-card');
const playstyleDetails = document.getElementById('playstyle-details');
const playstyleData = {
'large-town': {
title: '融入大型城镇',
subtitle: '快速启航,共建繁华 (10+人)',
target: '希望跳过艰难的初期积累,快速投入大规模建造与合作的玩家。',
pros: ['资源无忧:可直接从公共仓库获取建材与工具。', '工业完善:享受成熟的自动化生产带来的便利。'],
cons: ['为了整体美观与规划,可能需要遵守城镇的建筑风格与管理安排,自由度相对受限。']
},
'small-town': {
title: '加入小型城镇',
subtitle: '共同成长,见证历史 (3-10人)',
target: '喜欢参与从零到一的建设过程,享受亲手打造家园成就感的玩家。',
pros: ['发展参与感:亲身参与城镇的规划与扩张。', '自由度较高:在发展初期通常有更多的个人发挥空间。'],
cons: ['初期资源相对有限,需要与同伴共同努力。']
},
'friends': {
title: '与朋友共建家园',
subtitle: '白手起家,开创时代 (1-3人)',
target: '拥有固定小团体,渴望一片完全属于自己的领地的玩家。',
pros: ['绝对自由:从选址到规划,一切由你定义。', '纯粹体验:体验最原始的协作与创造乐趣。'],
cons: ['这是一条充满挑战的道路,但从无到有建立的一切都将格外珍贵。']
},
'solo': {
title: '独狼求生',
subtitle: '自力更生,隐于山林',
target: '享受孤独,崇尚一切亲力亲为的硬核生存玩家。',
pros: ['极致的自由与独立,你的世界只属于你。', '可尝试与其他玩家进行贸易换取无法独自获得的资源。'],
cons: ['一切都需要亲力亲为,生存挑战较大。']
}
};
if (playstyleCards.length > 0 && playstyleDetails) {
playstyleCards.forEach(card => {
card.addEventListener('click', () => {
// UI Visual Selection
playstyleCards.forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
// Get Data
const styleKey = card.dataset.style;
const data = playstyleData[styleKey];
if (data) {
// Populate Details
document.getElementById('detail-title').textContent = data.title;
document.getElementById('detail-subtitle').textContent = data.subtitle;
document.getElementById('detail-target').textContent = data.target;
const prosList = document.getElementById('detail-pros');
const consList = document.getElementById('detail-cons');
prosList.innerHTML = data.pros.map(p => `<li>${p}</li>`).join('');
consList.innerHTML = data.cons.map(c => `<li>${c}</li>`).join('');
// Show Details
playstyleDetails.classList.add('visible');
// Optional: scroll into view gently if needed, or stick to bottom
playstyleDetails.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
});
});
}
// Initial check
updateWizard();
});

69
js/marked.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,247 +0,0 @@
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);
});
}
// Dynamic Subtitle Rotation
const SUBTITLES = [
'纯净',
'原版',
'生存',
'养老',
'休闲'
];
let currentSubtitleIndex = 0;
function initDynamicSubtitle() {
const dynamicElement = document.getElementById('dynamic-subtitle');
if (!dynamicElement) return;
// Set initial subtitle
dynamicElement.textContent = SUBTITLES[0];
dynamicElement.classList.add('fade-enter-active');
// Start rotation
setInterval(() => {
// Fade out
dynamicElement.classList.remove('fade-enter-active');
dynamicElement.classList.add('fade-exit-active');
setTimeout(() => {
// Change text
currentSubtitleIndex = (currentSubtitleIndex + 1) % SUBTITLES.length;
dynamicElement.textContent = SUBTITLES[currentSubtitleIndex];
// Fade in
dynamicElement.classList.remove('fade-exit-active');
dynamicElement.classList.add('fade-enter-active');
}, 500);
}, 4000); // Change every 4 seconds
}
// Sponsors Logic
document.addEventListener('DOMContentLoaded', () => {
initDynamicSubtitle();
fetchSponsors();
fetchCrowdfunding();
// setupModal(); // Removed, modal is gone
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 player-item-muted">暂无玩家在线</div>';
}
} else {
countElement.innerText = '服务器离线';
dotElement.classList.add('offline');
listElement.innerHTML = '<div class="player-item player-item-error">服务器离线</div>';
}
} catch (error) {
console.error('Error fetching server status:', error);
countElement.innerText = '无法获取状态';
dotElement.classList.add('offline');
listElement.innerHTML = '<div class="player-item player-item-error">获取失败</div>';
}
}
async function fetchCrowdfunding() {
try {
console.log('Fetching crowdfunding data...');
const response = await fetch('data/fund_progress.txt');
if (!response.ok) {
console.error('Failed to fetch data/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" data-percentage="${percentage}"></div>
</div>
<div class="fund-percentage">${percentage.toFixed(1)}%</div>
</div>
`;
}).join('');
container.querySelectorAll('.progress-bar-fill').forEach(bar => {
const percentage = bar.dataset.percentage || '0';
bar.style.width = `${percentage}%`;
});
}
async function fetchSponsors() {
try {
const response = await fetch('data/sponsors.txt');
const text = await response.text();
const sponsors = DataUtils.parseSponsorsText(text);
const userTotals = DataUtils.buildSponsorTotals(sponsors);
// 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); // Removed as table is now on sponsor.html
} 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('');
}
/* Removed renderSponsorsTable and setupModal as they are no longer used on index page */

View File

@@ -1,223 +0,0 @@
let allSponsors = [];
let grandTotal = 0;
let filterState = { search: '', project: 'all' };
document.addEventListener('DOMContentLoaded', () => {
try {
setupUI();
} catch (e) {
console.error("UI Setup failed", e);
}
fetchSponsorsData();
setupListeners();
});
function setupUI() {
// Mobile menu toggle handled by components.js
// Modal Logic
const modal = document.getElementById('sponsor-modal');
const btn = document.getElementById('open-sponsor-modal');
const span = document.querySelector('.close-modal');
const desktopView = document.getElementById('desktop-qr-view');
const mobileView = document.getElementById('mobile-btn-view');
// Detect Mobile
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth < 768;
if (isMobile) {
if(desktopView) desktopView.style.display = 'none';
if(mobileView) mobileView.style.display = 'block';
} else {
if(desktopView) desktopView.style.display = 'block';
if(mobileView) mobileView.style.display = 'none';
}
if (modal && btn) {
btn.addEventListener('click', (e) => {
e.preventDefault();
modal.style.display = "flex";
// Trigger reflow
void modal.offsetWidth;
requestAnimationFrame(() => {
modal.classList.add('show');
});
});
}
if (span && modal) {
span.addEventListener('click', () => {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = "none";
}, 300);
});
}
window.addEventListener('click', (event) => {
if (modal && event.target == modal) {
modal.classList.remove('show');
setTimeout(() => {
modal.style.display = "none";
}, 300);
}
});
}
function setupListeners() {
const searchInput = document.getElementById('sponsor-search');
const filterContainer = document.getElementById('project-filters');
if (searchInput) {
searchInput.addEventListener('input', (e) => {
filterState.search = e.target.value.toLowerCase().trim();
applyFilters();
});
}
if (filterContainer) {
filterContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('filter-tag')) {
// Update active class
document.querySelectorAll('.filter-tag').forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
// Update filter state
filterState.project = e.target.dataset.project;
applyFilters();
}
});
}
}
async function fetchSponsorsData() {
try {
const response = await fetch('data/sponsors.txt');
if (!response.ok) {
throw new Error('Failed to fetch data/sponsors.txt');
}
const text = await response.text();
const sponsors = DataUtils.parseSponsorsText(text);
const projects = new Set();
grandTotal = 0;
sponsors.forEach(item => {
grandTotal += item.amount;
projects.add(item.project);
});
allSponsors = [...sponsors].reverse(); // Start with newest
// Animate Total
animateValue(grandTotal);
// Render everything
renderFilters(Array.from(projects));
applyFilters(); // Renders the grid initially
} catch (error) {
console.error('Error loading sponsors:', error);
const grid = document.getElementById('donation-list');
if(grid) grid.innerHTML = '<div class="sponsor-load-error">加载数据失败,请刷新重试</div>';
}
}
function animateValue(end) {
const obj = document.getElementById('total-amount-display');
if (!obj) return;
// Simple count up
let startTimestamp = null;
const duration = 2000;
const start = 0;
const step = (timestamp) => {
if (!startTimestamp) startTimestamp = timestamp;
const progress = Math.min((timestamp - startTimestamp) / duration, 1);
// Easing out Quart
const easeProgress = 1 - Math.pow(1 - progress, 4);
const current = Math.floor(easeProgress * (end - start) + start);
obj.innerHTML = `¥${current.toLocaleString('en-US')}`;
if (progress < 1) {
window.requestAnimationFrame(step);
} else {
obj.innerHTML = `¥${end.toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2})}`;
}
};
window.requestAnimationFrame(step);
}
function renderFilters(projects) {
const container = document.getElementById('project-filters');
if (!container) return;
// Remove existing project buttons, keep "All"
const existingButtons = container.querySelectorAll('button:not([data-project="all"])');
existingButtons.forEach(btn => btn.remove());
// Add project buttons
projects.forEach(proj => {
if (!proj) return;
const btn = document.createElement('button');
btn.className = 'filter-tag';
btn.textContent = proj;
btn.dataset.project = proj;
container.appendChild(btn);
});
}
function applyFilters() {
const { search, project } = filterState;
const grid = document.getElementById('donation-list');
const noResults = document.getElementById('no-results');
if (!grid) return;
const filtered = allSponsors.filter(item => {
const matchesProject = project === 'all' || item.project === project;
const matchesSearch = item.name.toLowerCase().includes(search);
return matchesProject && matchesSearch;
});
grid.innerHTML = '';
if (filtered.length === 0) {
if (noResults) noResults.style.display = 'block';
return;
}
if (noResults) noResults.style.display = 'none';
filtered.forEach((item, index) => {
const card = document.createElement('div');
card.className = 'donation-card';
// Max delay 1s to prevent long waits on huge lists
const delay = Math.min(index * 0.05, 1);
card.style.animationDelay = `${delay}s`;
const avatarUrl = `https://minotar.net/helm/${item.name}/64.png`;
card.innerHTML = `
<div class="donation-header">
<div class="donor-info">
<img src="${avatarUrl}" class="mini-avatar" onerror="this.src='https://minotar.net/helm/MHF_Steve/64.png'" alt="${item.name}">
<div class="donor-name">${item.name}</div>
</div>
<div class="donation-amount">¥${item.amount}</div>
</div>
<div class="donation-card-body">
<div class="donation-purpose">${item.project}</div>
<div class="donation-date">
<i class="far fa-clock donation-date-icon"></i>${item.date}
</div>
</div>
`;
grid.appendChild(card);
});
}

View File

@@ -1,408 +0,0 @@
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;
// Show update time
if (data.updated_at) {
document.getElementById('stats-updated-at').textContent = '数据更新于 ' + data.updated_at;
}
// 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;
// Special case for play_time which has raw sorting value
if (key === 'play_time_fmt') valA = a.stats.play_time_raw;
if (key === 'play_time_fmt') valB = b.stats.play_time_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 class="lb-top-name">${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 class="lb-item-main">
<span class="lb-rank">${i+1}</span>
<span class="lb-item-name">${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.toLocaleString());
// 5. Play Time (stats.play_time_raw)
const topPlayTime = getTop('play_time_fmt'); // uses play_time_raw internally
renderCard('lb-playtime', topPlayTime, p => p.stats.play_time_fmt);
// 6. Kills (stats.kills)
const topKills = getTop('kills');
renderCard('lb-kills', topKills, p => p.stats.kills.toLocaleString());
}
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) {
const modal = document.getElementById("player-modal");
// Top Section: Identity
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`; };
// Top Section: Summary Stats
// These are from summary.json which is already formatted
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;
document.getElementById('modal-kills').innerText = player.stats.kills;
document.getElementById('modal-playtime').innerText = player.stats.play_time_fmt;
// Bottom Section: Reset and Load Details
const accordion = document.getElementById('stats-accordion');
accordion.innerHTML = '';
document.getElementById('loading-details').style.display = 'block';
document.getElementById('loading-details').innerText = '正在加载详细数据...';
modal.style.display = "flex";
// Load existing details
loadPlayerDetails(player.uuid);
}
async function loadPlayerDetails(uuid) {
const accordion = document.getElementById('stats-accordion');
const loading = document.getElementById('loading-details');
try {
const response = await fetch(`stats/${uuid}.json`);
if (!response.ok) throw new Error('Stats file not found');
const data = await response.json();
if (!data.stats) {
loading.innerText = '暂无详细统计数据。';
return;
}
loading.style.display = 'none';
renderDetailsAccordion(data.stats);
} catch (error) {
console.error('Error loading details:', error);
loading.innerText = '无法加载详细数据。';
}
}
function renderDetailsAccordion(statsObj) {
const accordion = document.getElementById('stats-accordion');
// Define category mappings for better display names
const categoryMap = {
'minecraft:custom': { name: '通用统计', icon: 'fa-chart-bar' },
'minecraft:mined': { name: '挖掘统计', icon: 'fa-hammer' },
'minecraft:used': { name: '使用统计', icon: 'fa-hand-paper' },
'minecraft:crafted': { name: '合成统计', icon: 'fa-tools' },
'minecraft:broken': { name: '破坏统计', icon: 'fa-heart-broken' },
'minecraft:picked_up': { name: '拾取统计', icon: 'fa-box-open' },
'minecraft:dropped': { name: '丢弃统计', icon: 'fa-trash' },
'minecraft:killed': { name: '击杀统计', icon: 'fa-crosshairs' },
'minecraft:killed_by': { name: '死亡统计', icon: 'fa-skull' }
};
// Helper to format keys (minecraft:stone -> Stone)
const formatKey = (key) => {
if (key.includes(':')) key = key.split(':')[1];
return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
};
// Sort categories to put 'custom' first, then others alphabetically
const sortedKeys = Object.keys(statsObj).sort((a, b) => {
if (a === 'minecraft:custom') return -1;
if (b === 'minecraft:custom') return 1;
return a.localeCompare(b);
});
// Threshold for showing search filter
const SEARCH_THRESHOLD = 20;
sortedKeys.forEach(catKey => {
const subStats = statsObj[catKey];
const itemCount = Object.keys(subStats).length;
if (itemCount === 0) return;
const catInfo = categoryMap[catKey] || { name: formatKey(catKey), icon: 'fa-folder' };
// Create Accordion Item
const item = document.createElement('div');
item.className = 'accordion-item';
// Header with item count badge
const header = document.createElement('div');
header.className = 'accordion-header';
header.innerHTML = `
<span><i class="fas ${catInfo.icon} icon"></i> ${catInfo.name}<span class="item-count">${itemCount}</span></span>
<i class="fas fa-chevron-down arrow"></i>
`;
// Content
const content = document.createElement('div');
content.className = 'accordion-content';
// Search filter for large categories
let searchInput = null;
let noResults = null;
if (itemCount >= SEARCH_THRESHOLD) {
const searchWrapper = document.createElement('div');
searchWrapper.className = 'detail-search-wrapper';
searchWrapper.innerHTML = '<i class="fas fa-search"></i>';
searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'detail-search';
searchInput.placeholder = '搜索条目...';
searchWrapper.appendChild(searchInput);
content.appendChild(searchWrapper);
noResults = document.createElement('div');
noResults.className = 'detail-no-results';
noResults.textContent = '没有匹配的条目';
content.appendChild(noResults);
}
// Grid for stats
const grid = document.createElement('div');
grid.className = 'detail-stats-grid';
// Sort sub-items by value descending
const subItems = Object.entries(subStats).sort((a, b) => b[1] - a[1]);
subItems.forEach(([k, v], index) => {
let label = formatKey(k);
let val = v.toLocaleString();
// Special formatting for time/distance in 'custom'
if (catKey === 'minecraft:custom') {
if (k.includes('time') || k.includes('minute')) {
if (k.includes('play_time') || k.includes('time_since')) {
const sec = v / 20;
if (sec > 3600) val = (sec/3600).toFixed(1) + ' h';
else if (sec > 60) val = (sec/60).toFixed(1) + ' m';
else val = sec.toFixed(0) + ' s';
}
}
if (k.includes('cmt') || k.includes('one_cm')) {
const m = v / 100;
if (m > 1000) val = (m/1000).toFixed(2) + ' km';
else val = m.toFixed(1) + ' m';
}
}
// Rank class for top 3
let rankClass = '';
if (index === 0) rankClass = ' rank-1';
else if (index === 1) rankClass = ' rank-2';
else if (index === 2) rankClass = ' rank-3';
const statDiv = document.createElement('div');
statDiv.className = 'detail-stat-item' + rankClass;
statDiv.dataset.label = label.toLowerCase();
statDiv.innerHTML = `
<span class="detail-stat-value">${val}</span>
<span class="detail-stat-label" title="${label}">${label}</span>
`;
grid.appendChild(statDiv);
});
// Wire up search filter
if (searchInput) {
searchInput.addEventListener('input', () => {
const query = searchInput.value.toLowerCase().trim();
const items = grid.querySelectorAll('.detail-stat-item');
let visible = 0;
items.forEach(el => {
const match = !query || el.dataset.label.includes(query);
el.classList.toggle('hidden', !match);
if (match) visible++;
});
noResults.style.display = visible === 0 ? 'block' : 'none';
});
}
content.appendChild(grid);
item.appendChild(header);
item.appendChild(content);
accordion.appendChild(item);
// Click Event
header.addEventListener('click', () => {
const isActive = header.classList.contains('active');
// Close all others? Optional. Let's keep multiple openable.
// But if we want accordion behavior:
// document.querySelectorAll('.accordion-header').forEach(h => {
// h.classList.remove('active');
// h.nextElementSibling.classList.remove('show');
// });
if (!isActive) {
header.classList.add('active');
content.classList.add('show');
} else {
header.classList.remove('active');
content.classList.remove('show');
}
});
});
}

View File

@@ -1,931 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const DEFAULT_GRADIENT = {
from: '#667eea',
to: '#764ba2'
};
let townsData = [];
const grid = document.getElementById('towns-list');
const noResults = document.getElementById('no-results');
const scaleFilters = document.getElementById('scale-filters');
const typeFilters = document.getElementById('type-filters');
const recruitFilters = document.getElementById('recruit-filters');
const searchInput = document.getElementById('town-search');
// Modal Elements
const modal = document.getElementById('town-modal');
const closeModalBtn = modal.querySelector('.close-modal');
// Initial State
let currentFilters = {
scale: 'all',
townType: 'all',
recruitment: 'all',
search: ''
};
let currentDetailItem = null;
// Generate stable anchor ID for a town
function generateTownId(item) {
var raw = (item.title || '');
var hash = 0;
for (var i = 0; i < raw.length; i++) {
hash = ((hash << 5) - hash) + raw.charCodeAt(i);
hash |= 0;
}
return 't' + Math.abs(hash).toString(36);
}
// Handle URL hash: auto-open town modal
function handleHashNavigation() {
var hash = location.hash.replace('#', '');
if (!hash) return;
for (var i = 0; i < townsData.length; i++) {
if (generateTownId(townsData[i]) === hash) {
openModal(townsData[i]);
return;
}
}
}
// 1. Fetch Data
fetch('data/towns.json')
.then(response => response.json())
.then(data => {
townsData = data;
renderGrid();
handleHashNavigation();
})
.catch(err => {
console.error('Error loading towns:', err);
grid.innerHTML = '<p class="error">无法加载城镇数据。</p>';
});
// 2. Event Listeners
// Scale Filter
scaleFilters.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
Array.from(scaleFilters.children).forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
currentFilters.scale = e.target.dataset.filter;
renderGrid();
}
});
// Type Filter
typeFilters.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
Array.from(typeFilters.children).forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
currentFilters.townType = e.target.dataset.filter;
renderGrid();
}
});
// Recruit Filter
recruitFilters.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') {
Array.from(recruitFilters.children).forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
currentFilters.recruitment = e.target.dataset.filter;
renderGrid();
}
});
// Search
searchInput.addEventListener('input', (e) => {
currentFilters.search = e.target.value.toLowerCase().trim();
renderGrid();
});
// Modal Close
closeModalBtn.addEventListener('click', () => {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
history.replaceState(null, '', location.pathname + location.search);
});
window.addEventListener('click', (e) => {
if (e.target === modal) {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
history.replaceState(null, '', location.pathname + location.search);
}
});
// 3. Render Functions
function renderGrid() {
grid.innerHTML = '';
const filtered = townsData.filter(item => {
const matchScale = currentFilters.scale === 'all' || item.scale === currentFilters.scale;
const matchType = currentFilters.townType === 'all' || item.townType === currentFilters.townType;
const matchRecruit = currentFilters.recruitment === 'all' || item.recruitment === currentFilters.recruitment;
const matchSearch = !currentFilters.search ||
item.title.toLowerCase().includes(currentFilters.search);
return matchScale && matchType && matchRecruit && matchSearch;
});
if (filtered.length === 0) {
noResults.classList.remove('is-hidden');
return;
} else {
noResults.classList.add('is-hidden');
}
filtered.forEach(item => {
const card = document.createElement('div');
card.className = 'town-card';
card.onclick = () => openModal(item);
const hasLogo = item.logo && item.logo.trim() !== '';
const gradient = getTownGradient(item);
// Build card icon badges (scale + type + recruitment)
let iconsHtml = '';
iconsHtml += '<div class="town-card-icons">';
iconsHtml += '<span class="town-icon-badge icon-scale-' + item.scale + '" title="' + getScaleText(item.scale) + '"><i class="fas ' + getScaleIcon(item.scale) + '"></i></span>';
iconsHtml += '<span class="town-icon-badge icon-type-' + item.townType + '" title="' + getTownTypeText(item.townType) + '"><i class="fas ' + getTownTypeIcon(item.townType) + '"></i></span>';
iconsHtml += '<span class="town-icon-badge icon-recruit-' + item.recruitment + '" title="' + getRecruitText(item.recruitment) + '"><i class="fas ' + getRecruitIcon(item.recruitment) + '"></i></span>';
iconsHtml += '</div>';
card.innerHTML =
'<div class="town-card-bg' + (hasLogo ? '' : ' no-logo') + '"' +
(hasLogo ? ' style="background-image:url(\'' + escapeHtml(item.logo) + '\')"' : ' style="' + buildGradientBackgroundStyle(gradient) + '"') +
'>' +
(hasLogo ? '' : '<i class="fas fa-city town-logo-placeholder"></i>') +
iconsHtml +
'</div>' +
'<div class="town-card-body">' +
'<h3 class="town-card-title">' + escapeHtml(item.title) + '</h3>' +
'<div class="town-card-meta">' +
'<span class="town-meta-tag"><i class="fas ' + getScaleIcon(item.scale) + '"></i> ' + getScaleText(item.scale) + '</span>' +
'<span class="town-meta-tag"><i class="fas ' + getTownTypeIcon(item.townType) + '"></i> ' + getTownTypeText(item.townType) + '</span>' +
'<span class="town-meta-tag"><i class="fas ' + getRecruitIcon(item.recruitment) + '"></i> ' + getRecruitText(item.recruitment) + '</span>' +
'</div>' +
'</div>';
grid.appendChild(card);
});
}
function openModal(item) {
currentDetailItem = item;
// Banner
var banner = document.getElementById('town-modal-banner');
var hasLogo = item.logo && item.logo.trim() !== '';
var gradient = getTownGradient(item);
banner.className = 'town-modal-banner' + (hasLogo ? '' : ' no-logo');
if (hasLogo) {
banner.style.background = '';
banner.style.backgroundImage = "url('" + item.logo + "')";
banner.innerHTML = '';
} else {
banner.style.backgroundImage = '';
banner.style.background = buildGradientBackgroundValue(gradient);
banner.innerHTML = '<i class="fas fa-city town-banner-placeholder"></i>';
}
// Title
document.getElementById('town-modal-title').innerText = item.title;
// Badges
var badgesContainer = document.getElementById('town-modal-badges');
badgesContainer.innerHTML = '';
var scaleBadge = document.createElement('span');
scaleBadge.className = 'town-badge badge-scale-' + item.scale;
scaleBadge.innerHTML = '<i class="fas ' + getScaleIcon(item.scale) + '"></i> ' + getScaleText(item.scale);
badgesContainer.appendChild(scaleBadge);
var typeBadge = document.createElement('span');
typeBadge.className = 'town-badge badge-type-' + item.townType;
typeBadge.innerHTML = '<i class="fas ' + getTownTypeIcon(item.townType) + '"></i> ' + getTownTypeText(item.townType);
badgesContainer.appendChild(typeBadge);
var recruitBadge = document.createElement('span');
recruitBadge.className = 'town-badge badge-recruit-' + item.recruitment;
recruitBadge.innerHTML = '<i class="fas ' + getRecruitIcon(item.recruitment) + '"></i> ' + getRecruitText(item.recruitment);
badgesContainer.appendChild(recruitBadge);
// Coordinates & Dimension
var coords = item.coordinates || { x: 0, y: 64, z: 0 };
var dimension = item.dimension || 'overworld';
var isSecret = item.coordinatesSecret === true;
var locationTitleEl = document.getElementById('town-modal-location-title');
var dimensionEl = document.getElementById('town-modal-dimension');
var coordsEl = document.getElementById('town-modal-coords');
var mapLink = document.getElementById('town-modal-map-link');
locationTitleEl.innerHTML = '<i class="fas fa-map-marker-alt"></i> 位置信息';
if (isSecret) {
dimensionEl.innerText = '';
coordsEl.innerText = '保密';
mapLink.style.display = 'none';
} else {
dimensionEl.innerText = getDimensionText(dimension) + ': ';
coordsEl.innerText = 'X: ' + coords.x + ', Y: ' + coords.y + ', Z: ' + coords.z;
mapLink.style.display = '';
mapLink.href = 'https://mcmap.lunadeer.cn/#' + getDimensionMapWorld(dimension) + ':' + coords.x + ':' + coords.y + ':' + coords.z + ':500:0:0:0:1:flat';
}
// Founders
var foundersContainer = document.getElementById('town-modal-founders');
foundersContainer.innerHTML = '';
if (item.founders && item.founders.length > 0) {
item.founders.forEach(function(name) {
var tag = document.createElement('div');
tag.className = 'contributor-tag';
tag.innerHTML = '<img src="https://minotar.net/avatar/' + encodeURIComponent(name) + '/20" alt="' + escapeHtml(name) + '">' + escapeHtml(name);
foundersContainer.appendChild(tag);
});
} else {
foundersContainer.innerHTML = '<span class="text-secondary">暂无记录</span>';
}
// Members
var membersContainer = document.getElementById('town-modal-members');
membersContainer.innerHTML = '';
if (item.members && item.members.length > 0) {
item.members.forEach(function(name) {
var tag = document.createElement('div');
tag.className = 'contributor-tag';
tag.innerHTML = '<img src="https://minotar.net/avatar/' + encodeURIComponent(name) + '/20" alt="' + escapeHtml(name) + '">' + escapeHtml(name);
membersContainer.appendChild(tag);
});
} else {
membersContainer.innerHTML = '<span class="text-secondary">暂无记录</span>';
}
// Introduction
renderContentList(document.getElementById('town-modal-introduction'), item.introduction);
modal.style.display = 'block';
document.body.style.overflow = 'hidden';
// Update URL hash
var anchorId = generateTownId(item);
history.replaceState(null, '', '#' + anchorId);
}
function renderContentList(container, list) {
container.innerHTML = '';
if (!list || list.length === 0) {
container.innerHTML = '<p>无</p>';
return;
}
list.forEach(function(block) {
if (block.type === 'text') {
var p = document.createElement('p');
p.innerText = block.content;
container.appendChild(p);
} else if (block.type === 'image') {
var img = document.createElement('img');
img.src = block.content;
img.loading = 'lazy';
container.appendChild(img);
} else if (block.type === 'video') {
var bv = parseBVNumber(block.content);
if (bv) {
var wrapper = document.createElement('div');
wrapper.className = 'video-embed-wrapper';
var iframe = document.createElement('iframe');
iframe.src = 'https://player.bilibili.com/player.html?bvid=' + bv + '&autoplay=0&high_quality=1';
iframe.allowFullscreen = true;
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups');
iframe.loading = 'lazy';
wrapper.appendChild(iframe);
container.appendChild(wrapper);
} else {
var p = document.createElement('p');
p.className = 'text-secondary';
p.innerText = '无效的视频 BV 号';
container.appendChild(p);
}
}
});
}
function parseBVNumber(input) {
if (!input) return null;
input = input.trim();
var bvPattern = /^(BV[A-Za-z0-9]+)$/;
var directMatch = input.match(bvPattern);
if (directMatch) return directMatch[1];
var urlPattern = /bilibili\.com\/video\/(BV[A-Za-z0-9]+)/;
var urlMatch = input.match(urlPattern);
if (urlMatch) return urlMatch[1];
var generalPattern = /(BV[A-Za-z0-9]{10,})/;
var generalMatch = input.match(generalPattern);
if (generalMatch) return generalMatch[1];
return null;
}
// Helpers
function getScaleText(scale) {
var map = { 'small': '小型5人以下', 'medium': '中型2-10人', 'large': '大型10人以上' };
return map[scale] || scale;
}
function getScaleIcon(scale) {
var map = { 'small': 'fa-user', 'medium': 'fa-users', 'large': 'fa-city' };
return map[scale] || 'fa-users';
}
function getTownTypeText(type) {
var map = { 'building': '建筑', 'adventure': '冒险', 'industry': '工业' };
return map[type] || type;
}
function getTownTypeIcon(type) {
var map = { 'building': 'fa-building', 'adventure': 'fa-dragon', 'industry': 'fa-industry' };
return map[type] || 'fa-building';
}
function getRecruitText(recruitment) {
var map = { 'welcome': '欢迎加入', 'closed': '暂不招人', 'maybe': '可以考虑' };
return map[recruitment] || recruitment;
}
function getRecruitIcon(recruitment) {
var map = { 'welcome': 'fa-door-open', 'closed': 'fa-door-closed', 'maybe': 'fa-question-circle' };
return map[recruitment] || 'fa-info-circle';
}
function getDimensionText(dimension) {
var map = { 'overworld': '主世界', 'nether': '下界', 'the_end': '末地' };
return map[dimension] || '主世界';
}
function getDimensionMapWorld(dimension) {
var map = { 'overworld': 'world', 'nether': 'world_nether', 'the_end': 'world_the_end' };
return map[dimension] || 'world';
}
function normalizeHexColor(value, fallback) {
if (!value || typeof value !== 'string') return fallback;
var trimmed = value.trim();
if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return trimmed;
return fallback;
}
function getTownGradient(item) {
var gradient = item && item.gradient ? item.gradient : {};
return {
from: normalizeHexColor(gradient.from, DEFAULT_GRADIENT.from),
to: normalizeHexColor(gradient.to, DEFAULT_GRADIENT.to)
};
}
function buildGradientBackgroundValue(gradient) {
return 'linear-gradient(135deg, ' + gradient.from + ' 0%, ' + gradient.to + ' 100%)';
}
function buildGradientBackgroundStyle(gradient) {
return 'background:' + buildGradientBackgroundValue(gradient) + ';';
}
// Share town link
document.getElementById('btn-share-town').addEventListener('click', function() {
if (!currentDetailItem) return;
var anchorId = generateTownId(currentDetailItem);
var url = location.origin + location.pathname + '#' + anchorId;
var btn = document.getElementById('btn-share-town');
navigator.clipboard.writeText(url).then(function() {
btn.innerHTML = '<i class="fas fa-check"></i> 已复制链接';
btn.classList.add('shared');
setTimeout(function() {
btn.innerHTML = '<i class="fas fa-share-alt"></i> 分享';
btn.classList.remove('shared');
}, 2000);
}).catch(function() {
var tmp = document.createElement('input');
tmp.value = url;
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
btn.innerHTML = '<i class="fas fa-check"></i> 已复制链接';
setTimeout(function() {
btn.innerHTML = '<i class="fas fa-share-alt"></i> 分享';
}, 2000);
});
});
// Open editor from detail modal
document.getElementById('btn-edit-town').addEventListener('click', function() {
if (currentDetailItem) {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
openEditor(currentDetailItem);
}
});
// ========== Editor Modal Logic ==========
var editorModal = document.getElementById('town-editor-modal');
var jsonOutputModal = document.getElementById('town-json-output-modal');
var closeEditorModalBtn = editorModal.querySelector('.close-editor-modal');
var closeJsonModalBtn = jsonOutputModal.querySelector('.close-json-modal');
// Open empty editor for new town
document.getElementById('btn-add-town').addEventListener('click', function() {
openEditor(null);
});
// Close editor modal
closeEditorModalBtn.addEventListener('click', function() {
editorModal.style.display = 'none';
document.body.style.overflow = 'auto';
});
window.addEventListener('click', function(e) {
if (e.target === editorModal) {
editorModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
if (e.target === jsonOutputModal) {
jsonOutputModal.style.display = 'none';
}
});
closeJsonModalBtn.addEventListener('click', function() {
jsonOutputModal.style.display = 'none';
});
// State for editor
var editorFounders = [];
var editorMembers = [];
var editorIntroduction = [];
// Initialize custom selects
editorModal.querySelectorAll('.custom-select').forEach(function(select) {
var trigger = select.querySelector('.custom-select-trigger');
var options = select.querySelectorAll('.custom-option');
var input = select.querySelector('input[type="hidden"]');
var text = select.querySelector('.custom-select-text');
trigger.addEventListener('click', function(e) {
e.stopPropagation();
var isOpen = select.classList.contains('open');
editorModal.querySelectorAll('.custom-select').forEach(function(s) { s.classList.remove('open'); });
if (!isOpen) {
select.classList.add('open');
}
});
options.forEach(function(option) {
option.addEventListener('click', function(e) {
e.stopPropagation();
options.forEach(function(opt) { opt.classList.remove('selected'); });
option.classList.add('selected');
text.innerText = option.innerText;
input.value = option.dataset.value;
input.dispatchEvent(new Event('change'));
select.classList.remove('open');
});
});
});
document.addEventListener('click', function() {
editorModal.querySelectorAll('.custom-select').forEach(function(s) { s.classList.remove('open'); });
});
function setCustomSelectValue(id, value) {
var input = document.getElementById(id);
if (!input) return;
var select = input.closest('.custom-select');
var option = select.querySelector('.custom-option[data-value="' + value + '"]');
if (option) {
input.value = value;
select.querySelector('.custom-select-text').innerText = option.innerText;
select.querySelectorAll('.custom-option').forEach(function(opt) { opt.classList.remove('selected'); });
option.classList.add('selected');
}
}
function openEditor(item) {
var gradient = getTownGradient(item || {});
var coordinates = item && item.coordinates ? item.coordinates : { x: '', y: '', z: '' };
editorFounders = item && Array.isArray(item.founders) ? item.founders.slice() : [];
editorMembers = item && Array.isArray(item.members) ? item.members.slice() : [];
editorIntroduction = item && Array.isArray(item.introduction) ? item.introduction.map(function(i) { return {type: i.type, content: i.content}; }) : [];
document.getElementById('editor-town-title').value = item ? item.title : '';
document.getElementById('editor-town-logo').value = item ? (item.logo || '') : '';
document.getElementById('editor-town-gradient-from').value = gradient.from;
document.getElementById('editor-town-gradient-to').value = gradient.to;
setCustomSelectValue('editor-town-scale', item ? item.scale : 'small');
setCustomSelectValue('editor-town-type', item ? item.townType : 'building');
setCustomSelectValue('editor-town-recruit', item ? item.recruitment : 'welcome');
setCustomSelectValue('editor-town-dimension', item && item.dimension ? item.dimension : 'overworld');
var secretCheckbox = document.getElementById('editor-town-secret');
secretCheckbox.checked = item ? (item.coordinatesSecret === true) : false;
toggleCoordsRow();
document.getElementById('editor-town-x').value = item ? coordinates.x : '';
document.getElementById('editor-town-y').value = item ? coordinates.y : '';
document.getElementById('editor-town-z').value = item ? coordinates.z : '';
renderTagsList('editor-founders-tags', editorFounders);
renderTagsList('editor-members-tags', editorMembers);
renderSortableList('editor-introduction-list', editorIntroduction);
updatePreview();
editorModal.style.display = 'block';
document.body.style.overflow = 'hidden';
}
// --- Tags input helpers ---
function renderTagsList(containerId, list) {
var container = document.getElementById(containerId);
container.innerHTML = '';
list.forEach(function(name, idx) {
var tag = document.createElement('span');
tag.className = 'editor-tag';
tag.innerHTML = escapeHtml(name) + ' <span class="editor-tag-remove" data-idx="' + idx + '" data-list="' + containerId + '"><i class="fas fa-times"></i></span>';
container.appendChild(tag);
});
}
function commitTagInput(inputId, list, tagsContainerId) {
var input = document.getElementById(inputId);
var value = input.value.trim();
if (value && list.indexOf(value) === -1) {
list.push(value);
renderTagsList(tagsContainerId, list);
updatePreview();
}
input.value = '';
}
// Founders tags
document.getElementById('editor-founders-tags').addEventListener('click', function(e) {
var removeBtn = e.target.closest('.editor-tag-remove');
if (removeBtn) {
var idx = parseInt(removeBtn.dataset.idx);
editorFounders.splice(idx, 1);
renderTagsList('editor-founders-tags', editorFounders);
updatePreview();
}
});
document.getElementById('editor-founder-input').addEventListener('keydown', function(e) {
if (e.isComposing) return;
if (e.key === 'Enter' || e.key === ' ' || e.code === 'Space') {
e.preventDefault();
commitTagInput('editor-founder-input', editorFounders, 'editor-founders-tags');
}
});
document.getElementById('editor-founder-input').addEventListener('blur', function() {
commitTagInput('editor-founder-input', editorFounders, 'editor-founders-tags');
});
document.getElementById('editor-founders-wrapper').addEventListener('click', function() {
document.getElementById('editor-founder-input').focus();
});
// Members tags
document.getElementById('editor-members-tags').addEventListener('click', function(e) {
var removeBtn = e.target.closest('.editor-tag-remove');
if (removeBtn) {
var idx = parseInt(removeBtn.dataset.idx);
editorMembers.splice(idx, 1);
renderTagsList('editor-members-tags', editorMembers);
updatePreview();
}
});
document.getElementById('editor-member-input').addEventListener('keydown', function(e) {
if (e.isComposing) return;
if (e.key === 'Enter' || e.key === ' ' || e.code === 'Space') {
e.preventDefault();
commitTagInput('editor-member-input', editorMembers, 'editor-members-tags');
}
});
document.getElementById('editor-member-input').addEventListener('blur', function() {
commitTagInput('editor-member-input', editorMembers, 'editor-members-tags');
});
document.getElementById('editor-members-wrapper').addEventListener('click', function() {
document.getElementById('editor-member-input').focus();
});
// --- Sortable Lists (drag-and-drop) ---
var dragState = { listId: null, fromIdx: null };
function renderSortableList(listId, items) {
var container = document.getElementById(listId);
container.innerHTML = '';
items.forEach(function(item, idx) {
var div = document.createElement('div');
div.className = 'sortable-item';
div.draggable = true;
div.dataset.idx = idx;
div.dataset.listId = listId;
var typeBadgeClass = item.type === 'text' ? 'badge-text' : item.type === 'image' ? 'badge-image' : 'badge-video';
var typeBadgeLabel = item.type === 'text' ? '文字' : item.type === 'image' ? '图片' : '视频';
var contentHtml;
if (item.type === 'text') {
contentHtml = '<textarea class="item-content" rows="2" placeholder="输入文字内容...">' + escapeHtml(item.content) + '</textarea>';
} else if (item.type === 'image') {
contentHtml = '<input type="text" class="item-content" placeholder="输入图片URL..." value="' + escapeHtml(item.content) + '">';
} else {
contentHtml = '<input type="text" class="item-content" placeholder="BV1xxxxxxxxxx 或 bilibili 视频地址" value="' + escapeHtml(item.content) + '">';
}
div.innerHTML =
'<span class="drag-handle"><i class="fas fa-grip-vertical"></i></span>' +
'<span class="item-type-badge ' + typeBadgeClass + '">' + typeBadgeLabel + '</span>' +
contentHtml +
'<button type="button" class="remove-item-btn" title="删除"><i class="fas fa-trash-alt"></i></button>';
container.appendChild(div);
div.addEventListener('dragstart', onDragStart);
div.addEventListener('dragover', onDragOver);
div.addEventListener('dragenter', onDragEnter);
div.addEventListener('dragleave', onDragLeave);
div.addEventListener('drop', onDrop);
div.addEventListener('dragend', onDragEnd);
var contentEl = div.querySelector('.item-content');
contentEl.addEventListener('input', function() {
items[idx].content = contentEl.value;
updatePreview();
});
div.querySelector('.remove-item-btn').addEventListener('click', function() {
items.splice(idx, 1);
renderSortableList(listId, items);
updatePreview();
});
});
}
function onDragStart(e) {
var item = e.target.closest('.sortable-item');
if (!item) return;
dragState.listId = item.dataset.listId;
dragState.fromIdx = parseInt(item.dataset.idx);
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', '');
}
function onDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function onDragEnter(e) {
var item = e.target.closest('.sortable-item');
if (item && item.dataset.listId === dragState.listId) {
item.classList.add('drag-over');
}
}
function onDragLeave(e) {
var item = e.target.closest('.sortable-item');
if (item) {
item.classList.remove('drag-over');
}
}
function onDrop(e) {
e.preventDefault();
var item = e.target.closest('.sortable-item');
if (!item || item.dataset.listId !== dragState.listId) return;
var toIdx = parseInt(item.dataset.idx);
var fromIdx = dragState.fromIdx;
if (fromIdx === toIdx) return;
var listId = dragState.listId;
var items = editorIntroduction;
var moved = items.splice(fromIdx, 1)[0];
items.splice(toIdx, 0, moved);
renderSortableList(listId, items);
updatePreview();
}
function onDragEnd() {
document.querySelectorAll('.sortable-item').forEach(function(el) {
el.classList.remove('dragging', 'drag-over');
});
dragState = { listId: null, fromIdx: null };
}
// --- Add item buttons ---
editorModal.querySelectorAll('.add-item-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var type = btn.dataset.type;
var newItem = { type: type, content: '' };
editorIntroduction.push(newItem);
renderSortableList('editor-introduction-list', editorIntroduction);
updatePreview();
});
});
// --- Live Preview ---
['editor-town-title', 'editor-town-logo', 'editor-town-scale', 'editor-town-type',
'editor-town-recruit', 'editor-town-dimension', 'editor-town-x', 'editor-town-y', 'editor-town-z'].forEach(function(id) {
var el = document.getElementById(id);
el.addEventListener('input', updatePreview);
el.addEventListener('change', updatePreview);
});
['editor-town-gradient-from', 'editor-town-gradient-to'].forEach(function(id) {
var el = document.getElementById(id);
el.addEventListener('input', updatePreview);
el.addEventListener('change', updatePreview);
});
// Secret coordinates toggle
function toggleCoordsRow() {
var secret = document.getElementById('editor-town-secret').checked;
var dimensionGroup = document.getElementById('editor-dimension-group');
var coordsRow = document.getElementById('editor-coords-row');
dimensionGroup.style.display = secret ? 'none' : '';
coordsRow.style.display = secret ? 'none' : '';
}
document.getElementById('editor-town-secret').addEventListener('change', function() {
toggleCoordsRow();
updatePreview();
});
function updatePreview() {
var preview = document.getElementById('town-editor-preview-area');
var title = document.getElementById('editor-town-title').value || '未命名城镇';
var logo = document.getElementById('editor-town-logo').value.trim();
var scale = document.getElementById('editor-town-scale').value;
var townType = document.getElementById('editor-town-type').value;
var recruit = document.getElementById('editor-town-recruit').value;
var x = document.getElementById('editor-town-x').value || '0';
var y = document.getElementById('editor-town-y').value || '64';
var z = document.getElementById('editor-town-z').value || '0';
var dimension = document.getElementById('editor-town-dimension').value || 'overworld';
var isSecret = document.getElementById('editor-town-secret').checked;
var gradient = {
from: normalizeHexColor(document.getElementById('editor-town-gradient-from').value, DEFAULT_GRADIENT.from),
to: normalizeHexColor(document.getElementById('editor-town-gradient-to').value, DEFAULT_GRADIENT.to)
};
var hasLogo = logo !== '';
var html = '<div class="preview-stack">';
html += '<div class="preview-detail-shell">';
html += '<div class="town-preview-banner' + (hasLogo ? '' : ' no-logo') + '"' + (hasLogo ? ' style="background-image:url(\'' + escapeHtml(logo) + '\')"' : ' style="' + buildGradientBackgroundStyle(gradient) + '"') + '>';
if (!hasLogo) {
html += '<i class="fas fa-city town-banner-placeholder"></i>';
}
html += '</div>';
html += '<div class="preview-detail-header">';
html += '<h3 class="preview-detail-title">' + escapeHtml(title) + '</h3>';
html += '<div class="preview-badges">';
html += '<span class="town-badge badge-scale-' + scale + '"><i class="fas ' + getScaleIcon(scale) + '"></i> ' + getScaleText(scale) + '</span>';
html += '<span class="town-badge badge-type-' + townType + '"><i class="fas ' + getTownTypeIcon(townType) + '"></i> ' + getTownTypeText(townType) + '</span>';
html += '<span class="town-badge badge-recruit-' + recruit + '"><i class="fas ' + getRecruitIcon(recruit) + '"></i> ' + getRecruitText(recruit) + '</span>';
html += '</div>';
html += '</div>';
html += '<div class="preview-detail-body">';
html += '<div class="preview-section">';
html += '<h4 class="preview-section-title"><i class="fas fa-map-marker-alt"></i> ' + (isSecret ? '位置信息(保密)' : '位置信息') + '</h4>';
if (isSecret) {
html += '<p class="preview-inline-text">坐标保密</p>';
} else {
html += '<p class="preview-inline-text">' + escapeHtml(getDimensionText(dimension)) + ': X: ' + escapeHtml(x) + ', Y: ' + escapeHtml(y) + ', Z: ' + escapeHtml(z) + '</p>';
}
html += '</div>';
html += '<div class="preview-section">';
html += '<h4 class="preview-section-title"><i class="fas fa-crown"></i> 创始人</h4>';
if (editorFounders.length > 0) {
html += '<div class="contributors-list">';
editorFounders.forEach(function(name) {
html += '<div class="contributor-tag"><img src="https://minotar.net/avatar/' + encodeURIComponent(name) + '/20" alt="' + escapeHtml(name) + '">' + escapeHtml(name) + '</div>';
});
html += '</div>';
} else {
html += '<span class="text-secondary">暂无记录</span>';
}
html += '</div>';
html += '<div class="preview-section">';
html += '<h4 class="preview-section-title"><i class="fas fa-users"></i> 主要成员</h4>';
if (editorMembers.length > 0) {
html += '<div class="contributors-list">';
editorMembers.forEach(function(name) {
html += '<div class="contributor-tag"><img src="https://minotar.net/avatar/' + encodeURIComponent(name) + '/20" alt="' + escapeHtml(name) + '">' + escapeHtml(name) + '</div>';
});
html += '</div>';
} else {
html += '<span class="text-secondary">暂无记录</span>';
}
html += '</div>';
html += '<div class="preview-section">';
html += '<h4 class="preview-section-title"><i class="fas fa-book-open"></i> 城镇介绍</h4>';
html += '<div class="instruction-content">';
if (editorIntroduction.length > 0) {
editorIntroduction.forEach(function(block) {
if (block.type === 'text') {
html += '<p>' + (escapeHtml(block.content) || '<span class="text-secondary">空文字</span>') + '</p>';
} else if (block.type === 'image') {
html += block.content ? '<img src="' + escapeHtml(block.content) + '" loading="lazy">' : '<p class="text-secondary">空图片</p>';
} else if (block.type === 'video') {
html += renderVideoPreviewHtml(block.content);
}
});
} else {
html += '<p>无</p>';
}
html += '</div></div>';
html += '</div></div>';
html += '</div>';
preview.innerHTML = html;
}
// --- Save / Generate JSON ---
document.getElementById('btn-save-town').addEventListener('click', function() {
var title = document.getElementById('editor-town-title').value.trim();
if (!title) {
alert('请填写城镇名称');
document.getElementById('editor-town-title').focus();
return;
}
var isSecret = document.getElementById('editor-town-secret').checked;
var townObj = {
title: title,
logo: document.getElementById('editor-town-logo').value.trim(),
gradient: {
from: normalizeHexColor(document.getElementById('editor-town-gradient-from').value, DEFAULT_GRADIENT.from),
to: normalizeHexColor(document.getElementById('editor-town-gradient-to').value, DEFAULT_GRADIENT.to)
},
dimension: document.getElementById('editor-town-dimension').value || 'overworld',
coordinatesSecret: isSecret,
scale: document.getElementById('editor-town-scale').value,
townType: document.getElementById('editor-town-type').value,
recruitment: document.getElementById('editor-town-recruit').value,
founders: editorFounders.slice(),
members: editorMembers.slice(),
introduction: editorIntroduction.filter(function(i) { return i.content.trim() !== ''; }).map(function(i) {
return i.type === 'video' ? { type: 'video', content: parseBVNumber(i.content) || i.content } : { type: i.type, content: i.content };
})
};
if (!isSecret) {
townObj.coordinates = {
x: parseInt(document.getElementById('editor-town-x').value) || 0,
y: parseInt(document.getElementById('editor-town-y').value) || 64,
z: parseInt(document.getElementById('editor-town-z').value) || 0
};
}
var jsonStr = JSON.stringify(townObj, null, 4);
document.getElementById('town-json-output').value = jsonStr;
jsonOutputModal.style.display = 'block';
});
// --- Copy JSON ---
document.getElementById('btn-copy-town-json').addEventListener('click', function() {
var textArea = document.getElementById('town-json-output');
textArea.select();
textArea.setSelectionRange(0, 99999);
navigator.clipboard.writeText(textArea.value).then(function() {
var btn = document.getElementById('btn-copy-town-json');
var originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i> 已复制!';
btn.style.background = '#34c759';
setTimeout(function() {
btn.innerHTML = originalHTML;
btn.style.background = '';
}, 2000);
}).catch(function() {
document.execCommand('copy');
alert('已复制到剪贴板');
});
});
function renderVideoPreviewHtml(content) {
var bv = parseBVNumber(content);
if (bv) {
return '<div class="video-embed-wrapper"><iframe src="https://player.bilibili.com/player.html?bvid=' + bv + '&autoplay=0&high_quality=1" allowfullscreen sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe></div>';
}
return '<p class="text-secondary">请输入有效的 BV 号或 bilibili 视频地址</p>';
}
// --- Utility ---
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
});

View File

@@ -1,79 +0,0 @@
<!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>
<meta name="description" content="白鹿原Minecraft服务器在线实时3D动态地图全方位俯瞰服务器世界全貌。探索玩家精心建造的建筑作品浏览多样化的自然地形地貌实时查看服务器世界的最新变化。通过交互式地图发现白鹿原中的精彩角落感受玩家们的创造力与冒险足迹。">
<meta name="keywords" content="Minecraft在线地图,MC服务器地图,白鹿原地图,Minecraft 3D地图,服务器世界">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/map.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/map.html">
<meta property="og:title" content="在线地图 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="白鹿原Minecraft服务器在线实时3D动态地图全方位俯瞰服务器世界全貌。探索玩家建筑与自然地形发现白鹿原中的精彩角落与冒险足迹。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/map.html">
<meta property="twitter:title" content="在线地图 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="白鹿原Minecraft服务器在线实时3D动态地图全方位俯瞰服务器世界全貌。探索玩家建筑与自然地形发现白鹿原中的精彩角落与冒险足迹。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/style.css">
<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">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "白鹿原服务器在线地图",
"description": "白鹿原Minecraft服务器在线实时地图3D查看服务器世界全貌",
"url": "https://mcpure.lunadeer.cn/map.html",
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
<style>
body, html {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.iframe-container {
position: absolute;
top: 44px;
left: 0;
width: 100%;
height: calc(100% - 44px);
border: none;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<div id="navbar-component"></div>
<div class="iframe-container">
<iframe src="https://mcmap.lunadeer.cn/" title="白鹿原Minecraft服务器在线实时地图" loading="lazy"></iframe>
</div>
<script src="js/components.js"></script>
</body>
</html>

1207
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "bailuyuan-web",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"update:stats": "python scripts/statsprocess.py"
},
"dependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^5.4.14"
}
}

View File

@@ -1,79 +0,0 @@
<!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>
<meta name="description" content="白鹿原Minecraft服务器精美截图相册记录服务器中玩家精心建造的建筑作品、壮丽的自然风景与难忘的游戏精彩瞬间。浏览白鹿原最美的光影截图感受纯净原版Minecraft世界中玩家们的无限创造力与冒险故事一起欣赏这片美丽的虚拟世界。">
<meta name="keywords" content="Minecraft截图,MC服务器相册,白鹿原截图,Minecraft建筑,服务器风景">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/photo.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/photo.html">
<meta property="og:title" content="服务器相册 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="白鹿原Minecraft服务器精美截图相册记录玩家精心建造的建筑、壮丽自然风景与游戏精彩瞬间。感受纯净原版MC世界中玩家的创造力与冒险故事。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/photo.html">
<meta property="twitter:title" content="服务器相册 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="白鹿原Minecraft服务器精美截图相册记录玩家精心建造的建筑、壮丽自然风景与游戏精彩瞬间。感受纯净原版MC世界中玩家的创造力与冒险故事。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/style.css">
<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">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ImageGallery",
"name": "白鹿原Minecraft服务器相册",
"description": "白鹿原Minecraft服务器精美截图相册记录玩家建筑和服务器精彩瞬间",
"url": "https://mcpure.lunadeer.cn/photo.html",
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
<style>
body, html {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.iframe-container {
position: absolute;
top: 44px;
left: 0;
width: 100%;
height: calc(100% - 44px);
border: none;
}
iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
</head>
<body>
<div id="navbar-component"></div>
<div class="iframe-container">
<iframe src="https://mcphoto.lunadeer.cn/" title="白鹿原Minecraft服务器玩家截图相册" loading="lazy"></iframe>
</div>
<script src="js/components.js"></script>
</body>
</html>

1
public/CNAME Normal file
View File

@@ -0,0 +1 @@
bailuyuan.lunadeer.cn

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

5
public/robots.txt Normal file
View File

@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /stats/*.json
Sitemap: https://bailuyuan.lunadeer.cn/sitemap.xml

9
public/sitemap.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://bailuyuan.lunadeer.cn/</loc>
<lastmod>2026-03-18</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
</urlset>

View File

@@ -1,27 +0,0 @@
# robots.txt for 白鹿原 Minecraft 服务器
User-agent: *
Allow: /
Disallow: /stats/*.json
# Sitemap
Sitemap: https://bailuyuan.lunadeer.cn/sitemap.xml
# 主要搜索引擎爬虫
User-agent: Googlebot
Allow: /
User-agent: Baiduspider
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Sogou web spider
Allow: /
User-agent: 360Spider
Allow: /
# 爬取频率建议(仅供参考)
Crawl-delay: 1

234
scripts/statsprocess.py Normal file
View File

@@ -0,0 +1,234 @@
import json
import os
import re
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from pathlib import Path
import requests
from requests.adapters import HTTPAdapter
from tqdm import tqdm
from urllib3.util.retry import Retry
PROJECT_ROOT = Path(__file__).resolve().parent.parent
STATS_DIR = PROJECT_ROOT / "public" / "stats"
MAX_WORKERS = max(4, min(16, int(os.environ.get("STATS_MAX_WORKERS", (os.cpu_count() or 4) * 2))))
BASE_URL = os.environ.get("STATS_BASE_URL", "").rstrip("/")
if BASE_URL:
BASE_URL += "/"
STATS_USER = os.environ.get("STATS_USER", "")
STATS_PASS = os.environ.get("STATS_PASS", "")
BASE_AUTH = (STATS_USER, STATS_PASS) if STATS_USER else None
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
thread_local = threading.local()
def create_session():
session = requests.Session()
session.trust_env = False
adapter = HTTPAdapter(
max_retries=retry_strategy,
pool_connections=MAX_WORKERS,
pool_maxsize=MAX_WORKERS,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def get_session():
session = getattr(thread_local, "session", None)
if session is None:
session = create_session()
thread_local.session = session
return session
def load_name_cache():
summary_path = STATS_DIR / "summary.json"
if not summary_path.exists():
return {}
try:
with summary_path.open("r", encoding="utf-8") as file_handle:
summary = json.load(file_handle)
except Exception:
return {}
return {
player.get("uuid"): player.get("name")
for player in summary.get("players", [])
if player.get("uuid") and player.get("name") and player.get("name") != "Unknown"
}
def get_player_name(uuid):
try:
response = get_session().get(f"https://api.ashcon.app/mojang/v2/user/{uuid}", timeout=5)
if response.status_code == 200:
return response.json().get("username")
except Exception:
pass
try:
response = get_session().get(
f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}",
timeout=5,
)
if response.status_code == 200:
return response.json().get("name")
except Exception:
pass
return "Unknown"
def format_dist(cm):
meters = cm / 100
if meters < 1000:
return f"{meters:.1f} m"
return f"{meters / 1000:.2f} km"
def format_time(ticks):
seconds = ticks / 20
if seconds < 60:
return f"{seconds:.3f}"
minutes = seconds / 60
if minutes < 60:
return f"{minutes:.3f} 分钟"
hours = minutes / 60
if hours < 24:
return f"{hours:.3f} 小时"
return f"{hours / 24:.3f}"
def process_player(filename, name_cache):
uuid = filename.replace(".json", "")
json_path = STATS_DIR / filename
try:
response = get_session().get(f"{BASE_URL}{filename}", timeout=10, auth=BASE_AUTH)
response.raise_for_status()
data = response.json()
except Exception as exc:
print(f"Error downloading {filename}: {exc}")
return None
player_name = name_cache.get(uuid, "Unknown")
if player_name == "Unknown":
player_name = get_player_name(uuid)
stats = data.get("stats", {})
custom = stats.get("minecraft:custom", {})
walk_cm = custom.get("minecraft:walk_one_cm", 0)
play_time_ticks = custom.get("minecraft:play_time", 0)
total_mined = sum(stats.get("minecraft:mined", {}).values())
total_placed = sum(stats.get("minecraft:used", {}).values())
total_deaths = sum(stats.get("minecraft:killed_by", {}).values())
total_kills = sum(stats.get("minecraft:killed", {}).values())
data["extra"] = {
"player_name": player_name,
"formatted_walk": format_dist(walk_cm),
"walk_cm": walk_cm,
"total_mined": total_mined,
"total_placed": total_placed,
"total_deaths": total_deaths,
"total_kills": total_kills,
"play_time_fmt": format_time(play_time_ticks),
"play_time_ticks": play_time_ticks,
}
with json_path.open("w", encoding="utf-8") as file_handle:
json.dump(data, file_handle, 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": format_dist(walk_cm),
"walk_raw": walk_cm,
"mined": total_mined,
"placed": total_placed,
"deaths": total_deaths,
"kills": total_kills,
"play_time_fmt": format_time(play_time_ticks),
"play_time_raw": play_time_ticks,
},
}
def main():
STATS_DIR.mkdir(parents=True, exist_ok=True)
if BASE_AUTH:
print(f"Using authentication for BASE_URL (user: {STATS_USER})")
else:
print("No STATS_USER/STATS_PASS set, accessing BASE_URL without auth.")
if not BASE_URL:
raise SystemExit("STATS_BASE_URL is not set.")
print("Fetching file list...")
fetch_failed = False
files = []
try:
response = get_session().get(BASE_URL, timeout=10, auth=BASE_AUTH)
response.raise_for_status()
files = sorted(set(re.findall(r'href="([0-9a-f-]{36}\.json)"', response.text)))
print(f"Found {len(files)} player stats files.")
except Exception as exc:
print(f"Error fetching file list: {exc}")
fetch_failed = True
if fetch_failed:
raise SystemExit(1)
name_cache = load_name_cache()
results = []
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_map = {
executor.submit(process_player, filename, name_cache): filename
for filename in files
}
for future in tqdm(as_completed(future_map), total=len(future_map), desc="Processing players"):
try:
result = future.result()
except Exception as exc:
print(f"Worker failed for {future_map[future]}: {exc}")
continue
if result is not None:
results.append(result)
results.sort(key=lambda item: item["name"])
summary = {
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"players": results,
}
with (STATS_DIR / "summary.json").open("w", encoding="utf-8") as file_handle:
json.dump(summary, file_handle, ensure_ascii=False, indent=4)
print(f"Processing complete. Summary saved to {STATS_DIR / 'summary.json'}")
if __name__ == "__main__":
main()

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<!-- 首页 -->
<url>
<loc>https://mcpure.lunadeer.cn/</loc>
<lastmod>2026-02-11</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<!-- 玩家数据页 -->
<url>
<loc>https://mcpure.lunadeer.cn/stats.html</loc>
<lastmod>2026-02-11</lastmod>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<!-- 赞助榜 -->
<url>
<loc>https://mcpure.lunadeer.cn/sponsor.html</loc>
<lastmod>2026-03-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<!-- 加入游戏 -->
<url>
<loc>https://mcpure.lunadeer.cn/join.html</loc>
<lastmod>2026-03-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<!-- 共享资源 -->
<url>
<loc>https://mcpure.lunadeer.cn/facilities.html</loc>
<lastmod>2026-03-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<!-- 文档 -->
<url>
<loc>https://mcpure.lunadeer.cn/doc.html</loc>
<lastmod>2026-03-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.6</priority>
</url>
<!-- 在线地图 -->
<url>
<loc>https://mcpure.lunadeer.cn/map.html</loc>
<lastmod>2026-03-05</lastmod>
<changefreq>monthly</changefreq>
<priority>0.5</priority>
</url>
<!-- 相册 -->
<url>
<loc>https://mcpure.lunadeer.cn/photo.html</loc>
<lastmod>2026-03-05</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<!-- 活动公告 -->
<url>
<loc>https://mcpure.lunadeer.cn/announcements.html</loc>
<lastmod>2026-03-10</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
</urlset>

View File

@@ -1,134 +0,0 @@
<!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>
<meta name="description" content="查看白鹿原Minecraft服务器赞助者列表与众筹进度感谢每一位赞助者的慷慨支持了解服务器年度运营费用与当前筹集情况支持搜索和筛选赞助记录。如果您也热爱白鹿原欢迎通过赞助帮助服务器持续稳定运营共同守护这片纯净的Minecraft世界。">
<meta name="keywords" content="白鹿原赞助,Minecraft服务器赞助,MC服务器支持,白鹿原捐赠,服务器众筹">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/sponsor.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/sponsor.html">
<meta property="og:title" content="赞助榜 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="感谢每一位赞助者的支持查看白鹿原Minecraft服务器赞助者完整列表与众筹进度了解如何支持服务器持续运营共同守护纯净原版MC世界。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/sponsor.html">
<meta property="twitter:title" content="赞助榜 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="感谢每一位赞助者的支持查看白鹿原Minecraft服务器赞助者完整列表与众筹进度了解如何支持服务器持续运营共同守护纯净原版MC世界。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/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">
<link rel="stylesheet" href="css/pages/sponsor.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "赞助榜",
"description": "白鹿原Minecraft服务器赞助者列表和众筹进度",
"url": "https://mcpure.lunadeer.cn/sponsor.html",
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
</head>
<body>
<div id="navbar-component"></div>
<div class="sponsor-hero">
<h1>感谢每一位支持者</h1>
<div class="total-donations">
<span class="counter-label">累计获得赞助</span>
<span id="total-amount-display" class="counter-value">计算中...</span>
</div>
<p class="hero-subtitle">因为有你们,白鹿原才能走得更远。</p>
</div>
<div class="sponsor-container">
<!-- Controls -->
<div class="controls-section">
<h2 class="section-title sponsor-list-title">❤️ 赞助列表</h2>
<div class="controls-header">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="sponsor-search" placeholder="搜索赞助者姓名...">
</div>
<button class="cta-button outline" id="open-sponsor-modal">
<i class="fas fa-heart"></i> 我要支持
</button>
</div>
<div class="filter-tags" id="project-filters">
<button class="filter-tag active" data-project="all">全部</button>
<!-- JS injected filters -->
</div>
</div>
<!-- Recent Donations -->
<div class="recent-section">
<div class="donation-grid" id="donation-list">
<!-- JS will inject cards here -->
</div>
<div id="no-results" class="no-results-message is-hidden">
没有找到匹配的记录
</div>
</div>
</div>
<!-- Sponsor Modal -->
<div id="sponsor-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<div class="modal-gift-icon">
<i class="fas fa-gift"></i>
</div>
<h3 class="modal-title">支持白鹿原服务器</h3>
<p class="modal-subtitle">您的每一次支持,都将帮助我们提升服务器性能,维持更长久的运营。</p>
<!-- Desktop QR -->
<div class="desktop-only-block" id="desktop-qr-view">
<div class="qr-placeholder">
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https%3A%2F%2Fqr.alipay.com%2F2cz0344fnaulnbybhp04" alt="支付宝二维码" class="qr-img">
</div>
<p class="desktop-qr-hint">推荐使用支付宝扫码</p>
</div>
<!-- Mobile Button (will be shown via CSS media query logic in JS or CSS) -->
<div class="mobile-only-block" id="mobile-btn-view">
<a href="https://qr.alipay.com/2cz0344fnaulnbybhp04" class="alipay-btn" target="_blank">
<i class="fab fa-alipay"></i> 打开支付宝赞助
</a>
<p class="mobile-pay-hint">点击按钮将直接跳转至支付宝转账页面</p>
</div>
</div>
</div>
<div id="footer-component"></div>
<script src="js/components.js"></script>
<script src="js/data_utils.js"></script>
<script src="js/sponsor_script.js"></script>
</body>
</html>

26
src/App.vue Normal file
View File

@@ -0,0 +1,26 @@
<template>
<main class="app-shell">
<section class="hero-card">
<p class="eyebrow">Vue Migration Workspace</p>
<h1>白鹿原官网 Vue 重构基座已就绪</h1>
<p class="intro">
这里是新的 Vue 入口旧版静态站仍保留在 old-html-ver 目录中已迁入的新工作流会从 public 目录提供数据与静态资源
</p>
</section>
<section class="status-grid">
<article>
<h2>public 资源</h2>
<p>旧站数据文件统计 JSON SEO 静态文件会从这里进入 Vite 构建产物</p>
</article>
<article>
<h2>scripts 任务</h2>
<p>玩家统计脚本已迁到根目录工作流可在构建前更新 public/stats</p>
</article>
<article>
<h2>下一步</h2>
<p>后续可以在 src 下逐页重建组件路由和布局而不需要再改项目基础设施</p>
</article>
</section>
</main>
</template>

5
src/main.js Normal file
View File

@@ -0,0 +1,5 @@
import { createApp } from 'vue';
import App from './App.vue';
import './styles.css';
createApp(App).mount('#app');

114
src/styles.css Normal file
View File

@@ -0,0 +1,114 @@
:root {
color-scheme: light;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
line-height: 1.5;
font-weight: 400;
color: #172033;
background: #f4f7fb;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(81, 146, 255, 0.18), transparent 32%),
radial-gradient(circle at bottom right, rgba(48, 196, 141, 0.16), transparent 28%),
#f4f7fb;
}
button,
input,
textarea,
select {
font: inherit;
}
#app {
min-height: 100vh;
}
.app-shell {
width: min(1100px, calc(100% - 32px));
margin: 0 auto;
padding: 56px 0 72px;
}
.hero-card,
.status-grid article {
border: 1px solid rgba(23, 32, 51, 0.08);
border-radius: 24px;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 20px 60px rgba(40, 62, 98, 0.08);
backdrop-filter: blur(16px);
}
.hero-card {
padding: 36px;
}
.eyebrow {
margin: 0 0 12px;
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #2d6cdf;
}
.hero-card h1 {
margin: 0;
font-size: clamp(2rem, 5vw, 3.4rem);
line-height: 1.05;
}
.intro {
max-width: 680px;
margin: 18px 0 0;
font-size: 1.05rem;
color: #49556b;
}
.status-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px;
margin-top: 24px;
}
.status-grid article {
padding: 24px;
}
.status-grid h2 {
margin: 0 0 10px;
font-size: 1.1rem;
}
.status-grid p {
margin: 0;
color: #5b6780;
}
@media (max-width: 840px) {
.app-shell {
width: min(100% - 24px, 1100px);
padding-top: 28px;
}
.hero-card {
padding: 24px;
}
.status-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -1,217 +0,0 @@
<!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>
<meta name="description" content="查看白鹿原Minecraft服务器全面的玩家数据统计与排行榜包括总游戏时长、方块放置与破坏数、击杀数、死亡数等多项数据指标。搜索玩家名称查看个人详细统计信息实时了解服务器玩家活跃度与数据榜单发现白鹿原最活跃的冒险家们。">
<meta name="keywords" content="Minecraft玩家数据,服务器统计,玩家排行榜,白鹿原数据,MC统计,游戏时长排行">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/stats.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/stats.html">
<meta property="og:title" content="玩家数据统计 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="查看白鹿原Minecraft服务器玩家数据统计与排行榜包括游戏时长、方块放置破坏、击杀死亡等多项数据。搜索玩家名称查看详细信息发现最活跃的冒险家。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/stats.html">
<meta property="twitter:title" content="玩家数据统计 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="查看白鹿原Minecraft服务器玩家数据统计与排行榜包括游戏时长、方块放置破坏、击杀死亡等多项数据。搜索玩家名称查看详细信息发现最活跃的冒险家。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://img.lunadeer.cn">
<link rel="dns-prefetch" href="https://outline.lunadeer.cn">
<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">
<link rel="stylesheet" href="css/pages/stats.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "玩家数据统计",
"description": "白鹿原Minecraft服务器玩家数据统计和排行榜",
"url": "https://mcpure.lunadeer.cn/stats.html",
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
</head>
<body>
<div id="navbar-component"></div>
<!-- Hero Section -->
<header id="hero-component" data-title="数据中心" data-subtitle="记录每一位冒险者的足迹" data-class="stats-hero-bg"></header>
<!-- Main Content -->
<div class="features-section stats-main-section">
<div class="container">
<!-- Leaderboards -->
<h2 class="section-header">排行榜</h2>
<p class="stats-updated-at" id="stats-updated-at"></p>
<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>
<!-- 5. Play Time -->
<div class="lb-card purple">
<div class="lb-header">
<div class="lb-icon"><i class="fas fa-crown"></i></div>
<div class="lb-title">尊者</div>
</div>
<div class="lb-content" id="lb-playtime">
<div class="lb-top-player">加载中...</div>
</div>
</div>
<!-- 6. Kills -->
<div class="lb-card kill-red">
<div class="lb-header">
<div class="lb-icon"><i class="fas fa-crosshairs"></i></div>
<div class="lb-title">屠夫</div>
</div>
<div class="lb-content" id="lb-kills">
<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 expanded-modal">
<span class="close-modal">&times;</span>
<!-- Top Section: Header Info + Summary Stats -->
<div class="modal-top-section">
<!-- Left: Identity -->
<div class="modal-identity">
<img id="modal-avatar" src="" alt="Avatar">
<h2 id="modal-name">Player Name</h2>
<p id="modal-uuid">UUID</p>
</div>
<!-- Right: Summary Stats -->
<div class="stats-list-container compact-stats">
<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 class="stat-row">
<span class="stat-label"><i class="fas fa-crosshairs"></i> 击杀数量</span>
<span class="stat-value" id="modal-kills">0</span>
</div>
<div class="stat-row">
<span class="stat-label"><i class="fas fa-crown"></i> 游玩时间</span>
<span class="stat-value" id="modal-playtime">0 秒</span>
</div>
</div>
</div>
<!-- Bottom Section: Detailed Stats Accordion -->
<div class="modal-details-section">
<hr class="modal-divider">
<div id="loading-details" class="loading-details-text">正在加载详细数据...</div>
<div id="stats-accordion" class="accordion">
<!-- Dynamic Content -->
</div>
</div>
</div>
</div>
<div id="footer-component"></div>
<script src="js/components.js"></script>
<script src="js/stats_script.js"></script>
</body>
</html>

View File

@@ -1,261 +0,0 @@
import os
import json
import threading
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from tqdm import tqdm # Add tqdm for progress bars
STATS_DIR = "stats"
MAX_WORKERS = max(4, min(16, int(os.environ.get("STATS_MAX_WORKERS", (os.cpu_count() or 4) * 2))))
# HTTP Basic Auth for BASE_URL (from environment variables)
BASE_URL = os.environ.get("STATS_BASE_URL", "")
STATS_USER = os.environ.get("STATS_USER", "")
STATS_PASS = os.environ.get("STATS_PASS", "")
BASE_AUTH = (STATS_USER, STATS_PASS) if STATS_USER else None
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
thread_local = threading.local()
def create_session():
session = requests.Session()
session.trust_env = False # Ignore HTTP_PROXY / HTTPS_PROXY env vars
adapter = HTTPAdapter(
max_retries=retry_strategy,
pool_connections=MAX_WORKERS,
pool_maxsize=MAX_WORKERS,
)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
def get_session():
session = getattr(thread_local, "session", None)
if session is None:
session = create_session()
thread_local.session = session
return session
if BASE_AUTH:
print(f"Using authentication for BASE_URL (user: {STATS_USER})")
else:
print("No STATS_USER/STATS_PASS set, accessing BASE_URL without auth.")
# Ensure directories exist
os.makedirs(STATS_DIR, exist_ok=True)
print("Fetching file list...")
fetch_failed = False
try:
response = get_session().get(BASE_URL, timeout=10, auth=BASE_AUTH)
response.raise_for_status()
content = response.text
# Regex for UUID.json
files = sorted(set(re.findall(r'href="([0-9a-f-]{36}\.json)"', content)))
print(f"Found {len(files)} player stats files.")
except Exception as e:
print(f"Error fetching file list: {e}")
files = []
fetch_failed = True
def load_name_cache():
summary_path = os.path.join(STATS_DIR, 'summary.json')
if not os.path.exists(summary_path):
return {}
try:
with open(summary_path, 'r', encoding='utf-8') as f:
summary = json.load(f)
except Exception:
return {}
return {
player.get('uuid'): player.get('name')
for player in summary.get('players', [])
if player.get('uuid') and player.get('name') and player.get('name') != "Unknown"
}
def get_player_name(uuid):
# Try Ashcon first
try:
r = get_session().get(f"https://api.ashcon.app/mojang/v2/user/{uuid}", timeout=5)
if r.status_code == 200:
return r.json().get('username')
except Exception:
pass
# Try Mojang Session
try:
r = get_session().get(f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}", timeout=5)
if r.status_code == 200:
return r.json().get('name')
except Exception:
pass
return "Unknown"
def format_dist(cm):
m = cm / 100
if m < 1000:
return f"{m:.1f} m"
return f"{m / 1000:.2f} km"
def format_time(ticks):
seconds = ticks / 20
if seconds < 60:
return f"{seconds:.3f}"
minutes = seconds / 60
if minutes < 60:
return f"{minutes:.3f} 分钟"
hours = minutes / 60
if hours < 24:
return f"{hours:.3f} 小时"
days = hours / 24
return f"{days:.3f}"
def process_player(filename, name_cache):
uuid = filename.replace(".json", "")
json_path = os.path.join(STATS_DIR, filename)
# 1. Download/Load JSON
data = None
try:
r = get_session().get(BASE_URL + filename, timeout=10, auth=BASE_AUTH)
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
player_name = name_cache.get(uuid, "Unknown")
if player_name == "Unknown":
player_name = get_player_name(uuid)
# 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)
walk_fmt = format_dist(walk_cm)
# Play Time (1 tick = 1/20 second)
play_time_ticks = custom.get('minecraft:play_time', 0)
play_time_fmt = format_time(play_time_ticks)
# 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())
# Kills (Killed)
killed = stats.get('minecraft:killed', {})
total_kills = sum(killed.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,
'total_kills': total_kills,
'play_time_fmt': play_time_fmt,
'play_time_ticks': play_time_ticks
}
# 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,
'kills': total_kills,
'play_time_fmt': play_time_fmt,
'play_time_raw': play_time_ticks
}
}
name_cache = load_name_cache()
results = []
if files:
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
future_map = {
executor.submit(process_player, filename, name_cache): filename
for filename in files
}
for future in tqdm(as_completed(future_map), total=len(future_map), desc="Processing players"):
try:
result = future.result()
except Exception as e:
print(f"Worker failed for {future_map[future]}: {e}")
continue
if result is not None:
results.append(result)
if fetch_failed:
print("Skipping summary update because file list fetch failed.")
raise SystemExit(1)
# 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")

View File

@@ -1,360 +0,0 @@
<!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>
<meta name="description" content="白鹿原Minecraft服务器城镇一览查看各个城镇的坐标位置、规模类型、招募状态、创始人与成员信息以及城镇详细介绍与风貌展示。加入一个城镇开启你的冒险之旅。">
<meta name="keywords" content="Minecraft城镇,MC城镇介绍,白鹿原城镇,Minecraft社区,服务器城镇">
<meta name="author" content="白鹿原 Minecraft 服务器">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://mcpure.lunadeer.cn/towns.html">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://mcpure.lunadeer.cn/towns.html">
<meta property="og:title" content="城镇介绍 - 白鹿原 Minecraft 服务器">
<meta property="og:description" content="白鹿原Minecraft服务器城镇一览查看各个城镇的规模类型、招募状态与详细介绍加入一个城镇开启冒险之旅。">
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
<meta property="og:locale" content="zh_CN">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/towns.html">
<meta property="twitter:title" content="城镇介绍 - 白鹿原 Minecraft 服务器">
<meta property="twitter:description" content="白鹿原Minecraft服务器城镇一览查看各个城镇的规模类型、招募状态与详细介绍加入一个城镇开启冒险之旅。">
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="css/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">
<link rel="stylesheet" href="css/pages/towns.css">
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "城镇介绍",
"description": "白鹿原Minecraft服务器城镇一览",
"url": "https://mcpure.lunadeer.cn/towns.html",
"isPartOf": {
"@type": "WebSite",
"name": "白鹿原 Minecraft 服务器",
"url": "https://mcpure.lunadeer.cn/"
}
}
</script>
</head>
<body>
<div id="navbar-component"></div>
<!-- Hero Section -->
<header id="hero-component" data-title="城镇介绍" data-subtitle="探索各个城镇,找到属于你的家园。" data-class="towns-hero-bg"></header>
<div class="towns-container">
<!-- Controls -->
<div class="controls-section">
<div class="controls-header-row">
<div class="title-with-action">
<h2 class="section-title">城镇列表</h2>
<button class="btn-add-town" id="btn-add-town">
<i class="fas fa-plus"></i> 新增城镇
</button>
</div>
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="town-search" placeholder="搜索城镇名称...">
</div>
</div>
<div class="filters-wrapper">
<div class="filter-group">
<div class="filter-label"><i class="fas fa-users"></i> 规模</div>
<div class="filter-tags" id="scale-filters">
<button class="filter-tag active" data-filter="all">全部</button>
<button class="filter-tag" data-filter="small"><i class="fas fa-user"></i> 小型</button>
<button class="filter-tag" data-filter="medium"><i class="fas fa-users"></i> 中型</button>
<button class="filter-tag" data-filter="large"><i class="fas fa-city"></i> 大型</button>
</div>
</div>
<div class="filter-group">
<div class="filter-label"><i class="fas fa-tag"></i> 类型</div>
<div class="filter-tags" id="type-filters">
<button class="filter-tag active" data-filter="all">全部</button>
<button class="filter-tag" data-filter="building"><i class="fas fa-building"></i> 建筑</button>
<button class="filter-tag" data-filter="adventure"><i class="fas fa-dragon"></i> 冒险</button>
<button class="filter-tag" data-filter="industry"><i class="fas fa-industry"></i> 工业</button>
</div>
</div>
<div class="filter-group">
<div class="filter-label"><i class="fas fa-door-open"></i> 招募</div>
<div class="filter-tags" id="recruit-filters">
<button class="filter-tag active" data-filter="all">全部</button>
<button class="filter-tag" data-filter="welcome"><i class="fas fa-door-open"></i> 欢迎加入</button>
<button class="filter-tag" data-filter="closed"><i class="fas fa-door-closed"></i> 暂不招人</button>
<button class="filter-tag" data-filter="maybe"><i class="fas fa-question-circle"></i> 可以考虑</button>
</div>
</div>
</div>
</div>
<!-- Towns Grid -->
<div class="towns-grid" id="towns-list">
<!-- JS will inject cards here -->
</div>
<div id="no-results" class="no-results-message is-hidden">
没有找到匹配的城镇
</div>
</div>
<!-- Town Detail Modal -->
<div id="town-modal" class="modal town-modal">
<div class="modal-content town-modal-content">
<span class="close-modal">&times;</span>
<div class="town-modal-banner" id="town-modal-banner"></div>
<div class="town-modal-header">
<h3 class="town-modal-title" id="town-modal-title">城镇名称</h3>
<div class="town-modal-badges-row">
<div class="town-modal-badges" id="town-modal-badges">
<!-- Badges injected by JS -->
</div>
<div class="town-modal-actions">
<button class="btn-share-town" id="btn-share-town" title="分享此城镇">
<i class="fas fa-share-alt"></i> 分享
</button>
<button class="btn-edit-town" id="btn-edit-town" title="编辑此城镇">
<i class="fas fa-pen"></i> 编辑
</button>
</div>
</div>
</div>
<div class="town-modal-body">
<div class="modal-section" id="town-modal-location-section">
<h4 class="modal-section-title" id="town-modal-location-title"><i class="fas fa-map-marker-alt"></i> 位置信息</h4>
<p>
<span id="town-modal-dimension"></span>
<span id="town-modal-coords"></span>
<a href="#" target="_blank" id="town-modal-map-link" class="town-map-link">
<i class="fas fa-map-marked-alt"></i> 查看地图
</a>
</p>
</div>
<div class="modal-section">
<h4 class="modal-section-title"><i class="fas fa-crown"></i> 创始人</h4>
<div class="contributors-list" id="town-modal-founders">
<!-- Founders injected by JS -->
</div>
</div>
<div class="modal-section">
<h4 class="modal-section-title"><i class="fas fa-users"></i> 主要成员</h4>
<div class="contributors-list" id="town-modal-members">
<!-- Members injected by JS -->
</div>
</div>
<div class="modal-section">
<h4 class="modal-section-title"><i class="fas fa-book-open"></i> 城镇介绍</h4>
<div class="instruction-content" id="town-modal-introduction">
<!-- Introduction injected by JS -->
</div>
</div>
</div>
</div>
</div>
<!-- Editor Modal -->
<div id="town-editor-modal" class="modal">
<div class="modal-content town-editor-modal-content">
<span class="close-editor-modal">&times;</span>
<div class="editor-modal-header">
<h3><i class="fas fa-tools"></i> 城镇编辑器</h3>
</div>
<div class="editor-layout">
<!-- Left: Preview -->
<div class="editor-preview">
<div class="editor-panel-title"><i class="fas fa-eye"></i> 实时预览</div>
<div class="editor-preview-content" id="town-editor-preview-area"></div>
</div>
<!-- Right: Editor Form -->
<div class="editor-form">
<div class="editor-panel-title"><i class="fas fa-edit"></i> 编辑内容</div>
<div class="editor-form-scroll">
<div class="form-group">
<label for="editor-town-title">城镇名称</label>
<input type="text" id="editor-town-title" placeholder="输入城镇名称...">
</div>
<div class="form-group">
<label for="editor-town-logo">头图/Logo 图片地址(可空)</label>
<input type="text" id="editor-town-logo" placeholder="输入图片URL留空使用默认背景...">
</div>
<div class="form-group">
<label>卡片背景渐变色</label>
<div class="gradient-picker-row">
<label class="color-picker-field" for="editor-town-gradient-from">
<span>起始色</span>
<input type="color" id="editor-town-gradient-from" value="#667eea">
</label>
<label class="color-picker-field" for="editor-town-gradient-to">
<span>结束色</span>
<input type="color" id="editor-town-gradient-to" value="#764ba2">
</label>
</div>
<p class="field-hint">当未设置头图时,将使用这组渐变色作为卡片和详情头图背景。</p>
</div>
<div class="form-row">
<div class="form-group">
<label>规模</label>
<div class="custom-select">
<input type="hidden" id="editor-town-scale" value="small">
<div class="custom-select-trigger">
<span class="custom-select-text">小型5人以下</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="custom-select-options">
<div class="custom-option selected" data-value="small">小型5人以下</div>
<div class="custom-option" data-value="medium">中型2-10人</div>
<div class="custom-option" data-value="large">大型10人以上</div>
</div>
</div>
</div>
<div class="form-group">
<label>类型</label>
<div class="custom-select">
<input type="hidden" id="editor-town-type" value="building">
<div class="custom-select-trigger">
<span class="custom-select-text">建筑</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="custom-select-options">
<div class="custom-option selected" data-value="building">建筑</div>
<div class="custom-option" data-value="adventure">冒险</div>
<div class="custom-option" data-value="industry">工业</div>
</div>
</div>
</div>
<div class="form-group">
<label>招募状态</label>
<div class="custom-select">
<input type="hidden" id="editor-town-recruit" value="welcome">
<div class="custom-select-trigger">
<span class="custom-select-text">欢迎加入</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="custom-select-options">
<div class="custom-option selected" data-value="welcome">欢迎加入</div>
<div class="custom-option" data-value="closed">暂不招人</div>
<div class="custom-option" data-value="maybe">可以考虑</div>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="toggle-label">
<span>坐标保密</span>
<div class="toggle-switch">
<input type="checkbox" id="editor-town-secret">
<span class="toggle-slider"></span>
</div>
</label>
<p class="field-hint">开启后将隐藏坐标信息,适用于不希望公开位置的城镇。</p>
</div>
<div class="form-group" id="editor-dimension-group">
<label>所在世界</label>
<div class="custom-select">
<input type="hidden" id="editor-town-dimension" value="overworld">
<div class="custom-select-trigger">
<span class="custom-select-text">主世界</span>
<i class="fas fa-chevron-down"></i>
</div>
<div class="custom-select-options">
<div class="custom-option selected" data-value="overworld">主世界</div>
<div class="custom-option" data-value="nether">下界</div>
<div class="custom-option" data-value="the_end">末地</div>
</div>
</div>
</div>
<div class="form-row coords-row" id="editor-coords-row">
<div class="form-group">
<label for="editor-town-x">X 坐标</label>
<input type="number" id="editor-town-x" placeholder="0">
</div>
<div class="form-group">
<label for="editor-town-y">Y 坐标</label>
<input type="number" id="editor-town-y" placeholder="64">
</div>
<div class="form-group">
<label for="editor-town-z">Z 坐标</label>
<input type="number" id="editor-town-z" placeholder="0">
</div>
</div>
<div class="form-group">
<label>创始人</label>
<div class="tags-input-wrapper" id="editor-founders-wrapper">
<div class="tags-list" id="editor-founders-tags"></div>
<input type="text" id="editor-founder-input" placeholder="输入名称后按回车或空格添加...">
</div>
</div>
<div class="form-group">
<label>主要成员</label>
<div class="tags-input-wrapper" id="editor-members-wrapper">
<div class="tags-list" id="editor-members-tags"></div>
<input type="text" id="editor-member-input" placeholder="输入名称后按回车或空格添加...">
</div>
</div>
<div class="form-group">
<label>城镇介绍</label>
<div class="sortable-list" id="editor-introduction-list"></div>
<div class="add-item-row">
<button type="button" class="add-item-btn" data-type="text">
<i class="fas fa-plus"></i> 添加文字
</button>
<button type="button" class="add-item-btn" data-type="image">
<i class="fas fa-image"></i> 添加图片
</button>
<button type="button" class="add-item-btn" data-type="video">
<i class="fas fa-video"></i> 添加视频
</button>
</div>
</div>
<div class="editor-actions">
<button type="button" class="btn-save-town" id="btn-save-town">
<i class="fas fa-save"></i> 生成 JSON
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- JSON Output Modal -->
<div id="town-json-output-modal" class="modal">
<div class="modal-content town-json-output-content">
<span class="close-json-modal">&times;</span>
<h3><i class="fas fa-code"></i> 生成完成</h3>
<p class="json-output-hint">请复制以下 JSON 内容,发送给服主以更新到网站上。</p>
<textarea id="town-json-output" readonly></textarea>
<button type="button" class="btn-copy-json" id="btn-copy-town-json">
<i class="fas fa-copy"></i> 复制到剪贴板
</button>
</div>
</div>
<div id="footer-component"></div>
<script src="js/components.js"></script>
<script src="js/towns_script.js"></script>
</body>
</html>

6
vite.config.js Normal file
View File

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
});