- SecurityHeaders 中间件生成随机 nonce,注入 context - BuildPageData/BuildAdminPageData 将 CSPNonce 传递给模板 - 所有 19 个模板文件 <script> 标签添加 nonce 属性 - 移除所有内联事件处理(onclick/onerror),改用事件监听 - 移除所有 javascript:void(0) 链接,改用 # 号 + preventDefault - utils.js 添加全局 avatar 图片加载失败回退处理 - common.js 登出按钮添加 preventDefault - audit.js 关闭弹窗按钮改用事件监听
1130 lines
45 KiB
HTML
1130 lines
45 KiB
HTML
{{template "layout/header.html" .}}
|
||
<body>
|
||
|
||
{{template "layout/nav.html" .}}
|
||
|
||
<div class="container post-detail">
|
||
<article class="post-article">
|
||
<header class="post-detail-header">
|
||
<h1 class="post-detail-title">{{.Post.Title}}</h1>
|
||
<div class="post-detail-meta">
|
||
<span class="post-author">{{if .Post.AuthorName}}{{.Post.AuthorName}}{{else}}该用户已注销{{end}}</span>
|
||
<span class="post-time">{{.Post.CreatedAt.Format "2006-01-02 15:04"}}</span>
|
||
{{if ne .Post.Status "approved"}}
|
||
<span class="post-status post-status-{{.Post.Status}}">{{index $.StatusNames .Post.Status}}</span>
|
||
{{end}}
|
||
{{if .Post.IsLocked}}
|
||
<span class="post-status post-status-locked">已锁定</span>
|
||
{{end}}
|
||
{{if and .Post.PendingBody (eq $.Post.UserID $.UID)}}
|
||
<span class="post-status post-status-revision">修订审核中</span>
|
||
{{end}}
|
||
</div>
|
||
</header>
|
||
|
||
{{if .Post.RejectReason}}
|
||
<div class="post-reject-reason">
|
||
<strong>退回理由:</strong>{{.Post.RejectReason}}
|
||
</div>
|
||
{{end}}
|
||
|
||
<div class="post-detail-body" id="postContent"></div>
|
||
</article>
|
||
|
||
<!-- 赞/踩/收藏按钮 -->
|
||
<div class="post-reactions" id="postReactions" data-post-id="{{.Post.ID}}">
|
||
<button class="reaction-btn reaction-like-btn" id="likeBtn" title="点赞">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg>
|
||
<span class="reaction-like-count" id="likesCount">{{.Post.LikesCount}}</span>
|
||
</button>
|
||
<button class="reaction-btn reaction-dislike-btn" id="dislikeBtn" title="踩">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10zM17 2h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg>
|
||
</button>
|
||
<button class="reaction-btn reaction-fav-btn" id="favBtn" title="收藏">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
|
||
<span class="reaction-like-count" id="favCount">{{.Post.FavoritesCount}}</span>
|
||
</button>
|
||
</div>
|
||
|
||
{{if .IsLoggedIn}}
|
||
<div class="post-actions">
|
||
{{if and $.Post (or (eq $.Post.UserID $.UID) $.CanAccessAdmin) (ne $.Post.Status "pending") (not $.Post.IsLocked)}}
|
||
<a href="/studio/write?id={{$.Post.ID}}" class="btn btn-secondary">编辑</a>
|
||
{{end}}
|
||
{{if and $.Post (eq $.Post.UserID $.UID) (or (eq $.Post.Status "draft") (eq $.Post.Status "rejected")) (not $.Post.IsLocked)}}
|
||
<button class="btn btn-primary" id="submitAuditBtn" data-id="{{$.Post.ID}}">提交审核</button>
|
||
{{end}}
|
||
{{if and $.Post (ne $.Post.UserID $.UID)}}
|
||
<button class="btn btn-primary" id="energizeBtn" data-id="{{$.Post.ID}}">赋能</button>
|
||
{{end}}
|
||
</div>
|
||
|
||
<!-- 赋能弹窗 -->
|
||
<div class="energize-overlay" id="energizeOverlay" style="display:none">
|
||
<div class="energize-modal">
|
||
<div class="energize-modal-header">
|
||
<h3>为这篇文章注入能量</h3>
|
||
<button class="energize-close" id="energizeClose">×</button>
|
||
</div>
|
||
<div class="energize-modal-body">
|
||
<p class="energize-hint" id="energizeCapHint" style="display:none">
|
||
今日赋能经验奖励已达上限,继续赋能将消耗域能,对方仍会获得激励。
|
||
</p>
|
||
<div class="energize-options">
|
||
<button class="energize-option-btn" id="energizeLight" data-amount="10">
|
||
<span class="energize-option-title">轻赋</span>
|
||
<span class="energize-option-cost">消耗 1 域能</span>
|
||
<span class="energize-option-gain">获得 10 经验</span>
|
||
</button>
|
||
<button class="energize-option-btn" id="energizeHeavy" data-amount="20">
|
||
<span class="energize-option-title">重赋</span>
|
||
<span class="energize-option-cost">消耗 2 域能</span>
|
||
<span class="energize-option-gain">获得 20 经验</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 选择收藏夹弹窗 -->
|
||
<div class="fav-overlay" id="favOverlay" style="display:none">
|
||
<div class="fav-modal">
|
||
<div class="fav-modal-header">
|
||
<h3>选择收藏夹</h3>
|
||
<button class="fav-close" id="favClose">×</button>
|
||
</div>
|
||
<div class="fav-modal-body">
|
||
<div class="fav-folder-list" id="favFolderCheckList">
|
||
<div class="fav-loading">加载中...</div>
|
||
</div>
|
||
<div class="fav-new-folder">
|
||
<input type="text" class="fav-new-input" id="favNewInput" placeholder="新建收藏夹..." maxlength="50">
|
||
<button class="fav-new-btn" id="favNewConfirm">创建并收藏</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
<!-- 评论区 -->
|
||
{{if .Post.AllowComment}}
|
||
<section class="comments-section" id="comments">
|
||
<h3 class="comments-title">评论 <span class="comments-count" id="commentsTotal">0</span></h3>
|
||
|
||
{{if .IsLoggedIn}}
|
||
<div class="comment-form">
|
||
<textarea class="comment-input" id="commentInput" placeholder="写下你的评论..." rows="3" maxlength="5000"></textarea>
|
||
<div class="comment-form-actions">
|
||
<label class="comment-upload-btn" id="commentUploadLabel">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||
图片
|
||
</label>
|
||
<input type="file" id="commentImageInput" accept="image/*" style="display:none" />
|
||
<button class="btn btn-primary comment-submit-btn" id="commentSubmitBtn">发表评论</button>
|
||
</div>
|
||
<div class="comment-upload-preview" id="commentUploadPreview"></div>
|
||
</div>
|
||
{{else}}
|
||
<div class="comment-login-hint">
|
||
<a href="/auth/login?redirect=/posts/{{$.Post.ID}}">登录</a> 后参与评论
|
||
</div>
|
||
{{end}}
|
||
|
||
<div class="comments-list" id="commentsList"></div>
|
||
<div class="comments-loading" id="commentsLoading" style="display:none">加载中...</div>
|
||
<button class="btn btn-secondary comments-load-more" id="commentsLoadMore" style="display:none">加载更多</button>
|
||
</section>
|
||
{{end}}
|
||
|
||
<!-- @ 提及下拉 -->
|
||
<div class="mention-dropdown" id="mentionDropdown" style="display:none"></div>
|
||
</div>
|
||
|
||
{{template "layout/footer.html" .}}
|
||
|
||
{{/* MD 内容安全传递给 JS */}}
|
||
<textarea id="postMdContent" style="display:none">{{.Post.Body}}</textarea>
|
||
|
||
<link rel="stylesheet" href="/static/vditor/dist/index.css?v={{assetV "/static/vditor/dist/index.css"}}">
|
||
<script src="/static/vditor/dist/method.min.js?v={{assetV "/static/vditor/dist/method.min.js"}}" nonce="{{.CSPNonce}}"></script>
|
||
<script src="/static/js/common.js?v={{assetV "/static/js/common.js"}}" nonce="{{.CSPNonce}}"></script>
|
||
<script src="/static/js/shortcode.js?v={{assetV "/static/js/shortcode.js"}}" nonce="{{.CSPNonce}}"></script>
|
||
<script nonce="{{.CSPNonce}}">
|
||
(function() {
|
||
var el = document.getElementById('postContent');
|
||
var mdEl = document.getElementById('postMdContent');
|
||
if (!el || !mdEl) return;
|
||
|
||
var md = mdEl.value;
|
||
Vditor.preview(el, md, {
|
||
cdn: '/static/vditor',
|
||
theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
|
||
hljs: { style: 'github-dark', enable: true },
|
||
after: function() {
|
||
// 外部链接自动加 nofollow + target="_blank",避免权重流失
|
||
var host = window.location.host;
|
||
var links = el.querySelectorAll('a[href^="http"]');
|
||
for (var i = 0; i < links.length; i++) {
|
||
var a = links[i];
|
||
if (a.host && a.host !== host) {
|
||
a.setAttribute('rel', 'nofollow noopener noreferrer');
|
||
a.setAttribute('target', '_blank');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// Vditor.preview() 渲染 MD,shortcode 占位 div 原样保留
|
||
// 渲染完成后,将占位 div 替换为对应卡片 UI
|
||
renderShortcodes(el);
|
||
})();
|
||
</script>
|
||
|
||
<script nonce="{{.CSPNonce}}">
|
||
// 提交审核按钮
|
||
(function() {
|
||
var btn = document.getElementById('submitAuditBtn');
|
||
if (!btn) return;
|
||
btn.addEventListener('click', function() {
|
||
if (!confirm('确认提交审核?审核通过后将公开发布。')) return;
|
||
var csrf = document.querySelector('meta[name="csrf-token"]');
|
||
fetch('/api/posts/' + btn.dataset.id + '/submit', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-Token': csrf ? csrf.getAttribute('content') : ''
|
||
}
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success) { window.location.reload(); }
|
||
else { alert(d.message || '提交失败'); }
|
||
})
|
||
.catch(function() { alert('请求失败'); });
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<script nonce="{{.CSPNonce}}">
|
||
// 赋能按钮 + 弹窗
|
||
(function() {
|
||
var btn = document.getElementById('energizeBtn');
|
||
var overlay = document.getElementById('energizeOverlay');
|
||
var closeBtn = document.getElementById('energizeClose');
|
||
var capHint = document.getElementById('energizeCapHint');
|
||
var lightBtn = document.getElementById('energizeLight');
|
||
var heavyBtn = document.getElementById('energizeHeavy');
|
||
if (!btn || !overlay) return;
|
||
|
||
var postId = btn.dataset.id;
|
||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
||
|
||
function showOverlay() {
|
||
overlay.style.display = 'flex';
|
||
}
|
||
function hideOverlay() {
|
||
overlay.style.display = 'none';
|
||
capHint.style.display = 'none';
|
||
lightBtn.disabled = false;
|
||
heavyBtn.disabled = false;
|
||
}
|
||
|
||
btn.addEventListener('click', function() {
|
||
// 先获取域能状态,判断是否需要显示上限提示
|
||
fetch('/api/energy/info')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success && d.data && d.data.daily_exp_cap_reached) {
|
||
capHint.style.display = 'block';
|
||
}
|
||
showOverlay();
|
||
})
|
||
.catch(function() { showOverlay(); });
|
||
});
|
||
|
||
closeBtn.addEventListener('click', hideOverlay);
|
||
overlay.addEventListener('click', function(e) {
|
||
if (e.target === overlay) hideOverlay();
|
||
});
|
||
|
||
function doEnergize(amount) {
|
||
lightBtn.disabled = true;
|
||
heavyBtn.disabled = true;
|
||
|
||
fetch('/api/energy/energize', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-Token': csrfToken
|
||
},
|
||
body: JSON.stringify({ post_id: parseInt(postId), amount: amount })
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success) {
|
||
alert(d.data.message || '赋能成功!');
|
||
hideOverlay();
|
||
} else {
|
||
alert(d.message || '赋能失败');
|
||
lightBtn.disabled = false;
|
||
heavyBtn.disabled = false;
|
||
}
|
||
})
|
||
.catch(function() {
|
||
alert('请求失败');
|
||
lightBtn.disabled = false;
|
||
heavyBtn.disabled = false;
|
||
});
|
||
}
|
||
|
||
lightBtn.addEventListener('click', function() {
|
||
doEnergize(parseInt(lightBtn.dataset.amount));
|
||
});
|
||
heavyBtn.addEventListener('click', function() {
|
||
doEnergize(parseInt(heavyBtn.dataset.amount));
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<script nonce="{{.CSPNonce}}">
|
||
// ========== 赞/踩系统 ==========
|
||
(function() {
|
||
var container = document.getElementById('postReactions');
|
||
var likeBtn = document.getElementById('likeBtn');
|
||
var dislikeBtn = document.getElementById('dislikeBtn');
|
||
var likesCountEl = document.getElementById('likesCount');
|
||
if (!container || !likeBtn || !dislikeBtn) return;
|
||
|
||
var postId = container.dataset.postId;
|
||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
||
|
||
// 页面加载时获取当前反应状态
|
||
fetch('/api/posts/' + postId + '/reaction')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (!d.success || !d.data) return;
|
||
updateUI(d.data.reaction, d.data.likes_count);
|
||
})
|
||
.catch(function() {});
|
||
|
||
function updateUI(reaction, count) {
|
||
likeBtn.classList.remove('active');
|
||
dislikeBtn.classList.remove('active');
|
||
if (reaction === 'liked') likeBtn.classList.add('active');
|
||
if (reaction === 'disliked') dislikeBtn.classList.add('active');
|
||
if (typeof count === 'number') likesCountEl.textContent = count;
|
||
}
|
||
|
||
likeBtn.addEventListener('click', function() {
|
||
likeBtn.disabled = true;
|
||
fetch('/api/posts/' + postId + '/like', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }
|
||
})
|
||
.then(function(r) {
|
||
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; }
|
||
return r.json();
|
||
})
|
||
.then(function(d) {
|
||
likeBtn.disabled = false;
|
||
if (!d) return;
|
||
if (d.success) {
|
||
updateUI(d.data.liked ? 'liked' : 'none', d.data.likes_count);
|
||
} else {
|
||
alert(d.message || '操作失败');
|
||
}
|
||
})
|
||
.catch(function() { likeBtn.disabled = false; });
|
||
});
|
||
|
||
dislikeBtn.addEventListener('click', function() {
|
||
dislikeBtn.disabled = true;
|
||
fetch('/api/posts/' + postId + '/dislike', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }
|
||
})
|
||
.then(function(r) {
|
||
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; return null; }
|
||
return r.json();
|
||
})
|
||
.then(function(d) {
|
||
dislikeBtn.disabled = false;
|
||
if (!d) return;
|
||
if (d.success) {
|
||
updateUI(d.data.disliked ? 'disliked' : 'none');
|
||
} else {
|
||
alert(d.message || '操作失败');
|
||
}
|
||
})
|
||
.catch(function() { dislikeBtn.disabled = false; });
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<script nonce="{{.CSPNonce}}">
|
||
// ========== 收藏按钮(弹窗选择收藏夹)==========
|
||
(function() {
|
||
var favBtn = document.getElementById('favBtn');
|
||
var favCountEl = document.getElementById('favCount');
|
||
var container = document.getElementById('postReactions');
|
||
var overlay = document.getElementById('favOverlay');
|
||
var closeBtn = document.getElementById('favClose');
|
||
var folderListEl = document.getElementById('favFolderCheckList');
|
||
var newInput = document.getElementById('favNewInput');
|
||
var newBtn = document.getElementById('favNewConfirm');
|
||
if (!favBtn || !container) return;
|
||
|
||
var postId = container.dataset.postId;
|
||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
||
var isFavorited = false;
|
||
var currentFolderId = null;
|
||
var loading = false;
|
||
|
||
function api(method, url, data) {
|
||
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
|
||
if (csrfToken) opts.headers['X-CSRF-Token'] = csrfToken;
|
||
if (data) opts.body = JSON.stringify(data);
|
||
return fetch(url, opts).then(function(r) {
|
||
if (r.status === 401) { window.location.href = '/auth/login?redirect=/posts/' + postId; throw new Error('unauthorized'); }
|
||
return r.json();
|
||
});
|
||
}
|
||
|
||
// 更新按钮状态
|
||
function updateBtn(favorited) {
|
||
isFavorited = favorited;
|
||
if (favorited) {
|
||
favBtn.classList.add('active');
|
||
} else {
|
||
favBtn.classList.remove('active');
|
||
currentFolderId = null;
|
||
}
|
||
}
|
||
|
||
// 加载收藏状态
|
||
function loadStatus() {
|
||
return fetch('/api/posts/' + postId + '/folder-status')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success && d.data && d.data.favorited) {
|
||
currentFolderId = d.data.folder_id;
|
||
updateBtn(true);
|
||
}
|
||
})
|
||
.catch(function() {});
|
||
}
|
||
|
||
// HTML 转义
|
||
function esc(s) {
|
||
if (!s) return '';
|
||
var d = document.createElement('div');
|
||
d.textContent = s;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
// 渲染收藏夹列表
|
||
function renderFolderList(folders) {
|
||
if (!folders || folders.length === 0) {
|
||
folderListEl.innerHTML = '<div class="fav-empty">暂无收藏夹,请在下方创建</div>';
|
||
return;
|
||
}
|
||
var html = '';
|
||
folders.forEach(function(f) {
|
||
var isActive = currentFolderId === f.id;
|
||
html += '<div class="fav-folder-option' + (isActive ? ' active' : '') + '" data-id="' + f.id + '">'
|
||
+ '<span class="fav-folder-opt-name">' + esc(f.name) + (f.is_default ? ' <small>(默认)</small>' : '') + '</span>'
|
||
+ '<span class="fav-folder-opt-count">' + (f.item_count || 0) + ' 篇</span>'
|
||
+ '</div>';
|
||
});
|
||
folderListEl.innerHTML = html;
|
||
}
|
||
|
||
// 显示弹窗
|
||
function showModal() {
|
||
if (!overlay) { handleNoModal(); return; }
|
||
loading = true;
|
||
overlay.style.display = 'flex';
|
||
folderListEl.innerHTML = '<div class="fav-loading">加载中...</div>';
|
||
|
||
api('GET', '/api/folders')
|
||
.then(function(d) {
|
||
var folders = (d.success && d.data && d.data.folders) ? d.data.folders : [];
|
||
renderFolderList(folders);
|
||
loading = false;
|
||
})
|
||
.catch(function() {
|
||
folderListEl.innerHTML = '<div class="fav-loading fav-error">加载失败,请重试</div>';
|
||
loading = false;
|
||
});
|
||
}
|
||
|
||
// 关闭弹窗
|
||
function hideModal() {
|
||
if (overlay) overlay.style.display = 'none';
|
||
loading = false;
|
||
}
|
||
|
||
// 无弹窗回退(未登录跳转)
|
||
function handleNoModal() {
|
||
if (!csrfToken) {
|
||
window.location.href = '/auth/login?redirect=/posts/' + postId;
|
||
return;
|
||
}
|
||
// 无弹窗兜底:直接 toggle
|
||
favBtn.disabled = true;
|
||
api('POST', '/api/posts/' + postId + '/favorite')
|
||
.then(function(d) {
|
||
favBtn.disabled = false;
|
||
if (d.success) {
|
||
var f = d.data && d.data.favorited;
|
||
updateBtn(f);
|
||
if (favCountEl && d.data && typeof d.data.favorites_count === 'number') {
|
||
favCountEl.textContent = d.data.favorites_count;
|
||
} else if (favCountEl) {
|
||
var cur = parseInt(favCountEl.textContent) || 0;
|
||
favCountEl.textContent = f ? cur + 1 : Math.max(0, cur - 1);
|
||
}
|
||
}
|
||
})
|
||
.catch(function() { favBtn.disabled = false; });
|
||
}
|
||
|
||
// 收藏到指定收藏夹
|
||
function addToFolder(folderId) {
|
||
if (loading) return;
|
||
loading = true;
|
||
api('POST', '/api/folders/' + folderId + '/posts', { post_id: parseInt(postId) })
|
||
.then(function(d) {
|
||
loading = false;
|
||
if (d.success) {
|
||
currentFolderId = folderId;
|
||
updateBtn(true);
|
||
if (favCountEl) favCountEl.textContent = (parseInt(favCountEl.textContent) || 0) + 1;
|
||
hideModal();
|
||
} else {
|
||
alert(d.message || '操作失败');
|
||
}
|
||
})
|
||
.catch(function() { loading = false; });
|
||
}
|
||
|
||
// 取消收藏
|
||
function removeFromFolder(folderId) {
|
||
if (loading) return;
|
||
if (!confirm('确定取消收藏吗?')) return;
|
||
loading = true;
|
||
api('DELETE', '/api/folders/' + folderId + '/posts/' + postId)
|
||
.then(function(d) {
|
||
loading = false;
|
||
if (d.success) {
|
||
currentFolderId = null;
|
||
updateBtn(false);
|
||
if (favCountEl) favCountEl.textContent = Math.max(0, (parseInt(favCountEl.textContent) || 0) - 1);
|
||
hideModal();
|
||
} else {
|
||
alert(d.message || '操作失败');
|
||
}
|
||
})
|
||
.catch(function() { loading = false; });
|
||
}
|
||
|
||
// 创建收藏夹并收藏
|
||
function createAndAdd(name) {
|
||
if (loading) return;
|
||
loading = true;
|
||
newBtn.disabled = true;
|
||
newBtn.textContent = '创建中...';
|
||
api('POST', '/api/folders', { name: name, description: '', is_public: false })
|
||
.then(function(d) {
|
||
if (!d.success) {
|
||
loading = false;
|
||
newBtn.disabled = false;
|
||
newBtn.textContent = '创建并收藏';
|
||
alert(d.message || '创建失败');
|
||
return;
|
||
}
|
||
var newId = d.data && d.data.id;
|
||
return api('POST', '/api/folders/' + newId + '/posts', { post_id: parseInt(postId) })
|
||
.then(function(d2) {
|
||
loading = false;
|
||
newBtn.disabled = false;
|
||
newBtn.textContent = '创建并收藏';
|
||
if (d2.success) {
|
||
currentFolderId = newId;
|
||
updateBtn(true);
|
||
if (favCountEl) favCountEl.textContent = (parseInt(favCountEl.textContent) || 0) + 1;
|
||
hideModal();
|
||
} else {
|
||
alert(d2.message || '操作失败');
|
||
}
|
||
});
|
||
})
|
||
.catch(function() {
|
||
loading = false;
|
||
newBtn.disabled = false;
|
||
newBtn.textContent = '创建并收藏';
|
||
});
|
||
}
|
||
|
||
// 事件:点击收藏按钮 → 打开弹窗
|
||
favBtn.addEventListener('click', function() {
|
||
if (!csrfToken) { window.location.href = '/auth/login?redirect=/posts/' + postId; return; }
|
||
showModal();
|
||
});
|
||
|
||
// 事件:关闭弹窗
|
||
if (closeBtn) closeBtn.addEventListener('click', hideModal);
|
||
if (overlay) overlay.addEventListener('click', function(e) {
|
||
if (e.target === overlay) hideModal();
|
||
});
|
||
|
||
// 事件:点击收藏夹选项
|
||
if (folderListEl) {
|
||
folderListEl.addEventListener('click', function(e) {
|
||
var option = e.target.closest('.fav-folder-option');
|
||
if (!option) return;
|
||
var folderId = parseInt(option.getAttribute('data-id'));
|
||
if (isNaN(folderId)) return;
|
||
if (currentFolderId === folderId) {
|
||
removeFromFolder(folderId);
|
||
} else {
|
||
addToFolder(folderId);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 事件:新建收藏夹
|
||
if (newBtn && newInput) {
|
||
newBtn.addEventListener('click', function() {
|
||
var name = newInput.value.trim();
|
||
if (!name) { alert('请输入收藏夹名称'); return; }
|
||
if (name.length > 50) { alert('收藏夹名称不能超过 50 个字符'); return; }
|
||
createAndAdd(name);
|
||
});
|
||
newInput.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Enter') newBtn.click();
|
||
});
|
||
}
|
||
|
||
// 初始化
|
||
loadStatus();
|
||
})();
|
||
</script>
|
||
|
||
<script nonce="{{.CSPNonce}}">
|
||
// ========== 评论系统 ==========
|
||
(function() {
|
||
var commentsSection = document.getElementById('comments');
|
||
if (!commentsSection) return;
|
||
|
||
var postId = '{{.Post.ID}}';
|
||
var commentsList = document.getElementById('commentsList');
|
||
var commentsTotal = document.getElementById('commentsTotal');
|
||
var loadMoreBtn = document.getElementById('commentsLoadMore');
|
||
var commentInput = document.getElementById('commentInput');
|
||
var commentSubmitBtn = document.getElementById('commentSubmitBtn');
|
||
var mentionDropdown = document.getElementById('mentionDropdown');
|
||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||
var csrfToken = csrfMeta ? csrfMeta.getAttribute('content') : '';
|
||
var currentPage = 1;
|
||
var totalPages = 1;
|
||
|
||
// ====== 工具函数 ======
|
||
function api(method, url, data) {
|
||
var opts = { method: method, headers: { 'Content-Type': 'application/json' } };
|
||
if (csrfToken) opts.headers['X-CSRF-Token'] = csrfToken;
|
||
if (data) opts.body = JSON.stringify(data);
|
||
return fetch(url, opts).then(function(r) { return r.json(); });
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
var d = document.createElement('div');
|
||
d.textContent = text;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
function formatTime(iso) {
|
||
var d = new Date(iso);
|
||
var pad = function(n) { return n < 10 ? '0'+n : n; };
|
||
return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||
}
|
||
|
||
// ====== 渲染评论 HTML ======
|
||
function buildCommentCard(c) {
|
||
var time = c.created_at ? formatTime(c.created_at) : '';
|
||
var author = c.author_name ? escapeHtml(c.author_name) : '用户已注销';
|
||
var body = c.body || '';
|
||
|
||
// 渲染 @ 提及为链接
|
||
body = escapeHtml(body);
|
||
body = body.replace(/@(\S{1,16})/g, '<span class="reply-to">@$1</span>');
|
||
|
||
var html = '<div class="comment-card" data-id="' + c.id + '">' +
|
||
'<div class="comment-header">' +
|
||
'<span class="comment-author">' + author + '</span>' +
|
||
'<span class="comment-time">' + time + '</span>' +
|
||
'</div>' +
|
||
'<div class="comment-body">' + body + '</div>' +
|
||
'<div class="comment-actions">';
|
||
|
||
if (isLoggedIn()) {
|
||
html += '<button class="comment-action-btn comment-reply-btn" data-id="'+c.id+'">回复</button>';
|
||
}
|
||
|
||
html += '</div>' +
|
||
// 回复区域
|
||
'<div class="comment-replies" id="replies-' + c.id + '"></div>';
|
||
|
||
if (c.replies_count > 0) {
|
||
html += '<button class="comment-toggle-replies" id="toggle-' + c.id + '" data-id="'+c.id+'" data-loaded="false">共 ' + c.replies_count + ' 条回复</button>';
|
||
}
|
||
|
||
// 回复输入框(默认隐藏)
|
||
html += '<div class="comment-reply-form" id="replyForm-' + c.id + '" style="display:none">' +
|
||
'<textarea class="comment-reply-input" id="replyInput-' + c.id + '" rows="2" placeholder="写下回复..." maxlength="5000"></textarea>' +
|
||
'<button class="btn btn-primary comment-reply-submit" data-id="' + c.id + '">回复</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
return html;
|
||
}
|
||
|
||
function buildReplyCard(r) {
|
||
var time = r.created_at ? formatTime(r.created_at) : '';
|
||
var author = r.author_name ? escapeHtml(r.author_name) : '用户已注销';
|
||
var body = r.body || '';
|
||
body = escapeHtml(body);
|
||
body = body.replace(/@(\S{1,16})/g, '<span class="reply-to">@$1</span>');
|
||
|
||
var prefix = '';
|
||
if (r.reply_to_name) {
|
||
prefix = '<span class="comment-reply-to">回复 ' + escapeHtml(r.reply_to_name) + ':</span>';
|
||
}
|
||
|
||
return '<div class="comment-reply-card" data-id="' + r.id + '">' +
|
||
'<div class="comment-header">' +
|
||
'<span class="comment-author" style="font-size:13px">' + author + '</span>' +
|
||
'<span class="comment-time" style="font-size:11px">' + time + '</span>' +
|
||
'</div>' +
|
||
'<div class="comment-reply-body">' + prefix + body + '</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function isLoggedIn() {
|
||
return !!document.getElementById('commentInput');
|
||
}
|
||
|
||
// ====== 加载根评论 ======
|
||
function loadComments(page) {
|
||
var url = '/api/posts/' + postId + '/comments?page=' + page + '&page_size=20';
|
||
fetch(url).then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (!d.success) return;
|
||
totalPages = d.data.total_pages || 1;
|
||
commentsTotal.textContent = '(' + d.data.total + ')';
|
||
|
||
var items = d.data.items || [];
|
||
for (var i = 0; i < items.length; i++) {
|
||
commentsList.insertAdjacentHTML('beforeend', buildCommentCard(items[i]));
|
||
}
|
||
|
||
if (page < totalPages) {
|
||
loadMoreBtn.style.display = 'block';
|
||
loadMoreBtn.textContent = '加载更多';
|
||
} else {
|
||
loadMoreBtn.style.display = 'none';
|
||
}
|
||
bindEvents();
|
||
})
|
||
.catch(function() {
|
||
commentsList.innerHTML = '<div class="empty-state">评论加载失败</div>';
|
||
});
|
||
}
|
||
|
||
// ====== 绑定事件 ======
|
||
function bindEvents() {
|
||
// 回复按钮
|
||
var replyBtns = commentsList.querySelectorAll('.comment-reply-btn');
|
||
for (var i = 0; i < replyBtns.length; i++) {
|
||
if (replyBtns[i]._bound) continue;
|
||
replyBtns[i]._bound = true;
|
||
bindReplyBtn(replyBtns[i]);
|
||
}
|
||
|
||
// 展开回复按钮
|
||
var toggleBtns = commentsList.querySelectorAll('.comment-toggle-replies');
|
||
for (var j = 0; j < toggleBtns.length; j++) {
|
||
if (toggleBtns[j]._bound) continue;
|
||
toggleBtns[j]._bound = true;
|
||
bindToggleBtn(toggleBtns[j]);
|
||
}
|
||
|
||
// 回复提交按钮
|
||
var submitBtns = commentsList.querySelectorAll('.comment-reply-submit');
|
||
for (var k = 0; k < submitBtns.length; k++) {
|
||
if (submitBtns[k]._bound) continue;
|
||
submitBtns[k]._bound = true;
|
||
bindReplySubmit(submitBtns[k]);
|
||
}
|
||
|
||
// 回复输入框 @提及
|
||
var replyInputs = commentsList.querySelectorAll('.comment-reply-input');
|
||
for (var m = 0; m < replyInputs.length; m++) {
|
||
if (replyInputs[m]._mentionBound) continue;
|
||
replyInputs[m]._mentionBound = true;
|
||
bindMention(replyInputs[m]);
|
||
}
|
||
}
|
||
|
||
function bindReplyBtn(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var id = btn.dataset.id;
|
||
var form = document.getElementById('replyForm-' + id);
|
||
if (form) {
|
||
form.style.display = form.style.display === 'none' ? 'flex' : 'none';
|
||
if (form.style.display !== 'none') {
|
||
document.getElementById('replyInput-' + id).focus();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function bindToggleBtn(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var id = btn.dataset.id;
|
||
var loaded = btn.dataset.loaded === 'true';
|
||
if (loaded) {
|
||
// 切换显示/隐藏
|
||
var replies = document.getElementById('replies-' + id);
|
||
if (replies) {
|
||
replies.style.display = replies.style.display === 'none' ? 'block' : 'none';
|
||
btn.textContent = replies.style.display === 'none' ? '共 ' + (btn.dataset.count || replies.children.length) + ' 条回复' : '收起回复';
|
||
}
|
||
return;
|
||
}
|
||
// 首次加载
|
||
btn.dataset.loaded = 'true';
|
||
btn.textContent = '加载中...';
|
||
fetch('/api/posts/' + postId + '/comments/' + id + '/replies')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (!d.success) return;
|
||
var replies = d.data.replies || [];
|
||
var container = document.getElementById('replies-' + id);
|
||
if (!container) return;
|
||
var html = '';
|
||
for (var i = 0; i < replies.length; i++) {
|
||
html += buildReplyCard(replies[i]);
|
||
}
|
||
container.innerHTML = html;
|
||
btn.dataset.count = replies.length;
|
||
btn.textContent = '收起回复';
|
||
})
|
||
.catch(function() {
|
||
btn.textContent = '加载失败';
|
||
});
|
||
});
|
||
}
|
||
|
||
function bindReplySubmit(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var id = btn.dataset.id;
|
||
var input = document.getElementById('replyInput-' + id);
|
||
if (!input) return;
|
||
var body = input.value.trim();
|
||
if (!body) { alert('请输入回复内容'); return; }
|
||
|
||
btn.disabled = true;
|
||
api('POST', '/api/comments/' + id + '/reply', { body: body })
|
||
.then(function(d) {
|
||
btn.disabled = false;
|
||
if (d.success) {
|
||
input.value = '';
|
||
document.getElementById('replyForm-' + id).style.display = 'none';
|
||
// 刷新回复列表
|
||
var toggleBtn = document.getElementById('toggle-' + id);
|
||
if (toggleBtn) {
|
||
toggleBtn.dataset.loaded = 'false';
|
||
if (toggleBtn.dataset.count) {
|
||
toggleBtn.dataset.count = parseInt(toggleBtn.dataset.count) + 1;
|
||
toggleBtn.textContent = '共 ' + toggleBtn.dataset.count + ' 条回复';
|
||
}
|
||
toggleBtn.click();
|
||
} else {
|
||
// 没有展开按钮,新建一个
|
||
var replies = document.getElementById('replies-' + id);
|
||
if (replies) {
|
||
fetch('/api/posts/' + postId + '/comments/' + id + '/replies')
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d2) {
|
||
if (!d2.success) return;
|
||
var items = d2.data.replies || [];
|
||
var html = '';
|
||
for (var i = 0; i < items.length; i++) {
|
||
html += buildReplyCard(items[i]);
|
||
}
|
||
replies.innerHTML = html;
|
||
});
|
||
}
|
||
}
|
||
updateTotalCount(1);
|
||
} else {
|
||
alert(d.message || '回复失败');
|
||
}
|
||
})
|
||
.catch(function() {
|
||
btn.disabled = false;
|
||
alert('请求失败');
|
||
});
|
||
});
|
||
}
|
||
|
||
// ====== @提及自动补全 ======
|
||
var mentionTarget = null;
|
||
var mentionPos = 0;
|
||
var searchTimer = null;
|
||
|
||
function bindMention(input) {
|
||
input.addEventListener('input', function() {
|
||
var val = input.value;
|
||
var cursor = input.selectionStart;
|
||
// 查找光标前的最后一个 @
|
||
var atPos = val.lastIndexOf('@', cursor - 1);
|
||
if (atPos === -1 || (atPos > 0 && val[atPos - 1] !== ' ' && val[atPos - 1] !== '\n')) {
|
||
hideMention();
|
||
return;
|
||
}
|
||
var query = val.substring(atPos + 1, cursor);
|
||
if (query.length === 0 || query.includes(' ')) {
|
||
hideMention();
|
||
return;
|
||
}
|
||
|
||
mentionTarget = input;
|
||
mentionPos = atPos;
|
||
|
||
clearTimeout(searchTimer);
|
||
searchTimer = setTimeout(function() {
|
||
fetch('/api/users/search?q=' + encodeURIComponent(query))
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
if (d.success && d.data && d.data.length > 0) {
|
||
showMention(input, d.data, query);
|
||
} else {
|
||
hideMention();
|
||
}
|
||
})
|
||
.catch(function() { hideMention(); });
|
||
}, 200);
|
||
});
|
||
|
||
input.addEventListener('blur', function() {
|
||
setTimeout(hideMention, 150);
|
||
});
|
||
}
|
||
|
||
function showMention(input, users, query) {
|
||
var rect = input.getBoundingClientRect();
|
||
var dropdown = mentionDropdown;
|
||
dropdown.innerHTML = '';
|
||
dropdown.style.display = 'block';
|
||
dropdown.style.top = (rect.bottom + window.scrollY + 4) + 'px';
|
||
dropdown.style.left = (rect.left + window.scrollX) + 'px';
|
||
dropdown.style.width = Math.max(rect.width, 200) + 'px';
|
||
|
||
for (let i = 0; i < users.length; i++) {
|
||
let u = users[i];
|
||
var item = document.createElement('div');
|
||
item.className = 'mention-item';
|
||
item.innerHTML = '<span class="mention-username">' + escapeHtml(u.username) + '</span><span class="mention-uid">' + escapeHtml(u.uid) + '</span>';
|
||
item.addEventListener('mousedown', function(e) {
|
||
e.preventDefault();
|
||
selectMention(u);
|
||
});
|
||
dropdown.appendChild(item);
|
||
}
|
||
}
|
||
|
||
function hideMention() {
|
||
mentionDropdown.style.display = 'none';
|
||
mentionDropdown.innerHTML = '';
|
||
mentionTarget = null;
|
||
}
|
||
|
||
function selectMention(user) {
|
||
if (!mentionTarget) return;
|
||
var val = mentionTarget.value;
|
||
var before = val.substring(0, mentionPos);
|
||
var after = val.substring(mentionTarget.selectionStart);
|
||
var insert = '@' + user.username + ' ';
|
||
mentionTarget.value = before + insert + after;
|
||
mentionTarget.focus();
|
||
var newPos = mentionPos + insert.length;
|
||
mentionTarget.setSelectionRange(newPos, newPos);
|
||
hideMention();
|
||
}
|
||
|
||
// ====== 发表评论 ======
|
||
if (commentSubmitBtn) {
|
||
commentSubmitBtn.addEventListener('click', function() {
|
||
var body = commentInput.value.trim();
|
||
if (!body) { alert('请输入评论内容'); return; }
|
||
|
||
commentSubmitBtn.disabled = true;
|
||
api('POST', '/api/posts/' + postId + '/comments', { body: body })
|
||
.then(function(d) {
|
||
commentSubmitBtn.disabled = false;
|
||
if (d.success) {
|
||
commentInput.value = '';
|
||
// 刷新列表
|
||
commentsList.innerHTML = '';
|
||
currentPage = 1;
|
||
loadComments(currentPage);
|
||
updateTotalCount(1);
|
||
} else {
|
||
alert(d.message || '发表失败');
|
||
}
|
||
})
|
||
.catch(function() {
|
||
commentSubmitBtn.disabled = false;
|
||
alert('请求失败');
|
||
});
|
||
});
|
||
|
||
// 主输入框 @提及
|
||
bindMention(commentInput);
|
||
}
|
||
|
||
// ====== 加载更多 ======
|
||
loadMoreBtn.addEventListener('click', function() {
|
||
currentPage++;
|
||
loadComments(currentPage);
|
||
});
|
||
|
||
function updateTotalCount(delta) {
|
||
var el = commentsTotal;
|
||
var current = parseInt(el.textContent.replace(/[()]/g, '')) || 0;
|
||
el.textContent = '(' + (current + delta) + ')';
|
||
}
|
||
|
||
// ====== 图片上传 ======
|
||
var imageUploadInput = document.getElementById('commentImageInput');
|
||
var uploadLabel = document.getElementById('commentUploadLabel');
|
||
var uploadPreview = document.getElementById('commentUploadPreview');
|
||
|
||
if (imageUploadInput && uploadLabel) {
|
||
uploadLabel.addEventListener('click', function() {
|
||
imageUploadInput.click();
|
||
});
|
||
|
||
imageUploadInput.addEventListener('change', function() {
|
||
var file = imageUploadInput.files[0];
|
||
if (!file) return;
|
||
uploadLabel.style.pointerEvents = 'none';
|
||
uploadLabel.style.opacity = '0.5';
|
||
uploadLabel.textContent = '上传中...';
|
||
|
||
var formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
fetch('/api/comments/upload-image', {
|
||
method: 'POST',
|
||
headers: { 'X-CSRF-Token': csrfToken },
|
||
body: formData
|
||
})
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(d) {
|
||
uploadLabel.style.pointerEvents = 'auto';
|
||
uploadLabel.style.opacity = '1';
|
||
uploadLabel.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> 图片';
|
||
|
||
if (d.success && d.data && d.data.url) {
|
||
// 插入图片 URL 到文本框当前光标位置
|
||
var imgUrl = '\n\n';
|
||
var input = commentInput;
|
||
var start = input.selectionStart;
|
||
var end = input.selectionEnd;
|
||
input.value = input.value.substring(0, start) + imgUrl + input.value.substring(end);
|
||
input.focus();
|
||
input.selectionStart = input.selectionEnd = start + imgUrl.length;
|
||
} else {
|
||
alert(d.message || '上传失败');
|
||
}
|
||
})
|
||
.catch(function() {
|
||
uploadLabel.style.pointerEvents = 'auto';
|
||
uploadLabel.style.opacity = '1';
|
||
uploadLabel.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> 图片';
|
||
alert('上传失败');
|
||
});
|
||
|
||
imageUploadInput.value = '';
|
||
});
|
||
}
|
||
|
||
// ====== 评论高亮 + 已删除评论Toast ======
|
||
function showToast(msg) {
|
||
var toast = document.createElement('div');
|
||
toast.className = 'toast';
|
||
toast.textContent = msg;
|
||
toast.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#fff;padding:10px 24px;border-radius:6px;font-size:14px;z-index:9999;animation:fadeIn 0.3s ease';
|
||
document.body.appendChild(toast);
|
||
setTimeout(function() {
|
||
toast.style.opacity = '0';
|
||
toast.style.transition = 'opacity 0.3s';
|
||
setTimeout(function() { toast.remove(); }, 300);
|
||
}, 2500);
|
||
}
|
||
|
||
function highlightComment(commentId) {
|
||
var card = commentsList.querySelector('.comment-card[data-id="' + commentId + '"]') ||
|
||
commentsList.querySelector('.comment-reply-card[data-id="' + commentId + '"]');
|
||
if (card) {
|
||
// 置顶:移动卡片到列表顶部
|
||
commentsList.insertBefore(card, commentsList.firstChild);
|
||
card.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
card.style.background = '#fff8e1';
|
||
card.style.transition = 'background 1.5s';
|
||
setTimeout(function() { card.style.background = ''; }, 2000);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// 解析 hash(支持 #comment-{id}、#comments 以及回退)
|
||
var hash = window.location.hash;
|
||
var commentMatch = hash.match(/^#comment-(\d+)$/);
|
||
if (commentMatch) {
|
||
var targetCommentId = commentMatch[1];
|
||
// 等待评论加载完成后查找
|
||
var checkInterval = setInterval(function() {
|
||
if (highlightComment(targetCommentId)) {
|
||
clearInterval(checkInterval);
|
||
}
|
||
}, 300);
|
||
// 3秒后超时(评论可能已被删除)
|
||
setTimeout(function() {
|
||
clearInterval(checkInterval);
|
||
if (!commentsList.querySelector('.comment-card[data-id="' + targetCommentId + '"]') &&
|
||
!commentsList.querySelector('.comment-reply-card[data-id="' + targetCommentId + '"]')) {
|
||
showToast('该评论已不存在');
|
||
}
|
||
}, 3000);
|
||
} else if (hash === '#comments') {
|
||
setTimeout(function() {
|
||
var section = document.getElementById('comments');
|
||
if (section) {
|
||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
}, 400);
|
||
}
|
||
|
||
// ====== 初始化 ======
|
||
loadComments(currentPage);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|