mirror of
https://github.com/Coldsmiles/infstarweb.git
synced 2026-04-22 18:20:43 +08:00
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:
68
.github/copilot-instructions.md
vendored
68
.github/copilot-instructions.md
vendored
@@ -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.
|
||||
40
.github/workflows/deploy.yml
vendored
40
.github/workflows/deploy.yml
vendored
@@ -4,8 +4,7 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
schedule:
|
||||
# Every days at 04:00 UTC
|
||||
- cron: "0 4 */1 * *"
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -24,35 +23,37 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install requests tqdm
|
||||
|
||||
- name: Run statsprocess.py
|
||||
- name: Update player stats
|
||||
env:
|
||||
STATS_BASE_URL: ${{ secrets.STATS_BASE_URL }}
|
||||
STATS_USER: ${{ secrets.STATS_USER }}
|
||||
STATS_PASS: ${{ secrets.STATS_PASS }}
|
||||
run: python statsprocess.py
|
||||
run: python scripts/statsprocess.py
|
||||
|
||||
- name: Prepare Pages artifact
|
||||
run: |
|
||||
mkdir -p _site
|
||||
rsync -a --delete \
|
||||
--exclude '.git/' \
|
||||
--exclude '.github/' \
|
||||
--exclude 'README.md' \
|
||||
--exclude 'statsprocess.py' \
|
||||
./ ./_site/
|
||||
- name: Build site
|
||||
run: npm run build
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./_site
|
||||
path: ./dist
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
@@ -71,7 +72,6 @@ jobs:
|
||||
if: success()
|
||||
steps:
|
||||
- name: Submit URLs to IndexNow
|
||||
if: ${{ env.INDEXNOW_KEY != '' }}
|
||||
env:
|
||||
INDEXNOW_KEY: ${{ secrets.INDEXNOW_KEY }}
|
||||
run: |
|
||||
@@ -79,6 +79,7 @@ jobs:
|
||||
echo "INDEXNOW_KEY not set, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
HOST="bailuyuan.lunadeer.cn"
|
||||
curl -s -X POST "https://api.indexnow.org/indexnow" \
|
||||
-H "Content-Type: application/json" \
|
||||
@@ -87,14 +88,7 @@ jobs:
|
||||
\"key\": \"${INDEXNOW_KEY}\",
|
||||
\"keyLocation\": \"https://${HOST}/${INDEXNOW_KEY}.txt\",
|
||||
\"urlList\": [
|
||||
\"https://${HOST}/\",
|
||||
\"https://${HOST}/sponsor.html\",
|
||||
\"https://${HOST}/stats.html\",
|
||||
\"https://${HOST}/join.html\",
|
||||
\"https://${HOST}/facilities.html\",
|
||||
\"https://${HOST}/doc.html\",
|
||||
\"https://${HOST}/map.html\",
|
||||
\"https://${HOST}/photo.html\"
|
||||
\"https://${HOST}/\"
|
||||
]
|
||||
}"
|
||||
echo ""
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1 +1,15 @@
|
||||
/stats
|
||||
node_modules/
|
||||
dist/
|
||||
.vite/
|
||||
coverage/
|
||||
old-html-ver/
|
||||
public/stats/*.json
|
||||
|
||||
*.log
|
||||
*.local
|
||||
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
206
README.md
206
README.md
@@ -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)
|
||||
@@ -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">×</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">×</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
css/pages/join.css
1070
css/pages/join.css
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
css/pages/towns.css
1699
css/pages/towns.css
File diff suppressed because it is too large
Load Diff
968
css/style.css
968
css/style.css
@@ -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
doc.html
79
doc.html
@@ -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
facilities.html
320
facilities.html
@@ -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">×</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">×</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">×</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>
|
||||
259
index.html
259
index.html
@@ -1,258 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>白鹿原 Minecraft 服务器 - 永不换档的纯净原版生存Minecraft服务器</title>
|
||||
<meta name="description" content="白鹿原是一个永不换档的纯净原版生存Minecraft我的世界服务器,支持Java版与基岩版互通。提供免费圈地保护、自研管理插件,紧跟最新游戏版本更新。物理工作站保障7×24小时稳定运行,实时查看服务器在线状态与众筹进展。立即加入白鹿原,开启纯净原版生存冒险之旅!服务器地址:mcpure.lunadeer.cn">
|
||||
<meta name="keywords" content="白鹿原Minecraft,白鹿原我的世界,白鹿原mc,Minecraft服务器,我的世界,我的世界服务器,纯净服务器,原版服务器,纯净生存,基岩互通,白鹿原,MC服务器,永不换档,免费圈地,Minecraft中国">
|
||||
<meta name="author" content="白鹿原 Minecraft 服务器">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://mcpure.lunadeer.cn/">
|
||||
|
||||
<!-- Google Site Verification -->
|
||||
<meta name="google-site-verification" content="ZMGHsJuJU3soEw09Xa0lfKTxhxEBKN-h-goxg5lhCRw" />
|
||||
|
||||
<!-- Bing Site Verification -->
|
||||
<meta name="msvalidate.01" content="A46E723A4AEF6D9EEB1D9AB9DC1267FD" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://mcpure.lunadeer.cn/">
|
||||
<meta property="og:title" content="白鹿原 Minecraft 服务器 - 永不换档的纯净原版生存Minecraft服务器">
|
||||
<meta property="og:description" content="白鹿原——永不换档的纯净原版生存Minecraft服务器,支持Java版与基岩版互通。免费圈地保护、自研管理插件、紧跟最新版本,物理工作站保障全天候稳定运行。立即加入纯净生存冒险!">
|
||||
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
|
||||
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
|
||||
<meta property="og:locale" content="zh_CN">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/">
|
||||
<meta property="twitter:title" content="白鹿原 Minecraft 服务器 - 永不换档的纯净原版生存Minecraft服务器">
|
||||
<meta property="twitter:description" content="白鹿原——永不换档的纯净原版生存Minecraft服务器,支持Java版与基岩版互通。免费圈地保护、自研管理插件、紧跟最新版本,物理工作站保障全天候稳定运行。立即加入纯净生存冒险!">
|
||||
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
|
||||
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="preconnect" href="https://img.lunadeer.cn">
|
||||
<link rel="dns-prefetch" href="https://outline.lunadeer.cn">
|
||||
<link rel="dns-prefetch" href="https://mcmap.lunadeer.cn">
|
||||
<link rel="dns-prefetch" href="https://mcphoto.lunadeer.cn">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "GameServer",
|
||||
"name": "白鹿原 Minecraft 服务器",
|
||||
"description": "永不换档的纯净原版生存Minecraft服务器,支持Java版和基岩版互通",
|
||||
"url": "https://mcpure.lunadeer.cn/",
|
||||
"logo": "https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png",
|
||||
"image": "https://img.lunadeer.cn/i/2025/11/26/69267755e14e3.png",
|
||||
"game": {
|
||||
"@type": "VideoGame",
|
||||
"name": "Minecraft",
|
||||
"gamePlatform": ["Java Edition", "Bedrock Edition"]
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "CNY",
|
||||
"availability": "https://schema.org/InStock"
|
||||
},
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "5",
|
||||
"ratingCount": "100"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "白鹿原 Minecraft 服务器",
|
||||
"url": "https://mcpure.lunadeer.cn/",
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": "https://mcpure.lunadeer.cn/stats.html?q={search_term_string}",
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="白鹿原 Minecraft 服务器官网 Vue 重构工程。" />
|
||||
<title>白鹿原 Minecraft 服务器</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 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>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
320
join.html
320
join.html
@@ -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>
|
||||
@@ -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
js/components.js
151
js/components.js
@@ -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>© 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();
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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
js/marked.min.js
vendored
69
js/marked.min.js
vendored
File diff suppressed because one or more lines are too long
247
js/script.js
247
js/script.js
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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
map.html
79
map.html
@@ -1,79 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>在线地图 - 白鹿原 Minecraft 服务器</title>
|
||||
<meta name="description" content="白鹿原Minecraft服务器在线实时3D动态地图,全方位俯瞰服务器世界全貌。探索玩家精心建造的建筑作品,浏览多样化的自然地形地貌,实时查看服务器世界的最新变化。通过交互式地图发现白鹿原中的精彩角落,感受玩家们的创造力与冒险足迹。">
|
||||
<meta name="keywords" content="Minecraft在线地图,MC服务器地图,白鹿原地图,Minecraft 3D地图,服务器世界">
|
||||
<meta name="author" content="白鹿原 Minecraft 服务器">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://mcpure.lunadeer.cn/map.html">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://mcpure.lunadeer.cn/map.html">
|
||||
<meta property="og:title" content="在线地图 - 白鹿原 Minecraft 服务器">
|
||||
<meta property="og:description" content="白鹿原Minecraft服务器在线实时3D动态地图,全方位俯瞰服务器世界全貌。探索玩家建筑与自然地形,发现白鹿原中的精彩角落与冒险足迹。">
|
||||
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
|
||||
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
|
||||
<meta property="og:locale" content="zh_CN">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary">
|
||||
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/map.html">
|
||||
<meta property="twitter:title" content="在线地图 - 白鹿原 Minecraft 服务器">
|
||||
<meta property="twitter:description" content="白鹿原Minecraft服务器在线实时3D动态地图,全方位俯瞰服务器世界全貌。探索玩家建筑与自然地形,发现白鹿原中的精彩角落与冒险足迹。">
|
||||
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
|
||||
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "白鹿原服务器在线地图",
|
||||
"description": "白鹿原Minecraft服务器在线实时地图,3D查看服务器世界全貌",
|
||||
"url": "https://mcpure.lunadeer.cn/map.html",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "白鹿原 Minecraft 服务器",
|
||||
"url": "https://mcpure.lunadeer.cn/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.iframe-container {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 44px);
|
||||
border: none;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-component"></div>
|
||||
<div class="iframe-container">
|
||||
<iframe src="https://mcmap.lunadeer.cn/" title="白鹿原Minecraft服务器在线实时地图" loading="lazy"></iframe>
|
||||
</div>
|
||||
<script src="js/components.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1207
package-lock.json
generated
Normal file
1207
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal 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
photo.html
79
photo.html
@@ -1,79 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>服务器相册 - 白鹿原 Minecraft 服务器</title>
|
||||
<meta name="description" content="白鹿原Minecraft服务器精美截图相册,记录服务器中玩家精心建造的建筑作品、壮丽的自然风景与难忘的游戏精彩瞬间。浏览白鹿原最美的光影截图,感受纯净原版Minecraft世界中玩家们的无限创造力与冒险故事,一起欣赏这片美丽的虚拟世界。">
|
||||
<meta name="keywords" content="Minecraft截图,MC服务器相册,白鹿原截图,Minecraft建筑,服务器风景">
|
||||
<meta name="author" content="白鹿原 Minecraft 服务器">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://mcpure.lunadeer.cn/photo.html">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://mcpure.lunadeer.cn/photo.html">
|
||||
<meta property="og:title" content="服务器相册 - 白鹿原 Minecraft 服务器">
|
||||
<meta property="og:description" content="白鹿原Minecraft服务器精美截图相册,记录玩家精心建造的建筑、壮丽自然风景与游戏精彩瞬间。感受纯净原版MC世界中玩家的创造力与冒险故事。">
|
||||
<meta property="og:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
|
||||
<meta property="og:site_name" content="白鹿原 Minecraft 服务器">
|
||||
<meta property="og:locale" content="zh_CN">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary">
|
||||
<meta property="twitter:url" content="https://mcpure.lunadeer.cn/photo.html">
|
||||
<meta property="twitter:title" content="服务器相册 - 白鹿原 Minecraft 服务器">
|
||||
<meta property="twitter:description" content="白鹿原Minecraft服务器精美截图相册,记录玩家精心建造的建筑、壮丽自然风景与游戏精彩瞬间。感受纯净原版MC世界中玩家的创造力与冒险故事。">
|
||||
<meta property="twitter:image" content="https://img.lunadeer.cn/i/2024/04/22/6625ce6c8ddc1.png">
|
||||
|
||||
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ImageGallery",
|
||||
"name": "白鹿原Minecraft服务器相册",
|
||||
"description": "白鹿原Minecraft服务器精美截图相册,记录玩家建筑和服务器精彩瞬间",
|
||||
"url": "https://mcpure.lunadeer.cn/photo.html",
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "白鹿原 Minecraft 服务器",
|
||||
"url": "https://mcpure.lunadeer.cn/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.iframe-container {
|
||||
position: absolute;
|
||||
top: 44px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - 44px);
|
||||
border: none;
|
||||
}
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="navbar-component"></div>
|
||||
<div class="iframe-container">
|
||||
<iframe src="https://mcphoto.lunadeer.cn/" title="白鹿原Minecraft服务器玩家截图相册" loading="lazy"></iframe>
|
||||
</div>
|
||||
<script src="js/components.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
public/CNAME
Normal file
1
public/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
bailuyuan.lunadeer.cn
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
5
public/robots.txt
Normal file
5
public/robots.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /stats/*.json
|
||||
|
||||
Sitemap: https://bailuyuan.lunadeer.cn/sitemap.xml
|
||||
9
public/sitemap.xml
Normal file
9
public/sitemap.xml
Normal 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
robots.txt
27
robots.txt
@@ -1,27 +0,0 @@
|
||||
# robots.txt for 白鹿原 Minecraft 服务器
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /stats/*.json
|
||||
|
||||
# Sitemap
|
||||
Sitemap: https://bailuyuan.lunadeer.cn/sitemap.xml
|
||||
|
||||
# 主要搜索引擎爬虫
|
||||
User-agent: Googlebot
|
||||
Allow: /
|
||||
|
||||
User-agent: Baiduspider
|
||||
Allow: /
|
||||
|
||||
User-agent: Bingbot
|
||||
Allow: /
|
||||
|
||||
User-agent: Sogou web spider
|
||||
Allow: /
|
||||
|
||||
User-agent: 360Spider
|
||||
Allow: /
|
||||
|
||||
# 爬取频率建议(仅供参考)
|
||||
Crawl-delay: 1
|
||||
234
scripts/statsprocess.py
Normal file
234
scripts/statsprocess.py
Normal 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
sitemap.xml
79
sitemap.xml
@@ -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
sponsor.html
134
sponsor.html
@@ -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">×</span>
|
||||
<div class="modal-gift-icon">
|
||||
<i class="fas fa-gift"></i>
|
||||
</div>
|
||||
<h3 class="modal-title">支持白鹿原服务器</h3>
|
||||
<p class="modal-subtitle">您的每一次支持,都将帮助我们提升服务器性能,维持更长久的运营。</p>
|
||||
|
||||
<!-- Desktop QR -->
|
||||
<div class="desktop-only-block" id="desktop-qr-view">
|
||||
<div class="qr-placeholder">
|
||||
<img src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https%3A%2F%2Fqr.alipay.com%2F2cz0344fnaulnbybhp04" alt="支付宝二维码" class="qr-img">
|
||||
</div>
|
||||
<p class="desktop-qr-hint">推荐使用支付宝扫码</p>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Button (will be shown via CSS media query logic in JS or CSS) -->
|
||||
<div class="mobile-only-block" id="mobile-btn-view">
|
||||
<a href="https://qr.alipay.com/2cz0344fnaulnbybhp04" class="alipay-btn" target="_blank">
|
||||
<i class="fab fa-alipay"></i> 打开支付宝赞助
|
||||
</a>
|
||||
<p class="mobile-pay-hint">点击按钮将直接跳转至支付宝转账页面</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer-component"></div>
|
||||
<script src="js/components.js"></script>
|
||||
<script src="js/data_utils.js"></script>
|
||||
<script src="js/sponsor_script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
26
src/App.vue
Normal file
26
src/App.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<main class="app-shell">
|
||||
<section class="hero-card">
|
||||
<p class="eyebrow">Vue Migration Workspace</p>
|
||||
<h1>白鹿原官网 Vue 重构基座已就绪</h1>
|
||||
<p class="intro">
|
||||
这里是新的 Vue 入口。旧版静态站仍保留在 old-html-ver 目录中,已迁入的新工作流会从 public 目录提供数据与静态资源。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="status-grid">
|
||||
<article>
|
||||
<h2>public 资源</h2>
|
||||
<p>旧站数据文件、统计 JSON 与 SEO 静态文件会从这里进入 Vite 构建产物。</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>scripts 任务</h2>
|
||||
<p>玩家统计脚本已迁到根目录工作流,可在构建前更新 public/stats。</p>
|
||||
</article>
|
||||
<article>
|
||||
<h2>下一步</h2>
|
||||
<p>后续可以在 src 下逐页重建组件、路由和布局,而不需要再改项目基础设施。</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
5
src/main.js
Normal file
5
src/main.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import './styles.css';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
114
src/styles.css
Normal file
114
src/styles.css
Normal 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
stats.html
217
stats.html
@@ -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">×</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
statsprocess.py
261
statsprocess.py
@@ -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
towns.html
360
towns.html
@@ -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">×</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">×</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">×</span>
|
||||
<h3><i class="fas fa-code"></i> 生成完成</h3>
|
||||
<p class="json-output-hint">请复制以下 JSON 内容,发送给服主以更新到网站上。</p>
|
||||
<textarea id="town-json-output" readonly></textarea>
|
||||
<button type="button" class="btn-copy-json" id="btn-copy-town-json">
|
||||
<i class="fas fa-copy"></i> 复制到剪贴板
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer-component"></div>
|
||||
<script src="js/components.js"></script>
|
||||
<script src="js/towns_script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
6
vite.config.js
Normal file
6
vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
});
|
||||
Reference in New Issue
Block a user