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
-68
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.
+18 -24
View File
@@ -4,8 +4,7 @@ on:
push: push:
branches: [main] branches: [main]
schedule: schedule:
# Every days at 04:00 UTC - cron: "0 4 * * *"
- cron: "0 4 */1 * *"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -24,35 +23,37 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install frontend dependencies
run: npm ci
- name: Install Python dependencies - name: Install Python dependencies
run: pip install requests tqdm run: pip install requests tqdm
- name: Run statsprocess.py - name: Update player stats
env: env:
STATS_BASE_URL: ${{ secrets.STATS_BASE_URL }} STATS_BASE_URL: ${{ secrets.STATS_BASE_URL }}
STATS_USER: ${{ secrets.STATS_USER }} STATS_USER: ${{ secrets.STATS_USER }}
STATS_PASS: ${{ secrets.STATS_PASS }} STATS_PASS: ${{ secrets.STATS_PASS }}
run: python statsprocess.py run: python scripts/statsprocess.py
- name: Prepare Pages artifact - name: Build site
run: | run: npm run build
mkdir -p _site
rsync -a --delete \
--exclude '.git/' \
--exclude '.github/' \
--exclude 'README.md' \
--exclude 'statsprocess.py' \
./ ./_site/
- name: Upload Pages artifact - name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3 uses: actions/upload-pages-artifact@v3
with: with:
path: ./_site path: ./dist
deploy: deploy:
needs: build needs: build
@@ -71,7 +72,6 @@ jobs:
if: success() if: success()
steps: steps:
- name: Submit URLs to IndexNow - name: Submit URLs to IndexNow
if: ${{ env.INDEXNOW_KEY != '' }}
env: env:
INDEXNOW_KEY: ${{ secrets.INDEXNOW_KEY }} INDEXNOW_KEY: ${{ secrets.INDEXNOW_KEY }}
run: | run: |
@@ -79,6 +79,7 @@ jobs:
echo "INDEXNOW_KEY not set, skipping." echo "INDEXNOW_KEY not set, skipping."
exit 0 exit 0
fi fi
HOST="bailuyuan.lunadeer.cn" HOST="bailuyuan.lunadeer.cn"
curl -s -X POST "https://api.indexnow.org/indexnow" \ curl -s -X POST "https://api.indexnow.org/indexnow" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -87,15 +88,8 @@ jobs:
\"key\": \"${INDEXNOW_KEY}\", \"key\": \"${INDEXNOW_KEY}\",
\"keyLocation\": \"https://${HOST}/${INDEXNOW_KEY}.txt\", \"keyLocation\": \"https://${HOST}/${INDEXNOW_KEY}.txt\",
\"urlList\": [ \"urlList\": [
\"https://${HOST}/\", \"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\"
] ]
}" }"
echo "" echo ""
echo "IndexNow notification sent." echo "IndexNow notification sent."
+15 -1
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
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)
-189
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
-1070
View File
File diff suppressed because it is too large Load Diff
-599
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;
}
}
-568
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;
}
}
-1699
View File
File diff suppressed because it is too large Load Diff
-968
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);
}
-79
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>
-320
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>
+12 -257
View File
@@ -1,258 +1,13 @@
<!DOCTYPE html> <!doctype html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>白鹿原 Minecraft 服务器 - 永不换档的纯净原版生存Minecraft服务器</title> <meta name="description" content="白鹿原 Minecraft 服务器官网 Vue 重构工程。" />
<meta name="description" content="白鹿原是一个永不换档的纯净原版生存Minecraft我的世界服务器,支持Java版与基岩版互通。提供免费圈地保护、自研管理插件,紧跟最新游戏版本更新。物理工作站保障7×24小时稳定运行,实时查看服务器在线状态与众筹进展。立即加入白鹿原,开启纯净原版生存冒险之旅!服务器地址:mcpure.lunadeer.cn"> <title>白鹿原 Minecraft 服务器</title>
<meta name="keywords" content="白鹿原Minecraft,白鹿原我的世界,白鹿原mc,Minecraft服务器,我的世界,我的世界服务器,纯净服务器,原版服务器,纯净生存,基岩互通,白鹿原,MC服务器,永不换档,免费圈地,Minecraft中国"> </head>
<meta name="author" content="白鹿原 Minecraft 服务器"> <body>
<meta name="robots" content="index, follow"> <div id="app"></div>
<link rel="canonical" href="https://mcpure.lunadeer.cn/"> <script type="module" src="/src/main.js"></script>
</body>
<!-- Google Site Verification --> </html>
<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>
-320
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>
-605
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('已复制到剪贴板');
});
});
});
-151
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();
});
-42
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;
}
};
-825
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;
}
});
-634
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。若您的设备未越狱,请确保已启用 JIT(Just-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
View File
File diff suppressed because one or more lines are too long
-247
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 */
-223
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);
});
}
-408
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');
}
});
});
}
-931
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;
}
});
-79
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
View File
File diff suppressed because it is too large Load Diff
+19
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"
}
}
-79
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
View File
@@ -0,0 +1 @@
bailuyuan.lunadeer.cn
View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

+5
View File
@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Disallow: /stats/*.json
Sitemap: https://bailuyuan.lunadeer.cn/sitemap.xml
+9
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>
-27
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
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()
-79
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>
-134
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
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
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue';
import App from './App.vue';
import './styles.css';
createApp(App).mount('#app');
+114
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;
}
}
-217
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>
-261
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")
-360
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
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
});