Compare commits

..

21 Commits

Author SHA1 Message Date
DelLevin-Home
d7505e5b0c 优化星云相册 2026-06-05 19:02:52 +08:00
DelLevin-Home
a2de47355c ... 2026-06-05 02:24:32 +08:00
DelLevin-Home
1668c84436 添加星空相册 2026-06-05 02:13:42 +08:00
DelLevin-Home
a664c423fa 1 2026-06-05 00:08:46 +08:00
DelLevin-Home
9523db458c 222 2026-05-19 00:38:31 +08:00
DelLevin-Home
bd46afc3d6 优化博客文章接口 2026-05-19 00:21:01 +08:00
DelLevin-Home
e6ba5eb3fb Merge branch 'master' of https://github.com/dellevin/iletter-homepage 2026-05-18 22:35:02 +08:00
DelLevin-Home
cf92e08278 新增readme 2026-05-18 22:30:47 +08:00
MR.ZHANG
8d19e63ceb Create README.md 2026-05-18 22:21:32 +08:00
DelLevin-Home
e271cf342b 22 2026-05-18 22:13:57 +08:00
DelLevin-Home
4f52069560 2 2026-05-18 21:58:43 +08:00
DelLevin-Home
afc019b1fd 更新动画效果 2026-05-18 21:34:01 +08:00
DelLevin-Home
5c22d3f59d 111 2026-05-18 15:47:48 +08:00
DelLevin-Home
47396b88f0 我的网站优化 2026-05-18 14:36:19 +08:00
DelLevin-Home
40278c5d84 修复bug 2026-05-18 04:02:52 +08:00
DelLevin-Home
3f5ddde664 222 2026-05-18 03:42:43 +08:00
DelLevin-Home
be004854e2 11 2026-05-18 03:08:18 +08:00
DelLevin-Home
f0605dca4c 加入照片功能 2026-05-18 02:23:58 +08:00
DelLevin-Home
5e33d53ea3 博客文章优化 2026-05-18 00:54:08 +08:00
DelLevin-Home
fa4c784b96 加入留言板博客按钮 2026-05-18 00:02:14 +08:00
DelLevin-Home
66ece06271 优化内容 2026-05-17 18:56:05 +08:00
67 changed files with 5809 additions and 123 deletions

11
.claude/launch.json Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "local",
"runtimeExecutable": "npx",
"runtimeArgs": ["http-server", ".", "-p", "3000", "-c-1"],
"port": 3000
}
]
}

View File

@@ -0,0 +1,23 @@
{
"permissions": {
"allow": [
"mcp__Claude_Preview__preview_start",
"WebFetch(domain:cdnjs.cloudflare.com)",
"Bash(curl -L -o \"D:\\\\UserData\\\\Desktop\\\\my_proj\\\\www.iletter.top\\\\static\\\\js\\\\three.min.js\" \"https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js\")",
"Bash(curl -L -o sun.jpg \"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/The_Sun_by_the_Atmospheric_Imaging_Assembly_of_NASA%27s_Solar_Dynamics_Observatory_-_20100819.jpg/1200px-The_Sun_by_the_Atmospheric_Imaging_Assembly_of_NASA%27s_Solar_Dynamics_Observatory_-_20100819.jpg\")",
"Bash(curl -L -o earth.jpg \"https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/The_Blue_Marble_%28remastered%29.jpg/1200px-The_Blue_Marble_%28remastered%29.jpg\")",
"Bash(curl -L -o mars.jpg \"https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/Mars_-_August_30_2021_-_Flipped.jpg/1200px-Mars_-_August_30_2021_-_Flipped.jpg\")",
"Bash(curl -L -o saturn.jpg \"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c7/Saturn_during_Equinox.jpg/1200px-Saturn_during_Equinox.jpg\")",
"Bash(curl -L -o jupiter.jpg \"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Jupiter_New_Horizons.jpg/1200px-Jupiter_New_Horizons.jpg\")",
"Bash(curl -sL \"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/The_Sun_by_the_Atmospheric_Imaging_Assembly_of_NASA%27s_Solar_Dynamics_Observatory_-_20100819.jpg/600px-The_Sun_by_the_Atmospheric_Imaging_Assembly_of_NASA%27s_Solar_Dynamics_Observatory_-_20100819.jpg\" -o sun.jpg)",
"Bash(curl -sL \"https://images.unsplash.com/photo-1506318137071-a8e063b4bec0?w=300\" -o sun.jpg)",
"Bash(curl -sL \"https://images.unsplash.com/photo-1614730321146-b6fa6a46bcb4?w=300\" -o earth.jpg)",
"Bash(curl -sL \"https://images.unsplash.com/photo-1614728894747-a83421e2b9c9?w=300\" -o mars.jpg)",
"Bash(curl -sL \"https://images.unsplash.com/photo-1614732414444-096e5f1122d5?w=300\" -o saturn.jpg)",
"Bash(curl -sL \"https://images.unsplash.com/photo-1614313913007-2b4ae8ce32d6?w=300\" -o jupiter.jpg)",
"Bash(curl -sL \"https://images.unsplash.com/photo-1446776811953-b23d57bd21aa?w=2048&q=80\" -o moon.jpg)",
"Bash(curl -sL \"https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/lroc_color_poles_4096.jpg\" -o moon.jpg)",
"Bash(curl -sL \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Moonmap_from_clementine_data.png/1280px-Moonmap_from_clementine_data.png\" -o moon.png)"
]
}
}

67
README.md Normal file
View File

