Compare commits

..

18 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
65 changed files with 4177 additions and 258 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>
@@ -37,11 +39,49 @@
@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">
@@ -96,7 +136,7 @@
<!-- 完整博客文章弹窗(点击后显示) -->
<div id="blog-posts-full-modal" class="guestbook-full-modal">
<div class="full-modal-content">
<div class="full-modal-content solid-modal">
<div class="guestbook-header">
<h2>博客文章</h2>
<button id="blog-posts-close" class="guestbook-close" title="关闭">&times;</button>
@@ -107,9 +147,57 @@
</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">
<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>
@@ -161,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>
@@ -199,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">
@@ -254,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>
@@ -320,6 +470,51 @@
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>
@@ -328,6 +523,8 @@
<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

@@ -14,6 +14,31 @@
z-index: 999;
}
/* 旅途剪影按钮容器 */
.photo-album-btn-container {
position: fixed;
top: 210px;
right: 80px;
z-index: 999;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
/* 透明桥梁:向左延伸覆盖预览面板区域,确保鼠标滑入时 hover 不中断 */
.guestbook-btn-container::after,
.blog-posts-btn-container::after,
.photo-album-btn-container::after {
content: '';
position: absolute;
right: 80px; /* 从容器的左边缘开始 */
top: 0;
width: 50px; /* 15px 间距 + 380px 预览面板宽度 */
height: 80px; /* 与按钮高度一致 */
background: transparent;
pointer-events: auto;
}
.guestbook-btn {
background: #fff;
@@ -47,6 +72,24 @@
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.photo-album-btn {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 50%;
width: 80px;
height: 80px;
cursor: pointer;
display: flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
visibility: visible !important;
opacity: 1 !important;
}
.btn-label {
font-size: 13px;
color: #666;
@@ -76,9 +119,106 @@
stroke: #333;
}
/* 自定义确认弹窗样式 */
.custom-confirm-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 99999;
justify-content: center;
align-items: center;
animation: fadeIn 0.2s ease;
}
.custom-confirm-modal.active {
display: flex;
}
.custom-confirm-content {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.95) 0%,
rgba(255, 255, 255, 0.9) 100%
);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 16px;
padding: 24px;
max-width: 400px;
width: 90%;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.4) inset;
animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.custom-confirm-message {
font-size: 15px;
color: #333;
line-height: 1.6;
margin-bottom: 20px;
word-break: break-word;
}
.custom-confirm-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.confirm-btn-cancel,
.confirm-btn-ok {
padding: 8px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
}
.confirm-btn-cancel {
background: rgba(0, 0, 0, 0.05);
color: #666;
}
.confirm-btn-cancel:hover {
background: rgba(0, 0, 0, 0.1);
}
.confirm-btn-ok {
background: #1a73e8;
color: #fff;
}
.confirm-btn-ok:hover {
background: #1557b0;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(26, 115, 232, 0.3);
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Hover 浮动预览面板 */
.guestbook-preview,
.blog-posts-preview {
.blog-posts-preview,
.photo-album-preview {
visibility: hidden;
position: absolute;
top: 0;
@@ -103,7 +243,8 @@
/* 当鼠标悬停在容器上时显示预览面板 */
.guestbook-btn-container:hover .guestbook-preview,
.blog-posts-btn-container:hover .blog-posts-preview {
.blog-posts-btn-container:hover .blog-posts-preview,
.photo-album-btn-container:hover .photo-album-preview {
visibility: visible;
opacity: 1;
transform: translateX(0) scale(1);
@@ -130,6 +271,21 @@
padding: 0;
overflow-y: auto;
max-height: calc(400px - 50px);
scrollbar-width: thin; /* Firefox */
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; /* Firefox */
}
.preview-body::-webkit-scrollbar {
width: 6px;
}
.preview-body::-webkit-scrollbar-track {
background: transparent;
}
.preview-body::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.preview-item {
@@ -186,7 +342,7 @@
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
background-color: rgba(0, 0, 0, 0.15);
z-index: 10000;
justify-content: center;
align-items: center;
@@ -206,15 +362,200 @@
}
}
/* 旅途剪影弹窗样式 - iOS 26 风格极致玻璃拟态 */
.full-modal-content {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0.06) 50%,
rgba(255, 255, 255, 0.1) 100%
);
backdrop-filter: blur(80px) saturate(250%);
-webkit-backdrop-filter: blur(80px) saturate(250%);
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 28px;
width: 95%;
max-width: 1400px;
max-height: 95vh;
overflow: hidden;
box-shadow:
0 32px 100px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.3) inset,
inset 0 1px 1px rgba(255, 255, 255, 0.6),
inset 0 -1px 1px rgba(255, 255, 255, 0.15);
animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
isolation: isolate;
}
/* 顶部高光反射 - 增强版 */
.full-modal-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.5) 0%,
rgba(255, 255, 255, 0.2) 30%,
rgba(255, 255, 255, 0.05) 70%,
transparent 100%);
border-radius: 28px 28px 0 0;
pointer-events: none;
z-index: 1;
}
/* 底部环境光反射 */
.full-modal-content::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
right: 10%;
height: 80px;
background: radial-gradient(ellipse at center bottom,
rgba(255, 255, 255, 0.2) 0%,
transparent 60%);
pointer-events: none;
z-index: 1;
}
/* 侧边边缘反光 */
.full-modal-content .guestbook-header::after {
content: '';
position: absolute;
top: 20%;
left: 0;
bottom: 20%;
width: 2px;
background: linear-gradient(180deg,
transparent 0%,
rgba(255, 255, 255, 0.3) 30%,
rgba(255, 255, 255, 0.5) 50%,
rgba(255, 255, 255, 0.3) 70%,
transparent 100%);
pointer-events: none;
}
/* 旅途剪影弹窗样式 - iOS 26 风格极致玻璃拟态 */
.full-modal-content {
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0.06) 50%,
rgba(255, 255, 255, 0.1) 100%
);
backdrop-filter: blur(80px) saturate(250%);
-webkit-backdrop-filter: blur(80px) saturate(250%);
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 28px;
width: 95%;
height: 95vh;
max-width: 1400px;
overflow: hidden;
box-shadow:
0 32px 100px rgba(0, 0, 0, 0.25),
0 0 0 1px rgba(255, 255, 255, 0.3) inset,
inset 0 1px 1px rgba(255, 255, 255, 0.6),
inset 0 -1px 1px rgba(255, 255, 255, 0.15);
animation: slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
isolation: isolate;
}
/* 针对旅途剪影弹窗的绝对全屏处理 */
#photo-album-full-modal .full-modal-content {
width: 100vw !important;
height: 100vh !important;
max-width: none !important;
max-height: none !important;
border-radius: 0 !important;
border: none !important;
}
#photo-album-full-modal .full-modal-content::before {
border-radius: 0 !important;
}
#photo-album-full-modal .full-modal-content::after {
border-radius: 0 !important;
}
/* 顶部高光反射 - 增强版 */
.full-modal-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg,
rgba(255, 255, 255, 0.5) 0%,
rgba(255, 255, 255, 0.2) 30%,
rgba(255, 255, 255, 0.05) 70%,
transparent 100%);
border-radius: 28px 28px 0 0;
pointer-events: none;
z-index: 1;
}
/* 底部环境光反射 */
.full-modal-content::after {
content: '';
position: absolute;
bottom: 0;
left: 10%;
right: 10%;
height: 80px;
background: radial-gradient(ellipse at center bottom,
rgba(255, 255, 255, 0.2) 0%,
transparent 60%);
pointer-events: none;
z-index: 1;
}
/* 侧边边缘反光 */
.full-modal-content .guestbook-header::after {
content: '';
position: absolute;
top: 20%;
left: 0;
bottom: 20%;
width: 2px;
background: linear-gradient(180deg,
transparent 0%,
rgba(255, 255, 255, 0.3) 30%,
rgba(255, 255, 255, 0.5) 50%,
rgba(255, 255, 255, 0.3) 70%,
transparent 100%);
pointer-events: none;
}
/* 实心背景弹窗样式(留言板、博客文章专用) */
.full-modal-content.solid-modal {
background: #fff;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
border-radius: 12px;
width: 92%;
max-width: 850px;
max-height: 92vh;
overflow: hidden;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.18);
animation: slideUp 0.3s ease;
}
/* 隐藏弹窗内部滚动条 */
.guestbook-body {
padding: 0;
overflow-y: auto;
max-height: calc(95vh - 70px);
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.guestbook-body::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
@keyframes slideUp {
@@ -316,41 +657,68 @@
}
.guestbook-header {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
padding: 22px 30px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
background: transparent;
position: relative;
z-index: 2;
}
/* 旅途剪影弹窗标题样式优化 */
#photo-album-full-modal .guestbook-header h2 {
color: #1a1a1a;
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.5);
}
/* 旅途剪影弹窗关闭按钮样式优化 */
#photo-album-full-modal .guestbook-close {
background: rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.2);
color: #333;
}
.guestbook-header h2 {
margin: 0;
font-size: 16px;
color: #333;
font-weight: 500;
letter-spacing: 0.5px;
font-size: 24px;
color: #1a1a1a;
font-weight: 700;
letter-spacing: 1.5px;
text-shadow: 0 1px 3px rgba(255, 255, 255, 0.9), 0 0 20px rgba(255, 255, 255, 0.5);
}
.guestbook-close {
background: none;
border: none;
font-size: 24px;
color: #999;
background: rgba(255, 255, 255, 0.25);
border: 1px solid rgba(255, 255, 255, 0.5);
font-size: 28px;
color: #444;
cursor: pointer;
line-height: 1;
padding: 0;
width: 24px;
height: 24px;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.3s ease;
transition: all 0.3s ease;
border-radius: 50%;
backdrop-filter: blur(20px) saturate(150%);
-webkit-backdrop-filter: blur(20px) saturate(150%);
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.1),
inset 0 1px 1px rgba(255, 255, 255, 0.5);
z-index: 3;
}
.guestbook-close:hover {
color: #333;
background: rgba(255, 255, 255, 0.4);
color: #222;
transform: rotate(90deg) scale(1.05);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.15),
inset 0 1px 1px rgba(255, 255, 255, 0.6);
}
.guestbook-body {
@@ -376,6 +744,7 @@
flex-direction: column;
gap: 8px;
padding: 15px 20px;
margin-bottom: 60px;
}
.guestbook-item {
@@ -691,10 +1060,95 @@
font-size: 14px;
}
/* 加载更多按钮 */
/* 分页导航样式 - 极致玻璃拟态 */
.pagination-container {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
padding: 30px 0;
background: transparent;
position: relative;
z-index: 2;
}
.page-btn {
width: 44px;
height: 44px;
border: 1px solid rgba(255, 255, 255, 0.4);
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(20px) saturate(150%);
-webkit-backdrop-filter: blur(20px) saturate(150%);
border-radius: 14px;
cursor: pointer;
font-size: 16px;
color: #333;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.06),
inset 0 1px 1px rgba(255, 255, 255, 0.4);
}
.page-btn:hover {
background: rgba(255, 255, 255, 0.35);
transform: translateY(-3px) scale(1.05);
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.1),
inset 0 1px 1px rgba(255, 255, 255, 0.5);
border-color: rgba(255, 255, 255, 0.6);
}
.page-num {
min-width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14px;
cursor: pointer;
font-size: 14px;
color: #333;
transition: all 0.3s ease;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.35);
padding: 0 12px;
backdrop-filter: blur(10px) saturate(150%);
-webkit-backdrop-filter: blur(10px) saturate(150%);
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.3);
}
.page-num:hover {
background: rgba(255, 255, 255, 0.35);
border-color: rgba(255, 255, 255, 0.55);
transform: translateY(-2px);
}
.page-num.active {
background: rgba(255, 255, 255, 0.45);
color: #1a1a1a;
cursor: default;
backdrop-filter: blur(15px) saturate(180%);
-webkit-backdrop-filter: blur(15px) saturate(180%);
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.1),
inset 0 1px 1px rgba(255, 255, 255, 0.5);
border-color: rgba(255, 255, 255, 0.7);
font-weight: 700;
transform: translateY(-1px);
}
.page-dots {
color: #777;
padding: 0 8px;
}
/* 加载更多按钮样式(用于留言板等实心弹窗) */
.load-more-btn {
display: block;
margin: 20px auto 20px auto;
margin: 20px auto 50px auto;
background: #fff;
border: 1px solid #e8e8e8;
color: #666;
@@ -712,16 +1166,10 @@
transform: translateY(-1px);
}
.load-more-btn:disabled {
color: #ccc;
border-color: #eee;
cursor: not-allowed;
}
/* 加载完成提示 */
.load-complete {
text-align: center;
padding: 20px 25px;
padding: 20px 25px 50px 25px;
color: #bbb;
font-size: 13px;
font-style: italic;
@@ -753,6 +1201,17 @@
overflow: hidden;
}
.blog-post-digest {
font-size: 12px;
color: #888;
margin-bottom: 8px;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.blog-post-meta {
display: flex;
justify-content: space-between;
@@ -768,6 +1227,218 @@
font-size: 10px;
}
/* 旅途剪影预览面板样式 - 列表模式 */
.photo-album-container {
padding: 10px;
}
.photo-preview-item {
margin-bottom: 10px;
cursor: pointer;
transition: transform 0.3s ease;
}
.photo-preview-item:hover {
transform: scale(1.05);
}
.photo-preview-item img {
width: 100%;
height: 120px;
object-fit: cover;
display: block;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 旅途剪影瀑布流布局 - 紧密贴住 */
.photo-album-grid {
column-count: 3;
column-gap: 0;
padding: 0;
}
@media (max-width: 1200px) {
.photo-album-grid {
column-count: 2;
}
}
@media (max-width: 768px) {
.photo-album-grid {
column-count: 2;
}
}
.photo-album-item {
break-inside: avoid;
margin-bottom: 0;
position: relative;
overflow: hidden;
border-radius: 8px;
cursor: pointer;
transition: transform 0.3s ease;
box-shadow: none;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.25);
min-height: 200px;
}
/* 照片加载动画 */
.photo-loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-radius: 50%;
animation: photo-spin 0.8s linear infinite;
z-index: 1;
}
@keyframes photo-spin {
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.photo-album-item:hover {
transform: scale(1.02);
z-index: 1;
}
.photo-album-item img {
width: 100%;
height: auto;
display: block;
opacity: 0; /* 初始隐藏,加载完再显示 */
transition: opacity 0.5s ease, transform 0.5s ease;
}
.photo-album-item:hover img {
transform: scale(1.03);
}
/* 照片放大弹窗 */
.photo-lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 10000;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease;
}
.photo-lightbox.active {
display: flex;
}
.photo-lightbox img {
max-width: 85%;
max-height: 85%;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: zoomIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transition: opacity 0.3s ease;
}
.photo-lightbox-close {
position: absolute;
top: 30px;
right: 40px;
font-size: 40px;
color: #fff;
cursor: pointer;
transition: transform 0.3s ease;
z-index: 10001;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
.photo-lightbox-close:hover {
transform: rotate(90deg);
background: rgba(255, 255, 255, 0.2);
}
/* 左右切换按钮 */
.photo-lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 50px;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
z-index: 10001;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
user-select: none;
}
.photo-lightbox-nav:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-50%) scale(1.1);
}
.photo-lightbox-prev {
left: 40px;
}
.photo-lightbox-next {
right: 40px;
}
/* 图片计数器 */
.photo-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;
z-index: 10001;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.guestbook-btn-container {
@@ -777,12 +1448,18 @@
.blog-posts-btn-container {
right: 15px;
top: 90px;
top: 110px;
}
.photo-album-btn-container {
right: 15px;
top: 205px;
}
/* 移动端预览面板调整位置,避免超出屏幕 */
.guestbook-preview,
.blog-posts-preview {
.blog-posts-preview,
.photo-album-preview {
right: auto;
left: calc(100% + 10px);
transform-origin: center left;
@@ -790,7 +1467,8 @@
}
.guestbook-btn-container:hover .guestbook-preview,
.blog-posts-btn-container:hover .blog-posts-preview {
.blog-posts-btn-container:hover .blog-posts-preview,
.photo-album-btn-container:hover .photo-album-preview {
transform: translateX(0) scale(1);
}

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;
@@ -183,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 {
@@ -205,8 +696,7 @@ body {
.tool-item:hover,
.game-item:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
border-color: #000;
}
.tool-name,

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";

View File

@@ -19,6 +19,7 @@
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() {
@@ -29,23 +30,26 @@
// 关闭按钮
blogPostsClose.addEventListener('click', closeFullModal);
// 点击背景关闭
blogPostsFullModal.addEventListener('click', function(e) {
if (e.target === blogPostsFullModal) {
closeFullModal();
}
});
// ESC 键关闭
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && blogPostsFullModal.classList.contains('active')) {
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();
}
}
// 打开完整弹窗
@@ -79,7 +83,7 @@
}
try {
const response = await fetch(`${BLOG_API_URL}?page=${currentPage}&pageSize=${PAGE_SIZE}&showContent=false&showDigest=excerpt&limit=30`, {
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',
@@ -94,12 +98,13 @@
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);
renderPosts(posts, true, totalCount);
} else {
appendPosts(posts);
}
@@ -117,34 +122,54 @@
}
// 渲染文章列表
function renderPosts(posts, isReset = false) {
let html = '';
function renderPosts(posts, isReset = false, totalCount = 0) {
// 预览面板 HTML无底部分页按钮使用无限滚动
let previewHtml = '';
// 全屏弹窗 HTML保留底部分页按钮
let fullHtml = '';
if (!posts || posts.length === 0) {
html = '<div class="guestbook-empty">暂无文章</div>';
const emptyHtml = '<div class="guestbook-empty">暂无文章</div>';
previewHtml = emptyHtml;
fullHtml = emptyHtml;
} else {
html = '<div class="blog-posts-container">';
posts.forEach(post => {
html += renderPostItem(post);
});
html += '</div>';
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) {
html += `<button class="load-more-btn" onclick="window.loadMoreBlogPosts()">点击加载更多</button>`;
fullHtml += `<button class="load-more-btn" onclick="window.loadMoreBlogPosts()">点击加载更多</button>`;
} else {
html += `<div class="load-complete">已显示全部</div>`;
fullHtml += `<div class="load-complete">已显示全部</div>`;
}
}
if (blogPostsList) blogPostsList.innerHTML = html;
if (blogPostsFullBody) blogPostsFullBody.innerHTML = html;
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');
const loadMoreBtns = document.querySelectorAll('.load-more-btn');
const oldTips = document.querySelectorAll('.load-complete');
if (containers.length === 0) {
console.error('未找到 .blog-posts-container 容器');
@@ -158,29 +183,40 @@
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);
}
}
});
// 移除所有旧的加载更多按钮和提示
loadMoreBtns.forEach(btn => btn.remove());
oldTips.forEach(tip => tip.remove());
// 向所有容器后添加新的加载更多按钮或提示
if (hasMore) {
containers.forEach(container => {
const btn = document.createElement('button');
btn.className = 'load-more-btn';
btn.textContent = '点击加载更多';
btn.onclick = window.loadMoreBlogPosts;
container.after(btn);
});
} else {
containers.forEach(container => {
const tip = document.createElement('div');
tip.className = 'load-complete';
tip.textContent = '已显示全部';
container.after(tip);
});
}
console.log(`成功追加 ${posts.length} 篇文章到 ${containers.length} 个容器,当前页码: ${currentPage}`);
}
@@ -190,9 +226,13 @@
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="window.open('${post.permalink}', '_blank')">`;
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>`;

