加入留言板博客按钮

This commit is contained in:
DelLevin-Home
2026-05-18 00:02:14 +08:00
parent 66ece06271
commit fa4c784b96
10 changed files with 1735 additions and 9 deletions

View File

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

826
static/css/guestbook.css Normal file
View File

@@ -0,0 +1,826 @@
/* 留言板按钮容器 */
.guestbook-btn-container {
position: fixed;
top: 20px;
right: 80px;
z-index: 999;
}
/* 博客文章按钮容器 */
.blog-posts-btn-container {
position: fixed;
top: 95px;
right: 80px;
z-index: 999;
}
.guestbook-btn {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 50%;
width: 60px;
height: 60px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.blog-posts-btn {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 50%;
width: 60px;
height: 60px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.btn-label {
font-size: 12px;
color: #666;
line-height: 1;
}
.guestbook-btn:hover,
.blog-posts-btn:hover {
border-color: #d0d0d0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px) scale(1.05);
}
.guestbook-btn svg,
.blog-posts-btn svg {
width: 28px;
height: 28px;
}
.guestbook-btn svg path,
.blog-posts-btn svg path {
transition: stroke 0.3s ease;
}
.guestbook-btn:hover svg path,
.blog-posts-btn:hover svg path {
stroke: #333;
}
/* Hover 浮动预览面板 */
.guestbook-preview,
.blog-posts-preview {
visibility: hidden;
position: absolute;
top: 0;
right: calc(100% + 15px); /* 在按钮左侧显示 */
width: 380px;
max-height: 400px;
background: linear-gradient(to bottom, #fafafa, #fff);
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
z-index: 1000;
opacity: 0;
transform: translateX(20px) scale(0.95);
transform-origin: center right;
transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275),
visibility 0.5s;
overflow: hidden;
border: 1px solid rgba(240, 240, 240, 0.8);
pointer-events: none;
backdrop-filter: blur(20px);
}
/* 当鼠标悬停在容器上时显示预览面板 */
.guestbook-btn-container:hover .guestbook-preview,
.blog-posts-btn-container:hover .blog-posts-preview {
visibility: visible;
opacity: 1;
transform: translateX(0) scale(1);
pointer-events: auto;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.18), 0 8px 20px rgba(0, 0, 0, 0.1);
}
.preview-header {
padding: 14px 18px;
border-bottom: 1px solid #f0f0f0;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
.preview-header h3 {
margin: 0;
font-size: 15px;
color: #333;
font-weight: 500;
letter-spacing: 0.5px;
}
.preview-body {
padding: 0;
overflow-y: auto;
max-height: calc(400px - 50px);
}
.preview-item {
padding: 12px 18px;
border-bottom: 1px solid #f5f5f5;
transition: background 0.2s ease;
}
.preview-item:last-child {
border-bottom: none;
}
.preview-item:hover {
background: #fafafa;
}
.preview-nickname {
font-weight: 500;
color: #333;
font-size: 13px;
margin-bottom: 4px;
}
.preview-content {
color: #666;
font-size: 12px;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.preview-time {
color: #999;
font-size: 11px;
margin-top: 4px;
}
.preview-complete {
text-align: center;
padding: 15px;
color: #bbb;
font-size: 12px;
font-style: italic;
}
/* 完整留言板弹窗(点击后显示) */
.guestbook-full-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10000;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s ease;
}
.guestbook-full-modal.active {
display: flex;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.full-modal-content {
background: #fff;
border-radius: 12px;
width: 92%;
max-width: 850px;
max-height: 92vh;
overflow: hidden;
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.18);
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Toast 弹窗 */
.toast-container {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 100000;
pointer-events: none;
}
.toast {
background: rgba(51, 51, 51, 0.95);
color: #fff;
padding: 12px 24px;
border-radius: 8px;
font-size: 13px;
margin-bottom: 10px;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.toast.success {
background: rgba(76, 175, 80, 0.95);
}
.toast.error {
background: rgba(244, 67, 54, 0.95);
}
/* 留言板模态框 */
.guestbook-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10000;
animation: fadeIn 0.3s ease;
}
.guestbook-modal.active {
display: flex;
justify-content: center;
align-items: center;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.guestbook-content {
background: #fff;
border-radius: 8px;
width: 90%;
max-width: 800px;
max-height: 85vh;
overflow: hidden;
/* box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); */
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.guestbook-header {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
}
.guestbook-header h2 {
margin: 0;
font-size: 16px;
color: #333;
font-weight: 500;
letter-spacing: 0.5px;
}
.guestbook-close {
background: none;
border: none;
font-size: 24px;
color: #999;
cursor: pointer;
line-height: 1;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.3s ease;
}
.guestbook-close:hover {
color: #333;
}
.guestbook-body {
padding: 0;
overflow-y: auto;
max-height: calc(92vh - 60px);
}
/* 留言表单 - 固定在顶部 */
.guestbook-form {
margin-bottom: 15px;
padding: 15px 20px;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
background: #fff;
z-index: 10;
}
/* 留言列表 */
.guestbook-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 15px 20px;
}
.guestbook-item {
padding: 10px 12px;
border: 1px solid #f0f0f0;
border-radius: 6px;
background: #fff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-bottom: 0;
}
.guestbook-item:hover {
border-color: #e0e0e0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transform: translateY(-1px);
}
.guestbook-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f5f5f5;
}
/* 头像样式 */
.guestbook-item-header > div:first-child img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #f5f5f5;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
}
.guestbook-item:hover .guestbook-item-header > div:first-child img {
border-color: #e0e0e0;
transform: scale(1.05);
}
.guestbook-reply .guestbook-item-header > div:first-child img {
width: 28px;
height: 28px;
}
.guestbook-nickname {
font-weight: 500;
color: #333;
font-size: 13px;
}
.guestbook-nickname a {
color: #333;
text-decoration: none;
border-bottom: 1px dotted #999;
transition: all 0.3s ease;
}
.guestbook-nickname a:hover {
color: #000;
border-bottom-color: #333;
}
.guestbook-meta {
font-size: 11px;
color: #999;
}
.guestbook-content {
color: #555;
line-height: 1.6;
font-size: 13px;
word-wrap: break-word;
padding: 4px 0;
}
/* 美化留言内容中的 HTML 元素 */
.guestbook-content p {
margin: 0 0 6px 0;
line-height: 1.6;
}
.guestbook-content p:last-child {
margin-bottom: 0;
}
.guestbook-content a {
color: #333;
text-decoration: none;
border-bottom: 1px dotted #999;
transition: all 0.3s ease;
}
.guestbook-content a:hover {
color: #000;
border-bottom-color: #333;
}
.guestbook-content img {
max-width: 100%;
height: auto;
border-radius: 4px;
margin: 6px 0;
}
.guestbook-content blockquote {
margin: 6px 0;
padding: 8px 12px;
background: #f9f9f9;
border-left: 3px solid #ddd;
color: #666;
font-style: italic;
}
.guestbook-content code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #e74c3c;
}
.guestbook-content pre {
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
overflow-x: auto;
margin: 6px 0;
}
.guestbook-content pre code {
background: none;
padding: 0;
color: #333;
}
/* 留言底部信息(浏览器、系统、地址) */
.guestbook-footer {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f5f5f5;
}
.guestbook-info-item {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #999;
transition: color 0.3s ease;
}
.guestbook-info-item svg {
opacity: 0.6;
}
.guestbook-info-item:hover {
color: #666;
}
.guestbook-reply {
margin-top: 15px;
padding: 12px 15px;
background: #f9f9f9;
border-radius: 4px;
border-left: 3px solid #ddd;
}
.guestbook-reply .guestbook-item {
background: transparent;
border: none;
padding: 10px 0;
margin-bottom: 10px;
}
.guestbook-reply .guestbook-item:last-child {
margin-bottom: 0;
}
.guestbook-reply .guestbook-item:hover {
box-shadow: none;
}
.guestbook-reply .guestbook-nickname {
font-size: 13px;
color: #666;
}
.guestbook-reply .guestbook-content {
font-size: 13px;
color: #777;
line-height: 1.6;
}
/* 留言表单 - 固定在顶部 */
.guestbook-form {
margin-bottom: 12px;
padding: 12px 20px;
border-bottom: 1px solid #eee;
position: sticky;
top: 0;
background: #fff;
z-index: 10;
}
.form-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.form-group {
flex: 1;
}
.form-group-full {
margin-bottom: 8px;
}
.form-group label {
display: block;
margin-bottom: 3px;
color: #666;
font-size: 11px;
}
.form-group input,
.form-group textarea,
.form-group-full textarea {
width: 100%;
padding: 6px 10px;
border: 1px solid #e8e8e8;
border-radius: 4px;
font-size: 12px;
color: #333;
background: #fafafa;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-family: inherit;
box-sizing: border-box;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group-full textarea:focus {
outline: none;
border-color: #999;
background: #fff;
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.05);
}
.form-group textarea,
.form-group-full textarea {
resize: vertical;
min-height: 50px;
line-height: 1.5;
font-family: inherit;
}
.form-group textarea::placeholder,
.form-group-full textarea::placeholder {
color: #bbb;
}
.form-submit {
text-align: right;
margin-top: 10px;
}
.submit-btn {
background: #333;
color: #fff;
border: none;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.submit-btn:hover {
background: #000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.submit-btn:active {
transform: translateY(0);
}
.submit-btn:disabled {
background: #ccc;
box-shadow: none;
cursor: not-allowed;
}
/* 加载状态 */
.guestbook-loading {
text-align: center;
padding: 30px 25px;
color: #999;
font-size: 14px;
}
.guestbook-empty {
text-align: center;
padding: 40px 25px;
color: #999;
font-size: 14px;
}
/* 加载更多按钮 */
.load-more-btn {
display: block;
margin: 20px auto 20px auto;
background: #fff;
border: 1px solid #e8e8e8;
color: #666;
padding: 10px 30px;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.load-more-btn:hover {
border-color: #999;
color: #333;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transform: translateY(-1px);
}
.load-more-btn:disabled {
color: #ccc;
border-color: #eee;
cursor: not-allowed;
}
/* 加载完成提示 */
.load-complete {
text-align: center;
padding: 20px 25px;
color: #bbb;
font-size: 13px;
font-style: italic;
}
/* 博客文章列表样式 */
.blog-post-item {
padding: 12px;
border-bottom: 1px solid #f5f5f5;
transition: background-color 0.3s ease;
}
.blog-post-item:last-child {
border-bottom: none;
}
.blog-post-item:hover {
background-color: #fafafa;
}
.blog-post-title {
font-size: 14px;
color: #333;
margin-bottom: 6px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.blog-post-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: #999;
}
.blog-post-category {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.guestbook-btn-container {
right: 15px;
top: 15px;
}
.blog-posts-btn-container {
right: 15px;
top: 90px;
}
/* 移动端预览面板调整位置,避免超出屏幕 */
.guestbook-preview,
.blog-posts-preview {
right: auto;
left: calc(100% + 10px);
transform-origin: center left;
transform: translateX(-20px) scale(0.95);
}
.guestbook-btn-container:hover .guestbook-preview,
.blog-posts-btn-container:hover .blog-posts-preview {
transform: translateX(0) scale(1);
}
.guestbook-content {
width: 95%;
max-height: 90vh;
}
.guestbook-body {
padding: 0;
}
.guestbook-form {
padding: 15px 20px;
}
.guestbook-list {
padding: 20px;
}
.guestbook-loading,
.guestbook-empty,
.load-complete {
padding-left: 20px;
padding-right: 20px;
}
/* 移动端表单改为垂直排列 */
.form-row {
flex-direction: column;
gap: 15px;
}
}

View File

@@ -23,6 +23,7 @@ body {
}
.letter-container {
/* max-width: 65%; */
max-width: 850px;
margin: 0 auto;
background: white;
@@ -81,10 +82,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);
}
}
/* 信纸纹理效果 */
@@ -181,6 +198,15 @@ 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 {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.tool-name,
@@ -248,6 +274,7 @@ body {
/* 底部版权信息 - 在信纸外部 */
#footer-wrap {
max-width: 850px;
/* max-width: 65%; */
margin: 0 auto 20px auto;
text-align: center;
padding: 20px;

View File

@@ -6,7 +6,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({

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

@@ -0,0 +1,214 @@
/**
* 博客文章功能 - 获取并展示博客最新文章
*/
(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');
// 初始化
function init() {
if (!blogPostsBtn || !blogPostsFullModal) return;
// 点击按钮打开完整弹窗
blogPostsBtn.addEventListener('click', openFullModal);
// 关闭按钮
blogPostsClose.addEventListener('click', closeFullModal);
// 点击背景关闭
blogPostsFullModal.addEventListener('click', function(e) {
if (e.target === blogPostsFullModal) {
closeFullModal();
}
});
// ESC 键关闭
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && blogPostsFullModal.classList.contains('active')) {
closeFullModal();
}
});
// 页面加载时自动加载数据(用于 Hover 预览)
loadBlogPosts(true);
}
// 打开完整弹窗
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=30`, {
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;
// 判断是否还有更多数据
hasMore = currentPage < totalPages;
if (reset) {
renderPosts(posts, true);
} 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) {
let html = '';
if (!posts || posts.length === 0) {
html = '<div class="guestbook-empty">暂无文章</div>';
} else {
html = '<div class="blog-posts-container">';
posts.forEach(post => {
html += renderPostItem(post);
});
html += '</div>';
if (hasMore) {
html += `<button class="load-more-btn" onclick="window.loadMoreBlogPosts()">点击加载更多</button>`;
} else {
html += `<div class="load-complete">已显示全部</div>`;
}
}
if (blogPostsList) blogPostsList.innerHTML = html;
if (blogPostsFullBody) blogPostsFullBody.innerHTML = html;
}
// 追加文章
function appendPosts(posts) {
const container = document.querySelector('.blog-posts-container');
const loadMoreBtn = document.querySelector('.load-more-btn');
const oldTip = document.querySelector('.load-complete');
if (!container) return;
posts.forEach(post => {
const div = document.createElement('div');
div.innerHTML = renderPostItem(post);
container.appendChild(div.firstElementChild);
});
// 移除旧的加载更多按钮和提示
if (loadMoreBtn) loadMoreBtn.remove();
if (oldTip) oldTip.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);
}
}
// 渲染单个文章项
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 : '未分类';
let html = `<div class="blog-post-item" onclick="window.open('${post.permalink}', '_blank')">`;
html += `<div class="blog-post-title">${escapeHtml(post.title)}</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();
}
})();