@@ -0,0 +1,67 @@
# 白荼个人主页 (BaiTu Homepage)
这是一个简约风格的个人主页项目,采用纯 HTML/CSS/JavaScript 构建,旨在展示个人信息、开发作品、常用工具以及生活随笔。
## 项目特点
- **信纸风格**:独特的信纸视觉设计,营造“见字如面”的沉浸式阅读体验。
- **极简主义**:采用纯 HTML/CSS/JavaScript 构建,无繁重框架,加载迅速。
- **多语言支持**:内置中英文切换功能,满足不同访问者的需求。
- **动态内容**
- **闲言碎语**:通过 Memos API 实时同步生活点滴。
- **历史足迹**:基于 ECharts 的交互式地图,记录走过的城市。
- **我的网站/工具/游戏**:动态加载并展示个人作品集和常用资源。
- **交互体验**
- 全局搜索功能(支持 Google, Baidu, DuckDuckGo, Yandex
- 留言板与博客文章预览弹窗。
- “旅途剪影”相册模块,支持瀑布流展示与图片轮播。
- **性能优化**:使用 Nginx 缓存静态资源,集成 Umami 进行轻量级数据统计。
## 快速开始
1. **克隆仓库**
```bash
git clone https://github.com/dellevin/iletter-homepage.git
cd iletter-homepage
```
2. **本地运行**
由于项目包含一些动态数据加载(如 Memos 接口),建议在本地服务器环境下运行。你可以使用 Python 快速启动一个简易服务器:
```bash
python -m http.server 8080
```
然后在浏览器中访问 `http://localhost:8080`。
## 目录结构
```text
.
├── static/
│ ├── css/ # 样式文件
│ ├── js/ # JavaScript 逻辑(含多语言配置)
│ ├── img/ # 图片资源(含相册、图标等)
│ └── font/ # 字体文件
├── scripts/ # 自动化脚本(如照片索引生成)
├── index.html # 主页面入口
└── README.md # 项目说明
```
## 技术栈
- **前端核心**HTML5, CSS3, Vanilla JavaScript
- **图表库**ECharts (用于地图足迹可视化)
- **网络请求**Axios
- **后端接口**Memos API (闲言碎语), 自建评论系统
- **统计与防护**Umami, 堡塔云 WAF
## 人生相册自动化
项目包含 `scripts/generate-photos-json.py` 脚本,用于自动扫描 `static/img/photos/` 目录并生成 `photos.json` 索引文件,方便前端快速加载最新照片。
## 贡献与反馈
如果你对项目有任何建议或发现了 Bug欢迎在 [GitHub Issues](https://github.com/dellevin/iletter-homepage/issues) 中提出。
## 许可证
本项目仅供学习交流使用。

View File

@@ -28,6 +28,8 @@
<!-- <script defer src="https://umami.iletter.top/anyjs" data-website-id="ae6cd64c-5900-49c9-9c22-95cedc24a508"></script> -->
<!-- ECharts 库 -->
<script src="./static/js/echarts/echarts.min.js"></script>
<!-- Three.js 用于星空预览 -->
<script src="./static/js/three.min.js"></script>
<!-- 多语言 -->
<script src="./static/js/language/zh.js"></script>
<script src="./static/js/language/en.js"></script>
@@ -36,17 +38,176 @@
@import url("./static/css/main.css");
@import url("./static/css/memos.css");
@import url("./static/css/game_dialog.css");
@import url("./static/css/guestbook.css");
@import url("./static/css/galaxy.css");
</style>
<title data-i18n="page_title">白荼 - BAITU</title>
</head>
<body>
<!-- 搜索功能 -->
<div id="search-overlay" class="search-overlay">
<div class="search-modal">
<button class="search-lock-btn" id="search-lock-btn" title="搜索常驻">
<svg id="lock-icon-open" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<svg id="lock-icon-closed" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 9.9-1"></path>
</svg>
</button>
<div class="search-box">
<div class="search-engine-selector" id="search-engine-selector">
<img src="./static/img/search_icon/google.png" alt="Google" id="search-engine-icon" />
<div class="search-engine-dropdown" id="search-engine-dropdown">
<div class="engine-option active" data-engine="google" data-icon="./static/img/search_icon/google.png">
<img src="./static/img/search_icon/google.png" alt="Google" /> Google
</div>
<div class="engine-option" data-engine="baidu" data-icon="./static/img/search_icon/baidu.png">
<img src="./static/img/search_icon/baidu.png" alt="Baidu" /> Baidu
</div>
<div class="engine-option" data-engine="duckduckgo" data-icon="./static/img/search_icon/DuckDuckGo.png">
<img src="./static/img/search_icon/DuckDuckGo.png" alt="DuckDuckGo" /> DuckDuckGo
</div>
<div class="engine-option" data-engine="yandex" data-icon="./static/img/search_icon/Yandex.png">
<img src="./static/img/search_icon/Yandex.png" alt="Yandex" /> Yandex
</div>
</div>
</div>
<input type="text" id="global-search-input" placeholder="Search..." autocomplete="off" />
<button id="global-search-btn">搜索</button>
</div>
</div>
</div>
<div class="letter-container">
<!-- 语言切换按钮 -->
<div class="lang-switch-container">
<img id="lang-switch-btn" src="./static/img/中文.png" alt="Switch Language" title="切换语言" />
</div>
<!-- 留言板按钮 -->
<div class="guestbook-btn-container">
<button id="guestbook-btn" class="guestbook-btn" title="留言板">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V7C3 6.46957 3.21071 5.96086 3.58579 5.58579C3.96086 5.21071 4.46957 5 5 5H19C19.5304 5 20.0391 5.21071 20.4142 5.58579C20.7893 5.96086 21 6.46957 21 7V15Z"
stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="btn-label" data-i18n="btn_guestbook">留言板</span>
</button>
<!-- Hover 浮动预览面板 -->
<div id="guestbook-preview" class="guestbook-preview">
<div class="preview-header">
<h3 data-i18n="guestbook_title">留言板</h3>
</div>
<div class="preview-body">
<!-- 最新留言预览将在这里动态加载 -->
</div>
</div>
</div>
<!-- 博客文章按钮 -->
<div class="blog-posts-btn-container">
<button id="blog-posts-btn" class="blog-posts-btn" title="博客文章">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z"
stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 2V8H20" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 13H8" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 17H8" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 9H8" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="btn-label" data-i18n="btn_blog_posts">博客文章</span>
</button>
<!-- Hover 浮动预览面板 -->
<div id="blog-posts-preview" class="blog-posts-preview">
<div class="preview-header">
<h3 data-i18n="blog_latest_posts">最新文章</h3>
</div>
<div class="preview-body blog-posts-list">
<!-- 最新文章预览将在这里动态加载 -->
</div>
</div>
</div>
<!-- 完整博客文章弹窗(点击后显示) -->
<div id="blog-posts-full-modal" class="guestbook-full-modal">
<div class="full-modal-content solid-modal">
<div class="guestbook-header">
<h2>博客文章</h2>
<button id="blog-posts-close" class="guestbook-close" title="关闭">&times;</button>
</div>
<div class="guestbook-body">
<!-- 文章列表将在这里动态加载 -->
</div>
</div>
</div>
<!-- 旅途剪影按钮 -->
<div class="photo-album-btn-container">
<button id="photo-album-btn" class="photo-album-btn" title="旅途剪影">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" stroke="#666" stroke-width="2"/>
<circle cx="8.5" cy="8.5" r="1.5" fill="#666"/>
<path d="M21 15L16 10L5 21" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="btn-label" data-i18n="btn_photo_album">旅途剪影</span>
</button>
<!-- Hover 浮动预览面板 -->
<div id="photo-album-preview" class="photo-album-preview">
<div class="preview-header">
<h3 data-i18n="photo_latest_photos">最新照片</h3>
</div>
<div class="preview-body photo-album-list">
<!-- 最新照片预览将在这里动态加载 -->
</div>
</div>
</div>
<!-- 完整旅途剪影弹窗(点击后显示) -->
<div id="photo-album-full-modal" class="guestbook-full-modal">
<div class="full-modal-content">
<div class="guestbook-header">
<h2 data-i18n="photo_album_title">旅途剪影</h2>
<div class="header-controls">
<div class="mode-switch-container">
<button id="mode-switch" class="mode-switch-btn" title="切换模式">
<span class="mode-label active" data-mode="galaxy">星空模式</span>
<span class="mode-label" data-mode="waterfall">瀑布模式</span>
</button>
</div>
<button id="photo-album-close" class="guestbook-close" title="关闭">&times;</button>
</div>
</div>
<div class="guestbook-body photo-album-body">
<!-- 照片网格将在这里动态加载(瀑布模式) -->
</div>
<!-- 星空模式画布 -->
<div id="galaxy-canvas-wrapper" class="galaxy-canvas-wrapper">
<canvas id="galaxy-canvas"></canvas>
<div class="galaxy-tip">滚轮缩放 | 拖拽旋转 | 点击星星查看照片</div>
</div>
</div>
</div>
<!-- 完整留言板弹窗(点击后显示) -->
<div id="guestbook-full-modal" class="guestbook-full-modal">
<div class="full-modal-content solid-modal">
<div class="guestbook-header">
<h2 data-i18n="guestbook_title">留言板</h2>
<button id="guestbook-close" class="guestbook-close" title="关闭">&times;</button>
</div>
<div class="guestbook-body">
<!-- 留言列表将在这里动态加载 -->
</div>
</div>
</div>
<!-- 头部导航 -->
<div class="nav-tabs">
<div class="nav-tab active" data-tab="about"><span data-i18n="nav_about"></span></div>
@@ -88,19 +249,22 @@
<h3 data-i18n="social_media_h3">社交媒体</h3>
<div class="website-links">
<a href="https://github.com/dellevin" target="_blank" data-i18n="social_link1"
data-i18n-attr="href">GitHub</a>
data-i18n-attr="href" onclick="return confirmLink(this.href)">GitHub</a>
<a href="https://weibo.com/u/6094785109999999999" target="_blank" data-i18n="social_link2"
data-i18n-attr="href">微博</a>
data-i18n-attr="href" onclick="return confirmLink(this.href)">微博</a>
<a href="https://www.zhihu.com/people/bing-xue-chen-xi-59" target="_blank" data-i18n="social_link3"
data-i18n-attr="href">知乎</a>
data-i18n-attr="href" onclick="return confirmLink(this.href)">知乎</a>
<a href="https://space.bilibili.com/403551212" target="_blank" data-i18n="social_link4"
data-i18n-attr="href">B站</a>
data-i18n-attr="href" onclick="return confirmLink(this.href)">B站</a>
<a href="https://www.xiaohongshu.com/user/profile/65aabb99000000000e0271f1" target="_blank"
data-i18n="social_link6"
data-i18n-attr="href" onclick="return confirmLink(this.href)">小红书</a>
<a href="https://www.52pojie.cn/home.php?mod=space&uid=2059006" target="_blank"
data-i18n="social_link5" data-i18n-attr="href">52pojie</a>
data-i18n="social_link5" data-i18n-attr="href" onclick="return confirmLink(this.href)">52pojie</a>
</div>
<h3 data-i18n="leave_message_h3">留言</h3>
<p data-i18n="leave_message_p"><a href="https://blog.iletter.top/401.html" target="_blank"
data-i18n="leave_message_link" data-i18n-attr="href">点击此链接</a>去我的博客下面给我留言!</p>
data-i18n="leave_message_link" data-i18n-attr="href" onclick="return confirmLink(this.href)">点击此链接</a>去我的博客下面给我留言!</p>
</div>
</div>
@@ -126,6 +290,65 @@
</div>
</div>
<!-- 自定义确认弹窗 -->
<div id="custom-confirm-modal" class="custom-confirm-modal">
<div class="custom-confirm-content">
<div class="custom-confirm-message"></div>
<div class="custom-confirm-buttons">
<button class="confirm-btn-cancel">取消</button>
<button class="confirm-btn-ok">确定</button>
</div>
</div>
</div>
<!-- MookNote 作品详情弹窗 -->
<div id="mooknote-modal" class="guestbook-full-modal">
<div class="full-modal-content solid-modal mooknote-modal-content">
<div class="guestbook-header">
<h2>MookNote为记录而生</h2>
<button id="mooknote-close" class="guestbook-close" title="关闭">&times;</button>
</div>
<div class="guestbook-body mooknote-body">
<div class="mooknote-banner-carousel">
<div class="carousel-container">
<img src="./static/img/my_work/mooknote/banner.jpg" alt="MookNote Banner" class="carousel-image active" onclick="openMooknoteLightbox(0)">
<img src="./static/img/my_work/mooknote/1.jpg" alt="MookNote Screenshot 1" class="carousel-image" onclick="openMooknoteLightbox(1)">
<img src="./static/img/my_work/mooknote/2.jpg" alt="MookNote Screenshot 2" class="carousel-image" onclick="openMooknoteLightbox(2)">
<img src="./static/img/my_work/mooknote/3.jpg" alt="MookNote Screenshot 3" class="carousel-image" onclick="openMooknoteLightbox(3)">
<img src="./static/img/my_work/mooknote/4.jpg" alt="MookNote Screenshot 4" class="carousel-image" onclick="openMooknoteLightbox(4)">
<img src="./static/img/my_work/mooknote/5.jpg" alt="MookNote Screenshot 5" class="carousel-image" onclick="openMooknoteLightbox(5)">
<img src="./static/img/my_work/mooknote/6.jpg" alt="MookNote Screenshot 6" class="carousel-image" onclick="openMooknoteLightbox(6)">
</div>
<button class="carousel-btn carousel-prev" onclick="prevMooknoteImage()">&#10094;</button>
<button class="carousel-btn carousel-next" onclick="nextMooknoteImage()">&#10095;</button>
<div class="carousel-indicators"></div>
</div>
<div class="mooknote-content">
<p class="mooknote-intro">一款专注于个人记录的极简工具,让每一份感动与思考都有迹可循。</p>
<p>在这个信息爆炸的时代,我们每天都在消费大量的内容——看一部电影、读一本书、产生一个灵感。然而,这些内容往往如流水般逝去,留下的只有模糊的记忆。</p>
<p><strong>MookNote 诞生于对简洁工具的追求。</strong></p>
<p>我相信,好的工具应该像纸笔一样自然,不打扰思考,只记录生活。它不应该有繁琐的注册流程,不应该将你的数据存储在陌生的服务器上,更不应该用花哨的功能分散你的注意力。</p>
<p>我厌烦了一款软件让用户不断地注册来注册去使用手机号验证来验证去数据不于自己甚至自己写的笔记也不属于自己我不喜欢这样我想一款优秀的软件应该抛去繁杂的设计就像太极一般的黑白两色。就像专业的人做专业的事情一样MookNote主打记录生活一切有迹可循的影视娱乐想法笔记。</p>
<p>MookNote名字的由来也是让我冥思苦想Mo取自Movie的前两个字母ok取自book的后两个字母Note则名为笔记名字就这么来了。影视书籍笔记的三项生活记录因为热爱所以记录因为想法所以书写。</p>
<p>目前笔记还处于基础功能的版本,希望各位能多给我提提建议,我也会在其中做一些取舍,能与不能我都会一一记录下来的。在此,希望大家能多多帮助我这一个不知名的小开发者。</p>
<p class="mooknote-thanks">最后的最后感谢阿里千问的通义灵码让我一个小小的java开发者接触到Android开发帮了我的大忙帮了我最纯真的梦想。</p>
<div class="mooknote-download">
<a href="https://mooknote.iletter.top/" target="_blank" class="download-btn" onclick="return confirmLink(this.href)">前往下载 / 了解更多</a>
</div>
</div>
</div>
</div>
</div>
<!-- MookNote 全屏图片预览 -->
<div id="mooknote-lightbox" class="photo-lightbox mooknote-lightbox">
<button class="lightbox-close" onclick="closeMooknoteLightbox()">&times;</button>
<button class="lightbox-nav lightbox-prev" onclick="prevLightboxImage()">&#10094;</button>
<button class="lightbox-nav lightbox-next" onclick="nextLightboxImage()">&#10095;</button>
<div class="lightbox-counter"></div>
<img src="" alt="MookNote Fullscreen" class="lightbox-img">
</div>
<!-- 工具内容 -->
<div class="tab-content" id="tools-content">
<div class="content-section">
@@ -181,7 +404,7 @@
</div>
</div>
<div class="copyright" style="margin-top: 15px;"><a href="http://beian.miit.gov.cn"
target="_blank">冀ICP备2025122609号</a> &copy;<span>2024 - 2026</span> <span>Power BY</span> <span>Del
target="_blank" onclick="return confirmLink(this.href)">冀ICP备2025122609号</a> &copy;<span>2024 - 2026</span> <span>Power BY</span> <span>Del
Levin </span>
</div>
</div>
@@ -247,12 +470,61 @@
switchButton.addEventListener('click', switchLanguage);
});
</script>
<!-- 自定义确认弹窗逻辑 -->
<script>
// 链接跳转确认函数
function confirmLink(url) {
showCustomConfirm(`确定要打开网站吗?\n${url}`, () => {
window.open(url, '_blank');
});
return false; // 阻止默认跳转行为
}
// 自定义确认弹窗
let confirmCallback = null;
const customConfirmModal = document.getElementById('custom-confirm-modal');
const confirmMessage = customConfirmModal.querySelector('.custom-confirm-message');
const confirmBtnOk = customConfirmModal.querySelector('.confirm-btn-ok');
const confirmBtnCancel = customConfirmModal.querySelector('.confirm-btn-cancel');
function showCustomConfirm(message, onConfirm) {
confirmMessage.textContent = message;
confirmCallback = onConfirm;
customConfirmModal.classList.add('active');
}
function hideCustomConfirm() {
customConfirmModal.classList.remove('active');
confirmCallback = null;
}
confirmBtnOk.addEventListener('click', () => {
if (confirmCallback) {
confirmCallback();
}
hideCustomConfirm();
});
confirmBtnCancel.addEventListener('click', hideCustomConfirm);
// 点击背景关闭
customConfirmModal.addEventListener('click', (e) => {
if (e.target === customConfirmModal) {
hideCustomConfirm();
}
});
</script>
<script src="./static/js/anypage.js"></script>
<script src="./static/js/game_my.js"></script>
<script src="./static/js/tools_my.js"></script>
<script src="./static/js/websites_my.js"></script>
<script src="./static/js/memos.js"></script>
<script src="./static/js/map-footprint.js"></script>
<script src="./static/js/guestbook.js"></script>
<script src="./static/js/blog_posts.js"></script>
<script src="./static/js/photo_album.js"></script>
<script src="./static/js/galaxy.js"></script>
<script src="./static/js/tabchange.js"></script>
</body>

14
scripts/build-photos.bat Normal file
View File