1039
static/js/galaxy.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
'use strict';
// Flask API 地址
const API_BASE_URL = 'http://localhost:8360';
const API_BASE_URL = 'https://comments.iletter.top';
const PAGE_SIZE = 10;
let currentPage = 1;
@@ -63,20 +63,6 @@
// 关闭按钮
guestbookClose.addEventListener('click', closeFullModal);
// 点击背景关闭
guestbookFullModal.addEventListener('click', function(e) {
if (e.target === guestbookFullModal) {
closeFullModal();
}
});
// ESC 键关闭
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && guestbookFullModal.classList.contains('active')) {
closeFullModal();
}
});
// 预览面板触底加载
if (previewBody) {
previewBody.addEventListener('scroll', handlePreviewScroll);
@@ -138,7 +124,7 @@
guestbookBody.innerHTML = '<div class="guestbook-loading" data-i18n="guestbook_loading">正在加载留言...</div>';
} else {
// 添加加载更多按钮状态
const loadMoreBtn = document.querySelector('.load-more-btn');
const loadMoreBtn = guestbookBody.querySelector('.load-more-btn');
if (loadMoreBtn) {
loadMoreBtn.disabled = true;
loadMoreBtn.textContent = '加载中...';
@@ -210,19 +196,35 @@
// 判断是否还有更多数据
if (hasMore) {
html += `<button class="load-more-btn" onclick="window.loadMoreGuestbook()">点击加载更多</button>`;
} else {
html += `<div class="load-complete">已显示全部</div>`;
}
// 否则不显示任何内容
guestbookBody.innerHTML = html;
// 同时更新 Hover 预览面板(使用累积的所有留言)
renderPreview(allComments);
renderPreview(allComments, totalCount);
}
// 渲染 Hover 预览面板
function renderPreview(comments) {
if (!previewBody || !comments || comments.length === 0) return;
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 => {
@@ -268,9 +270,8 @@
// 追加留言
function appendGuestbook(comments) {
const listContainer = document.querySelector('.guestbook-list');
const loadMoreBtn = document.querySelector('.load-more-btn');
const allDataTip = document.querySelector('.guestbook-body > div:last-child');
const listContainer = guestbookBody.querySelector('.guestbook-list');
const loadMoreBtn = guestbookBody.querySelector('.load-more-btn');
if (!listContainer) return;
@@ -280,16 +281,12 @@
listContainer.appendChild(div.firstElementChild);
});
// 移除旧的加载更多按钮和提示
// 移除旧的加载更多按钮
if (loadMoreBtn) {
loadMoreBtn.remove();
}
const oldTip = document.querySelector('.load-complete');
if (oldTip) {
oldTip.remove();
}
// 如果还有更多,添加新的加载更多按钮
// 如果还有更多,添加新的加载更多按钮;否则显示"已显示全部"
if (hasMore) {
const btn = document.createElement('button');
btn.className = 'load-more-btn';
@@ -297,7 +294,6 @@
btn.onclick = window.loadMoreGuestbook;
listContainer.after(btn);
} else {
// 没有更多数据,显示提示
const tip = document.createElement('div');
tip.className = 'load-complete';
tip.textContent = '已显示全部';
@@ -305,7 +301,7 @@
}
// 更新预览面板
renderPreview(allComments);
renderPreview(allComments, allComments.length);
}
// 渲染单个留言项

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, or through F12 view website source code download.',
'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,6 +34,8 @@ const translationsEN = {
social_link3: "Zhihu",
social_link4: "Bilibili",
social_link5: "52pojie",
social_link6: "XiaoHongShu",
// 留言
leave_message_h3: "Leave a Message",
leave_message_p:
@@ -73,5 +76,8 @@ const translationsEN = {
// 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> 并使用或者通过F12查看网站源代码下载。',
'本站秉承书信形式的简约风格采用纯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,6 +33,8 @@ const translationsZH = {
social_link3: "知乎",
social_link4: "B站",
social_link5: "52pojie",
social_link6: "小红书",
// 留言
leave_message_h3: "留言",
leave_message_p:
@@ -73,5 +74,8 @@ const translationsZH = {
// 右上角按钮
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,96 +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: 'EsayImage2 - 私人图床工具',
linkText_en: 'EsayImage2 - Image Uploader Tool'
},
{
url: 'https://gitea.iletter.top/',
linkText_zh: 'Gitea - 私人git托管仓库',
linkText_en: 'Gitea - Private Git Repository'
},
{
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: 'https://memos.iletter.top/',
linkText_zh: 'Memos - 个人随想',
linkText_en: 'Memos - Personal Thoughts'
},
{
url: 'http://frp.iletter.top/',
linkText_zh: 'FRP - 个人反向代理',
linkText_en: 'FRP - Personal Reverse Proxy'
},
{
url: 'https://bitwarden.iletter.top/',
linkText_zh: 'Bitwarden - 我的密码库',
linkText_en: 'Bitwarden - My Password Library'
},
{
url: 'http://docmost.iletter.top/',
linkText_zh: 'Docmost - 在线笔记',
linkText_en: 'Docmost - Online Notes'
},
{
url: 'http://openlist.iletter.top/',
linkText_zh: 'OpenList - 在线云盘合集',
linkText_en: 'OpenList - Online Cloud Disk Collection'
},
// {
// 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) {
@@ -109,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>
`;
const contentArea = container.querySelector('#website-content-area');
arrayToRender.forEach(categoryObj => {
// 根据当前语言选择分类标题
const categoryName = currentLang === 'en' ? categoryObj.category_en : categoryObj.category_zh;
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);
// 创建分类标题
const heading = document.createElement('h3');
heading.textContent = categoryName;
container.appendChild(heading);
const linksDiv = document.createElement('div');
linksDiv.className = 'category-links';
// 创建一个 div 来包裹该分类下的所有链接
const linksDiv = document.createElement('div');
linksDiv.className = 'category-links'; // 可选,方便 CSS 样式化
categoryObj.items.forEach(item => {
const linkElement = document.createElement('a');
linkElement.href = item.url;
linkElement.target = '_blank';
// 根据当前语言选择链接文本
const linkText = currentLang === 'en' ? item.linkText_en : item.linkText_zh;
linkElement.textContent = linkText;
linksDiv.appendChild(linkElement);
// 添加一个间隔(例如空格或换行符,取决于你的 CSS 布局)
const space = document.createTextNode(' ');
linksDiv.appendChild(space);
categoryObj.items.forEach(item => {
const linkElement = document.createElement('a');
linkElement.href = '#';
linkElement.textContent = currentLang === 'en' ? item.linkText_en : item.linkText_zh;
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);
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);
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);
});
}
}
container.appendChild(linksDiv);
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 = '';
}
});
}