generated from dellevin/template
加入留言板博客按钮
This commit is contained in:
548
static/js/guestbook.js
Normal file
548
static/js/guestbook.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user