Compare commits
12 Commits
6edd5569fa
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e271cf342b | ||
|
|
4f52069560 | ||
|
|
afc019b1fd | ||
|
|
5c22d3f59d | ||
|
|
47396b88f0 | ||
|
|
40278c5d84 | ||
|
|
3f5ddde664 | ||
|
|
be004854e2 | ||
|
|
f0605dca4c | ||
|
|
5e33d53ea3 | ||
|
|
fa4c784b96 | ||
|
|
66ece06271 |
269
index.html
@@ -36,17 +36,162 @@
|
||||
@import url("./static/css/main.css");
|
||||
@import url("./static/css/memos.css");
|
||||
@import url("./static/css/game_dialog.css");
|
||||
@import url("./static/css/guestbook.css");
|
||||
</style>
|
||||
<title data-i18n="page_title">白荼 - BAITU</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 搜索功能 -->
|
||||
<div id="search-overlay" class="search-overlay">
|
||||
<div class="search-modal">
|
||||
<button class="search-lock-btn" id="search-lock-btn" title="搜索常驻">
|
||||
<svg id="lock-icon-open" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
<svg id="lock-icon-closed" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 9.9-1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="search-box">
|
||||
<div class="search-engine-selector" id="search-engine-selector">
|
||||
<img src="./static/img/search_icon/google.png" alt="Google" id="search-engine-icon" />
|
||||
<div class="search-engine-dropdown" id="search-engine-dropdown">
|
||||
<div class="engine-option active" data-engine="google" data-icon="./static/img/search_icon/google.png">
|
||||
<img src="./static/img/search_icon/google.png" alt="Google" /> Google
|
||||
</div>
|
||||
<div class="engine-option" data-engine="baidu" data-icon="./static/img/search_icon/baidu.png">
|
||||
<img src="./static/img/search_icon/baidu.png" alt="Baidu" /> Baidu
|
||||
</div>
|
||||
<div class="engine-option" data-engine="duckduckgo" data-icon="./static/img/search_icon/DuckDuckGo.png">
|
||||
<img src="./static/img/search_icon/DuckDuckGo.png" alt="DuckDuckGo" /> DuckDuckGo
|
||||
</div>
|
||||
<div class="engine-option" data-engine="yandex" data-icon="./static/img/search_icon/Yandex.png">
|
||||
<img src="./static/img/search_icon/Yandex.png" alt="Yandex" /> Yandex
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="global-search-input" placeholder="Search..." autocomplete="off" />
|
||||
<button id="global-search-btn">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="letter-container">
|
||||
<!-- 语言切换按钮 -->
|
||||
<div class="lang-switch-container">
|
||||
<img id="lang-switch-btn" src="./static/img/中文.png" alt="Switch Language" title="切换语言" />
|
||||
</div>
|
||||
|
||||
<!-- 留言板按钮 -->
|
||||
<div class="guestbook-btn-container">
|
||||
<button id="guestbook-btn" class="guestbook-btn" title="留言板">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V7C3 6.46957 3.21071 5.96086 3.58579 5.58579C3.96086 5.21071 4.46957 5 5 5H19C19.5304 5 20.0391 5.21071 20.4142 5.58579C20.7893 5.96086 21 6.46957 21 7V15Z"
|
||||
stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span class="btn-label" data-i18n="btn_guestbook">留言板</span>
|
||||
</button>
|
||||
|
||||
<!-- Hover 浮动预览面板 -->
|
||||
<div id="guestbook-preview" class="guestbook-preview">
|
||||
<div class="preview-header">
|
||||
<h3 data-i18n="guestbook_title">留言板</h3>
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<!-- 最新留言预览将在这里动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 博客文章按钮 -->
|
||||
<div class="blog-posts-btn-container">
|
||||
<button id="blog-posts-btn" class="blog-posts-btn" title="博客文章">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z"
|
||||
stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 2V8H20" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 13H8" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16 17H8" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 9H8" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span class="btn-label" data-i18n="btn_blog_posts">博客文章</span>
|
||||
</button>
|
||||
|
||||
<!-- Hover 浮动预览面板 -->
|
||||
<div id="blog-posts-preview" class="blog-posts-preview">
|
||||
<div class="preview-header">
|
||||
<h3 data-i18n="blog_latest_posts">最新文章</h3>
|
||||
</div>
|
||||
<div class="preview-body blog-posts-list">
|
||||
<!-- 最新文章预览将在这里动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 完整博客文章弹窗(点击后显示) -->
|
||||
<div id="blog-posts-full-modal" class="guestbook-full-modal">
|
||||
<div class="full-modal-content solid-modal">
|
||||
<div class="guestbook-header">
|
||||
<h2>博客文章</h2>
|
||||
<button id="blog-posts-close" class="guestbook-close" title="关闭">×</button>
|
||||
</div>
|
||||
<div class="guestbook-body">
|
||||
<!-- 文章列表将在这里动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 旅途剪影按钮 -->
|
||||
<div class="photo-album-btn-container">
|
||||
<button id="photo-album-btn" class="photo-album-btn" title="旅途剪影">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" stroke="#666" stroke-width="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5" fill="#666"/>
|
||||
<path d="M21 15L16 10L5 21" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span class="btn-label" data-i18n="btn_photo_album">旅途剪影</span>
|
||||
</button>
|
||||
|
||||
<!-- Hover 浮动预览面板 -->
|
||||
<div id="photo-album-preview" class="photo-album-preview">
|
||||
<div class="preview-header">
|
||||
<h3 data-i18n="photo_latest_photos">最新照片</h3>
|
||||
</div>
|
||||
<div class="preview-body photo-album-list">
|
||||
<!-- 最新照片预览将在这里动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 完整旅途剪影弹窗(点击后显示) -->
|
||||
<div id="photo-album-full-modal" class="guestbook-full-modal">
|
||||
<div class="full-modal-content">
|
||||
<div class="guestbook-header">
|
||||
<h2 data-i18n="photo_album_title">旅途剪影</h2>
|
||||
<button id="photo-album-close" class="guestbook-close" title="关闭">×</button>
|
||||
</div>
|
||||
<div class="guestbook-body photo-album-body">
|
||||
<!-- 照片网格将在这里动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 完整留言板弹窗(点击后显示) -->
|
||||
<div id="guestbook-full-modal" class="guestbook-full-modal">
|
||||
<div class="full-modal-content solid-modal">
|
||||
<div class="guestbook-header">
|
||||
<h2 data-i18n="guestbook_title">留言板</h2>
|
||||
<button id="guestbook-close" class="guestbook-close" title="关闭">×</button>
|
||||
</div>
|
||||
<div class="guestbook-body">
|
||||
<!-- 留言列表将在这里动态加载 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 头部导航 -->
|
||||
<div class="nav-tabs">
|
||||
<div class="nav-tab active" data-tab="about"><span data-i18n="nav_about"></span></div>
|
||||
@@ -88,19 +233,22 @@
|
||||
<h3 data-i18n="social_media_h3">社交媒体</h3>
|
||||
<div class="website-links">
|
||||
<a href="https://github.com/dellevin" target="_blank" data-i18n="social_link1"
|
||||
data-i18n-attr="href">GitHub</a>
|
||||
data-i18n-attr="href" onclick="return confirmLink(this.href)">GitHub</a>
|
||||
<a href="https://weibo.com/u/6094785109999999999" target="_blank" data-i18n="social_link2"
|
||||
data-i18n-attr="href">微博</a>
|
||||
data-i18n-attr="href" onclick="return confirmLink(this.href)">微博</a>
|
||||
<a href="https://www.zhihu.com/people/bing-xue-chen-xi-59" target="_blank" data-i18n="social_link3"
|
||||
data-i18n-attr="href">知乎</a>
|
||||
data-i18n-attr="href" onclick="return confirmLink(this.href)">知乎</a>
|
||||
<a href="https://space.bilibili.com/403551212" target="_blank" data-i18n="social_link4"
|
||||
data-i18n-attr="href">B站</a>
|
||||
data-i18n-attr="href" onclick="return confirmLink(this.href)">B站</a>
|
||||
<a href="https://www.xiaohongshu.com/user/profile/65aabb99000000000e0271f1" target="_blank"
|
||||
data-i18n="social_link6"
|
||||
data-i18n-attr="href" onclick="return confirmLink(this.href)">小红书</a>
|
||||
<a href="https://www.52pojie.cn/home.php?mod=space&uid=2059006" target="_blank"
|
||||
data-i18n="social_link5" data-i18n-attr="href">52pojie</a>
|
||||
data-i18n="social_link5" data-i18n-attr="href" onclick="return confirmLink(this.href)">52pojie</a>
|
||||
</div>
|
||||
<h3 data-i18n="leave_message_h3">留言</h3>
|
||||
<p data-i18n="leave_message_p"><a href="https://blog.iletter.top/401.html" target="_blank"
|
||||
data-i18n="leave_message_link" data-i18n-attr="href">点击此链接</a>去我的博客下面给我留言!</p>
|
||||
data-i18n="leave_message_link" data-i18n-attr="href" onclick="return confirmLink(this.href)">点击此链接</a>去我的博客下面给我留言!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -126,6 +274,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="关闭">×</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()">❮</button>
|
||||
<button class="carousel-btn carousel-next" onclick="nextMooknoteImage()">❯</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()">×</button>
|
||||
<button class="lightbox-nav lightbox-prev" onclick="prevLightboxImage()">❮</button>
|
||||
<button class="lightbox-nav lightbox-next" onclick="nextLightboxImage()">❯</button>
|
||||
<div class="lightbox-counter"></div>
|
||||
<img src="" alt="MookNote Fullscreen" class="lightbox-img">
|
||||
</div>
|
||||
|
||||
<!-- 工具内容 -->
|
||||
<div class="tab-content" id="tools-content">
|
||||
<div class="content-section">
|
||||
@@ -181,7 +388,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="copyright" style="margin-top: 15px;"><a href="http://beian.miit.gov.cn"
|
||||
target="_blank">冀ICP备2025122609号</a> ©<span>2024 - 2026</span> <span>Power BY</span> <span>Del
|
||||
target="_blank" onclick="return confirmLink(this.href)">冀ICP备2025122609号</a> ©<span>2024 - 2026</span> <span>Power BY</span> <span>Del
|
||||
Levin </span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -247,12 +454,60 @@
|
||||
switchButton.addEventListener('click', switchLanguage);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- 自定义确认弹窗逻辑 -->
|
||||
<script>
|
||||
// 链接跳转确认函数
|
||||
function confirmLink(url) {
|
||||
showCustomConfirm(`确定要打开网站吗?\n${url}`, () => {
|
||||
window.open(url, '_blank');
|
||||
});
|
||||
return false; // 阻止默认跳转行为
|
||||
}
|
||||
|
||||
// 自定义确认弹窗
|
||||
let confirmCallback = null;
|
||||
const customConfirmModal = document.getElementById('custom-confirm-modal');
|
||||
const confirmMessage = customConfirmModal.querySelector('.custom-confirm-message');
|
||||
const confirmBtnOk = customConfirmModal.querySelector('.confirm-btn-ok');
|
||||
const confirmBtnCancel = customConfirmModal.querySelector('.confirm-btn-cancel');
|
||||
|
||||
function showCustomConfirm(message, onConfirm) {
|
||||
confirmMessage.textContent = message;
|
||||
confirmCallback = onConfirm;
|
||||
customConfirmModal.classList.add('active');
|
||||
}
|
||||
|
||||
function hideCustomConfirm() {
|
||||
customConfirmModal.classList.remove('active');
|
||||
confirmCallback = null;
|
||||
}
|
||||
|
||||
confirmBtnOk.addEventListener('click', () => {
|
||||
if (confirmCallback) {
|
||||
confirmCallback();
|
||||
}
|
||||
hideCustomConfirm();
|
||||
});
|
||||
|
||||
confirmBtnCancel.addEventListener('click', hideCustomConfirm);
|
||||
|
||||
// 点击背景关闭
|
||||
customConfirmModal.addEventListener('click', (e) => {
|
||||
if (e.target === customConfirmModal) {
|
||||
hideCustomConfirm();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="./static/js/anypage.js"></script>
|
||||
<script src="./static/js/game_my.js"></script>
|
||||
<script src="./static/js/tools_my.js"></script>
|
||||
<script src="./static/js/websites_my.js"></script>
|
||||
<script src="./static/js/memos.js"></script>
|
||||
<script src="./static/js/map-footprint.js"></script>
|
||||
<script src="./static/js/guestbook.js"></script>
|
||||
<script src="./static/js/blog_posts.js"></script>
|
||||
<script src="./static/js/photo_album.js"></script>
|
||||
<script src="./static/js/tabchange.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
14
scripts/build-photos.bat
Normal 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
|
||||
93
scripts/generate-photos-json.py
Normal 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()
|
||||
@@ -8,7 +8,10 @@
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
width: 300px;
|
||||
display: none;
|
||||
transition: opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.95);
|
||||
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 悬浮框内容 */
|
||||
|
||||
1503
static/css/guestbook.css
Normal file
@@ -4,6 +4,177 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 搜索功能样式 */
|
||||
.search-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.search-overlay.active {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 常驻搜索模式:背景保持模糊,阻止页面交互 */
|
||||
body.persistent-search-mode .search-overlay {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
pointer-events: auto; /* 确保遮罩层接收点击事件以阻止页面交互 */
|
||||
}
|
||||
|
||||
body.persistent-search-mode .search-modal {
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.search-modal {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
transform: translateY(-20px);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.search-lock-btn {
|
||||
position: absolute;
|
||||
top: -45px;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-lock-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.search-lock-btn.locked {
|
||||
background: #1a73e8;
|
||||
border-color: #1a73e8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-overlay.active .search-modal {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
overflow: visible;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.search-engine-selector {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background 0.3s ease;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.search-engine-selector:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.search-engine-selector img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#global-search-input {
|
||||
flex: 1;
|
||||
padding: 12px 15px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#global-search-btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background 0.3s ease;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#global-search-btn:hover {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.search-engine-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 8px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 180px;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.search-engine-dropdown.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.engine-option {
|
||||
padding: 12px 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.engine-option:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.engine-option.active {
|
||||
background: #e8f0fe;
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
.engine-option img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Georgia', 'Times New Roman', 'STSong', serif;
|
||||
line-height: 1.8;
|
||||
@@ -23,6 +194,7 @@ body {
|
||||
}
|
||||
|
||||
.letter-container {
|
||||
/* max-width: 65%; */
|
||||
max-width: 850px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
@@ -81,10 +253,26 @@ body {
|
||||
/* 内容区域 */
|
||||
.tab-content {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
animation: tabFadeIn 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes tabFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 信纸纹理效果 */
|
||||
@@ -166,6 +354,326 @@ body {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 视图切换文字样式 */
|
||||
.website-view-toggle {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.website-view-toggle:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 卡片视图样式 */
|
||||
.website-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.website-card {
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.website-card:hover {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.website-card-icon {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
background: #f9f9f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.website-card-placeholder {
|
||||
background: linear-gradient(135deg, #e0e0e0 0%, #f5f5f5 100%);
|
||||
color: #999;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.website-card-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.website-card-info {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.website-card-title {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.website-card-desc {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* MookNote 弹窗样式 */
|
||||
.mooknote-modal-content {
|
||||
max-width: 800px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.mooknote-banner-carousel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.carousel-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.carousel-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.carousel-image.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.carousel-image:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.carousel-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 60px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 28px;
|
||||
transition: background 0.3s ease;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.carousel-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.carousel-prev {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.carousel-next {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.carousel-indicators {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.carousel-indicator {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.carousel-indicator.active {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* MookNote 全屏预览样式 */
|
||||
.photo-lightbox {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.photo-lightbox.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.lightbox-img {
|
||||
max-width: 95%;
|
||||
max-height: 95%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 36px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.lightbox-close:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.lightbox-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-size: 32px;
|
||||
width: 60px;
|
||||
height: 80px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.lightbox-nav:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.lightbox-prev {
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
.lightbox-next {
|
||||
right: 30px;
|
||||
}
|
||||
|
||||
.lightbox-counter {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.mooknote-body {
|
||||
padding: 40px 50px;
|
||||
}
|
||||
|
||||
.mooknote-content {
|
||||
padding: 40px 35px;
|
||||
}
|
||||
|
||||
.mooknote-content p {
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.8;
|
||||
color: #444;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.mooknote-intro {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.mooknote-thanks {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-left: 4px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mooknote-download {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 60px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px dashed #eee;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: inline-block;
|
||||
padding: 12px 30px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border-radius: 25px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
background: #000;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 工具和游戏网格 */
|
||||
.tools-grid,
|
||||
.games-grid {
|
||||
@@ -181,6 +689,14 @@ body {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
cursor: pointer;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.tool-item:hover,
|
||||
.game-item:hover {
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.tool-name,
|
||||
@@ -248,6 +764,7 @@ body {
|
||||
/* 底部版权信息 - 在信纸外部 */
|
||||
#footer-wrap {
|
||||
max-width: 850px;
|
||||
/* max-width: 65%; */
|
||||
margin: 0 auto 20px auto;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
BIN
static/img/my_work/bitwarden/banner.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
static/img/my_work/blog_iletter/banner.png
Normal file
|
After Width: | Height: | Size: 342 KiB |
BIN
static/img/my_work/easyimage2/banner.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
static/img/my_work/frp_panel/banner.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
static/img/my_work/ittoolman/banner.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
static/img/my_work/mooknote/1.jpg
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
static/img/my_work/mooknote/2.jpg
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
static/img/my_work/mooknote/3.jpg
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
static/img/my_work/mooknote/4.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
static/img/my_work/mooknote/5.jpg
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
static/img/my_work/mooknote/6.jpg
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
static/img/my_work/mooknote/banner.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
static/img/my_work/openlist/banner.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
static/img/photos/0c49fd33f4e83dcedde820d858ec44e9.jpg
Normal file
|
After Width: | Height: | Size: 467 KiB |
BIN
static/img/photos/1 (1).jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
static/img/photos/1 (10).jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
static/img/photos/1 (11).jpg
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
static/img/photos/1 (12).jpg
Normal file
|
After Width: | Height: | Size: 722 KiB |
BIN
static/img/photos/1 (13).jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
static/img/photos/1 (2).jpg
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
static/img/photos/1 (3).jpg
Normal file
|
After Width: | Height: | Size: 442 KiB |
BIN
static/img/photos/1 (4).jpg
Normal file
|
After Width: | Height: | Size: 473 KiB |
BIN
static/img/photos/1 (5).jpg
Normal file
|
After Width: | Height: | Size: 345 KiB |
BIN
static/img/photos/1 (6).jpg
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
static/img/photos/1 (7).jpg
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
static/img/photos/1 (8).jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
static/img/photos/1 (9).jpg
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
static/img/photos/1b72bd604e55942f6a7d7c28bc5ed5be.jpg
Normal file
|
After Width: | Height: | Size: 453 KiB |
BIN
static/img/photos/20955b34b4cbc661f3ec7ff2eda0b815.jpg
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
static/img/photos/2799e89f4632184d151941741b59257f.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
static/img/photos/3e0ad66c001c9894c26eac4b98601c73.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
static/img/photos/4c5a2a39e6ae9e01bf2ea9860ae8e41f.jpg
Normal file
|
After Width: | Height: | Size: 423 KiB |
BIN
static/img/photos/522915fe4632f9591faca7c46a068ea7.jpg
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
static/img/photos/64b539e20378d3e6ac3324e9dcc616ce.jpg
Normal file
|
After Width: | Height: | Size: 815 KiB |
BIN
static/img/photos/6b736db3eeaa315422e98e5e550f23ce.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
static/img/photos/74641a449ac2d8f9d113d27ca7a7254c.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
static/img/photos/7480c6e1755218592bd4881338750d8c.jpg
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
static/img/photos/7b9532126cb8cd0752f8bdcd2d683cb6.jpg
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
static/img/photos/7fe76931d5653349a7876166db59d25e.jpg
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
static/img/photos/8498ee3191ba50dc27b85b7fbfd1e0fc.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
static/img/photos/85306709e27843201604a7b0efc88565.jpg
Normal file
|
After Width: | Height: | Size: 456 KiB |
BIN
static/img/photos/9f647be6532ea2acf9778eb302a19742.jpg
Normal file
|
After Width: | Height: | Size: 547 KiB |
BIN
static/img/photos/a09ebc8f61856503211b42c975fe5cd5.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
static/img/photos/a62e6036f35722b1f0dd131a929d3a17.jpg
Normal file
|
After Width: | Height: | Size: 490 KiB |
BIN
static/img/photos/c0ca11cc8eaaa16564c75263357e19ad.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
static/img/photos/c53486080dd63ac735f1c5d27203b1c4.jpg
Normal file
|
After Width: | Height: | Size: 401 KiB |
BIN
static/img/photos/c8be53d1cb4d3689420aa56dea0b16f5.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
static/img/photos/dae46343c972876a77f6b765ce1e57ea.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
static/img/photos/e1349403bdf5692c209d809ddb1e026d.jpg
Normal file
|
After Width: | Height: | Size: 275 KiB |
BIN
static/img/photos/f3aece19d8e1fdcd0bb140f5ef3ff882.jpg
Normal file
|
After Width: | Height: | Size: 242 KiB |
43
static/img/photos/photos.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"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",
|
||||
"1 (7).jpg",
|
||||
"1 (11).jpg",
|
||||
"1 (9).jpg",
|
||||
"1 (6).jpg",
|
||||
"1 (5).jpg",
|
||||
"1 (4).jpg",
|
||||
"1 (3).jpg",
|
||||
"1 (2).jpg",
|
||||
"1 (1).jpg",
|
||||
"1 (8).jpg",
|
||||
"1 (13).jpg",
|
||||
"1 (10).jpg",
|
||||
"1 (12).jpg"
|
||||
],
|
||||
"total": 37,
|
||||
"lastUpdated": "2026-05-18 02:36:56"
|
||||
}
|
||||
BIN
static/img/search_icon/DuckDuckGo.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
static/img/search_icon/Yandex.png
Normal file
|
After Width: | Height: | Size: 1000 B |
BIN
static/img/search_icon/baidu.png
Normal file
|
After Width: | Height: | Size: 1012 B |
BIN
static/img/search_icon/google.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
@@ -1,3 +1,195 @@
|
||||
// ==================== 全局搜索功能 ====================
|
||||
(function() {
|
||||
const searchEngines = {
|
||||
google: 'https://www.google.com/search?q=',
|
||||
baidu: 'https://www.baidu.com/s?wd=',
|
||||
duckduckgo: 'https://duckduckgo.com/?q=',
|
||||
yandex: 'https://yandex.com/search/?text='
|
||||
};
|
||||
|
||||
let currentEngine = localStorage.getItem('search_engine') || 'google';
|
||||
let enterCount = 0;
|
||||
let enterTimer = null;
|
||||
|
||||
const searchOverlay = document.getElementById('search-overlay');
|
||||
const searchLockBtn = document.getElementById('search-lock-btn');
|
||||
const lockIconOpen = document.getElementById('lock-icon-open');
|
||||
const lockIconClosed = document.getElementById('lock-icon-closed');
|
||||
const searchInput = document.getElementById('global-search-input');
|
||||
const searchBtn = document.getElementById('global-search-btn');
|
||||
const searchIcon = document.getElementById('search-engine-icon');
|
||||
const dropdown = document.getElementById('search-engine-dropdown');
|
||||
const engineSelector = document.getElementById('search-engine-selector');
|
||||
const engineOptions = document.querySelectorAll('.engine-option');
|
||||
|
||||
// 检查是否开启常驻模式
|
||||
const isPersistentSearch = localStorage.getItem('persistent_search') === 'true';
|
||||
if (isPersistentSearch) {
|
||||
document.body.classList.add('persistent-search-mode');
|
||||
// 确保搜索框也显示出来
|
||||
if (searchOverlay) {
|
||||
searchOverlay.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化搜索引擎图标
|
||||
function updateSearchIcon() {
|
||||
const activeOption = document.querySelector(`.engine-option[data-engine="${currentEngine}"]`);
|
||||
if (activeOption) {
|
||||
const iconSrc = activeOption.getAttribute('data-icon');
|
||||
searchIcon.src = iconSrc;
|
||||
searchIcon.alt = currentEngine.charAt(0).toUpperCase() + currentEngine.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新锁图标状态
|
||||
function updateLockIcon() {
|
||||
const isLocked = document.body.classList.contains('persistent-search-mode');
|
||||
if (isLocked) {
|
||||
lockIconOpen.style.display = 'none';
|
||||
lockIconClosed.style.display = 'block';
|
||||
searchLockBtn.classList.add('locked');
|
||||
searchLockBtn.title = '取消常驻';
|
||||
} else {
|
||||
lockIconOpen.style.display = 'block';
|
||||
lockIconClosed.style.display = 'none';
|
||||
searchLockBtn.classList.remove('locked');
|
||||
searchLockBtn.title = '搜索常驻';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示搜索框
|
||||
function showSearch() {
|
||||
searchOverlay.classList.add('active');
|
||||
setTimeout(() => {
|
||||
searchInput.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 隐藏搜索框(仅非驻留模式下调用)
|
||||
function hideSearch() {
|
||||
if (document.body.classList.contains('persistent-search-mode')) {
|
||||
return; // 常驻模式下不允许通过此函数关闭
|
||||
}
|
||||
searchOverlay.classList.remove('active');
|
||||
searchInput.value = '';
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
|
||||
// 切换常驻模式
|
||||
function togglePersistentMode() {
|
||||
const isLocked = document.body.classList.contains('persistent-search-mode');
|
||||
if (isLocked) {
|
||||
// 退出常驻模式(解锁):只改变锁状态,不关闭搜索框
|
||||
document.body.classList.remove('persistent-search-mode');
|
||||
localStorage.setItem('persistent_search', 'false');
|
||||
// 注意:不移除 active 类,保持搜索框显示
|
||||
} else {
|
||||
// 开启常驻模式(锁定)
|
||||
document.body.classList.add('persistent-search-mode');
|
||||
localStorage.setItem('persistent_search', 'true');
|
||||
searchOverlay.classList.add('active');
|
||||
}
|
||||
updateLockIcon();
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
function performSearch() {
|
||||
const query = searchInput.value.trim();
|
||||
if (query) {
|
||||
const url = searchEngines[currentEngine] + encodeURIComponent(query);
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
// 监听双击回车键
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const now = Date.now();
|
||||
|
||||
if (enterTimer && (now - enterTimer < 500)) {
|
||||
// 双击回车:如果当前不是常驻模式,则打开/关闭搜索框;如果是常驻模式,则不处理
|
||||
if (!document.body.classList.contains('persistent-search-mode')) {
|
||||
if (searchOverlay.classList.contains('active')) {
|
||||
hideSearch();
|
||||
} else {
|
||||
showSearch();
|
||||
}
|
||||
}
|
||||
enterCount = 0;
|
||||
enterTimer = null;
|
||||
} else {
|
||||
enterCount = 1;
|
||||
enterTimer = now;
|
||||
|
||||
// 如果搜索框已经打开,按单下回车执行搜索
|
||||
if (searchOverlay.classList.contains('active') && document.activeElement === searchInput) {
|
||||
performSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ESC 键关闭搜索框(仅在非驻留模式下生效)
|
||||
if (e.key === 'Escape' && searchOverlay.classList.contains('active') && !document.body.classList.contains('persistent-search-mode')) {
|
||||
hideSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// 点击搜索引擎图标切换下拉菜单
|
||||
engineSelector.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.classList.toggle('show');
|
||||
});
|
||||
|
||||
// 选择搜索引擎
|
||||
engineOptions.forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
currentEngine = option.getAttribute('data-engine');
|
||||
localStorage.setItem('search_engine', currentEngine);
|
||||
|
||||
// 更新激活状态
|
||||
engineOptions.forEach(opt => opt.classList.remove('active'));
|
||||
option.classList.add('active');
|
||||
|
||||
updateSearchIcon();
|
||||
dropdown.classList.remove('show');
|
||||
searchInput.focus();
|
||||
});
|
||||
});
|
||||
|
||||
// 点击页面其他地方关闭下拉菜单
|
||||
document.addEventListener('click', (e) => {
|
||||
// 如果不在常驻模式下,且点击的是搜索框外部,则关闭搜索框
|
||||
if (!document.body.classList.contains('persistent-search-mode') && !searchOverlay.contains(e.target)) {
|
||||
hideSearch();
|
||||
}
|
||||
dropdown.classList.remove('show');
|
||||
});
|
||||
|
||||
// 阻止下拉菜单内部点击事件冒泡
|
||||
dropdown.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// 点击搜索按钮
|
||||
searchBtn.addEventListener('click', performSearch);
|
||||
|
||||
// 点击锁图标切换常驻模式
|
||||
searchLockBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // 阻止事件冒泡,防止触发 document 的点击关闭逻辑
|
||||
togglePersistentMode();
|
||||
});
|
||||
|
||||
// 初始化
|
||||
updateSearchIcon();
|
||||
updateLockIcon();
|
||||
const initialActiveOption = document.querySelector(`.engine-option[data-engine="${currentEngine}"]`);
|
||||
if (initialActiveOption) {
|
||||
engineOptions.forEach(opt => opt.classList.remove('active'));
|
||||
initialActiveOption.classList.add('active');
|
||||
}
|
||||
})();
|
||||
|
||||
// ==================== Umami Stats 请求 ====================
|
||||
(async () => {
|
||||
const webid = "ae6cd64c-5900-49c9-9c22-95cedc24a508";
|
||||
@@ -6,7 +198,7 @@
|
||||
const endAt = Date.now(); // 当前时间的时间戳(毫秒)
|
||||
const headers = {
|
||||
Authorization:
|
||||
"Bearer 3VvA8ETw0ahPhzuNNY+Zxi3agtfOBT2vNRbm0GcPqIyUhm7rExuwj8F8IwiQWcn/rOD2G/TnONPCFIvUECQYp6GuZRTnfOojki533vP/skqf0D6puOZDQQk8Y7ssihXnfyRu5naGhIoj1BCAC7S0D0RiYvzpSYF9zvZqvgxETrCbFazZsqUBolyJd8H2iZiM4Xx3VC+GnkZHZFgQfaaYUvm33a7CLM74PyFpPby63UExMjIPiLQRAOR2hs5wl5JAs5CTYUaq+QHCCz+tWgDQ4FPtIgoZoG8Ugnywv/YEEn1Jv9p3t8ge7m8ttThnPiZWw62PYPWQ3LpFh7nxX9jQX/Y/vaaAyacCoIP5J4VpiClA40GMMptZrThzEQjheegCilb9",
|
||||
"Bearer JMQBYQLIlwUsx1tnfYI35APN3bh75JrdpWkx1uk0LJTi6QEGPlno2W2D7j8ogCAS12pXTT36VAZaGw3Pqgiw3BEViZG7o3+93zcmR4Txm0wjmHsTnig7GAxWfQMs1eRY9ACFl9KAo2n0UNgC+CqZt64PFxsKwsaIE2UzuGq1ByrCW8TQwtf76ZHg/qYYjFKAGIUEoQeDvXyCjoew25f7nN3o9GTdimkqoUt0xK/2tWCW1ROOowf6r5KDJK17jRNJGV6jwUJ9AsAd53CNp+MBOHy+E8Scrk/0ATZL3HUGSAuaFKyeITkaQTCjjwunnkxq/VAaeGXZIwI1Vt1llz0Qhbjy2G7l28FhOXm4QZLTdWXtduKwHkfAoFB+HcAWGsIdanoX",
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
const params = new URLSearchParams({
|
||||
|
||||
251
static/js/blog_posts.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* 博客文章功能 - 获取并展示博客最新文章
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const BLOG_API_URL = 'https://blog.iletter.top/api/posts';
|
||||
const BLOG_TOKEN = '67192cda-156a-415a-9f3c-97e90a6b818a';
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
let currentPage = 1;
|
||||
let hasMore = true;
|
||||
let isLoading = false;
|
||||
|
||||
// DOM 元素
|
||||
const blogPostsBtn = document.getElementById('blog-posts-btn');
|
||||
const blogPostsPreview = document.getElementById('blog-posts-preview');
|
||||
const blogPostsFullModal = document.getElementById('blog-posts-full-modal');
|
||||
const blogPostsClose = document.getElementById('blog-posts-close');
|
||||
const blogPostsList = document.querySelector('.blog-posts-list');
|
||||
const blogPostsFullBody = document.querySelector('#blog-posts-full-modal .guestbook-body');
|
||||
const previewBody = blogPostsPreview ? blogPostsPreview.querySelector('.preview-body') : null;
|
||||
|
||||
// 初始化
|
||||
function init() {
|
||||
if (!blogPostsBtn || !blogPostsFullModal) return;
|
||||
|
||||
// 点击按钮打开完整弹窗
|
||||
blogPostsBtn.addEventListener('click', openFullModal);
|
||||
|
||||
// 关闭按钮
|
||||
blogPostsClose.addEventListener('click', closeFullModal);
|
||||
|
||||
// 页面加载时自动加载数据(用于 Hover 预览)
|
||||
loadBlogPosts(true);
|
||||
|
||||
// 监听预览面板滚动事件实现触底加载
|
||||
if (previewBody) {
|
||||
previewBody.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理滚动事件
|
||||
function handleScroll(e) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
// 距离底部 50px 时触发加载
|
||||
if (scrollHeight - scrollTop - clientHeight < 50) {
|
||||
loadMoreBlogPosts();
|
||||
}
|
||||
}
|
||||
|
||||
// 打开完整弹窗
|
||||
function openFullModal() {
|
||||
blogPostsFullModal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 首次打开时加载文章
|
||||
if (!document.querySelector('.blog-post-item')) {
|
||||
loadBlogPosts(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭完整弹窗
|
||||
function closeFullModal() {
|
||||
blogPostsFullModal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 加载文章列表
|
||||
async function loadBlogPosts(reset = false) {
|
||||
if (isLoading || (!hasMore && !reset)) return;
|
||||
|
||||
isLoading = true;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 1;
|
||||
hasMore = true;
|
||||
if (blogPostsList) blogPostsList.innerHTML = '<div class="guestbook-loading">正在加载文章...</div>';
|
||||
if (blogPostsFullBody) blogPostsFullBody.innerHTML = '<div class="guestbook-loading">正在加载文章...</div>';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BLOG_API_URL}?page=${currentPage}&pageSize=${PAGE_SIZE}&showContent=false&showDigest=excerpt&limit=60`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'token': BLOG_TOKEN
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const posts = result.data.dataSet || [];
|
||||
const totalPages = result.data.pages || 0;
|
||||
const totalCount = result.data.count || 0; // 获取总数
|
||||
|
||||
// 判断是否还有更多数据
|
||||
hasMore = currentPage < totalPages;
|
||||
|
||||
if (reset) {
|
||||
renderPosts(posts, true, totalCount);
|
||||
} else {
|
||||
appendPosts(posts);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载文章失败:', error);
|
||||
if (reset) {
|
||||
const errorMsg = '<div class="guestbook-empty">加载文章失败,请稍后重试</div>';
|
||||
if (blogPostsList) blogPostsList.innerHTML = errorMsg;
|
||||
if (blogPostsFullBody) blogPostsFullBody.innerHTML = errorMsg;
|
||||
}
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染文章列表
|
||||
function renderPosts(posts, isReset = false, totalCount = 0) {
|
||||
// 预览面板 HTML(无底部分页按钮,使用无限滚动)
|
||||
let previewHtml = '';
|
||||
// 全屏弹窗 HTML(保留底部分页按钮)
|
||||
let fullHtml = '';
|
||||
|
||||
if (!posts || posts.length === 0) {
|
||||
const emptyHtml = '<div class="guestbook-empty">暂无文章</div>';
|
||||
previewHtml = emptyHtml;
|
||||
fullHtml = emptyHtml;
|
||||
} else {
|
||||
const containerHtml = '<div class="blog-posts-container">' +
|
||||
posts.map(post => renderPostItem(post)).join('') +
|
||||
'</div>';
|
||||
|
||||
previewHtml = containerHtml;
|
||||
if (!hasMore) {
|
||||
previewHtml += `<div class="load-complete">已显示全部</div>`;
|
||||
}
|
||||
|
||||
fullHtml = containerHtml;
|
||||
if (hasMore) {
|
||||
fullHtml += `<button class="load-more-btn" onclick="window.loadMoreBlogPosts()">点击加载更多</button>`;
|
||||
} else {
|
||||
fullHtml += `<div class="load-complete">已显示全部</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (blogPostsList) blogPostsList.innerHTML = previewHtml;
|
||||
if (blogPostsFullBody) blogPostsFullBody.innerHTML = fullHtml;
|
||||
|
||||
// 更新预览面板标题显示总数
|
||||
const previewHeader = document.querySelector('#blog-posts-preview .preview-header h3');
|
||||
if (previewHeader && totalCount > 0) {
|
||||
const lang = localStorage.getItem('preferred_language') || 'zh';
|
||||
const translations = {
|
||||
'zh': typeof translationsZH !== 'undefined' ? translationsZH : {},
|
||||
'en': typeof translationsEN !== 'undefined' ? translationsEN : {}
|
||||
};
|
||||
const titleText = translations[lang]['blog_latest_posts'] || '最新文章';
|
||||
previewHeader.textContent = `${titleText} (${totalCount})`;
|
||||
}
|
||||
}
|
||||
|
||||
// 追加文章
|
||||
function appendPosts(posts) {
|
||||
const containers = document.querySelectorAll('.blog-posts-container');
|
||||
|
||||
if (containers.length === 0) {
|
||||
console.error('未找到 .blog-posts-container 容器');
|
||||
return;
|
||||
}
|
||||
|
||||
// 向所有容器追加新文章
|
||||
containers.forEach(container => {
|
||||
posts.forEach(post => {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = renderPostItem(post);
|
||||
container.appendChild(div.firstElementChild);
|
||||
});
|
||||
|
||||
// 移除当前容器后的旧按钮和提示(只操作当前容器的兄弟元素)
|
||||
let nextSibling = container.nextElementSibling;
|
||||
while (nextSibling && (nextSibling.classList.contains('load-more-btn') || nextSibling.classList.contains('load-complete'))) {
|
||||
const toRemove = nextSibling;
|
||||
nextSibling = nextSibling.nextElementSibling;
|
||||
toRemove.remove();
|
||||
}
|
||||
|
||||
// 在当前容器后添加新的加载更多按钮或提示
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`成功追加 ${posts.length} 篇文章到 ${containers.length} 个容器,当前页码: ${currentPage}`);
|
||||
}
|
||||
|
||||
// 渲染单个文章项
|
||||
function renderPostItem(post) {
|
||||
const date = new Date(post.created * 1000);
|
||||
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
const category = post.categories && post.categories.length > 0 ? post.categories[0].name : '未分类';
|
||||
const digest = post.digest || '';
|
||||
|
||||
let html = `<div class="blog-post-item" onclick="showCustomConfirm('确定要打开网站吗?\\n${post.permalink}', () => window.open('${post.permalink}', '_blank'))">`;
|
||||
html += `<div class="blog-post-title">${escapeHtml(post.title)}</div>`;
|
||||
if (digest) {
|
||||
html += `<div class="blog-post-digest">${escapeHtml(digest)}</div>`;
|
||||
}
|
||||
html += `<div class="blog-post-meta">`;
|
||||
html += `<span>${dateStr}</span>`;
|
||||
html += `<span class="blog-post-category">${escapeHtml(category)}</span>`;
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// HTML 转义
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
window.loadMoreBlogPosts = function() {
|
||||
if (!isLoading && hasMore) {
|
||||
currentPage++;
|
||||
loadBlogPosts(false);
|
||||
}
|
||||
};
|
||||
|
||||
// DOM 加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -287,6 +287,11 @@ function toggleGameDetailTooltip(gameElement, gameIndex) {
|
||||
tooltipDiv.style.left = `${left}px`;
|
||||
tooltipDiv.style.display = "block";
|
||||
|
||||
// 触发重绘以启动过渡动画
|
||||
void tooltipDiv.offsetWidth;
|
||||
tooltipDiv.style.opacity = "1";
|
||||
tooltipDiv.style.transform = "translateY(0) scale(1)";
|
||||
|
||||
// 记录当前打开的tooltip
|
||||
currentlyOpenTooltip = { element: tooltipDiv, index: gameIndex };
|
||||
|
||||
@@ -299,11 +304,13 @@ function toggleGameDetailTooltip(gameElement, gameIndex) {
|
||||
// ========== 隐藏Tooltip的辅助函数 ==========
|
||||
function hideTooltip(tooltipElement) {
|
||||
if (tooltipElement) {
|
||||
tooltipElement.style.display = "none";
|
||||
// 如果想完全移除DOM,可以取消下面两行注释,但通常隐藏即可
|
||||
// if (tooltipElement.parentNode) {
|
||||
// tooltipElement.parentNode.removeChild(tooltipElement);
|
||||
// }
|
||||
tooltipElement.style.opacity = "0";
|
||||
tooltipElement.style.transform = "translateY(10px) scale(0.95)";
|
||||
|
||||
// 等待动画结束后再隐藏 display
|
||||
setTimeout(() => {
|
||||
tooltipElement.style.display = "none";
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
544
static/js/guestbook.js
Normal file
@@ -0,0 +1,544 @@
|
||||
/**
|
||||
* 留言板功能 - 基于 Flask API
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Flask API 地址
|
||||
const API_BASE_URL = 'https://comments.iletter.top';
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
let currentPage = 1;
|
||||
let hasMore = true;
|
||||
let isLoading = false;
|
||||
let allComments = []; // 存储所有已加载的留言
|
||||
|
||||
// DOM 元素
|
||||
const guestbookBtn = document.getElementById('guestbook-btn');
|
||||
const guestbookPreview = document.getElementById('guestbook-preview');
|
||||
const guestbookFullModal = document.getElementById('guestbook-full-modal');
|
||||
const guestbookClose = document.getElementById('guestbook-close');
|
||||
const guestbookBody = document.querySelector('#guestbook-full-modal .guestbook-body');
|
||||
const previewBody = document.querySelector('.preview-body');
|
||||
|
||||
// Toast 容器
|
||||
let toastContainer = document.querySelector('.toast-container');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.className = 'toast-container';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// 表单元素(需要动态获取,因为表单会被重新渲染)
|
||||
let nicknameInput, emailInput, linkInput, contentInput, submitBtn;
|
||||
|
||||
// 显示 Toast 提示
|
||||
function showToast(message, type = 'success', duration = 2000) {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
toastContainer.appendChild(toast);
|
||||
|
||||
// 触发动画
|
||||
setTimeout(() => {
|
||||
toast.classList.add('show');
|
||||
}, 10);
|
||||
|
||||
// 自动消失
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
// 初始化
|
||||
function init() {
|
||||
if (!guestbookBtn || !guestbookFullModal) return;
|
||||
|
||||
// 点击按钮打开完整弹窗
|
||||
guestbookBtn.addEventListener('click', openFullModal);
|
||||
|
||||
// 关闭按钮
|
||||
guestbookClose.addEventListener('click', closeFullModal);
|
||||
|
||||
// 预览面板触底加载
|
||||
if (previewBody) {
|
||||
previewBody.addEventListener('scroll', handlePreviewScroll);
|
||||
}
|
||||
|
||||
// 表单提交事件使用事件委托
|
||||
guestbookBody.addEventListener('submit', function(e) {
|
||||
if (e.target && e.target.id === 'guestbook-form') {
|
||||
handleSubmit(e);
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时自动加载数据(用于 Hover 预览)
|
||||
loadGuestbook(true);
|
||||
}
|
||||
|
||||
// 切换留言板显示/隐藏
|
||||
function toggleGuestbook() {
|
||||
if (guestbookFullModal.classList.contains('active')) {
|
||||
closeFullModal();
|
||||
} else {
|
||||
openFullModal();
|
||||
}
|
||||
}
|
||||
|
||||
// 打开完整弹窗
|
||||
function openFullModal() {
|
||||
guestbookFullModal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 首次打开时加载留言
|
||||
if (!document.querySelector('.guestbook-list')) {
|
||||
loadGuestbook(true);
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭完整弹窗
|
||||
function closeFullModal() {
|
||||
guestbookFullModal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 点击模态框背景关闭
|
||||
function handleModalClick(e) {
|
||||
if (e.target === guestbookModal) {
|
||||
closeGuestbook();
|
||||
}
|
||||
}
|
||||
|
||||
// 加载留言列表
|
||||
async function loadGuestbook(reset = false) {
|
||||
if (isLoading || (!hasMore && !reset)) return;
|
||||
|
||||
isLoading = true;
|
||||
|
||||
if (reset) {
|
||||
currentPage = 1;
|
||||
hasMore = true;
|
||||
guestbookBody.innerHTML = '<div class="guestbook-loading" data-i18n="guestbook_loading">正在加载留言...</div>';
|
||||
} else {
|
||||
// 添加加载更多按钮状态
|
||||
const loadMoreBtn = guestbookBody.querySelector('.load-more-btn');
|
||||
if (loadMoreBtn) {
|
||||
loadMoreBtn.disabled = true;
|
||||
loadMoreBtn.textContent = '加载中...';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/comment?path=www.iletter.top&pageSize=${PAGE_SIZE}&page=${currentPage}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const comments = result.data.data || [];
|
||||
const totalCount = result.data.count || 0;
|
||||
const totalPages = result.data.totalPages || 0;
|
||||
|
||||
// 判断是否还有更多数据(根据总页数)
|
||||
hasMore = currentPage < totalPages;
|
||||
|
||||
// 累积所有留言
|
||||
if (reset) {
|
||||
allComments = comments;
|
||||
} else {
|
||||
allComments = allComments.concat(comments);
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
renderGuestbook(comments, totalCount);
|
||||
} else {
|
||||
appendGuestbook(comments);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载留言失败:', error);
|
||||
if (reset) {
|
||||
guestbookBody.innerHTML = `<div class="guestbook-empty" data-i18n="guestbook_load_error">加载留言失败,请稍后重试</div>`;
|
||||
}
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染留言列表
|
||||
function renderGuestbook(comments, totalCount) {
|
||||
let html = '';
|
||||
|
||||
// 先渲染表单(放在顶部)
|
||||
html += renderForm();
|
||||
|
||||
if (!comments || comments.length === 0) {
|
||||
html += `<div class="guestbook-empty" data-i18n="guestbook_no_data">暂无留言,快来抢沙发吧!</div>`;
|
||||
guestbookBody.innerHTML = html;
|
||||
return;
|
||||
}
|
||||
|
||||
html += '<div class="guestbook-list">';
|
||||
comments.forEach(comment => {
|
||||
html += renderCommentItem(comment);
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
// 判断是否还有更多数据
|
||||
if (hasMore) {
|
||||
html += `<button class="load-more-btn" onclick="window.loadMoreGuestbook()">点击加载更多</button>`;
|
||||
}
|
||||
// 否则不显示任何内容
|
||||
|
||||
guestbookBody.innerHTML = html;
|
||||
|
||||
// 同时更新 Hover 预览面板(使用累积的所有留言)
|
||||
renderPreview(allComments, totalCount);
|
||||
}
|
||||
|
||||
// 渲染 Hover 预览面板
|
||||
function renderPreview(comments, totalCount = 0) {
|
||||
if (!previewBody) return;
|
||||
|
||||
if (!comments || comments.length === 0) {
|
||||
previewBody.innerHTML = '<div class="guestbook-empty">暂无留言,快来抢沙发吧!</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新标题显示总数
|
||||
const previewHeader = document.querySelector('#guestbook-preview .preview-header h3');
|
||||
if (previewHeader && totalCount > 0) {
|
||||
const lang = localStorage.getItem('preferred_language') || 'zh';
|
||||
const translations = {
|
||||
'zh': typeof translationsZH !== 'undefined' ? translationsZH : {},
|
||||
'en': typeof translationsEN !== 'undefined' ? translationsEN : {}
|
||||
};
|
||||
const titleText = translations[lang]['guestbook_title'] || '留言板';
|
||||
previewHeader.textContent = `${titleText} (${totalCount})`;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
comments.forEach(comment => {
|
||||
const date = new Date(comment.time);
|
||||
const timeStr = formatDate(date);
|
||||
|
||||
html += `<div class="preview-item">`;
|
||||
html += `<div class="preview-nickname">${escapeHtml(comment.nick)}</div>`;
|
||||
|
||||
// 提取纯文本内容(去除 HTML 标签)
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = comment.comment || '';
|
||||
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
||||
|
||||
html += `<div class="preview-content">${escapeHtml(textContent)}</div>`;
|
||||
html += `<div class="preview-time">${timeStr}</div>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
|
||||
// 如果已经加载完所有数据,显示提示
|
||||
if (!hasMore && allComments.length > 0) {
|
||||
html += `<div class="preview-complete">已全部加载</div>`;
|
||||
}
|
||||
|
||||
previewBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 处理预览面板滚动事件
|
||||
function handlePreviewScroll(e) {
|
||||
const target = e.target;
|
||||
const scrollTop = target.scrollTop;
|
||||
const scrollHeight = target.scrollHeight;
|
||||
const clientHeight = target.clientHeight;
|
||||
|
||||
// 距离底部 50px 时触发加载
|
||||
if (scrollHeight - scrollTop - clientHeight < 50) {
|
||||
if (!isLoading && hasMore) {
|
||||
currentPage++;
|
||||
loadGuestbook(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 追加留言
|
||||
function appendGuestbook(comments) {
|
||||
const listContainer = guestbookBody.querySelector('.guestbook-list');
|
||||
const loadMoreBtn = guestbookBody.querySelector('.load-more-btn');
|
||||
|
||||
if (!listContainer) return;
|
||||
|
||||
comments.forEach(comment => {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = renderCommentItem(comment);
|
||||
listContainer.appendChild(div.firstElementChild);
|
||||
});
|
||||
|
||||
// 移除旧的加载更多按钮
|
||||
if (loadMoreBtn) {
|
||||
loadMoreBtn.remove();
|
||||
}
|
||||
|
||||
// 如果还有更多,添加新的加载更多按钮;否则显示"已显示全部"
|
||||
if (hasMore) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'load-more-btn';
|
||||
btn.textContent = '点击加载更多';
|
||||
btn.onclick = window.loadMoreGuestbook;
|
||||
listContainer.after(btn);
|
||||
} else {
|
||||
const tip = document.createElement('div');
|
||||
tip.className = 'load-complete';
|
||||
tip.textContent = '已显示全部';
|
||||
listContainer.after(tip);
|
||||
}
|
||||
|
||||
// 更新预览面板
|
||||
renderPreview(allComments, allComments.length);
|
||||
}
|
||||
|
||||
// 渲染单个留言项
|
||||
function renderCommentItem(comment) {
|
||||
const date = new Date(comment.time);
|
||||
const timeStr = formatDate(date);
|
||||
|
||||
let html = `<div class="guestbook-item">`;
|
||||
html += `<div class="guestbook-item-header">`;
|
||||
|
||||
// 头像和昵称
|
||||
html += `<div style="display: flex; align-items: center; gap: 8px;">`;
|
||||
if (comment.avatar) {
|
||||
html += `<img src="${escapeHtml(comment.avatar)}" alt="">`;
|
||||
}
|
||||
|
||||
// 昵称和链接
|
||||
if (comment.link) {
|
||||
html += `<span class="guestbook-nickname"><a href="${escapeHtml(comment.link)}" target="_blank">${escapeHtml(comment.nick)}</a></span>`;
|
||||
} else {
|
||||
html += `<span class="guestbook-nickname">${escapeHtml(comment.nick)}</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
html += `<span class="guestbook-meta">${timeStr}</span>`;
|
||||
html += `</div>`;
|
||||
|
||||
// 内容(comment 字段已经是 HTML)
|
||||
html += `<div class="guestbook-content">${comment.comment || ''}</div>`;
|
||||
|
||||
// 底部信息(浏览器、系统、地址)
|
||||
html += renderCommentFooter(comment);
|
||||
|
||||
// 回复列表
|
||||
if (comment.children && comment.children.length > 0) {
|
||||
html += `<div class="guestbook-reply">`;
|
||||
comment.children.forEach(reply => {
|
||||
html += renderReplyItem(reply);
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
// 渲染留言底部信息
|
||||
function renderCommentFooter(comment) {
|
||||
const browser = comment.browser || '';
|
||||
const os = comment.os || '';
|
||||
const addr = comment.addr || '';
|
||||
|
||||
if (!browser && !os && !addr) return '';
|
||||
|
||||
let html = '<div class="guestbook-footer">';
|
||||
|
||||
if (browser) {
|
||||
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg> ${escapeHtml(browser)}</span>`;
|
||||
}
|
||||
|
||||
if (os) {
|
||||
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg> ${escapeHtml(os)}</span>`;
|
||||
}
|
||||
|
||||
if (addr) {
|
||||
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg> ${escapeHtml(addr)}</span>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// 渲染回复项
|
||||
function renderReplyItem(reply) {
|
||||
const date = new Date(reply.time);
|
||||
const timeStr = formatDate(date);
|
||||
|
||||
let html = `<div class="guestbook-item" style="margin-top: 10px;">`;
|
||||
html += `<div class="guestbook-item-header">`;
|
||||
|
||||
html += `<div style="display: flex; align-items: center; gap: 8px;">`;
|
||||
if (reply.avatar) {
|
||||
html += `<img src="${escapeHtml(reply.avatar)}" alt="">`;
|
||||
}
|
||||
|
||||
if (reply.link) {
|
||||
html += `<span class="guestbook-nickname"><a href="${escapeHtml(reply.link)}" target="_blank">${escapeHtml(reply.nick)}</a></span>`;
|
||||
} else {
|
||||
html += `<span class="guestbook-nickname">${escapeHtml(reply.nick)}</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
|
||||
html += `<span class="guestbook-meta">${timeStr}</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="guestbook-content">${reply.comment || ''}</div>`;
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// 渲染表单
|
||||
function renderForm() {
|
||||
return `
|
||||
<form id="guestbook-form" class="guestbook-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="guestbook-nickname" data-i18n="guestbook_nickname">昵称(选填)</label>
|
||||
<input type="text" id="guestbook-nickname" placeholder="匿名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="guestbook-email" data-i18n="guestbook_email">邮箱(选填)</label>
|
||||
<input type="email" id="guestbook-email" placeholder="用于接收回复通知">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="guestbook-link" data-i18n="guestbook_website">网站(选填)</label>
|
||||
<input type="url" id="guestbook-link" placeholder="https://example.com">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group-full">
|
||||
<label for="guestbook-content" data-i18n="guestbook_placeholder">说点什么吧...</label>
|
||||
<textarea id="guestbook-content" placeholder="请输入留言内容..." required maxlength="500"></textarea>
|
||||
</div>
|
||||
<div class="form-submit">
|
||||
<button type="submit" id="guestbook-submit" class="submit-btn" data-i18n="guestbook_submit">提交留言</button>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
// 提交留言
|
||||
async function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// 重新获取表单元素(因为表单可能被重新渲染)
|
||||
nicknameInput = document.getElementById('guestbook-nickname');
|
||||
emailInput = document.getElementById('guestbook-email');
|
||||
linkInput = document.getElementById('guestbook-link');
|
||||
contentInput = document.getElementById('guestbook-content');
|
||||
submitBtn = document.getElementById('guestbook-submit');
|
||||
|
||||
const nick = nicknameInput.value.trim() || '匿名';
|
||||
const email = emailInput.value.trim();
|
||||
const link = linkInput.value.trim();
|
||||
const comment = contentInput.value.trim();
|
||||
|
||||
// 验证
|
||||
if (!comment) {
|
||||
showToast('请输入留言内容', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用提交按钮
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '提交中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/comment`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
nick: nick,
|
||||
mail: email || '',
|
||||
link: link || '',
|
||||
comment: comment,
|
||||
path: 'www.iletter.top',
|
||||
ua: navigator.userAgent,
|
||||
url: window.location.href
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.errno === 0) {
|
||||
showToast(getTranslation('guestbook_submit_success') || '留言提交成功!', 'success');
|
||||
|
||||
// 清空表单
|
||||
nicknameInput.value = '';
|
||||
emailInput.value = '';
|
||||
linkInput.value = '';
|
||||
contentInput.value = '';
|
||||
|
||||
// 重新加载第一页
|
||||
loadGuestbook(true);
|
||||
} else {
|
||||
throw new Error(data.errmsg || '提交失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交留言失败:', error);
|
||||
showToast(getTranslation('guestbook_submit_error') || '留言提交失败,请重试', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = getTranslation('guestbook_submit') || '提交留言';
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// HTML 转义
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// 获取翻译
|
||||
function getTranslation(key) {
|
||||
const lang = localStorage.getItem('preferred_language') || 'zh';
|
||||
const translations = {
|
||||
'zh': typeof translationsZH !== 'undefined' ? translationsZH : {},
|
||||
'en': typeof translationsEN !== 'undefined' ? translationsEN : {}
|
||||
};
|
||||
return translations[lang][key] || key;
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
window.loadMoreGuestbook = function() {
|
||||
if (!isLoading && hasMore) {
|
||||
currentPage++;
|
||||
loadGuestbook(false);
|
||||
}
|
||||
};
|
||||
|
||||
// DOM 加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -17,7 +17,7 @@ const translationsEN = {
|
||||
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.',
|
||||
about_me_p3:
|
||||
'This site follows a minimalist style, using pure HTML. Umami tracks UV/PV at the bottom. The Memos section uses the Memos API. It also employs Baota Cloud WAF protection and nginx caching for static resources. If you are interested, you can directly download the source code from <a href="https://gitea.iletter.top/dellevin/BaiTu-homepage" target="_blank">https://gitea.iletter.top/dellevin/BaiTu-homepage </a> and use it.',
|
||||
'This site follows a minimalist 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.',
|
||||
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!",
|
||||
// 联系方式
|
||||
@@ -33,10 +33,12 @@ const translationsEN = {
|
||||
social_link3: "Zhihu",
|
||||
social_link4: "Bilibili",
|
||||
social_link5: "52pojie",
|
||||
social_link6: "XiaoHongShu",
|
||||
|
||||
// 留言
|
||||
leave_message_h3: "Leave a Message",
|
||||
leave_message_p:
|
||||
'If you have any questions or suggestions, feel free to <a href="https://blog.iletter.top/401.html" target="_blank">click here</a> to leave me a message on my blog!',
|
||||
'If you have any questions or suggestions, feel free to <a href="https://blog.iletter.top/401.html" target="_blank">click here</a> to leave me a message on my blog, or click the guestbook link in the top right corner to leave a message!',
|
||||
leave_message_link: "click here",
|
||||
// 小界面标题
|
||||
my_website_h2: "My Websites",
|
||||
@@ -57,4 +59,24 @@ const translationsEN = {
|
||||
"I still choose to spend the rest of my life in my own way although I am a failure.",
|
||||
visitor_count_label: "Visitors:",
|
||||
visit_count_label: "Total Visits:",
|
||||
// Guestbook
|
||||
guestbook_title: "Guestbook",
|
||||
guestbook_placeholder: "Say something...",
|
||||
guestbook_nickname: "Nickname (optional)",
|
||||
guestbook_email: "Email (optional)",
|
||||
guestbook_website: "Website (optional)",
|
||||
guestbook_submit: "Submit",
|
||||
guestbook_loading: "Loading messages...",
|
||||
guestbook_no_data: "No messages yet, be the first to comment!",
|
||||
guestbook_load_error: "Failed to load messages, please try again later",
|
||||
guestbook_submit_success: "Message submitted successfully!",
|
||||
guestbook_submit_error: "Failed to submit message, please try again",
|
||||
guestbook_required_nickname: "Please enter a nickname",
|
||||
// Top-right buttons
|
||||
btn_guestbook: "Guestbook",
|
||||
btn_blog_posts: "Blog Posts",
|
||||
btn_photo_album: "Travel Shadows",
|
||||
blog_latest_posts: "Latest Posts",
|
||||
photo_latest_photos: "Latest Photos",
|
||||
photo_album_title: "Travel Shadows",
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ const translationsZH = {
|
||||
about_me_p2:
|
||||
'平时的爱好也就是玩玩游戏,看看书,刷刷动漫,不爱出门,肥宅一个。讨厌重复简单的工作,会想办法偷懒写写自己的顺手的小工具什么的,毕竟懒惰是我的生产力。我也经常混迹于各大网络论坛博客并在各个技术博主下面直呼"大佬666"、"大佬牛牛牛"。自己的博客也在断断续续中更新。主要是记录生活随笔和碰到的技术难题。',
|
||||
about_me_p3:
|
||||
'本站秉承简约风格,采用纯HTML,Umami做网站底部的uv/pv ,闲言碎语模块使用memos接口。同时使用堡塔云WAF防护,nginx缓存加速网站静态资源。如果您有兴趣,可以直接下载本站的 <a href="https://gitea.iletter.top/dellevin/BaiTu-homepage" target="_blank">https://gitea.iletter.top/dellevin/BaiTu-homepage </a> 并使用。',
|
||||
'本站秉承简约风格,采用纯HTML,Umami做网站底部的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查看网站源代码下载。',
|
||||
about_me_p4:
|
||||
"作为一个99年的码农,很庆幸我在工作之后还对技术有着极大的热情!如果你也对技术有着同样的兴趣,也很希望认识你!一同进步共同成长!",
|
||||
|
||||
@@ -34,10 +34,12 @@ const translationsZH = {
|
||||
social_link3: "知乎",
|
||||
social_link4: "B站",
|
||||
social_link5: "52pojie",
|
||||
social_link6: "小红书",
|
||||
|
||||
// 留言
|
||||
leave_message_h3: "留言",
|
||||
leave_message_p:
|
||||
'如果你有任何问题或建议,欢迎<a href="https://blog.iletter.top/401.html" target="_blank">点击此链接</a>去我的博客下面给我留言!',
|
||||
'如果你有任何问题或建议,欢迎<a href="https://blog.iletter.top/401.html" target="_blank">点击此链接</a>去我的博客下面给我留言,或者点击右上角的留言板进行留言哦!',
|
||||
leave_message_link: "点击此链接",
|
||||
// 小界面标题
|
||||
my_website_h2: "我的网站",
|
||||
@@ -57,4 +59,24 @@ const translationsZH = {
|
||||
footer_text: "我虽然是个废物,但我仍然选择用自己喜欢的方式度过自己的余生",
|
||||
visitor_count_label: "本站访客数 :",
|
||||
visit_count_label: "本站总访问量 :",
|
||||
// 留言板
|
||||
guestbook_title: "留言板",
|
||||
guestbook_placeholder: "说点什么吧...",
|
||||
guestbook_nickname: "昵称(选填)",
|
||||
guestbook_email: "邮箱(选填)",
|
||||
guestbook_website: "网站(选填)",
|
||||
guestbook_submit: "提交留言",
|
||||
guestbook_loading: "正在加载留言...",
|
||||
guestbook_no_data: "暂无留言,快来抢沙发吧!",
|
||||
guestbook_load_error: "加载留言失败,请稍后重试",
|
||||
guestbook_submit_success: "留言提交成功!",
|
||||
guestbook_submit_error: "留言提交失败,请重试",
|
||||
guestbook_required_nickname: "请输入昵称",
|
||||
// 右上角按钮
|
||||
btn_guestbook: "留言板",
|
||||
btn_blog_posts: "博客文章",
|
||||
btn_photo_album: "旅途剪影",
|
||||
blog_latest_posts: "最新文章",
|
||||
photo_latest_photos: "最新照片",
|
||||
photo_album_title: "旅途剪影",
|
||||
};
|
||||
|
||||
@@ -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
@@ -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})"><</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})">></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">×</span>
|
||||
<span class="photo-lightbox-nav photo-lightbox-prev">❮</span>
|
||||
<img src="" alt="放大照片">
|
||||
<span class="photo-lightbox-nav photo-lightbox-next">❯</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();
|
||||
}
|
||||
})();
|
||||
@@ -1,75 +1,53 @@
|
||||
// ========== 网站链接数据 (包含中英双语) ==========
|
||||
const websitesData = [
|
||||
{
|
||||
// 分类标题
|
||||
category_zh: '个人博客',
|
||||
category_en: 'Personal Blogs',
|
||||
items: [
|
||||
{
|
||||
url: 'https://www.ittoolman.top/',
|
||||
linkText_zh: 'ittoolman.top - Github托管',
|
||||
linkText_en: 'ittoolman.top - Github Page'
|
||||
},
|
||||
{
|
||||
url: 'https://blog.iletter.top/',
|
||||
linkText_zh: 'blog.iletter.top - typecho用户的最后坚守',
|
||||
linkText_en: 'blog.iletter.top - Last Stand for Typecho Users'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category_zh: '私人在线应用',
|
||||
category_en: 'Personal Online Apps',
|
||||
items: [
|
||||
{
|
||||
url: 'https://img.iletter.top/',
|
||||
linkText_zh: '简单图床 - 私人图床工具',
|
||||
linkText_en: 'EsayImage2 - Image Uploader Tool'
|
||||
},
|
||||
{
|
||||
url: 'https://gitea.iletter.top/',
|
||||
linkText_zh: 'Gitea - 私人git托管仓库',
|
||||
linkText_en: 'Gitea - Private Git Repository'
|
||||
},
|
||||
// {
|
||||
// url: 'http://openlist.iletter.top/',
|
||||
// linkText_zh: 'OpenList - 在线云盘合集',
|
||||
// linkText_en: 'OpenList - Online Cloud Disk Collection'
|
||||
// },
|
||||
{
|
||||
url: 'http://umami.iletter.top/',
|
||||
linkText_zh: 'Umami - 网站访问分析',
|
||||
linkText_en: 'Umami - Website visit analysis'
|
||||
},
|
||||
{
|
||||
url: 'http://beszel.iletter.top/',
|
||||
linkText_zh: 'Beszel - 服务器监控',
|
||||
linkText_en: 'Beszel - Server Monitoring'
|
||||
},
|
||||
{
|
||||
url: 'http://py.iletter.top/en-de-code',
|
||||
linkText_zh: 'Base64在线加密解密 - 自写demo',
|
||||
linkText_en: 'Base64 Online Encryption and Decryption - My Demo'
|
||||
},
|
||||
]
|
||||
}
|
||||
// 如果你想添加新分类,可以仿照上面的格式:
|
||||
/*
|
||||
{
|
||||
category_zh: '新的分类名',
|
||||
category_en: 'New Category Name',
|
||||
items: [
|
||||
{
|
||||
url: 'https://example.com/',
|
||||
linkText_zh: '示例网站',
|
||||
linkText_en: 'Example Site'
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
{ category_zh: '我的作品', category_en: 'My Works', items: [
|
||||
{ url: 'https://mooknote.iletter.top/#/',
|
||||
linkText_zh: 'MookNote - 观影阅读笔记管理',
|
||||
linkText_en: 'MookNote - Movie & Reading Note Manager',
|
||||
desc_zh: '用于记录观影、阅读及笔记的个人知识管理应用。',
|
||||
desc_en: 'Personal knowledge management app for movies, reading, and notes.',
|
||||
img: './static/img/my_work/mooknote/banner.jpg'
|
||||
}
|
||||
]},
|
||||
{ category_zh: '个人博客', category_en: 'Personal Blogs', items: [
|
||||
{ url: 'https://www.ittoolman.top/',
|
||||
linkText_zh: 'ittoolman - Github托管',
|
||||
linkText_en: 'ittoolman - Github Page',
|
||||
desc_zh: '基于 GitHub Pages 搭建的个人技术站点。',
|
||||
desc_en: 'Personal tech site based on GitHub Pages.',
|
||||
img: './static/img/my_work/ittoolman/banner.png'
|
||||
},
|
||||
{ url: 'https://blog.iletter.top/',
|
||||
linkText_zh: 'iletter - typecho极简与高效并存',
|
||||
linkText_en: 'iletter - typecho Minimalism and Efficiency',
|
||||
desc_zh: '记录日常开发与生活点滴的 Typecho 博客。',
|
||||
desc_en: 'A Typecho blog recording daily development and life.',
|
||||
img: './static/img/my_work/blog_iletter/banner.png'
|
||||
}
|
||||
]},
|
||||
{ category_zh: '私人在线应用', category_en: 'Personal Online Apps', items: [
|
||||
{ img: './static/img/my_work/easyimage2/banner.png',
|
||||
url: 'https://img.iletter.top/', linkText_zh: 'EsayImage2 - 私人图床工具', linkText_en: 'EsayImage2 - Image Uploader Tool', desc_zh: '稳定高效的图片上传与托管服务。', desc_en: 'Stable and efficient image hosting service.' },
|
||||
{ url: 'https://gitea.iletter.top/', linkText_zh: 'Gitea - 私人git托管仓库', linkText_en: 'Gitea - Private Git Repository', desc_zh: '轻量级自托管 Git 代码管理平台。', desc_en: 'Lightweight self-hosted Git code management platform.' },
|
||||
{ url: 'http://umami.iletter.top/', linkText_zh: 'Umami - 网站访问分析', linkText_en: 'Umami - Website visit analysis', desc_zh: '简洁、隐私友好的网站流量统计工具。', desc_en: 'Simple, privacy-friendly website analytics tool.' },
|
||||
{ url: 'http://beszel.iletter.top/', linkText_zh: 'Beszel - 服务器监控', linkText_en: 'Beszel - Server Monitoring', desc_zh: '实时监控服务器资源占用情况。', desc_en: 'Real-time monitoring of server resource usage.' },
|
||||
{ url: 'https://memos.iletter.top/', linkText_zh: 'Memos - 个人随想', linkText_en: 'Memos - Personal Thoughts', desc_zh: '碎片化知识管理与灵感记录空间。', desc_en: 'Fragmented knowledge management and inspiration space.' },
|
||||
{ url: 'http://frp.iletter.top/', linkText_zh: 'FRP - 个人反向代理',
|
||||
linkText_en: 'FRP - Personal Reverse Proxy', desc_zh: '用于内网穿透的高性能反向代理。',
|
||||
desc_en: 'High-performance reverse proxy for intranet penetration.',
|
||||
img: './static/img/my_work/frp_panel/banner.png'
|
||||
},
|
||||
{ img: './static/img/my_work/bitwarden/banner.png',
|
||||
url: 'https://bitwarden.iletter.top/', linkText_zh: 'Bitwarden - 我的密码库', linkText_en: 'Bitwarden - My Password Library', desc_zh: '安全可靠的开源密码管理方案。', desc_en: 'Secure and reliable open-source password management solution.' },
|
||||
{ url: 'http://docmost.iletter.top/', linkText_zh: 'Docmost - 在线笔记', linkText_en: 'Docmost - Online Notes', desc_zh: '支持协作的实时在线文档编辑平台。', desc_en: 'Real-time online document editing platform supporting collaboration.' },
|
||||
{ img: './static/img/my_work/openlist/banner.png',
|
||||
url: 'http://openlist.iletter.top/', linkText_zh: 'OpenList - 在线云盘合集', linkText_en: 'OpenList - Online Cloud Disk Collection', desc_zh: '聚合多种网盘服务的文件列表管理。', desc_en: 'File list management aggregating various cloud storage services.' }
|
||||
]}
|
||||
];
|
||||
|
||||
// ========== 渲染网站链接的函数 ==========
|
||||
let currentViewMode = localStorage.getItem('website_view_mode') || 'card'; // 'list' or 'card'
|
||||
|
||||
function renderWebsites(websitesArray) {
|
||||
const container = document.getElementById('dynamic-website-links');
|
||||
if (!container) {
|
||||
@@ -88,39 +66,233 @@ function renderWebsites(websitesArray) {
|
||||
|
||||
const currentLang = localStorage.getItem("preferred_language") || "zh";
|
||||
|
||||
// 清空容器
|
||||
container.innerHTML = '';
|
||||
// 清空容器并添加切换按钮
|
||||
const toggleText = currentViewMode === 'list' ? '切换为卡片视图' : '切换为列表视图';
|
||||
container.innerHTML = `
|
||||
<div style="text-align:right;margin-bottom:10px;">
|
||||
<span class="website-view-toggle" onclick="switchWebsiteView('${currentViewMode === 'list' ? 'card' : 'list'}')">${toggleText}</span>
|
||||
</div>
|
||||
<div id="website-content-area"></div>
|
||||
`;
|
||||
|
||||
arrayToRender.forEach(categoryObj => {
|
||||
// 根据当前语言选择分类标题
|
||||
const categoryName = currentLang === 'en' ? categoryObj.category_en : categoryObj.category_zh;
|
||||
const contentArea = container.querySelector('#website-content-area');
|
||||
|
||||
// 创建分类标题
|
||||
const heading = document.createElement('h3');
|
||||
heading.textContent = categoryName;
|
||||
container.appendChild(heading);
|
||||
if (currentViewMode === 'list') {
|
||||
arrayToRender.forEach(categoryObj => {
|
||||
const categoryName = currentLang === 'en' ? categoryObj.category_en : categoryObj.category_zh;
|
||||
const heading = document.createElement('h3');
|
||||
heading.textContent = categoryName;
|
||||
contentArea.appendChild(heading);
|
||||
|
||||
// 创建一个 div 来包裹该分类下的所有链接
|
||||
const linksDiv = document.createElement('div');
|
||||
linksDiv.className = 'category-links'; // 可选,方便 CSS 样式化
|
||||
const linksDiv = document.createElement('div');
|
||||
linksDiv.className = 'category-links';
|
||||
|
||||
categoryObj.items.forEach(item => {
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.href = item.url;
|
||||
linkElement.target = '_blank';
|
||||
categoryObj.items.forEach(item => {
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.href = '#';
|
||||
linkElement.textContent = currentLang === 'en' ? item.linkText_en : item.linkText_zh;
|
||||
|
||||
// 根据当前语言选择链接文本
|
||||
const linkText = currentLang === 'en' ? item.linkText_en : item.linkText_zh;
|
||||
linkElement.textContent = linkText;
|
||||
if (item.url === 'https://mooknote.iletter.top/#/') {
|
||||
linkElement.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
openMookNoteModal();
|
||||
};
|
||||
} else {
|
||||
linkElement.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
showCustomConfirm(`确定要打开网站吗?\n${item.url}`, () => {
|
||||
window.open(item.url, '_blank');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
linksDiv.appendChild(linkElement);
|
||||
|
||||
// 添加一个间隔(例如空格或换行符,取决于你的 CSS 布局)
|
||||
const space = document.createTextNode(' ');
|
||||
linksDiv.appendChild(space);
|
||||
linksDiv.appendChild(linkElement);
|
||||
const space = document.createTextNode(' ');
|
||||
linksDiv.appendChild(space);
|
||||
});
|
||||
contentArea.appendChild(linksDiv);
|
||||
});
|
||||
} else {
|
||||
arrayToRender.forEach(categoryObj => {
|
||||
const categoryName = currentLang === 'en' ? categoryObj.category_en : categoryObj.category_zh;
|
||||
const heading = document.createElement('h3');
|
||||
heading.textContent = categoryName;
|
||||
contentArea.appendChild(heading);
|
||||
|
||||
container.appendChild(linksDiv);
|
||||
const gridDiv = document.createElement('div');
|
||||
gridDiv.className = 'website-card-grid';
|
||||
|
||||
categoryObj.items.forEach(item => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'website-card';
|
||||
if (item.url === 'https://mooknote.iletter.top/#/') {
|
||||
card.onclick = () => openMookNoteModal();
|
||||
} else {
|
||||
card.onclick = () => {
|
||||
showCustomConfirm(`确定要打开网站吗?\n${item.url}`, () => {
|
||||
window.open(item.url, '_blank');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const title = currentLang === 'en' ? item.linkText_en : item.linkText_zh;
|
||||
const desc = currentLang === 'en' ? (item.desc_en || '') : (item.desc_zh || '');
|
||||
|
||||
const imgSrc = item.img;
|
||||
const placeholderText = item.linkText_zh.split(' - ')[0];
|
||||
const imgHtml = imgSrc
|
||||
? `<div class="website-card-icon"><img src="${imgSrc}" alt="${placeholderText}" onerror="this.style.display='none';this.parentNode.classList.add('website-card-placeholder');this.parentNode.textContent='${placeholderText}'"></div>`
|
||||
: `<div class="website-card-icon website-card-placeholder">${placeholderText}</div>`;
|
||||
|
||||
card.innerHTML = `
|
||||
${imgHtml}
|
||||
<div class="website-card-info">
|
||||
<div class="website-card-title">${title}</div>
|
||||
<div class="website-card-desc">${desc}</div>
|
||||
</div>
|
||||
`;
|
||||
gridDiv.appendChild(card);
|
||||
});
|
||||
|
||||
contentArea.appendChild(gridDiv);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.switchWebsiteView = function(mode) {
|
||||
currentViewMode = mode;
|
||||
localStorage.setItem('website_view_mode', mode);
|
||||
renderWebsites();
|
||||
};
|
||||
|
||||
// MookNote 弹窗逻辑
|
||||
let currentImageIndex = 0;
|
||||
const mooknoteImages = document.querySelectorAll('.carousel-image');
|
||||
const totalImages = mooknoteImages.length;
|
||||
|
||||
// 全屏预览相关变量
|
||||
let lightboxImageIndex = 0;
|
||||
const lightboxImgUrls = [
|
||||
'./static/img/my_work/mooknote/banner.jpg',
|
||||
'./static/img/my_work/mooknote/1.jpg',
|
||||
'./static/img/my_work/mooknote/2.jpg',
|
||||
'./static/img/my_work/mooknote/3.jpg',
|
||||
'./static/img/my_work/mooknote/4.jpg',
|
||||
'./static/img/my_work/mooknote/5.jpg',
|
||||
'./static/img/my_work/mooknote/6.jpg'
|
||||
];
|
||||
|
||||
// 初始化指示器
|
||||
function initCarouselIndicators() {
|
||||
const indicatorsContainer = document.querySelector('.carousel-indicators');
|
||||
if (!indicatorsContainer) return;
|
||||
|
||||
indicatorsContainer.innerHTML = '';
|
||||
for (let i = 0; i < totalImages; i++) {
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'carousel-indicator' + (i === 0 ? ' active' : '');
|
||||
indicator.onclick = () => goToMooknoteImage(i);
|
||||
indicatorsContainer.appendChild(indicator);
|
||||
}
|
||||
}
|
||||
|
||||
// 切换到指定图片
|
||||
function goToMooknoteImage(index) {
|
||||
if (index < 0) index = totalImages - 1;
|
||||
if (index >= totalImages) index = 0;
|
||||
|
||||
mooknoteImages.forEach((img, i) => {
|
||||
img.classList.toggle('active', i === index);
|
||||
});
|
||||
|
||||
const indicators = document.querySelectorAll('.carousel-indicator');
|
||||
indicators.forEach((indicator, i) => {
|
||||
indicator.classList.toggle('active', i === index);
|
||||
});
|
||||
|
||||
currentImageIndex = index;
|
||||
}
|
||||
|
||||
// 上一张图片
|
||||
window.prevMooknoteImage = function() {
|
||||
goToMooknoteImage(currentImageIndex - 1);
|
||||
};
|
||||
|
||||
// 下一张图片
|
||||
window.nextMooknoteImage = function() {
|
||||
goToMooknoteImage(currentImageIndex + 1);
|
||||
};
|
||||
|
||||
// 全屏预览功能
|
||||
window.openMooknoteLightbox = function(index) {
|
||||
lightboxImageIndex = index;
|
||||
const lightbox = document.querySelector('.mooknote-lightbox');
|
||||
const img = lightbox.querySelector('.lightbox-img');
|
||||
const counter = lightbox.querySelector('.lightbox-counter');
|
||||
|
||||
img.src = lightboxImgUrls[index];
|
||||
counter.textContent = `${index + 1} / ${lightboxImgUrls.length}`;
|
||||
lightbox.classList.add('active');
|
||||
};
|
||||
|
||||
window.closeMooknoteLightbox = function() {
|
||||
const lightbox = document.querySelector('.mooknote-lightbox');
|
||||
lightbox.classList.remove('active');
|
||||
};
|
||||
|
||||
window.prevLightboxImage = function() {
|
||||
lightboxImageIndex = (lightboxImageIndex - 1 + lightboxImgUrls.length) % lightboxImgUrls.length;
|
||||
updateLightboxImage();
|
||||
};
|
||||
|
||||
window.nextLightboxImage = function() {
|
||||
lightboxImageIndex = (lightboxImageIndex + 1) % lightboxImgUrls.length;
|
||||
updateLightboxImage();
|
||||
};
|
||||
|
||||
function updateLightboxImage() {
|
||||
const lightbox = document.querySelector('.mooknote-lightbox');
|
||||
const img = lightbox.querySelector('.lightbox-img');
|
||||
const counter = lightbox.querySelector('.lightbox-counter');
|
||||
|
||||
img.src = lightboxImgUrls[lightboxImageIndex];
|
||||
counter.textContent = `${lightboxImageIndex + 1} / ${lightboxImgUrls.length}`;
|
||||
}
|
||||
|
||||
// 键盘事件支持
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const lightbox = document.querySelector('.mooknote-lightbox');
|
||||
if (!lightbox || !lightbox.classList.contains('active')) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
closeMooknoteLightbox();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
prevLightboxImage();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
nextLightboxImage();
|
||||
}
|
||||
});
|
||||
|
||||
function openMookNoteModal() {
|
||||
const modal = document.getElementById('mooknote-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
// 重置轮播到第一张
|
||||
currentImageIndex = 0;
|
||||
initCarouselIndicators();
|
||||
goToMooknoteImage(0);
|
||||
}
|
||||
}
|
||||
|
||||
const mooknoteClose = document.getElementById('mooknote-close');
|
||||
if (mooknoteClose) {
|
||||
mooknoteClose.addEventListener('click', () => {
|
||||
const modal = document.getElementById('mooknote-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||