Files
tech-forum/frontend/post.html
hubian c140a869c9 fix: 编辑帖子时验证token有效性
- checkLogin时调用API验证token
- saveEdit/submitReply/likePost函数增加token检查
- token过期时自动跳转登录页
2026-04-12 18:50:25 +08:00

441 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<title>帖子详情 - 技术论坛</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
.gradient-bg { background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%); }
.prose { max-width: none; }
.prose pre { background: #1f2937; color: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; }
.prose code { background: #f3f4f6; padding: 0.125rem 0.25rem; border-radius: 0.25rem; font-size: 0.875rem; }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- 导航栏 -->
<nav class="bg-white border-b border-gray-100 sticky top-0 z-50">
<div class="max-w-4xl mx-auto px-4">
<div class="flex items-center justify-between h-16">
<a href="/" class="text-gray-600 hover:text-gray-800 flex items-center gap-1">
<i class="ri-arrow-left-line"></i> 返回首页
</a>
<div id="userArea"></div>
</div>
</div>
</nav>
<main class="max-w-4xl mx-auto px-4 py-8">
<!-- 帖子内容 -->
<article id="postContent" class="bg-white rounded-lg border border-gray-100">
<div class="p-6">
<div class="text-center py-12 text-gray-500">加载中...</div>
</div>
</article>
<!-- 回复区域 -->
<div id="replySection" class="mt-6 bg-white rounded-lg border border-gray-100 p-6">
<h3 class="font-medium text-gray-800 mb-4">发表回复</h3>
<div id="replyForm" class="hidden">
<textarea id="replyContent" rows="4" placeholder="输入你的回复..."
class="w-full px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 resize-none"></textarea>
<div class="flex justify-end mt-3">
<button onclick="submitReply()" class="px-4 py-2 gradient-bg text-white rounded-lg">
发送回复
</button>
</div>
</div>
<div id="loginHint" class="text-center py-4 text-gray-500">
<a href="/login" class="text-blue-500 hover:text-blue-600">登录</a> 后参与讨论
</div>
</div>
<!-- 回复列表 -->
<div id="repliesList" class="mt-6 space-y-4"></div>
</main>
<!-- 编辑弹窗 -->
<div id="editModal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
<div class="bg-white rounded-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="font-bold text-lg">编辑帖子</h3>
<button onclick="closeEditModal()" class="text-gray-400 hover:text-gray-600">
<i class="ri-close-line text-xl"></i>
</button>
</div>
<div class="p-6 flex-1 overflow-auto">
<div id="editWarning" class="mb-4 p-3 bg-yellow-50 text-yellow-700 rounded-lg text-sm hidden"></div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">标题</label>
<input type="text" id="editTitle" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">内容</label>
<textarea id="editContent" rows="10" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 resize-none"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">标签(逗号分隔)</label>
<input type="text" id="editTags" class="w-full px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500" placeholder="如: Python, 后端, API">
</div>
</div>
</div>
<div class="p-4 border-t flex gap-3 justify-end">
<button onclick="closeEditModal()" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50">
取消
</button>
<button onclick="saveEdit()" class="px-4 py-2 gradient-bg text-white rounded-lg hover:opacity-90">
保存修改
</button>
</div>
</div>
</div>
<script>
let currentUser = null;
let currentPostId = null;
let currentPost = null;
let postLiked = false;
// 获取帖子ID
const pathParts = window.location.pathname.split('/');
currentPostId = pathParts[2];
document.addEventListener('DOMContentLoaded', () => {
checkLogin();
loadPost();
});
async function checkLogin() {
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
if (token && user) {
// 验证 token 是否有效
try {
const res = await fetch('/api/user', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (res.ok) {
currentUser = JSON.parse(user);
document.getElementById('replyForm').classList.remove('hidden');
document.getElementById('loginHint').classList.add('hidden');
} else {
// token 无效,清除
localStorage.removeItem('token');
localStorage.removeItem('user');
currentUser = null;
}
} catch (e) {
console.error('验证登录失败', e);
}
}
}
async function loadPost() {
const res = await fetch('/api/posts/' + currentPostId);
const post = await res.json();
if (post.error) {
document.getElementById('postContent').innerHTML = '<div class="p-6 text-center text-gray-500">' + post.error + '</div>';
return;
}
currentPost = post;
document.title = post.title + ' - 技术论坛';
// 检查是否可编辑
let canEdit = false;
let editInfo = '';
if (currentUser && post.author.id === currentUser.id) {
const createdTime = new Date(post.created_at);
const now = new Date();
const hoursPassed = (now - createdTime) / (1000 * 60 * 60);
const editCount = post.edit_count || 0;
if (hoursPassed > 24) {
editInfo = '发布超过24小时无法编辑';
} else if (editCount >= 5) {
editInfo = '已达到最大修改次数5次';
} else {
canEdit = true;
editInfo = '剩余修改次数: ' + (5 - editCount) + ' 次,时限: ' + Math.ceil(24 - hoursPassed) + ' 小时';
}
}
// 渲染帖子
let editBtnHtml = '';
if (canEdit) {
editBtnHtml = '<button onclick="openEditModal()" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 flex items-center gap-1"><i class="ri-edit-line"></i> 编辑</button>';
} else if (currentUser && post.author.id === currentUser.id && editInfo) {
editBtnHtml = '<span class="text-sm text-gray-400">' + editInfo + '</span>';
}
let editCountHtml = '';
if (post.edit_count > 0) {
editCountHtml = '<span class="text-xs text-gray-400 ml-2">已编辑 ' + post.edit_count + ' 次' + (post.last_edit_at ? ',最后编辑: ' + new Date(post.last_edit_at).toLocaleString() : '') + '</span>';
}
document.getElementById('postContent').innerHTML = `
<div class="p-6">
<!-- 类型标签 -->
<div class="flex items-center gap-2 mb-3">
<span class="px-2 py-1 rounded text-xs ${post.type === 'discussion' ? 'bg-blue-100 text-blue-600' : 'bg-purple-100 text-purple-600'}">
${post.type === 'discussion' ? '技术交流' : '工具分享'}
</span>
${editCountHtml}
</div>
<!-- 标题 -->
<h1 class="text-2xl font-bold text-gray-900 mb-4">${post.title}</h1>
<!-- 作者信息 -->
<div class="flex items-center gap-4 mb-6 pb-6 border-b border-gray-100">
<img src="${post.author.avatar}" class="w-12 h-12 rounded-full">
<div>
<p class="font-medium text-gray-800">${post.author.username}</p>
<p class="text-sm text-gray-500">${new Date(post.created_at).toLocaleString()}</p>
</div>
</div>
<!-- 内容 -->
<div class="prose text-gray-700 leading-relaxed mb-6 whitespace-pre-wrap">${post.content}</div>
<!-- 标签 -->
${post.tags && post.tags.length > 0 ? `
<div class="flex flex-wrap gap-2 mb-6">
${post.tags.map(tag => '<span class="px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-sm">' + tag + '</span>').join('')}
</div>
` : ''}
<!-- 统计和操作 -->
<div class="flex items-center justify-between pt-6 border-t border-gray-100">
<div class="flex items-center gap-6 text-sm text-gray-500">
<span><i class="ri-eye-line mr-1"></i> ${post.views} 浏览</span>
<span id="likesCount"><i class="ri-heart-line mr-1"></i> ${post.likes} 赞</span>
</div>
<div class="flex gap-3 items-center">
${editBtnHtml}
<button onclick="likePost()" id="likeBtn" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 flex items-center gap-1">
<i class="ri-heart-line"></i> 赞
</button>
<button onclick="sharePost()" class="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 flex items-center gap-1">
<i class="ri-share-line"></i> 分享
</button>
</div>
</div>
</div>
`;
// 渲染回复
renderReplies(post.replies);
}
function renderReplies(replies) {
const container = document.getElementById('repliesList');
if (!replies || replies.length === 0) {
container.innerHTML = '<p class="text-center text-gray-500 py-8">暂无回复</p>';
return;
}
container.innerHTML = replies.map(reply => `
<div class="bg-white rounded-lg border border-gray-100 p-4">
<div class="flex items-start gap-3">
<img src="${reply.author.avatar}" class="w-8 h-8 rounded-full">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-800">${reply.author.username}</span>
<span class="text-xs text-gray-400">${new Date(reply.created_at).toLocaleString()}</span>
</div>
<p class="text-gray-700 whitespace-pre-wrap">${reply.content}</p>
<div class="flex items-center gap-4 mt-2 text-sm text-gray-500">
<button onclick="likeReply('${reply.id}')" class="hover:text-blue-500">
<i class="ri-heart-line"></i> ${reply.likes}
</button>
<button onclick="replyTo('${reply.author.username}')" class="hover:text-blue-500">
<i class="ri-reply-line"></i> 回复
</button>
</div>
</div>
</div>
</div>
`).join('');
}
function openEditModal() {
document.getElementById('editTitle').value = currentPost.title;
document.getElementById('editContent').value = currentPost.content;
document.getElementById('editTags').value = currentPost.tags ? currentPost.tags.join(', ') : '';
// 显示编辑限制提示
const createdTime = new Date(currentPost.created_at);
const now = new Date();
const hoursPassed = (now - createdTime) / (1000 * 60 * 60);
const editCount = currentPost.edit_count || 0;
document.getElementById('editWarning').innerHTML = '剩余修改次数: ' + (5 - editCount) + ' 次,时限: ' + Math.ceil(24 - hoursPassed) + ' 小时';
document.getElementById('editWarning').classList.remove('hidden');
document.getElementById('editModal').classList.remove('hidden');
document.getElementById('editModal').classList.add('flex');
}
function closeEditModal() {
document.getElementById('editModal').classList.add('hidden');
document.getElementById('editModal').classList.remove('flex');
}
async function saveEdit() {
if (!currentUser) {
alert('请先登录');
return;
}
const title = document.getElementById('editTitle').value.trim();
const content = document.getElementById('editContent').value.trim();
const tagsStr = document.getElementById('editTags').value.trim();
const tags = tagsStr ? tagsStr.split(',').map(t => t.trim()).filter(t => t) : [];
if (!title || title.length < 5) {
alert('标题至少5个字符');
return;
}
if (!content || content.length < 10) {
alert('内容至少10个字符');
return;
}
const token = localStorage.getItem('token');
if (!token) {
alert('登录已过期,请重新登录');
window.location.href = '/login';
return;
}
try {
const res = await fetch('/api/posts/' + currentPostId + '/edit', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, content, tags })
});
const data = await res.json();
if (data.success) {
closeEditModal();
loadPost();
alert('编辑成功!');
} else {
alert(data.error || '编辑失败');
}
} catch (err) {
alert('网络错误');
}
}
async function submitReply() {
if (!currentUser) {
alert('请先登录');
return;
}
const content = document.getElementById('replyContent').value.trim();
if (!content) {
alert('请输入回复内容');
return;
}
const token = localStorage.getItem('token');
if (!token) {
alert('登录已过期,请重新登录');
window.location.href = '/login';
return;
}
try {
const res = await fetch('/api/posts/' + currentPostId + '/reply', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content })
});
const data = await res.json();
if (data.success) {
document.getElementById('replyContent').value = '';
loadPost();
} else {
alert(data.error || '回复失败');
}
} catch (err) {
alert('网络错误');
}
}
async function likePost() {
if (!currentUser) {
alert('请先登录');
return;
}
const token = localStorage.getItem('token');
if (!token) {
alert('登录已过期,请重新登录');
window.location.href = '/login';
return;
}
try {
const res = await fetch('/api/posts/' + currentPostId + '/like', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token
}
});
const data = await res.json();
if (data.success) {
const btn = document.getElementById('likeBtn');
const countEl = document.getElementById('likesCount');
if (data.liked) {
btn.innerHTML = '<i class="ri-heart-fill text-red-500"></i> 已赞';
btn.classList.add('border-red-300', 'text-red-500');
} else {
btn.innerHTML = '<i class="ri-heart-line"></i> 赞';
btn.classList.remove('border-red-300', 'text-red-500');
}
countEl.innerHTML = '<i class="ri-heart-line mr-1"></i> ' + data.likes_count + ' 赞';
}
} catch (err) {
console.error(err);
}
}
function sharePost() {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
alert('链接已复制到剪贴板');
});
}
function replyTo(username) {
document.getElementById('replyContent').value = '@' + username + ' ';
document.getElementById('replyContent').focus();
}
function likeReply(replyId) {
alert('功能开发中');
}
</script>
</body>
</html>