@@ -0,0 +1,14 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 人生相册构建脚本
echo ========================================
echo.
cd /d "%~dp0.."
python scripts\generate-photos-json.py
echo.
echo ========================================
pause

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import json
import re
from pathlib import Path
from datetime import datetime
# ==================== 配置区域 ====================
# 请在此处指定你的照片目录和输出文件路径
# DEFAULT_PHOTOS_DIR = Path("/www/wwwroot/www.iletter.top/static/img/photos")
# DEFAULT_OUTPUT_FILE = Path("/www/wwwroot/www.iletter.top/static/img/photos/photos.json")
DEFAULT_PHOTOS_DIR = Path("D:\\UserData\\Desktop\\my_proj\\www.iletter.top\\static\\img\\photos")
DEFAULT_OUTPUT_FILE = Path("D:\\UserData\\Desktop\\my_proj\\www.iletter.top\\static\\img\\photos\\photos.json")
# ================================================
# 支持的图片格式
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'}
def natural_sort_key(filename):
"""自然排序键函数,支持数字正确排序"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(r'(\d+)', filename)]
def scan_photos(photos_dir):
"""扫描照片目录,返回按修改时间排序的文件名列表(最新的在最前面)"""
if not photos_dir.exists():
print(f"❌ 目录不存在: {photos_dir}")
return []
photos = []
for file in photos_dir.iterdir():
if file.is_file() and file.suffix.lower() in IMAGE_EXTENSIONS:
# 排除 JSON 文件本身
if file.name == "photos.json":
continue
# 存储文件名和修改时间戳
photos.append((file.name, file.stat().st_mtime))
# 按修改时间降序排序(最新的在最前面)
photos.sort(key=lambda x: x[1], reverse=True)
# 只返回文件名列表
return [p[0] for p in photos]
def generate_json(photos, output_file):
"""生成 photos.json 文件"""
data = {
"photos": photos,
"total": len(photos),
"lastUpdated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
# 写入文件
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return len(photos)
def main():
# 确定照片目录和输出文件路径
photos_dir = DEFAULT_PHOTOS_DIR.resolve()
output_file = DEFAULT_OUTPUT_FILE.resolve()
print("🚀 开始扫描照片目录...")
print(f"📁 照片目录: {photos_dir}")
print(f"📝 输出文件: {output_file}")
photos = scan_photos(photos_dir)
if not photos:
print("⚠️ 未找到任何照片文件")
# 仍然生成空的 JSON
generate_json([], output_file)
print(f"✅ 已生成空的 {output_file}")
return
count = generate_json(photos, output_file)
print(f"✅ 成功生成 {output_file}")
print(f"📊 共扫描到 {count} 张照片:")
for i, photo in enumerate(photos, 1):
print(f" {i}. {photo}")
if __name__ == "__main__":
main()

531
static/css/galaxy.css Normal file
View File

@@ -0,0 +1,531 @@
/* ==================== 星空预览样式 ==================== */
/* 星空模式切换按钮在弹窗header中 - 透明玻璃效果 + 黑色字体 */
.galaxy-toggle-btn {
display: none; /* 隐藏旧按钮 */
}
/* 模式切换容器 */
.mode-switch-container {
margin-right: 12px;
}
/* 模式切换按钮 */
.mode-switch-btn {
display: flex;
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 6px;
padding: 2px;
cursor: pointer;
gap: 2px;
}
.mode-label {
padding: 6px 12px;
font-size: 13px;
color: #666;
border-radius: 4px;
transition: all 0.3s ease;
}
.mode-label.active {
background: rgba(0, 0, 0, 0.1);
color: #1a1a1a;
}
.mode-label:hover:not(.active) {
color: #333;
}
/* 瀑布模式容器 */
.waterfall-wrapper {
display: none;
flex: 1;
overflow-y: auto;
padding: 20px;
background: linear-gradient(180deg, #f5f5f5 0%, #fff 100%);
}
.waterfall-wrapper.active {
display: block;
}
.waterfall-container {
column-count: 3;
column-gap: 16px;
max-width: 1200px;
margin: 0 auto;
}
/* 星空模式header样式 */
.galaxy-mode-header {
background: rgba(10, 10, 46, 0.95) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.1) !important;
}
.galaxy-mode-header h2 {
color: #fff !important;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5), 0 1px 2px rgba(0, 0, 0, 0.5) !important;
}
.galaxy-mode-header .guestbook-close {
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
color: #fff !important;
}
.galaxy-mode-header .guestbook-close:hover {
background: rgba(255, 0, 0, 0.5) !important;
border-color: rgba(255, 0, 0, 0.6) !important;
}
.galaxy-mode-header .mode-switch-btn {
background: rgba(255, 255, 255, 0.1) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
}
.galaxy-mode-header .mode-label {
color: rgba(255, 255, 255, 0.7) !important;
}
.galaxy-mode-header .mode-label.active {
background: rgba(255, 255, 255, 0.2) !important;
color: #fff !important;
}
.waterfall-item {
break-inside: avoid;
margin-bottom: 16px;
background: #fff;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
}
.waterfall-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.waterfall-item img {
width: 100%;
display: block;
border-radius: 12px 12px 0 0;
}
.waterfall-item-info {
padding: 12px;
font-size: 12px;
color: #666;
text-align: center;
background: #fff;
}
/* 响应式设计 */
@media (max-width: 900px) {
.waterfall-container {
column-count: 2;
}
}
@media (max-width: 600px) {
.waterfall-container {
column-count: 1;
}
}
.header-controls {
display: flex;
align-items: center;
}
/* 星空画布容器(在弹窗内) */
.galaxy-canvas-wrapper {
display: none;
flex: 1;
position: relative;
background: radial-gradient(ellipse at center, #1a1a3e 0%, #0a0a2e 50%, #050520 100%);
overflow: hidden;
}
.galaxy-canvas-wrapper.active {
display: block;
}
/* 星空背景动画 - 繁杂无序 */
.galaxy-canvas-wrapper::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url('../img/planets/bg.jpg');
background-size: cover;
background-position: center;
pointer-events: none;
z-index: 0;
}
.galaxy-canvas-wrapper canvas {
width: 100%;
height: 100%;
display: block;
position: relative;
z-index: 1;
}
.galaxy-tip {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.6);
color: rgba(255, 255, 255, 0.8);
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.galaxy-canvas-wrapper.active .galaxy-tip {
opacity: 1;
animation: fadeInOut 3s ease-in-out forwards;
}
@keyframes fadeInOut {
0%, 100% { opacity: 0; }
20%, 80% { opacity: 1; }
}
/* 星空模式独立按钮(旧版本,保留备用) */
.galaxy-mode-btn {
background: transparent;
border: none;
padding: 8px 12px;
cursor: pointer;
display: none;
flex-direction: column;
align-items: center;
gap: 4px;
transition: all 0.3s ease;
border-radius: 8px;
position: relative;
margin-top: 8px;
}
.galaxy-mode-btn:hover {
background: rgba(255, 215, 0, 0.1);
transform: scale(1.1);
}
.galaxy-mode-btn svg {
transition: transform 0.3s ease;
}
.galaxy-mode-btn:hover svg {
transform: rotate(15deg);
}
/* 星空预览容器 */
.galaxy-container {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0a0a2e 0%, #16162e 50%, #0d0d3d 100%);
z-index: 10000;
flex-direction: column;
}
.galaxy-container.active {
display: flex;
animation: galaxyFadeIn 0.5s ease;
}
@keyframes galaxyFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 星空头部 */
.galaxy-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10001;
}
.galaxy-header h2 {
color: #FFD700;
font-size: 20px;
margin: 0;
text-shadow: 0 0 10px rgba(255, 215, 0, 0.5);
}
.galaxy-controls {
display: flex;
align-items: center;
gap: 16px;
}
.galaxy-tip {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
}
.galaxy-close-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 24px;
width: 36px;
height: 36px;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.galaxy-close-btn:hover {
background: rgba(255, 0, 0, 0.5);
border-color: rgba(255, 0, 0, 0.6);
transform: rotate(90deg);
}
/* 星空画布 */
#galaxy-canvas {
flex: 1;
cursor: grab;
}
#galaxy-canvas:active {
cursor: grabbing;
}
/* 星星点击提示 */
.star-tooltip {
position: fixed;
background: rgba(0, 0, 0, 0.88);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 14px 18px;
pointer-events: none;
z-index: 10002;
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
max-width: 320px;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.star-tooltip.visible {
opacity: 1;
transform: translateY(0);
}
.star-tooltip img {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 8px;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.star-tooltip p {
color: rgba(255, 255, 255, 0.9);
margin: 10px 0 0;
font-size: 12px;
text-align: center;
letter-spacing: 0.5px;
}
/* 星星放大弹窗 */
.star-lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.92);
z-index: 10003;
align-items: center;
justify-content: center;
flex-direction: column;
backdrop-filter: blur(10px);
}
.star-lightbox.active {
display: flex;
animation: lightboxFadeIn 0.25s ease;
}
@keyframes lightboxFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.star-lightbox-container {
position: relative;
max-width: 90vw;
max-height: 85vh;
display: flex;
flex-direction: column;
align-items: center;
}
.star-lightbox img {
max-width: 90vw;
max-height: 80vh;
object-fit: contain;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
animation: imageZoomIn 0.25s ease;
}
@keyframes imageZoomIn {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.star-lightbox-close {
position: absolute;
top: -50px;
right: -10px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 24px;
width: 40px;
height: 40px;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.star-lightbox-close:hover {
background: rgba(255, 255, 255, 0.25);
transform: rotate(90deg);
border-color: rgba(255, 255, 255, 0.4);
}
.star-lightbox-info {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
margin-top: 16px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
backdrop-filter: blur(10px);
letter-spacing: 0.5px;
}
.star-lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
font-size: 24px;
width: 50px;
height: 50px;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.star-lightbox-nav:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.4);
}
.star-lightbox-nav.prev {
left: -70px;
}
.star-lightbox-nav.next {
right: -70px;
}
.star-lightbox-counter {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
margin-top: 10px;
}
/* 加载动画 */
.galaxy-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #FFD700;
font-size: 18px;
text-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.galaxy-header {
padding: 12px 16px;
}
.galaxy-header h2 {
font-size: 16px;
}
.galaxy-tip {
display: none;
}
.star-lightbox img {
max-width: 95%;
}
}

View File

@@ -8,7 +8,10 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 300px;
display: none;
transition: opacity 0.2s ease;
opacity: 0;
transform: translateY(10px) scale(0.95);
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 悬浮框内容 */

1504
static/css/guestbook.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,177 @@
box-sizing: border-box;
}
/* 搜索功能样式 */
.search-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 9999;
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.search-overlay.active {
display: flex;
opacity: 1;
}
/* 常驻搜索模式:背景保持模糊,阻止页面交互 */
body.persistent-search-mode .search-overlay {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
pointer-events: auto; /* 确保遮罩层接收点击事件以阻止页面交互 */
}
body.persistent-search-mode .search-modal {
pointer-events: auto;
transform: translateY(0);
}
.search-modal {
position: relative;
width: 90%;
max-width: 600px;
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.search-lock-btn {
position: absolute;
top: -45px;
right: 0;
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.4);
color: #fff;
padding: 8px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.search-lock-btn:hover {
background: rgba(255, 255, 255, 0.4);
transform: scale(1.1);
}
.search-lock-btn.locked {
background: #1a73e8;
border-color: #1a73e8;
color: #fff;
}
.search-overlay.active .search-modal {
transform: translateY(0);
}
.search-box {
display: flex;
align-items: center;
background: #fff;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
overflow: visible;
padding: 8px;
}
.search-engine-selector {
position: relative;
cursor: pointer;
padding: 10px 15px;
display: flex;
align-items: center;
transition: background 0.3s ease;
border-radius: 6px;
}
.search-engine-selector:hover {
background: #f5f5f5;
}
.search-engine-selector img {
width: 24px;
height: 24px;
object-fit: contain;
}
#global-search-input {
flex: 1;
padding: 12px 15px;
border: none;
outline: none;
font-size: 16px;
background: transparent;
}
#global-search-btn {
padding: 12px 24px;
border: none;
background: #333;
color: #fff;
cursor: pointer;
border-radius: 6px;
transition: background 0.3s ease;
font-size: 15px;
}
#global-search-btn:hover {
background: #000;
}
.search-engine-dropdown {
position: absolute;
top: 100%;
left: 0;
margin-top: 8px;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 180px;
display: none;
overflow: hidden;
z-index: 10000;
}
.search-engine-dropdown.show {
display: block;
}
.engine-option {
padding: 12px 15px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: background 0.2s ease;
}
.engine-option:hover {
background: #f5f5f5;
}
.engine-option.active {
background: #e8f0fe;
color: #1a73e8;
}
.engine-option img {
width: 20px;
height: 20px;
object-fit: contain;
}
body {
font-family: 'Georgia', 'Times New Roman', 'STSong', serif;
line-height: 1.8;
@@ -23,6 +194,7 @@ body {
}
.letter-container {
/* max-width: 65%; */
max-width: 850px;
margin: 0 auto;
background: white;
@@ -81,10 +253,26 @@ body {
/* 内容区域 */
.tab-content {
display: none;
opacity: 0;
transform: translateY(10px);
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.tab-content.active {
display: block;
animation: tabFadeIn 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes tabFadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 信纸纹理效果 */
@@ -166,6 +354,326 @@ body {
text-decoration: underline;
}
/* 视图切换文字样式 */
.website-view-toggle {
font-size: 13px;
color: #999;
cursor: pointer;
transition: color 0.3s ease;
user-select: none;
}
.website-view-toggle:hover {
color: #333;
text-decoration: underline;
}
/* 卡片视图样式 */
.website-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 20px;
margin-top: 20px;
}
.website-card {
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: border-color 0.3s ease;
}
.website-card:hover {
border-color: #333;
}
.website-card-icon {
width: 100%;
height: 120px;
overflow: hidden;
background: #f9f9f9;
display: flex;
align-items: center;
justify-content: center;
}
.website-card-placeholder {
background: linear-gradient(135deg, #e0e0e0 0%, #f5f5f5 100%);
color: #999;
font-size: 30px;
font-weight: bold;
font-family: serif;
}
.website-card-icon img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.website-card-info {
padding: 12px;
}
.website-card-title {
font-weight: bold;
color: #333;
font-size: 14px;
margin-bottom: 6px;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.website-card-desc {
font-size: 12px;
color: #888;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* MookNote 弹窗样式 */
.mooknote-modal-content {
max-width: 800px;
width: 90%;
}
.mooknote-banner-carousel {
position: relative;
width: 100%;
overflow: hidden;
border-radius: 8px 8px 0 0;
}
.carousel-container {
position: relative;
width: 100%;
height: 400px;
background: #f5f5f5;
}
.carousel-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
opacity: 0;
transition: opacity 0.5s ease;
cursor: pointer;
pointer-events: none;
}
.carousel-image.active {
opacity: 1;
pointer-events: auto;
}
.carousel-image:hover {
filter: brightness(1.1);
}
.carousel-btn {
position: absolute;
top: 0;
bottom: 0;
width: 60px;
background: rgba(255, 255, 255, 0.3);
color: #fff;
border: none;
cursor: pointer;
font-size: 28px;
transition: background 0.3s ease;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
}
.carousel-btn:hover {
background: rgba(0, 0, 0, 0.1);
}
.carousel-prev {
left: 0;
}
.carousel-next {
right: 0;
}
.carousel-indicators {
position: absolute;
bottom: 15px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 10;
}
.carousel-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: background 0.3s ease;
}
.carousel-indicator.active {
background: #fff;
}
/* MookNote 全屏预览样式 */
.photo-lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.95);
z-index: 10000;
align-items: center;
justify-content: center;
}
.photo-lightbox.active {
display: flex;
}
.lightbox-img {
max-width: 95%;
max-height: 95%;
object-fit: contain;
border-radius: 4px;
}
.lightbox-close {
position: absolute;
top: 20px;
right: 30px;
background: rgba(255, 255, 255, 0.2);
color: #fff;
border: none;
font-size: 36px;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
transition: background 0.3s ease;
z-index: 10001;
}
.lightbox-close:hover {
background: rgba(255, 255, 255, 0.4);
}
.lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.2);
color: #fff;
border: none;
font-size: 32px;
width: 60px;
height: 80px;
cursor: pointer;
transition: background 0.3s ease;
z-index: 10001;
}
.lightbox-nav:hover {
background: rgba(255, 255, 255, 0.4);
}
.lightbox-prev {
left: 30px;
}
.lightbox-next {
right: 30px;
}
.lightbox-counter {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 16px;
background: rgba(0, 0, 0, 0.5);
padding: 8px 20px;
border-radius: 20px;
}
.mooknote-body {
padding: 40px 50px;
}
.mooknote-content {
padding: 40px 35px;
}
.mooknote-content p {
margin-bottom: 15px;
line-height: 1.8;
color: #444;
text-align: justify;
}
.mooknote-intro {
font-size: 16px;
color: #333;
font-weight: 500;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.mooknote-thanks {
font-style: italic;
color: #666;
background: #f9f9f9;
padding: 15px;
border-left: 4px solid #ddd;
border-radius: 4px;
}
.mooknote-download {
text-align: center;
margin-top: 30px;
margin-bottom: 60px;
padding-top: 20px;
border-top: 1px dashed #eee;
}
.download-btn {
display: inline-block;
padding: 12px 30px;
background: #333;
color: #fff;
text-decoration: none;
border-radius: 25px;
transition: all 0.3s ease;
}
.download-btn:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 工具和游戏网格 */
.tools-grid,
.games-grid {
@@ -181,6 +689,14 @@ body {
border: 1px solid #eee;
border-radius: 4px;
background-color: #fafafa;
cursor: pointer;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.tool-item:hover,
.game-item:hover {
border-color: #000;
}
.tool-name,
@@ -248,6 +764,7 @@ body {
/* 底部版权信息 - 在信纸外部 */
#footer-wrap {
max-width: 850px;
/* max-width: 65%; */
margin: 0 auto 20px auto;
text-align: center;
padding: 20px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

View File

@@ -0,0 +1,30 @@
{
"photos": [
"74641a449ac2d8f9d113d27ca7a7254c.jpg",
"9f647be6532ea2acf9778eb302a19742.jpg",
"a09ebc8f61856503211b42c975fe5cd5.jpg",
"a62e6036f35722b1f0dd131a929d3a17.jpg",
"1b72bd604e55942f6a7d7c28bc5ed5be.jpg",
"0c49fd33f4e83dcedde820d858ec44e9.jpg",
"64b539e20378d3e6ac3324e9dcc616ce.jpg",
"c0ca11cc8eaaa16564c75263357e19ad.jpg",
"7480c6e1755218592bd4881338750d8c.jpg",
"8498ee3191ba50dc27b85b7fbfd1e0fc.jpg",
"e1349403bdf5692c209d809ddb1e026d.jpg",
"85306709e27843201604a7b0efc88565.jpg",
"c53486080dd63ac735f1c5d27203b1c4.jpg",
"f3aece19d8e1fdcd0bb140f5ef3ff882.jpg",
"dae46343c972876a77f6b765ce1e57ea.jpg",
"6b736db3eeaa315422e98e5e550f23ce.jpg",
"522915fe4632f9591faca7c46a068ea7.jpg",
"7fe76931d5653349a7876166db59d25e.jpg",
"7b9532126cb8cd0752f8bdcd2d683cb6.jpg",
"c8be53d1cb4d3689420aa56dea0b16f5.jpg",
"2799e89f4632184d151941741b59257f.jpg",
"4c5a2a39e6ae9e01bf2ea9860ae8e41f.jpg",
"3e0ad66c001c9894c26eac4b98601c73.jpg",
"20955b34b4cbc661f3ec7ff2eda0b815.jpg"
],
"total": 37,
"lastUpdated": "2026-05-18 02:36:56"
}

BIN
static/img/planets/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
static/img/planets/moon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,3 +1,195 @@
// ==================== 全局搜索功能 ====================
(function() {
const searchEngines = {
google: 'https://www.google.com/search?q=',
baidu: 'https://www.baidu.com/s?wd=',
duckduckgo: 'https://duckduckgo.com/?q=',
yandex: 'https://yandex.com/search/?text='
};
let currentEngine = localStorage.getItem('search_engine') || 'google';
let enterCount = 0;
let enterTimer = null;
const searchOverlay = document.getElementById('search-overlay');
const searchLockBtn = document.getElementById('search-lock-btn');
const lockIconOpen = document.getElementById('lock-icon-open');
const lockIconClosed = document.getElementById('lock-icon-closed');
const searchInput = document.getElementById('global-search-input');
const searchBtn = document.getElementById('global-search-btn');
const searchIcon = document.getElementById('search-engine-icon');
const dropdown = document.getElementById('search-engine-dropdown');
const engineSelector = document.getElementById('search-engine-selector');
const engineOptions = document.querySelectorAll('.engine-option');
// 检查是否开启常驻模式
const isPersistentSearch = localStorage.getItem('persistent_search') === 'true';
if (isPersistentSearch) {
document.body.classList.add('persistent-search-mode');
// 确保搜索框也显示出来
if (searchOverlay) {
searchOverlay.classList.add('active');
}
}
// 初始化搜索引擎图标
function updateSearchIcon() {
const activeOption = document.querySelector(`.engine-option[data-engine="${currentEngine}"]`);
if (activeOption) {
const iconSrc = activeOption.getAttribute('data-icon');
searchIcon.src = iconSrc;
searchIcon.alt = currentEngine.charAt(0).toUpperCase() + currentEngine.slice(1);
}
}
// 更新锁图标状态
function updateLockIcon() {
const isLocked = document.body.classList.contains('persistent-search-mode');
if (isLocked) {
lockIconOpen.style.display = 'none';
lockIconClosed.style.display = 'block';
searchLockBtn.classList.add('locked');
searchLockBtn.title = '取消常驻';
} else {
lockIconOpen.style.display = 'block';
lockIconClosed.style.display = 'none';
searchLockBtn.classList.remove('locked');
searchLockBtn.title = '搜索常驻';
}
}
// 显示搜索框
function showSearch() {
searchOverlay.classList.add('active');
setTimeout(() => {
searchInput.focus();
}, 100);
}
// 隐藏搜索框(仅非驻留模式下调用)
function hideSearch() {
if (document.body.classList.contains('persistent-search-mode')) {
return; // 常驻模式下不允许通过此函数关闭
}
searchOverlay.classList.remove('active');
searchInput.value = '';
dropdown.classList.remove('show');
}
// 切换常驻模式
function togglePersistentMode() {
const isLocked = document.body.classList.contains('persistent-search-mode');
if (isLocked) {
// 退出常驻模式(解锁):只改变锁状态,不关闭搜索框
document.body.classList.remove('persistent-search-mode');
localStorage.setItem('persistent_search', 'false');
// 注意:不移除 active 类,保持搜索框显示
} else {
// 开启常驻模式(锁定)
document.body.classList.add('persistent-search-mode');
localStorage.setItem('persistent_search', 'true');
searchOverlay.classList.add('active');
}
updateLockIcon();
}
// 执行搜索
function performSearch() {
const query = searchInput.value.trim();
if (query) {
const url = searchEngines[currentEngine] + encodeURIComponent(query);
window.open(url, '_blank');
}
}
// 监听双击回车键
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const now = Date.now();
if (enterTimer && (now - enterTimer < 500)) {
// 双击回车:如果当前不是常驻模式,则打开/关闭搜索框;如果是常驻模式,则不处理
if (!document.body.classList.contains('persistent-search-mode')) {
if (searchOverlay.classList.contains('active')) {
hideSearch();
} else {
showSearch();
}
}
enterCount = 0;
enterTimer = null;
} else {
enterCount = 1;
enterTimer = now;
// 如果搜索框已经打开,按单下回车执行搜索
if (searchOverlay.classList.contains('active') && document.activeElement === searchInput) {
performSearch();
}
}
}
// ESC 键关闭搜索框(仅在非驻留模式下生效)
if (e.key === 'Escape' && searchOverlay.classList.contains('active') && !document.body.classList.contains('persistent-search-mode')) {
hideSearch();
}
});
// 点击搜索引擎图标切换下拉菜单
engineSelector.addEventListener('click', (e) => {
e.stopPropagation();
dropdown.classList.toggle('show');
});
// 选择搜索引擎
engineOptions.forEach(option => {
option.addEventListener('click', () => {
currentEngine = option.getAttribute('data-engine');
localStorage.setItem('search_engine', currentEngine);
// 更新激活状态
engineOptions.forEach(opt => opt.classList.remove('active'));
option.classList.add('active');
updateSearchIcon();
dropdown.classList.remove('show');
searchInput.focus();
});
});
// 点击页面其他地方关闭下拉菜单
document.addEventListener('click', (e) => {
// 如果不在常驻模式下,且点击的是搜索框外部,则关闭搜索框
if (!document.body.classList.contains('persistent-search-mode') && !searchOverlay.contains(e.target)) {
hideSearch();
}
dropdown.classList.remove('show');
});
// 阻止下拉菜单内部点击事件冒泡
dropdown.addEventListener('click', (e) => {
e.stopPropagation();
});
// 点击搜索按钮
searchBtn.addEventListener('click', performSearch);
// 点击锁图标切换常驻模式
searchLockBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡,防止触发 document 的点击关闭逻辑
togglePersistentMode();
});
// 初始化
updateSearchIcon();
updateLockIcon();
const initialActiveOption = document.querySelector(`.engine-option[data-engine="${currentEngine}"]`);
if (initialActiveOption) {
engineOptions.forEach(opt => opt.classList.remove('active'));
initialActiveOption.classList.add('active');
}
})();
// ==================== Umami Stats 请求 ====================
(async () => {
const webid = "ae6cd64c-5900-49c9-9c22-95cedc24a508";
@@ -6,7 +198,7 @@
const endAt = Date.now(); // 当前时间的时间戳(毫秒)
const headers = {
Authorization:
"Bearer 3VvA8ETw0ahPhzuNNY+Zxi3agtfOBT2vNRbm0GcPqIyUhm7rExuwj8F8IwiQWcn/rOD2G/TnONPCFIvUECQYp6GuZRTnfOojki533vP/skqf0D6puOZDQQk8Y7ssihXnfyRu5naGhIoj1BCAC7S0D0RiYvzpSYF9zvZqvgxETrCbFazZsqUBolyJd8H2iZiM4Xx3VC+GnkZHZFgQfaaYUvm33a7CLM74PyFpPby63UExMjIPiLQRAOR2hs5wl5JAs5CTYUaq+QHCCz+tWgDQ4FPtIgoZoG8Ugnywv/YEEn1Jv9p3t8ge7m8ttThnPiZWw62PYPWQ3LpFh7nxX9jQX/Y/vaaAyacCoIP5J4VpiClA40GMMptZrThzEQjheegCilb9",
"Bearer JMQBYQLIlwUsx1tnfYI35APN3bh75JrdpWkx1uk0LJTi6QEGPlno2W2D7j8ogCAS12pXTT36VAZaGw3Pqgiw3BEViZG7o3+93zcmR4Txm0wjmHsTnig7GAxWfQMs1eRY9ACFl9KAo2n0UNgC+CqZt64PFxsKwsaIE2UzuGq1ByrCW8TQwtf76ZHg/qYYjFKAGIUEoQeDvXyCjoew25f7nN3o9GTdimkqoUt0xK/2tWCW1ROOowf6r5KDJK17jRNJGV6jwUJ9AsAd53CNp+MBOHy+E8Scrk/0ATZL3HUGSAuaFKyeITkaQTCjjwunnkxq/VAaeGXZIwI1Vt1llz0Qhbjy2G7l28FhOXm4QZLTdWXtduKwHkfAoFB+HcAWGsIdanoX",
"Content-Type": "application/json",
};
const params = new URLSearchParams({

266
static/js/blog_posts.js Normal file
View File

@@ -0,0 +1,266 @@
/**
* 博客文章功能 - 获取并展示博客最新文章
*/
(function () {
'use strict';
const BLOG_API_URL = 'https://blog.iletter.top/api/posts';
const BLOG_TOKEN = '67192cda-156a-415a-9f3c-97e90a6b818a';
const PAGE_SIZE = 10;
let currentPage = 1;
let hasMore = true;
let isLoading = false;
// DOM 元素
const blogPostsBtn = document.getElementById('blog-posts-btn');
const blogPostsPreview = document.getElementById('blog-posts-preview');
const blogPostsFullModal = document.getElementById('blog-posts-full-modal');
const blogPostsClose = document.getElementById('blog-posts-close');
const blogPostsList = document.querySelector('.blog-posts-list');
const blogPostsFullBody = document.querySelector('#blog-posts-full-modal .guestbook-body');
const previewBody = blogPostsPreview ? blogPostsPreview.querySelector('.preview-body') : null;
// 初始化
function init() {
if (!blogPostsBtn || !blogPostsFullModal) return;
// 点击按钮打开完整弹窗
blogPostsBtn.addEventListener('click', openFullModal);
// 关闭按钮
blogPostsClose.addEventListener('click', closeFullModal);
// 页面加载时自动加载数据(用于 Hover 预览)
loadBlogPosts(true);
// 监听预览面板滚动事件实现触底加载
if (previewBody) {
previewBody.addEventListener('scroll', handleScroll);
}
}
// 处理滚动事件(仅针对预览面板)
function handleScroll(e) {
// 如果滚动的是全屏弹窗,则不触发自动加载
if (e.target.closest('#blog-posts-full-modal')) return;
const { scrollTop, scrollHeight, clientHeight } = e.target;
// 距离底部 50px 时触发加载
if (scrollHeight - scrollTop - clientHeight < 50) {
loadMoreBlogPosts();
}
}
// 打开完整弹窗
function openFullModal() {
blogPostsFullModal.classList.add('active');
document.body.style.overflow = 'hidden';
// 首次打开时加载文章
if (!document.querySelector('.blog-post-item')) {
loadBlogPosts(true);
}
}
// 关闭完整弹窗
function closeFullModal() {
blogPostsFullModal.classList.remove('active');
document.body.style.overflow = '';
}
// 加载文章列表
async function loadBlogPosts(reset = false) {
if (isLoading || (!hasMore && !reset)) return;
isLoading = true;
if (reset) {
currentPage = 1;
hasMore = true;
if (blogPostsList) blogPostsList.innerHTML = '<div class="guestbook-loading">正在加载文章...</div>';
if (blogPostsFullBody) blogPostsFullBody.innerHTML = '<div class="guestbook-loading">正在加载文章...</div>';
}
try {
const response = await fetch(`${BLOG_API_URL}?page=${currentPage}&pageSize=${PAGE_SIZE}&showContent=false&showDigest=excerpt&limit=60`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'token': BLOG_TOKEN
}
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
const posts = result.data.dataSet || [];
const totalPages = result.data.pages || 0;
const totalCount = result.data.count || 0; // 获取总数
// 判断是否还有更多数据
hasMore = currentPage < totalPages;
if (reset) {
renderPosts(posts, true, totalCount);
} else {
appendPosts(posts);
}
} catch (error) {
console.error('加载文章失败:', error);
if (reset) {
const errorMsg = '<div class="guestbook-empty">加载文章失败,请稍后重试</div>';
if (blogPostsList) blogPostsList.innerHTML = errorMsg;
if (blogPostsFullBody) blogPostsFullBody.innerHTML = errorMsg;
}
} finally {
isLoading = false;
}
}
// 渲染文章列表
function renderPosts(posts, isReset = false, totalCount = 0) {
// 预览面板 HTML无底部分页按钮使用无限滚动
let previewHtml = '';
// 全屏弹窗 HTML保留底部分页按钮
let fullHtml = '';
if (!posts || posts.length === 0) {
const emptyHtml = '<div class="guestbook-empty">暂无文章</div>';
previewHtml = emptyHtml;
fullHtml = emptyHtml;
} else {
const containerHtml = '<div class="blog-posts-container">' +
posts.map(post => renderPostItem(post)).join('') +
'</div>';
previewHtml = containerHtml;
if (!hasMore) {
previewHtml += `<div class="load-complete">已显示全部</div>`;
}
fullHtml = containerHtml;
// 弹窗界面保留“点击加载更多”按钮
if (hasMore) {
fullHtml += `<button class="load-more-btn" onclick="window.loadMoreBlogPosts()">点击加载更多</button>`;
} else {
fullHtml += `<div class="load-complete">已显示全部</div>`;
}
}
if (blogPostsList) blogPostsList.innerHTML = previewHtml;
if (blogPostsFullBody) blogPostsFullBody.innerHTML = fullHtml;
// 更新预览面板标题显示总数
const previewHeader = document.querySelector('#blog-posts-preview .preview-header h3');
if (previewHeader && totalCount > 0) {
const lang = localStorage.getItem('preferred_language') || 'zh';
const translations = {
'zh': typeof translationsZH !== 'undefined' ? translationsZH : {},
'en': typeof translationsEN !== 'undefined' ? translationsEN : {}
};
const titleText = translations[lang]['blog_latest_posts'] || '最新文章';
previewHeader.textContent = `${titleText} (${totalCount})`;
}
}
// 追加文章
function appendPosts(posts) {
const containers = document.querySelectorAll('.blog-posts-container');
if (containers.length === 0) {
console.error('未找到 .blog-posts-container 容器');
return;
}
// 向所有容器追加新文章
containers.forEach(container => {
posts.forEach(post => {
const div = document.createElement('div');
div.innerHTML = renderPostItem(post);
container.appendChild(div.firstElementChild);
});
// 移除当前容器后的旧按钮和提示(只操作当前容器的兄弟元素)
let nextSibling = container.nextElementSibling;
while (nextSibling && (nextSibling.classList.contains('load-more-btn') || nextSibling.classList.contains('load-complete'))) {
const toRemove = nextSibling;
nextSibling = nextSibling.nextElementSibling;
toRemove.remove();
}
// 区分预览面板和全屏弹窗:预览面板不显示按钮,全屏弹窗显示按钮
const isPreview = container.closest('#blog-posts-preview');
if (!isPreview) {
if (hasMore) {
const btn = document.createElement('button');
btn.className = 'load-more-btn';
btn.textContent = '点击加载更多';
btn.onclick = window.loadMoreBlogPosts;
container.after(btn);
} else {
const tip = document.createElement('div');
tip.className = 'load-complete';
tip.textContent = '已显示全部';
container.after(tip);
}
} else {
// 预览面板仅显示完成提示
if (!hasMore) {
const tip = document.createElement('div');
tip.className = 'load-complete';
tip.textContent = '已显示全部';
container.after(tip);
}
}
});
console.log(`成功追加 ${posts.length} 篇文章到 ${containers.length} 个容器,当前页码: ${currentPage}`);
}
// 渲染单个文章项
function renderPostItem(post) {
const date = new Date(post.created * 1000);
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
const category = post.categories && post.categories.length > 0 ? post.categories[0].name : '未分类';
const digest = post.digest || '';
let html = `<div class="blog-post-item" onclick="showCustomConfirm('确定要打开网站吗?\\n${post.permalink}', () => window.open('${post.permalink}', '_blank'))">`;
html += `<div class="blog-post-title">${escapeHtml(post.title)}</div>`;
if (digest) {
html += `<div class="blog-post-digest">${escapeHtml(digest)}</div>`;
}
html += `<div class="blog-post-meta">`;
html += `<span>${dateStr}</span>`;
html += `<span class="blog-post-category">${escapeHtml(category)}</span>`;
html += `</div>`;
html += `</div>`;
return html;
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 加载更多
window.loadMoreBlogPosts = function() {
if (!isLoading && hasMore) {
currentPage++;
loadBlogPosts(false);
}
};
// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

1039
static/js/galaxy.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -287,6 +287,11 @@ function toggleGameDetailTooltip(gameElement, gameIndex) {
tooltipDiv.style.left = `${left}px`;
tooltipDiv.style.display = "block";
// 触发重绘以启动过渡动画
void tooltipDiv.offsetWidth;
tooltipDiv.style.opacity = "1";
tooltipDiv.style.transform = "translateY(0) scale(1)";
// 记录当前打开的tooltip
currentlyOpenTooltip = { element: tooltipDiv, index: gameIndex };
@@ -299,11 +304,13 @@ function toggleGameDetailTooltip(gameElement, gameIndex) {
// ========== 隐藏Tooltip的辅助函数 ==========
function hideTooltip(tooltipElement) {
if (tooltipElement) {
tooltipElement.style.display = "none";
// 如果想完全移除DOM可以取消下面两行注释但通常隐藏即可
// if (tooltipElement.parentNode) {
// tooltipElement.parentNode.removeChild(tooltipElement);
// }
tooltipElement.style.opacity = "0";
tooltipElement.style.transform = "translateY(10px) scale(0.95)";
// 等待动画结束后再隐藏 display
setTimeout(() => {
tooltipElement.style.display = "none";
}, 600);
}
}

544
static/js/guestbook.js Normal file
View File

@@ -0,0 +1,544 @@
/**
* 留言板功能 - 基于 Flask API
*/
(function () {
'use strict';
// Flask API 地址
const API_BASE_URL = 'https://comments.iletter.top';
const PAGE_SIZE = 10;
let currentPage = 1;
let hasMore = true;
let isLoading = false;
let allComments = []; // 存储所有已加载的留言
// DOM 元素
const guestbookBtn = document.getElementById('guestbook-btn');
const guestbookPreview = document.getElementById('guestbook-preview');
const guestbookFullModal = document.getElementById('guestbook-full-modal');
const guestbookClose = document.getElementById('guestbook-close');
const guestbookBody = document.querySelector('#guestbook-full-modal .guestbook-body');
const previewBody = document.querySelector('.preview-body');
// Toast 容器
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container';
document.body.appendChild(toastContainer);
}
// 表单元素(需要动态获取,因为表单会被重新渲染)
let nicknameInput, emailInput, linkInput, contentInput, submitBtn;
// 显示 Toast 提示
function showToast(message, type = 'success', duration = 2000) {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
toastContainer.appendChild(toast);
// 触发动画
setTimeout(() => {
toast.classList.add('show');
}, 10);
// 自动消失
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
toast.remove();
}, 300);
}, duration);
}
// 初始化
function init() {
if (!guestbookBtn || !guestbookFullModal) return;
// 点击按钮打开完整弹窗
guestbookBtn.addEventListener('click', openFullModal);
// 关闭按钮
guestbookClose.addEventListener('click', closeFullModal);
// 预览面板触底加载
if (previewBody) {
previewBody.addEventListener('scroll', handlePreviewScroll);
}
// 表单提交事件使用事件委托
guestbookBody.addEventListener('submit', function(e) {
if (e.target && e.target.id === 'guestbook-form') {
handleSubmit(e);
}
});
// 页面加载时自动加载数据(用于 Hover 预览)
loadGuestbook(true);
}
// 切换留言板显示/隐藏
function toggleGuestbook() {
if (guestbookFullModal.classList.contains('active')) {
closeFullModal();
} else {
openFullModal();
}
}
// 打开完整弹窗
function openFullModal() {
guestbookFullModal.classList.add('active');
document.body.style.overflow = 'hidden';
// 首次打开时加载留言
if (!document.querySelector('.guestbook-list')) {
loadGuestbook(true);
}
}
// 关闭完整弹窗
function closeFullModal() {
guestbookFullModal.classList.remove('active');
document.body.style.overflow = '';
}
// 点击模态框背景关闭
function handleModalClick(e) {
if (e.target === guestbookModal) {
closeGuestbook();
}
}
// 加载留言列表
async function loadGuestbook(reset = false) {
if (isLoading || (!hasMore && !reset)) return;
isLoading = true;
if (reset) {
currentPage = 1;
hasMore = true;
guestbookBody.innerHTML = '<div class="guestbook-loading" data-i18n="guestbook_loading">正在加载留言...</div>';
} else {
// 添加加载更多按钮状态
const loadMoreBtn = guestbookBody.querySelector('.load-more-btn');
if (loadMoreBtn) {
loadMoreBtn.disabled = true;
loadMoreBtn.textContent = '加载中...';
}
}
try {
const response = await fetch(`${API_BASE_URL}/api/comment?path=www.iletter.top&pageSize=${PAGE_SIZE}&page=${currentPage}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
const comments = result.data.data || [];
const totalCount = result.data.count || 0;
const totalPages = result.data.totalPages || 0;
// 判断是否还有更多数据(根据总页数)
hasMore = currentPage < totalPages;
// 累积所有留言
if (reset) {
allComments = comments;
} else {
allComments = allComments.concat(comments);
}
if (reset) {
renderGuestbook(comments, totalCount);
} else {
appendGuestbook(comments);
}
} catch (error) {
console.error('加载留言失败:', error);
if (reset) {
guestbookBody.innerHTML = `<div class="guestbook-empty" data-i18n="guestbook_load_error">加载留言失败,请稍后重试</div>`;
}
} finally {
isLoading = false;
}
}
// 渲染留言列表
function renderGuestbook(comments, totalCount) {
let html = '';
// 先渲染表单(放在顶部)
html += renderForm();
if (!comments || comments.length === 0) {
html += `<div class="guestbook-empty" data-i18n="guestbook_no_data">暂无留言,快来抢沙发吧!</div>`;
guestbookBody.innerHTML = html;
return;
}
html += '<div class="guestbook-list">';
comments.forEach(comment => {
html += renderCommentItem(comment);
});
html += '</div>';
// 判断是否还有更多数据
if (hasMore) {
html += `<button class="load-more-btn" onclick="window.loadMoreGuestbook()">点击加载更多</button>`;
}
// 否则不显示任何内容
guestbookBody.innerHTML = html;
// 同时更新 Hover 预览面板(使用累积的所有留言)
renderPreview(allComments, totalCount);
}
// 渲染 Hover 预览面板
function renderPreview(comments, totalCount = 0) {
if (!previewBody) return;
if (!comments || comments.length === 0) {
previewBody.innerHTML = '<div class="guestbook-empty">暂无留言,快来抢沙发吧!</div>';
return;
}
// 更新标题显示总数
const previewHeader = document.querySelector('#guestbook-preview .preview-header h3');
if (previewHeader && totalCount > 0) {
const lang = localStorage.getItem('preferred_language') || 'zh';
const translations = {
'zh': typeof translationsZH !== 'undefined' ? translationsZH : {},
'en': typeof translationsEN !== 'undefined' ? translationsEN : {}
};
const titleText = translations[lang]['guestbook_title'] || '留言板';
previewHeader.textContent = `${titleText} (${totalCount})`;
}
let html = '';
comments.forEach(comment => {
const date = new Date(comment.time);
const timeStr = formatDate(date);
html += `<div class="preview-item">`;
html += `<div class="preview-nickname">${escapeHtml(comment.nick)}</div>`;
// 提取纯文本内容(去除 HTML 标签)
const tempDiv = document.createElement('div');
tempDiv.innerHTML = comment.comment || '';
const textContent = tempDiv.textContent || tempDiv.innerText || '';
html += `<div class="preview-content">${escapeHtml(textContent)}</div>`;
html += `<div class="preview-time">${timeStr}</div>`;
html += `</div>`;
});
// 如果已经加载完所有数据,显示提示
if (!hasMore && allComments.length > 0) {
html += `<div class="preview-complete">已全部加载</div>`;
}
previewBody.innerHTML = html;
}
// 处理预览面板滚动事件
function handlePreviewScroll(e) {
const target = e.target;
const scrollTop = target.scrollTop;
const scrollHeight = target.scrollHeight;
const clientHeight = target.clientHeight;
// 距离底部 50px 时触发加载
if (scrollHeight - scrollTop - clientHeight < 50) {
if (!isLoading && hasMore) {
currentPage++;
loadGuestbook(false);
}
}
}
// 追加留言
function appendGuestbook(comments) {
const listContainer = guestbookBody.querySelector('.guestbook-list');
const loadMoreBtn = guestbookBody.querySelector('.load-more-btn');
if (!listContainer) return;
comments.forEach(comment => {
const div = document.createElement('div');
div.innerHTML = renderCommentItem(comment);
listContainer.appendChild(div.firstElementChild);
});
// 移除旧的加载更多按钮
if (loadMoreBtn) {
loadMoreBtn.remove();
}
// 如果还有更多,添加新的加载更多按钮;否则显示"已显示全部"
if (hasMore) {
const btn = document.createElement('button');
btn.className = 'load-more-btn';
btn.textContent = '点击加载更多';
btn.onclick = window.loadMoreGuestbook;
listContainer.after(btn);
} else {
const tip = document.createElement('div');
tip.className = 'load-complete';
tip.textContent = '已显示全部';
listContainer.after(tip);
}
// 更新预览面板
renderPreview(allComments, allComments.length);
}
// 渲染单个留言项
function renderCommentItem(comment) {
const date = new Date(comment.time);
const timeStr = formatDate(date);
let html = `<div class="guestbook-item">`;
html += `<div class="guestbook-item-header">`;
// 头像和昵称
html += `<div style="display: flex; align-items: center; gap: 8px;">`;
if (comment.avatar) {
html += `<img src="${escapeHtml(comment.avatar)}" alt="">`;
}
// 昵称和链接
if (comment.link) {
html += `<span class="guestbook-nickname"><a href="${escapeHtml(comment.link)}" target="_blank">${escapeHtml(comment.nick)}</a></span>`;
} else {
html += `<span class="guestbook-nickname">${escapeHtml(comment.nick)}</span>`;
}
html += `</div>`;
html += `<span class="guestbook-meta">${timeStr}</span>`;
html += `</div>`;
// 内容comment 字段已经是 HTML
html += `<div class="guestbook-content">${comment.comment || ''}</div>`;
// 底部信息(浏览器、系统、地址)
html += renderCommentFooter(comment);
// 回复列表
if (comment.children && comment.children.length > 0) {
html += `<div class="guestbook-reply">`;
comment.children.forEach(reply => {
html += renderReplyItem(reply);
});
html += `</div>`;
}
html += `</div>`;
return html;
}
// 渲染留言底部信息
function renderCommentFooter(comment) {
const browser = comment.browser || '';
const os = comment.os || '';
const addr = comment.addr || '';
if (!browser && !os && !addr) return '';
let html = '<div class="guestbook-footer">';
if (browser) {
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg> ${escapeHtml(browser)}</span>`;
}
if (os) {
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg> ${escapeHtml(os)}</span>`;
}
if (addr) {
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg> ${escapeHtml(addr)}</span>`;
}
html += '</div>';
return html;
}
// 渲染回复项
function renderReplyItem(reply) {
const date = new Date(reply.time);
const timeStr = formatDate(date);
let html = `<div class="guestbook-item" style="margin-top: 10px;">`;
html += `<div class="guestbook-item-header">`;
html += `<div style="display: flex; align-items: center; gap: 8px;">`;
if (reply.avatar) {
html += `<img src="${escapeHtml(reply.avatar)}" alt="">`;
}
if (reply.link) {
html += `<span class="guestbook-nickname"><a href="${escapeHtml(reply.link)}" target="_blank">${escapeHtml(reply.nick)}</a></span>`;
} else {
html += `<span class="guestbook-nickname">${escapeHtml(reply.nick)}</span>`;
}
html += `</div>`;
html += `<span class="guestbook-meta">${timeStr}</span>`;
html += `</div>`;
html += `<div class="guestbook-content">${reply.comment || ''}</div>`;
html += `</div>`;
return html;
}
// 渲染表单
function renderForm() {
return `
<form id="guestbook-form" class="guestbook-form">
<div class="form-row">
<div class="form-group">
<label for="guestbook-nickname" data-i18n="guestbook_nickname">昵称(选填)</label>
<input type="text" id="guestbook-nickname" placeholder="匿名">
</div>
<div class="form-group">
<label for="guestbook-email" data-i18n="guestbook_email">邮箱(选填)</label>
<input type="email" id="guestbook-email" placeholder="用于接收回复通知">
</div>
<div class="form-group">
<label for="guestbook-link" data-i18n="guestbook_website">网站(选填)</label>
<input type="url" id="guestbook-link" placeholder="https://example.com">
</div>
</div>
<div class="form-group-full">
<label for="guestbook-content" data-i18n="guestbook_placeholder">说点什么吧...</label>
<textarea id="guestbook-content" placeholder="请输入留言内容..." required maxlength="500"></textarea>
</div>
<div class="form-submit">
<button type="submit" id="guestbook-submit" class="submit-btn" data-i18n="guestbook_submit">提交留言</button>
</div>
</form>
`;
}
// 提交留言
async function handleSubmit(e) {
e.preventDefault();
// 重新获取表单元素(因为表单可能被重新渲染)
nicknameInput = document.getElementById('guestbook-nickname');
emailInput = document.getElementById('guestbook-email');
linkInput = document.getElementById('guestbook-link');
contentInput = document.getElementById('guestbook-content');
submitBtn = document.getElementById('guestbook-submit');
const nick = nicknameInput.value.trim() || '匿名';
const email = emailInput.value.trim();
const link = linkInput.value.trim();
const comment = contentInput.value.trim();
// 验证
if (!comment) {
showToast('请输入留言内容', 'error');
return;
}
// 禁用提交按钮
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
try {
const response = await fetch(`${API_BASE_URL}/api/comment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
nick: nick,
mail: email || '',
link: link || '',
comment: comment,
path: 'www.iletter.top',
ua: navigator.userAgent,
url: window.location.href
})
});
const data = await response.json();
if (response.ok && data.errno === 0) {
showToast(getTranslation('guestbook_submit_success') || '留言提交成功!', 'success');
// 清空表单
nicknameInput.value = '';
emailInput.value = '';
linkInput.value = '';
contentInput.value = '';
// 重新加载第一页
loadGuestbook(true);
} else {
throw new Error(data.errmsg || '提交失败');
}
} catch (error) {
console.error('提交留言失败:', error);
showToast(getTranslation('guestbook_submit_error') || '留言提交失败,请重试', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = getTranslation('guestbook_submit') || '提交留言';
}
}
// 格式化日期
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 获取翻译
function getTranslation(key) {
const lang = localStorage.getItem('preferred_language') || 'zh';
const translations = {
'zh': typeof translationsZH !== 'undefined' ? translationsZH : {},
'en': typeof translationsEN !== 'undefined' ? translationsEN : {}
};
return translations[lang][key] || key;
}
// 加载更多
window.loadMoreGuestbook = function() {
if (!isLoading && hasMore) {
currentPage++;
loadGuestbook(false);
}
};
// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -13,14 +13,15 @@ const translationsEN = {
greeting: "",
my_name: "BaiTu",
about_me_p1:
"My name is <strong>BaiTu</strong>, from Hengshui, Hebei, a native northerner. I graduated from an obscure second-tier university with a major in Information Management and Information Systems. I self-studied development technologies like Java, Vue, Linux, MySQL, Python, and some software reverse engineering techniques. I also have basic skills in PS, PR, and AE. I have experience with systems, networking, PLCs, microcontrollers, server maintenance, and managing public accounts and websites. In short, I know a little bit about many things. Others might think I'm an expert, but actually, I'm just a rookie. However, I won't give up and will strive to become an expert. As Liu Lian from NARAKA said, \"The most important skill in the world is perseverance.\"",
"My name is <strong>BaiTu</strong>, from Hengshui, Hebei, a native northerner. I graduated from a second-tier university with a major in Information Management and Information Systems. I self-taught development languages like Java, Vue ecosystem, Python, as well as operations and database technologies like Linux, MySQL, Redis, and Nginx. I also dabble in software reverse engineering, PS, PR, AE production, and other fancy stuff. I've built systems, crimped network cables, worked with PLCs, played with microcontrollers, repaired servers (physically 🤭), operated public accounts, and maintained and managed websites. You could say I know a bit of everything, but just a tiny bit. Others may think I'm a pro, but I'm actually just a noob. Still, I won't give up and will keep striving to become a pro. As Liu Lian from NARAKA said, \"The most important skill in the world is perseverance.\"",
about_me_p2:
'My hobbies usually involve playing games, reading books, watching anime, and I prefer staying indoors. I hate repetitive and simple work, so I often write small tools to make life easier because laziness drives my productivity. I frequently lurk on various forums and blogs, shouting "666" or "Amazing" under tech bloggers. My own blog updates sporadically, mainly recording daily life notes and technical challenges encountered.',
'My hobbies are playing games, reading books, and watching anime — a complete homebody who hates going out. I hate repetitive and simple work, so I often write small tools to slack off. After all, laziness is my primary productivity. I frequently lurk on various forums and blogs, shouting "666" or "Amazing" under tech bloggers. My own blog updates sporadically, mainly recording daily life notes and technical challenges encountered.',
about_me_p3:
'This site follows a minimalist style, using pure HTML. Umami tracks UV/PV at the bottom. The Memos section uses the Memos API. It also employs Baota Cloud WAF protection and nginx caching for static resources. If you are interested, you can directly download the source code from <a href="https://gitea.iletter.top/dellevin/BaiTu-homepage" target="_blank">https://gitea.iletter.top/dellevin/BaiTu-homepage </a> and use it.',
'This site follows a minimalist letter-style design, using pure HTML. Umami tracks UV/PV at the bottom. The Memos section uses the Memos API. The guestbook is self-built, and blog posts use the Typecho Restful plugin (customized by me). It also employs Baota Cloud WAF protection and nginx caching for static resources. If you are interested, you can directly download the source code from <a href="https://github.com/dellevin/iletter-homepage" target="_blank">https://github.com/dellevin/iletter-homepage </a> and use it, or download by viewing the website source code through F12.',
about_me_p4:
"As a 99er programmer, I am glad to still have great passion for technology after starting work! If you share the same interest in technology, I would love to meet you! Let's progress and grow together!",
// 联系方式
// 联系方式
contact_intro: "Feel free to reach out via the following methods:",
email_label: "Email:",
wechat_label: "WeChat:",
@@ -33,10 +34,12 @@ const translationsEN = {
social_link3: "Zhihu",
social_link4: "Bilibili",
social_link5: "52pojie",
social_link6: "XiaoHongShu",
// 留言
leave_message_h3: "Leave a Message",
leave_message_p:
'If you have any questions or suggestions, feel free to <a href="https://blog.iletter.top/401.html" target="_blank">click here</a> to leave me a message on my blog!',
'If you have any questions or suggestions, feel free to <a href="https://blog.iletter.top/401.html" target="_blank">click here</a> to leave me a message on my blog, or click the guestbook link in the top right corner to leave a message!',
leave_message_link: "click here",
// 小界面标题
my_website_h2: "My Websites",
@@ -57,4 +60,24 @@ const translationsEN = {
"I still choose to spend the rest of my life in my own way although I am a failure.",
visitor_count_label: "Visitors:",
visit_count_label: "Total Visits:",
// Guestbook
guestbook_title: "Guestbook",
guestbook_placeholder: "Say something...",
guestbook_nickname: "Nickname (optional)",
guestbook_email: "Email (optional)",
guestbook_website: "Website (optional)",
guestbook_submit: "Submit",
guestbook_loading: "Loading messages...",
guestbook_no_data: "No messages yet, be the first to comment!",
guestbook_load_error: "Failed to load messages, please try again later",
guestbook_submit_success: "Message submitted successfully!",
guestbook_submit_error: "Failed to submit message, please try again",
guestbook_required_nickname: "Please enter a nickname",
// Top-right buttons
btn_guestbook: "Guestbook",
btn_blog_posts: "Blog Posts",
btn_photo_album: "Travel Shadows",
blog_latest_posts: "Latest Posts",
photo_latest_photos: "Latest Photos",
photo_album_title: "Travel Shadows",
};

View File

@@ -13,14 +13,13 @@ const translationsZH = {
greeting: "见字如面,展信舒颜。",
my_name: "白荼",
about_me_p1:
"我叫<strong>白荼</strong>,河北衡水人,土生土长的北方汉子,毕业于一所不知名的二本院校。信息管理与信息系统专业。自学java、vue、linux、mysql、python等开发技术和一些软件逆向技术同时还略懂些PS图片、PR视频、AE特效制作。做系统,剪网线,搞过plc,耍过单片机,维护修理过服务器,运营过公众号网站等等。可谓是会,但只会一点点。别人都觉得我是个大佬,其实我就是个小菜鸡。但我不会气馁,努力成为一个大佬的。正如永劫无间刘炼的一句话“天下万般之绝学,莫过于恒心。”",
"我叫<strong>白荼</strong>,河北衡水人,土生土长的北方汉子,毕业于一所二本院校。信息管理与信息系统专业。自学java、vue全家桶、python等开发语言还有linux、mysql、redis、nginx等运维和数据库的技术,同时还略懂些软件逆向,PS、PR、AE制作等其他花里胡哨的东西。做系统,剪网线,搞过PLC,耍过单片机,修过服务器(物理层面🤭),运营过公众号,维护管理过网站等等。可谓是会,但只会一点点。别人都觉得我是个大佬,其实我就是个小菜鸡。但我不会气馁,努力成为一个大佬的。正如永劫无间刘炼的一句话“天下万般之绝学,莫过于恒心。”",
about_me_p2:
'平时的爱好就是玩玩游戏,看看书,刷刷动漫,不爱出门肥宅一个。讨厌重复简单的工作,会想办法偷懒写写自己的顺手的小工具什么的,毕竟懒惰是我的生产力。我也经常混迹于各大网络论坛博客并在各个技术博主下面直呼"大佬666"、"大佬牛牛牛"。自己的博客也在断断续续中更新。主要是记录生活随笔和碰到的技术难题。',
'平时的爱好就是玩玩游戏,看看书,刷刷动漫,一个彻头彻尾的不爱出门肥宅。讨厌重复简单的工作,会想办法偷懒写一下小工具什么的,毕竟懒惰是我的第一生产力。我也经常混迹于各大网络论坛博客并在各个技术博主下面直呼"大佬666"、"大佬牛牛牛"。自己的博客也在断断续续中更新。主要是记录生活随笔和碰到的技术难题。',
about_me_p3:
'本站秉承简约风格采用纯HTMLUmami做网站底部的uv/pv 闲言碎语模块使用memos接口。同时使用堡塔云WAF防护nginx缓存加速网站静态资源。如果您有兴趣可以直接下载本站的 <a href="https://gitea.iletter.top/dellevin/BaiTu-homepage" target="_blank">https://gitea.iletter.top/dellevin/BaiTu-homepage </a> 并使用。',
'本站秉承书信形式的简约风格采用纯HTMLUmami做网站底部的uv/pv 闲言碎语模块使用memos接口留言板自己写的博客文章使用的是typecho的Restful插件自己魔改了一下。同时使用堡塔云WAF防护nginx缓存加速网站静态资源。如果您有兴趣可以直接下载本站的 <a href="https://github.com/dellevin/iletter-homepage" target="_blank">https://github.com/dellevin/iletter-homepage </a> 并使用或者通过F12查看网站源代码进行下载。',
about_me_p4:
"作为一个99年的码农很庆幸我在工作之后还对技术有着极大的热情如果你也对技术有着同样的兴趣也很希望认识你一同进步共同成长",
// 联系方式
contact_intro: "欢迎通过以下方式与我联系:",
email_label: "邮箱:",
@@ -34,10 +33,12 @@ const translationsZH = {
social_link3: "知乎",
social_link4: "B站",
social_link5: "52pojie",
social_link6: "小红书",
// 留言
leave_message_h3: "留言",
leave_message_p:
'如果你有任何问题或建议,欢迎<a href="https://blog.iletter.top/401.html" target="_blank">点击此链接</a>去我的博客下面给我留言!',
'如果你有任何问题或建议,欢迎<a href="https://blog.iletter.top/401.html" target="_blank">点击此链接</a>去我的博客下面给我留言,或者点击右上角的留言板进行留言哦',
leave_message_link: "点击此链接",
// 小界面标题
my_website_h2: "我的网站",
@@ -57,4 +58,24 @@ const translationsZH = {
footer_text: "我虽然是个废物,但我仍然选择用自己喜欢的方式度过自己的余生",
visitor_count_label: "本站访客数 :",
visit_count_label: "本站总访问量 :",
// 留言板
guestbook_title: "留言板",
guestbook_placeholder: "说点什么吧...",
guestbook_nickname: "昵称(选填)",
guestbook_email: "邮箱(选填)",
guestbook_website: "网站(选填)",
guestbook_submit: "提交留言",
guestbook_loading: "正在加载留言...",
guestbook_no_data: "暂无留言,快来抢沙发吧!",
guestbook_load_error: "加载留言失败,请稍后重试",
guestbook_submit_success: "留言提交成功!",
guestbook_submit_error: "留言提交失败,请重试",
guestbook_required_nickname: "请输入昵称",
// 右上角按钮
btn_guestbook: "留言板",
btn_blog_posts: "博客文章",
btn_photo_album: "旅途剪影",
blog_latest_posts: "最新文章",
photo_latest_photos: "最新照片",
photo_album_title: "旅途剪影",
};

View File

@@ -32,7 +32,7 @@
let html = "";
memos.forEach((memo) => {
const formattedDate = formatDate(memo.displayTime);
const formattedDate = formatDate(memo.createTime);
const tagsHtml =
memo.tags && memo.tags.length > 0
? `<div class="memo-tags">${memo.tags

318
static/js/photo_album.js Normal file
View File

@@ -0,0 +1,318 @@
// ==================== 旅途剪影功能 ====================
(function() {
// DOM 元素
const photoAlbumBtn = document.getElementById('photo-album-btn');
const photoAlbumPreview = document.getElementById('photo-album-preview');
const photoAlbumFullModal = document.getElementById('photo-album-full-modal');
const photoAlbumClose = document.getElementById('photo-album-close');
const photoAlbumList = document.querySelector('.photo-album-list');
const photoAlbumBody = document.querySelector('#photo-album-full-modal .photo-album-body');
// 照片数据
let allPhotos = [];
let currentPhotoIndex = 0; // 当前查看的照片索引
const PHOTO_BASE_PATH = './static/img/photos/';
// 分页加载配置
const PAGE_SIZE = 20; // 每页加载数量
let currentPage = 1; // 当前页码
let hasMore = true; // 是否还有更多数据
let isLoading = false; // 是否正在加载中
// 初始化
function init() {
if (!photoAlbumBtn || !photoAlbumFullModal) return;
// 点击按钮打开完整弹窗
photoAlbumBtn.addEventListener('click', openFullModal);
// 关闭按钮
photoAlbumClose.addEventListener('click', closeFullModal);
// 页面加载时自动加载照片(用于 Hover 预览)
loadPhotos(true);
}
// 加载照片
async function loadPhotos(reset = false) {
if (reset) {
if (photoAlbumList) photoAlbumList.innerHTML = '<div class="guestbook-loading">正在加载照片...</div>';
if (photoAlbumBody) photoAlbumBody.innerHTML = '<div class="guestbook-loading">正在加载照片...</div>';
}
try {
// 从 JSON 文件获取照片列表
const response = await fetch(`${PHOTO_BASE_PATH}photos.json`);
if (!response.ok) {
throw new Error('Failed to load photos.json');
}
const data = await response.json();
const photoFiles = data.photos || [];
allPhotos = photoFiles.map((filename, index) => ({
id: index + 1,
src: `${PHOTO_BASE_PATH}${encodeURIComponent(filename)}`,
filename: filename
}));
renderPhotos(allPhotos, reset);
} catch (error) {
console.error('加载照片失败:', error);
if (reset) {
const errorMsg = '<div class="guestbook-empty">加载照片失败,请稍后重试</div>';
if (photoAlbumList) photoAlbumList.innerHTML = errorMsg;
if (photoAlbumBody) photoAlbumBody.innerHTML = errorMsg;
}
}
}
// 渲染照片列表
function renderPhotos(photos, isReset = false) {
let html = '';
if (!photos || photos.length === 0) {
html = '<div class="guestbook-empty">暂无照片</div>';
} else {
// 预览面板:显示最新的几张照片
if (isReset && photoAlbumList) {
const previewPhotos = photos.slice(0, 5); // 只显示前5张
let previewHtml = '<div class="photo-album-container">';
previewPhotos.forEach((photo, index) => {
previewHtml += `
<div class="photo-preview-item" onclick="window.openPhotoLightbox(${index})">
<img src="${photo.src}" alt="照片 ${photo.id}" loading="lazy">
</div>
`;
});
previewHtml += '</div>';
photoAlbumList.innerHTML = previewHtml;
}
// 完整弹窗:瀑布流布局显示当前页的照片
const startIndex = (currentPage - 1) * PAGE_SIZE;
const endIndex = Math.min(currentPage * PAGE_SIZE, photos.length);
const currentPhotos = photos.slice(startIndex, endIndex);
// 首次渲染:创建完整的 HTML
html = '<div class="photo-album-grid">';
currentPhotos.forEach((photo, index) => {
const globalIndex = startIndex + index;
html += `
<div class="photo-album-item" onclick="window.openPhotoLightbox(${globalIndex})">
<div class="photo-loading-spinner"></div>
<img src="${photo.src}" alt="照片 ${photo.id}" loading="lazy" onload="this.style.opacity=1; this.previousElementSibling.style.display='none'">
</div>
`;
});
html += '</div>';
// 分页导航
const totalPages = Math.ceil(photos.length / PAGE_SIZE);
if (totalPages > 1) {
html += '<div class="pagination-container">';
// 上一页
if (currentPage > 1) {
html += `<button class="page-btn" onclick="window.goToPhotoPage(${currentPage - 1})">&lt;</button>`;
}
// 页码
for (let i = 1; i <= totalPages; i++) {
if (i === currentPage) {
html += `<span class="page-num active">${i}</span>`;
} else {
// 只显示当前页附近的页码,避免太多
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
html += `<span class="page-num" onclick="window.goToPhotoPage(${i})">${i}</span>`;
} else if (i === currentPage - 3 || i === currentPage + 3) {
html += `<span class="page-dots">...</span>`;
}
}
}
// 下一页
if (currentPage < totalPages) {
html += `<button class="page-btn" onclick="window.goToPhotoPage(${currentPage + 1})">&gt;</button>`;
}
html += '</div>';
}
}
if (photoAlbumBody) photoAlbumBody.innerHTML = html;
}
// 打开完整弹窗
function openFullModal() {
photoAlbumFullModal.classList.add('active');
document.body.style.overflow = 'hidden';
// 重置分页状态
currentPage = 1;
hasMore = allPhotos.length > PAGE_SIZE;
// 清空弹窗内容,避免重复渲染
if (photoAlbumBody) photoAlbumBody.innerHTML = '';
renderPhotos(allPhotos, false);
}
// 关闭完整弹窗
function closeFullModal() {
photoAlbumFullModal.classList.remove('active');
document.body.style.overflow = '';
}
// 打开照片放大查看器
window.openPhotoLightbox = function(photoIndex) {
currentPhotoIndex = photoIndex;
// 检查是否已经存在 lightbox使用特定的类名避免与MookNote冲突
let lightbox = document.querySelector('.photo-album-lightbox');
if (!lightbox) {
lightbox = document.createElement('div');
lightbox.className = 'photo-lightbox photo-album-lightbox'; // 同时保留两个类名以兼容CSS
lightbox.innerHTML = `
<span class="photo-lightbox-close">&times;</span>
<span class="photo-lightbox-nav photo-lightbox-prev">&#10094;</span>
<img src="" alt="放大照片">
<span class="photo-lightbox-nav photo-lightbox-next">&#10095;</span>
<div class="photo-lightbox-counter"></div>
`;
document.body.appendChild(lightbox);
// 点击关闭按钮
lightbox.querySelector('.photo-lightbox-close').addEventListener('click', closePhotoLightbox);
// 上一张按钮
lightbox.querySelector('.photo-lightbox-prev').addEventListener('click', function(e) {
e.stopPropagation();
showPreviousPhoto();
});
// 下一张按钮
lightbox.querySelector('.photo-lightbox-next').addEventListener('click', function(e) {
e.stopPropagation();
showNextPhoto();
});
// 键盘事件
document.addEventListener('keydown', handleLightboxKeydown);
}
// 更新图片显示
updateLightboxImage();
lightbox.classList.add('active');
};
// 更新 Lightbox 图片
function updateLightboxImage() {
const lightbox = document.querySelector('.photo-album-lightbox');
if (!lightbox || !allPhotos[currentPhotoIndex]) return;
const img = lightbox.querySelector('img');
const counter = lightbox.querySelector('.photo-lightbox-counter');
// 淡出效果
img.style.opacity = '0';
setTimeout(() => {
img.src = allPhotos[currentPhotoIndex].src;
img.alt = `照片 ${currentPhotoIndex + 1}`;
// 更新计数器
if (counter) {
counter.textContent = `${currentPhotoIndex + 1} / ${allPhotos.length}`;
}
// 淡入效果
img.onload = () => {
img.style.opacity = '1';
};
}, 150);
}
// 显示上一张照片
function showPreviousPhoto() {
if (currentPhotoIndex > 0) {
currentPhotoIndex--;
} else {
currentPhotoIndex = allPhotos.length - 1; // 循环到最后一张
}
updateLightboxImage();
}
// 显示下一张照片
function showNextPhoto() {
if (currentPhotoIndex < allPhotos.length - 1) {
currentPhotoIndex++;
} else {
currentPhotoIndex = 0; // 循环到第一张
}
updateLightboxImage();
}
// 处理键盘事件
function handleLightboxKeydown(e) {
// 只在旅途剪影的lightbox激活时处理
const albumLightbox = document.querySelector('.photo-album-lightbox');
if (!albumLightbox || !albumLightbox.classList.contains('active')) return;
if (e.key === 'ArrowLeft') {
showPreviousPhoto();
} else if (e.key === 'ArrowRight') {
showNextPhoto();
} else if (e.key === 'Escape') {
closePhotoLightbox();
}
}
// 关闭照片放大查看器
function closePhotoLightbox() {
const lightbox = document.querySelector('.photo-album-lightbox');
if (lightbox) {
lightbox.classList.remove('active');
// 移除键盘事件监听器
document.removeEventListener('keydown', handleLightboxKeydown);
setTimeout(() => {
lightbox.remove();
}, 300);
}
}
// 加载更多照片
window.loadMorePhotos = function() {
if (isLoading || !hasMore) return;
isLoading = true;
currentPage++;
// 判断是否还有更多数据
hasMore = currentPage * PAGE_SIZE < allPhotos.length;
renderPhotos(allPhotos, false);
isLoading = false;
};
// 跳转到指定页码
window.goToPhotoPage = function(page) {
if (page < 1 || page > Math.ceil(allPhotos.length / PAGE_SIZE)) return;
currentPage = page;
renderPhotos(allPhotos, false);
// 滚动到顶部
if (photoAlbumBody) photoAlbumBody.scrollTop = 0;
};
// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

6
static/js/three.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -168,9 +168,9 @@ const toolsData = [
desc_en: "Who writes a diary, seriously?!",
},
{
name_zh: "Ollama",
desc_zh: "本地部署对话AI微调模型32b是4070遭不住",
name_en: "Ollama",
name_zh: "Ollama / llama.cpp",
desc_zh: "本地部署对话AI微调模型",
name_en: "Ollama / llama.cpp",
desc_en:
"Locally deployed conversational AI, fine-tuning models. 32b is too much for a 4070.",
},
@@ -207,6 +207,39 @@ const toolsData = [
name_en: "QtScrcpy",
desc_en: "LAN phone control tool, remote adb is awesome.",
},
{
name_zh: "Snipaste",
desc_zh: "很厉害的强大且好用的截屏工具",
name_en: "Snipaste",
desc_en: "Very powerful and useful screenshot tool.",
},
{
name_zh: "Redis Desktop Manager",
desc_zh: "超级好用的Redis数据库管理工具",
name_en: "Redis Desktop Manager",
desc_en: "Redis database management tool.",
},
{
name_zh: "Snipaste",
desc_zh: "很厉害的强大且好用的截屏工具",
name_en: "Snipaste",
desc_en: "Very powerful and useful screenshot tool.",
},{
name_zh: "Everything",
desc_zh: "windows搜索文件利器毫秒搜索快速定位",
name_en: "Everything",
desc_en: "windows search file利器millisecond search, quickly locate files",
},{
name_zh: "RaiDrive",
desc_zh: "好用爱用的网盘挂载",
name_en: "RaiDrive",
desc_en: "Good and loved netdisk mount",
},{
name_zh: "Ultimate Vocal Remover",
desc_zh: "音频转换提取,十分好用",
name_en: "Ultimate Vocal Remover",
desc_en: "Audio conversion and extraction, very useful.",
}
];
// ========== 渲染工具列表的函数 ==========

View File

@@ -1,75 +1,53 @@
// ========== 网站链接数据 (包含中英双语) ==========
const websitesData = [
{
// 分类标题
category_zh: '个人博客',
category_en: 'Personal Blogs',
items: [
{
url: 'https://www.ittoolman.top/',
linkText_zh: 'ittoolman.top - Github托管',
linkText_en: 'ittoolman.top - Github Page'
},
{
url: 'https://blog.iletter.top/',
linkText_zh: 'blog.iletter.top - typecho用户的最后坚守',
linkText_en: 'blog.iletter.top - Last Stand for Typecho Users'
}
]
},
{
category_zh: '私人在线应用',
category_en: 'Personal Online Apps',
items: [
{
url: 'https://img.iletter.top/',
linkText_zh: '简单图床 - 私人图床工具',
linkText_en: 'EsayImage2 - Image Uploader Tool'
},
{
url: 'https://gitea.iletter.top/',
linkText_zh: 'Gitea - 私人git托管仓库',
linkText_en: 'Gitea - Private Git Repository'
},
// {
// url: 'http://openlist.iletter.top/',
// linkText_zh: 'OpenList - 在线云盘合集',
// linkText_en: 'OpenList - Online Cloud Disk Collection'
// },
{
url: 'http://umami.iletter.top/',
linkText_zh: 'Umami - 网站访问分析',
linkText_en: 'Umami - Website visit analysis'
},
{
url: 'http://beszel.iletter.top/',
linkText_zh: 'Beszel - 服务器监控',
linkText_en: 'Beszel - Server Monitoring'
},
{
url: 'http://py.iletter.top/en-de-code',
linkText_zh: 'Base64在线加密解密 - 自写demo',
linkText_en: 'Base64 Online Encryption and Decryption - My Demo'
},
]
}
// 如果你想添加新分类,可以仿照上面的格式:
/*
{
category_zh: '新的分类名',
category_en: 'New Category Name',
items: [
{
url: 'https://example.com/',
linkText_zh: '示例网站',
linkText_en: 'Example Site'
}
]
}
*/
{ category_zh: '我的作品', category_en: 'My Works', items: [
{ url: 'https://mooknote.iletter.top/#/',
linkText_zh: 'MookNote - 观影阅读笔记管理',
linkText_en: 'MookNote - Movie & Reading Note Manager',
desc_zh: '用于记录观影、阅读及笔记的个人知识管理应用。',
desc_en: 'Personal knowledge management app for movies, reading, and notes.',
img: './static/img/my_work/mooknote/banner.jpg'
}
]},
{ category_zh: '个人博客', category_en: 'Personal Blogs', items: [
{ url: 'https://www.ittoolman.top/',
linkText_zh: 'ittoolman - Github托管',
linkText_en: 'ittoolman - Github Page',
desc_zh: '基于 GitHub Pages 搭建的个人技术站点。',
desc_en: 'Personal tech site based on GitHub Pages.',
img: './static/img/my_work/ittoolman/banner.png'
},
{ url: 'https://blog.iletter.top/',
linkText_zh: 'iletter - typecho极简与高效并存',
linkText_en: 'iletter - typecho Minimalism and Efficiency',
desc_zh: '记录日常开发与生活点滴的 Typecho 博客。',
desc_en: 'A Typecho blog recording daily development and life.',
img: './static/img/my_work/blog_iletter/banner.png'
}
]},
{ category_zh: '私人在线应用', category_en: 'Personal Online Apps', items: [
{ img: './static/img/my_work/easyimage2/banner.png',
url: 'https://img.iletter.top/', linkText_zh: 'EsayImage2 - 私人图床工具', linkText_en: 'EsayImage2 - Image Uploader Tool', desc_zh: '稳定高效的图片上传与托管服务。', desc_en: 'Stable and efficient image hosting service.' },
{ url: 'https://gitea.iletter.top/', linkText_zh: 'Gitea - 私人git托管仓库', linkText_en: 'Gitea - Private Git Repository', desc_zh: '轻量级自托管 Git 代码管理平台。', desc_en: 'Lightweight self-hosted Git code management platform.' },
{ url: 'http://umami.iletter.top/', linkText_zh: 'Umami - 网站访问分析', linkText_en: 'Umami - Website visit analysis', desc_zh: '简洁、隐私友好的网站流量统计工具。', desc_en: 'Simple, privacy-friendly website analytics tool.' },
{ url: 'http://beszel.iletter.top/', linkText_zh: 'Beszel - 服务器监控', linkText_en: 'Beszel - Server Monitoring', desc_zh: '实时监控服务器资源占用情况。', desc_en: 'Real-time monitoring of server resource usage.' },
{ url: 'https://memos.iletter.top/', linkText_zh: 'Memos - 个人随想', linkText_en: 'Memos - Personal Thoughts', desc_zh: '碎片化知识管理与灵感记录空间。', desc_en: 'Fragmented knowledge management and inspiration space.' },
{ url: 'http://frp.iletter.top/', linkText_zh: 'FRP - 个人反向代理',
linkText_en: 'FRP - Personal Reverse Proxy', desc_zh: '用于内网穿透的高性能反向代理。',
desc_en: 'High-performance reverse proxy for intranet penetration.',
img: './static/img/my_work/frp_panel/banner.png'
},
{ img: './static/img/my_work/bitwarden/banner.png',
url: 'https://bitwarden.iletter.top/', linkText_zh: 'Bitwarden - 我的密码库', linkText_en: 'Bitwarden - My Password Library', desc_zh: '安全可靠的开源密码管理方案。', desc_en: 'Secure and reliable open-source password management solution.' },
{ url: 'http://docmost.iletter.top/', linkText_zh: 'Docmost - 在线笔记', linkText_en: 'Docmost - Online Notes', desc_zh: '支持协作的实时在线文档编辑平台。', desc_en: 'Real-time online document editing platform supporting collaboration.' },
{ img: './static/img/my_work/openlist/banner.png',
url: 'http://openlist.iletter.top/', linkText_zh: 'OpenList - 在线云盘合集', linkText_en: 'OpenList - Online Cloud Disk Collection', desc_zh: '聚合多种网盘服务的文件列表管理。', desc_en: 'File list management aggregating various cloud storage services.' }
]}
];
// ========== 渲染网站链接的函数 ==========
let currentViewMode = localStorage.getItem('website_view_mode') || 'card'; // 'list' or 'card'
function renderWebsites(websitesArray) {
const container = document.getElementById('dynamic-website-links');
if (!container) {
@@ -88,39 +66,233 @@ function renderWebsites(websitesArray) {
const currentLang = localStorage.getItem("preferred_language") || "zh";
// 清空容器
container.innerHTML = '';
// 清空容器并添加切换按钮
const toggleText = currentViewMode === 'list' ? '切换为卡片视图' : '切换为列表视图';
container.innerHTML = `
<div style="text-align:right;margin-bottom:10px;">
<span class="website-view-toggle" onclick="switchWebsiteView('${currentViewMode === 'list' ? 'card' : 'list'}')">${toggleText}</span>
</div>
<div id="website-content-area"></div>
`;
arrayToRender.forEach(categoryObj => {
// 根据当前语言选择分类标题
const categoryName = currentLang === 'en' ? categoryObj.category_en : categoryObj.category_zh;
const contentArea = container.querySelector('#website-content-area');
// 创建分类标题
const heading = document.createElement('h3');
heading.textContent = categoryName;
container.appendChild(heading);
if (currentViewMode === 'list') {
arrayToRender.forEach(categoryObj => {
const categoryName = currentLang === 'en' ? categoryObj.category_en : categoryObj.category_zh;
const heading = document.createElement('h3');
heading.textContent = categoryName;
contentArea.appendChild(heading);
// 创建一个 div 来包裹该分类下的所有链接
const linksDiv = document.createElement('div');
linksDiv.className = 'category-links'; // 可选,方便 CSS 样式化
const linksDiv = document.createElement('div');
linksDiv.className = 'category-links';
categoryObj.items.forEach(item => {
const linkElement = document.createElement('a');
linkElement.href = item.url;
linkElement.target = '_blank';
categoryObj.items.forEach(item => {
const linkElement = document.createElement('a');
linkElement.href = '#';
linkElement.textContent = currentLang === 'en' ? item.linkText_en : item.linkText_zh;
// 根据当前语言选择链接文本
const linkText = currentLang === 'en' ? item.linkText_en : item.linkText_zh;
linkElement.textContent = linkText;
if (item.url === 'https://mooknote.iletter.top/#/') {
linkElement.onclick = (e) => {
e.preventDefault();
openMookNoteModal();
};
} else {
linkElement.onclick = (e) => {
e.preventDefault();
showCustomConfirm(`确定要打开网站吗?\n${item.url}`, () => {
window.open(item.url, '_blank');
});
};
}
linksDiv.appendChild(linkElement);
// 添加一个间隔(例如空格或换行符,取决于你的 CSS 布局)
const space = document.createTextNode(' ');
linksDiv.appendChild(space);
linksDiv.appendChild(linkElement);
const space = document.createTextNode(' ');
linksDiv.appendChild(space);
});
contentArea.appendChild(linksDiv);
});
} else {
arrayToRender.forEach(categoryObj => {
const categoryName = currentLang === 'en' ? categoryObj.category_en : categoryObj.category_zh;
const heading = document.createElement('h3');
heading.textContent = categoryName;
contentArea.appendChild(heading);
container.appendChild(linksDiv);
const gridDiv = document.createElement('div');
gridDiv.className = 'website-card-grid';
categoryObj.items.forEach(item => {
const card = document.createElement('div');
card.className = 'website-card';
if (item.url === 'https://mooknote.iletter.top/#/') {
card.onclick = () => openMookNoteModal();
} else {
card.onclick = () => {
showCustomConfirm(`确定要打开网站吗?\n${item.url}`, () => {
window.open(item.url, '_blank');
});
};
}
const title = currentLang === 'en' ? item.linkText_en : item.linkText_zh;
const desc = currentLang === 'en' ? (item.desc_en || '') : (item.desc_zh || '');
const imgSrc = item.img;
const placeholderText = item.linkText_zh.split(' - ')[0];
const imgHtml = imgSrc
? `<div class="website-card-icon"><img src="${imgSrc}" alt="${placeholderText}" onerror="this.style.display='none';this.parentNode.classList.add('website-card-placeholder');this.parentNode.textContent='${placeholderText}'"></div>`
: `<div class="website-card-icon website-card-placeholder">${placeholderText}</div>`;
card.innerHTML = `
${imgHtml}
<div class="website-card-info">
<div class="website-card-title">${title}</div>
<div class="website-card-desc">${desc}</div>
</div>
`;
gridDiv.appendChild(card);
});
contentArea.appendChild(gridDiv);
});
}
}
window.switchWebsiteView = function(mode) {
currentViewMode = mode;
localStorage.setItem('website_view_mode', mode);
renderWebsites();
};
// MookNote 弹窗逻辑
let currentImageIndex = 0;
const mooknoteImages = document.querySelectorAll('.carousel-image');
const totalImages = mooknoteImages.length;
// 全屏预览相关变量
let lightboxImageIndex = 0;
const lightboxImgUrls = [
'./static/img/my_work/mooknote/banner.jpg',
'./static/img/my_work/mooknote/1.jpg',
'./static/img/my_work/mooknote/2.jpg',
'./static/img/my_work/mooknote/3.jpg',
'./static/img/my_work/mooknote/4.jpg',
'./static/img/my_work/mooknote/5.jpg',
'./static/img/my_work/mooknote/6.jpg'
];
// 初始化指示器
function initCarouselIndicators() {
const indicatorsContainer = document.querySelector('.carousel-indicators');
if (!indicatorsContainer) return;
indicatorsContainer.innerHTML = '';
for (let i = 0; i < totalImages; i++) {
const indicator = document.createElement('div');
indicator.className = 'carousel-indicator' + (i === 0 ? ' active' : '');
indicator.onclick = () => goToMooknoteImage(i);
indicatorsContainer.appendChild(indicator);
}
}
// 切换到指定图片
function goToMooknoteImage(index) {
if (index < 0) index = totalImages - 1;
if (index >= totalImages) index = 0;
mooknoteImages.forEach((img, i) => {
img.classList.toggle('active', i === index);
});
const indicators = document.querySelectorAll('.carousel-indicator');
indicators.forEach((indicator, i) => {
indicator.classList.toggle('active', i === index);
});
currentImageIndex = index;
}
// 上一张图片
window.prevMooknoteImage = function() {
goToMooknoteImage(currentImageIndex - 1);
};
// 下一张图片
window.nextMooknoteImage = function() {
goToMooknoteImage(currentImageIndex + 1);
};
// 全屏预览功能
window.openMooknoteLightbox = function(index) {
lightboxImageIndex = index;
const lightbox = document.querySelector('.mooknote-lightbox');
const img = lightbox.querySelector('.lightbox-img');
const counter = lightbox.querySelector('.lightbox-counter');
img.src = lightboxImgUrls[index];
counter.textContent = `${index + 1} / ${lightboxImgUrls.length}`;
lightbox.classList.add('active');
};
window.closeMooknoteLightbox = function() {
const lightbox = document.querySelector('.mooknote-lightbox');
lightbox.classList.remove('active');
};
window.prevLightboxImage = function() {
lightboxImageIndex = (lightboxImageIndex - 1 + lightboxImgUrls.length) % lightboxImgUrls.length;
updateLightboxImage();
};
window.nextLightboxImage = function() {
lightboxImageIndex = (lightboxImageIndex + 1) % lightboxImgUrls.length;
updateLightboxImage();
};
function updateLightboxImage() {
const lightbox = document.querySelector('.mooknote-lightbox');
const img = lightbox.querySelector('.lightbox-img');
const counter = lightbox.querySelector('.lightbox-counter');
img.src = lightboxImgUrls[lightboxImageIndex];
counter.textContent = `${lightboxImageIndex + 1} / ${lightboxImgUrls.length}`;
}
// 键盘事件支持
document.addEventListener('keydown', (e) => {
const lightbox = document.querySelector('.mooknote-lightbox');
if (!lightbox || !lightbox.classList.contains('active')) return;
if (e.key === 'Escape') {
closeMooknoteLightbox();
} else if (e.key === 'ArrowLeft') {
prevLightboxImage();
} else if (e.key === 'ArrowRight') {
nextLightboxImage();
}
});
function openMookNoteModal() {
const modal = document.getElementById('mooknote-modal');
if (modal) {
modal.classList.add('active');
document.body.style.overflow = 'hidden';
// 重置轮播到第一张
currentImageIndex = 0;
initCarouselIndicators();
goToMooknoteImage(0);
}
}
const mooknoteClose = document.getElementById('mooknote-close');
if (mooknoteClose) {
mooknoteClose.addEventListener('click', () => {
const modal = document.getElementById('mooknote-modal');
if (modal) {
modal.classList.remove('active');
document.body.style.overflow = '';
}
});
}