View File

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

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

@@ -0,0 +1,548 @@
/**
* 留言板功能 - 基于 Flask API
*/
(function () {
'use strict';
// Flask API 地址
const API_BASE_URL = 'http://localhost:8360';
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);
// 点击背景关闭
guestbookFullModal.addEventListener('click', function(e) {
if (e.target === guestbookFullModal) {
closeFullModal();
}
});
// ESC 键关闭
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && guestbookFullModal.classList.contains('active')) {
closeFullModal();
}
});
// 预览面板触底加载
if (previewBody) {
previewBody.addEventListener('scroll', handlePreviewScroll);
}
// 表单提交事件使用事件委托
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 = document.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>`;
} else {
html += `<div class="load-complete">已显示全部</div>`;
}
guestbookBody.innerHTML = html;
// 同时更新 Hover 预览面板(使用累积的所有留言)
renderPreview(allComments);
}
// 渲染 Hover 预览面板
function renderPreview(comments) {
if (!previewBody || !comments || comments.length === 0) return;
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 = document.querySelector('.guestbook-list');
const loadMoreBtn = document.querySelector('.load-more-btn');
const allDataTip = document.querySelector('.guestbook-body > div:last-child');
if (!listContainer) return;
comments.forEach(comment => {
const div = document.createElement('div');
div.innerHTML = renderCommentItem(comment);
listContainer.appendChild(div.firstElementChild);
});
// 移除旧的加载更多按钮和提示
if (loadMoreBtn) {
loadMoreBtn.remove();
}
const oldTip = document.querySelector('.load-complete');
if (oldTip) {
oldTip.remove();
}
// 如果还有更多,添加新的加载更多按钮
if (hasMore) {
const btn = document.createElement('button');
btn.className = 'load-more-btn';
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);
}
// 渲染单个留言项
function renderCommentItem(comment) {
const date = new Date(comment.time);
const timeStr = formatDate(date);
let html = `<div class="guestbook-item">`;
html += `<div class="guestbook-item-header">`;
// 头像和昵称
html += `<div style="display: flex; align-items: center; gap: 8px;">`;
if (comment.avatar) {
html += `<img src="${escapeHtml(comment.avatar)}" alt="">`;
}
// 昵称和链接
if (comment.link) {
html += `<span class="guestbook-nickname"><a href="${escapeHtml(comment.link)}" target="_blank">${escapeHtml(comment.nick)}</a></span>`;
} else {
html += `<span class="guestbook-nickname">${escapeHtml(comment.nick)}</span>`;
}
html += `</div>`;
html += `<span class="guestbook-meta">${timeStr}</span>`;
html += `</div>`;
// 内容comment 字段已经是 HTML
html += `<div class="guestbook-content">${comment.comment || ''}</div>`;
// 底部信息(浏览器、系统、地址)
html += renderCommentFooter(comment);
// 回复列表
if (comment.children && comment.children.length > 0) {
html += `<div class="guestbook-reply">`;
comment.children.forEach(reply => {
html += renderReplyItem(reply);
});
html += `</div>`;
}
html += `</div>`;
return html;
}
// 渲染留言底部信息
function renderCommentFooter(comment) {
const browser = comment.browser || '';
const os = comment.os || '';
const addr = comment.addr || '';
if (!browser && !os && !addr) return '';
let html = '<div class="guestbook-footer">';
if (browser) {
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg> ${escapeHtml(browser)}</span>`;
}
if (os) {
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg> ${escapeHtml(os)}</span>`;
}
if (addr) {
html += `<span class="guestbook-info-item"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg> ${escapeHtml(addr)}</span>`;
}
html += '</div>';
return html;
}
// 渲染回复项
function renderReplyItem(reply) {
const date = new Date(reply.time);
const timeStr = formatDate(date);
let html = `<div class="guestbook-item" style="margin-top: 10px;">`;
html += `<div class="guestbook-item-header">`;
html += `<div style="display: flex; align-items: center; gap: 8px;">`;
if (reply.avatar) {
html += `<img src="${escapeHtml(reply.avatar)}" alt="">`;
}
if (reply.link) {
html += `<span class="guestbook-nickname"><a href="${escapeHtml(reply.link)}" target="_blank">${escapeHtml(reply.nick)}</a></span>`;
} else {
html += `<span class="guestbook-nickname">${escapeHtml(reply.nick)}</span>`;
}
html += `</div>`;
html += `<span class="guestbook-meta">${timeStr}</span>`;
html += `</div>`;
html += `<div class="guestbook-content">${reply.comment || ''}</div>`;
html += `</div>`;
return html;
}
// 渲染表单
function renderForm() {
return `
<form id="guestbook-form" class="guestbook-form">
<div class="form-row">
<div class="form-group">
<label for="guestbook-nickname" data-i18n="guestbook_nickname">昵称(选填)</label>
<input type="text" id="guestbook-nickname" placeholder="匿名">
</div>
<div class="form-group">
<label for="guestbook-email" data-i18n="guestbook_email">邮箱(选填)</label>
<input type="email" id="guestbook-email" placeholder="用于接收回复通知">
</div>
<div class="form-group">
<label for="guestbook-link" data-i18n="guestbook_website">网站(选填)</label>
<input type="url" id="guestbook-link" placeholder="https://example.com">
</div>
</div>
<div class="form-group-full">
<label for="guestbook-content" data-i18n="guestbook_placeholder">说点什么吧...</label>
<textarea id="guestbook-content" placeholder="请输入留言内容..." required maxlength="500"></textarea>
</div>
<div class="form-submit">
<button type="submit" id="guestbook-submit" class="submit-btn" data-i18n="guestbook_submit">提交留言</button>
</div>
</form>
`;
}
// 提交留言
async function handleSubmit(e) {
e.preventDefault();
// 重新获取表单元素(因为表单可能被重新渲染)
nicknameInput = document.getElementById('guestbook-nickname');
emailInput = document.getElementById('guestbook-email');
linkInput = document.getElementById('guestbook-link');
contentInput = document.getElementById('guestbook-content');
submitBtn = document.getElementById('guestbook-submit');
const nick = nicknameInput.value.trim() || '匿名';
const email = emailInput.value.trim();
const link = linkInput.value.trim();
const comment = contentInput.value.trim();
// 验证
if (!comment) {
showToast('请输入留言内容', 'error');
return;
}
// 禁用提交按钮
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
try {
const response = await fetch(`${API_BASE_URL}/api/comment`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
nick: nick,
mail: email || '',
link: link || '',
comment: comment,
path: 'www.iletter.top',
ua: navigator.userAgent,
url: window.location.href
})
});
const data = await response.json();
if (response.ok && data.errno === 0) {
showToast(getTranslation('guestbook_submit_success') || '留言提交成功!', 'success');
// 清空表单
nicknameInput.value = '';
emailInput.value = '';
linkInput.value = '';
contentInput.value = '';
// 重新加载第一页
loadGuestbook(true);
} else {
throw new Error(data.errmsg || '提交失败');
}
} catch (error) {
console.error('提交留言失败:', error);
showToast(getTranslation('guestbook_submit_error') || '留言提交失败,请重试', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = getTranslation('guestbook_submit') || '提交留言';
}
}
// 格式化日期
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
// HTML 转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 获取翻译
function getTranslation(key) {
const lang = localStorage.getItem('preferred_language') || 'zh';
const translations = {
'zh': typeof translationsZH !== 'undefined' ? translationsZH : {},
'en': typeof translationsEN !== 'undefined' ? translationsEN : {}
};
return translations[lang][key] || key;
}
// 加载更多
window.loadMoreGuestbook = function() {
if (!isLoading && hasMore) {
currentPage++;
loadGuestbook(false);
}
};
// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -36,7 +36,7 @@ const translationsEN = {
// 留言
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 +57,17 @@ 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",
};

View File

@@ -37,7 +37,7 @@ const translationsZH = {
// 留言
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 +57,17 @@ 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: "请输入昵称",
};