Files
tech-forum/frontend/index.html
hubian 7f8fbc9605 初始化技术论坛与技术分享网站
功能模块:
- 技术交流: 发帖、评论回复、点赞收藏、标签分类
- 工具分享: 创建主题、子主题分支、问题追问、关注功能
- 用户系统: 用户名+邮箱(必填)+手机(可选)+密码确认

页面:
- 首页: 帖子列表、热门标签、工具分享主题
- 登录/注册页
- 发帖页
- 帖子详情页
- 主题详情页
- 用户主页

技术栈:
- Flask + Tailwind CSS
- JSON文件存储
- JWT认证
- 响应式设计

端口: 19004
2026-04-08 12:35:09 +08:00

327 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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%); }
</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-7xl mx-auto px-4">
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-8">
<a href="/" class="text-xl font-bold text-gray-800 flex items-center gap-2">
<span class="text-2xl">👨‍💻</span> 技术论坛
</a>
<div class="hidden md:flex items-center gap-6">
<a href="/" class="text-blue-600 font-medium">首页</a>
<a href="#topics" class="text-gray-600 hover:text-blue-600">工具分享</a>
<a href="#tags" class="text-gray-600 hover:text-blue-600">标签</a>
</div>
</div>
<div class="flex items-center gap-4">
<!-- 搜索框 -->
<div class="relative hidden sm:block">
<i class="ri-search-line absolute left-3 top-2.5 text-gray-400"></i>
<input type="text" id="searchInput" placeholder="搜索帖子..."
class="pl-9 pr-4 py-2 border border-gray-200 rounded-lg w-48 focus:w-64 transition-all focus:ring-2 focus:ring-blue-500">
</div>
<!-- 用户状态 -->
<div id="userArea"></div>
</div>
</div>
</div>
</nav>
<!-- 主内容 -->
<main class="max-w-7xl mx-auto px-4 py-8">
<div class="flex gap-8">
<!-- 左侧内容 -->
<div class="flex-1">
<!-- Tab切换 -->
<div class="bg-white rounded-lg border border-gray-100 mb-6">
<div class="flex border-b border-gray-100">
<button onclick="loadPosts('all')" id="tab-all" class="px-6 py-4 text-blue-600 border-b-2 border-blue-600 font-medium">
全部
</button>
<button onclick="loadPosts('discussion')" id="tab-discussion" class="px-6 py-4 text-gray-500 hover:text-gray-700">
技术交流
</button>
<button onclick="loadPosts('share')" id="tab-share" class="px-6 py-4 text-gray-500 hover:text-gray-700">
工具分享
</button>
</div>
</div>
<!-- 帖子列表 -->
<div id="postList" class="space-y-4">
<div class="text-center py-12 text-gray-500">加载中...</div>
</div>
<!-- 加载更多 -->
<div id="loadMore" class="text-center py-8 hidden">
<button onclick="loadMorePosts()" class="px-6 py-2 border border-gray-300 rounded-lg text-gray-600 hover:bg-gray-50">
加载更多
</button>
</div>
</div>
<!-- 右侧边栏 -->
<div class="hidden lg:block w-80">
<!-- 发帖按钮 -->
<a href="/create" class="block w-full py-3 gradient-bg text-white text-center rounded-lg font-medium hover:opacity-90 mb-6">
<i class="ri-add-line mr-1"></i> 发布帖子
</a>
<!-- 热门标签 -->
<div id="tags" class="bg-white rounded-lg border border-gray-100 p-4 mb-6">
<h3 class="font-medium text-gray-800 mb-3 flex items-center gap-2">
<i class="ri-price-tag-3-line text-blue-500"></i> 热门标签
</h3>
<div id="tagList" class="flex flex-wrap gap-2">
<span class="text-gray-400 text-sm">加载中...</span>
</div>
</div>
<!-- 热门工具分享主题 -->
<div id="topics" class="bg-white rounded-lg border border-gray-100 p-4">
<h3 class="font-medium text-gray-800 mb-3 flex items-center gap-2">
<i class="ri-tools-line text-purple-500"></i> 工具分享
</h3>
<div id="topicList" class="space-y-3">
<span class="text-gray-400 text-sm">加载中...</span>
</div>
</div>
</div>
</div>
</main>
<!-- 搜索结果弹窗 -->
<div id="searchModal" 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-[80vh] overflow-hidden">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="font-bold">搜索结果</h3>
<button onclick="closeSearchModal()" class="text-gray-400"><i class="ri-close-line text-xl"></i></button>
</div>
<div id="searchResults" class="p-4 overflow-auto max-h-[60vh]"></div>
</div>
</div>
<script>
let currentUser = null;
let currentType = 'all';
let currentPage = 1;
// 初始化
document.addEventListener('DOMContentLoaded', () => {
checkLogin();
loadPosts('all');
loadTags();
loadTopics();
});
// 检查登录状态
async function checkLogin() {
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
if (token && user) {
currentUser = JSON.parse(user);
renderUserArea();
} else {
renderLoginButton();
}
}
function renderUserArea() {
document.getElementById('userArea').innerHTML = `
<div class="flex items-center gap-3">
<a href="/create" class="px-4 py-2 gradient-bg text-white rounded-lg text-sm hidden sm:block">
发布帖子
</a>
<div class="relative group">
<button class="flex items-center gap-2">
<img src="${currentUser.avatar}" class="w-8 h-8 rounded-full">
<span class="hidden sm:inline text-gray-700">${currentUser.username}</span>
</button>
<div class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-100 hidden group-hover:block">
<a href="/user/${currentUser.id}" class="block px-4 py-2 text-gray-700 hover:bg-gray-50">个人主页</a>
<button onclick="logout()" class="block w-full text-left px-4 py-2 text-red-500 hover:bg-gray-50">退出登录</button>
</div>
</div>
</div>
`;
}
function renderLoginButton() {
document.getElementById('userArea').innerHTML = `
<a href="/login" class="px-4 py-2 text-gray-600 hover:text-blue-600">登录</a>
<a href="/register" class="px-4 py-2 gradient-bg text-white rounded-lg">注册</a>
`;
}
function logout() {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.reload();
}
// 加载帖子
async function loadPosts(type) {
currentType = type;
currentPage = 1;
// 更新Tab样式
document.querySelectorAll('[id^="tab-"]').forEach(tab => {
tab.classList.remove('text-blue-600', 'border-b-2', 'border-blue-600', 'font-medium');
tab.classList.add('text-gray-500');
});
document.getElementById(`tab-${type}`).classList.remove('text-gray-500');
document.getElementById(`tab-${type}`).classList.add('text-blue-600', 'border-b-2', 'border-blue-600', 'font-medium');
const url = type === 'all' ? '/api/posts' : `/api/posts?type=${type}`;
const res = await fetch(url);
const data = await res.json();
renderPosts(data.posts);
if (data.posts.length >= 20) {
document.getElementById('loadMore').classList.remove('hidden');
} else {
document.getElementById('loadMore').classList.add('hidden');
}
}
function renderPosts(posts) {
const container = document.getElementById('postList');
if (posts.length === 0) {
container.innerHTML = '<div class="text-center py-12 text-gray-500">暂无帖子</div>';
return;
}
container.innerHTML = posts.map(post => `
<a href="/post/${post.id}" class="block bg-white rounded-lg border border-gray-100 p-5 hover:shadow-md transition-shadow">
<div class="flex items-start gap-4">
<img src="${post.author.avatar}" class="w-10 h-10 rounded-full">
<div class="flex-1">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs px-2 py-0.5 rounded ${post.type === 'discussion' ? 'bg-blue-100 text-blue-600' : 'bg-purple-100 text-purple-600'}">
${post.type === 'discussion' ? '技术交流' : '工具分享'}
</span>
${post.is_pinned ? '<span class="text-xs px-2 py-0.5 rounded bg-red-100 text-red-600">置顶</span>' : ''}
</div>
<h3 class="font-medium text-gray-900 hover:text-blue-600 mb-1">${post.title}</h3>
<p class="text-sm text-gray-500 line-clamp-2">${post.content_preview}</p>
<div class="flex items-center gap-4 mt-3 text-xs text-gray-400">
<span>${post.author.username}</span>
<span>${new Date(post.created_at).toLocaleDateString()}</span>
<span><i class="ri-eye-line"></i> ${post.views}</span>
<span><i class="ri-heart-line"></i> ${post.likes}</span>
<span><i class="ri-chat-3-line"></i> ${post.replies}</span>
</div>
${post.tags.length > 0 ? `
<div class="flex gap-2 mt-2">
${post.tags.slice(0, 3).map(tag => `
<span class="text-xs px-2 py-0.5 bg-gray-100 text-gray-600 rounded">${tag}</span>
`).join('')}
</div>
` : ''}
</div>
</div>
</a>
`).join('');
}
// 加载标签
async function loadTags() {
const res = await fetch('/api/tags');
const tags = await res.json();
const container = document.getElementById('tagList');
container.innerHTML = tags.slice(0, 10).map(tag => `
<a href="?tag=${tag.name}" class="px-3 py-1 bg-gray-100 hover:bg-blue-100 text-gray-700 hover:text-blue-700 rounded-full text-sm">
${tag.name} (${tag.count})
</a>
`).join('');
}
// 加载工具分享主题
async function loadTopics() {
const res = await fetch('/api/topics');
const topics = await res.json();
const container = document.getElementById('topicList');
container.innerHTML = topics.slice(0, 5).map(topic => `
<a href="/topic/${topic.id}" class="flex items-center gap-3 p-2 hover:bg-gray-50 rounded-lg">
<span class="text-2xl">${topic.icon}</span>
<div class="flex-1 min-w-0">
<p class="font-medium text-gray-800 truncate">${topic.name}</p>
<p class="text-xs text-gray-500">${topic.questions_count} 问题 · ${topic.followers} 关注</p>
</div>
</a>
`).join('');
}
// 搜索功能
document.getElementById('searchInput').addEventListener('keyup', async (e) => {
if (e.key === 'Enter') {
const query = e.target.value.trim();
if (!query) return;
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await res.json();
const container = document.getElementById('searchResults');
if (data.posts.length === 0 && data.topics.length === 0) {
container.innerHTML = '<p class="text-center text-gray-500 py-8">未找到相关内容</p>';
} else {
container.innerHTML = `
${data.posts.length > 0 ? `
<div class="mb-6">
<h4 class="font-medium text-gray-700 mb-3">帖子 (${data.posts.length})</h4>
<div class="space-y-2">
${data.posts.map(p => `
<a href="/post/${p.id}" class="block p-3 hover:bg-gray-50 rounded-lg">
<p class="font-medium text-gray-800">${p.title}</p>
<p class="text-sm text-gray-500">${p.author} · ${new Date(p.created_at).toLocaleDateString()}</p>
</a>
`).join('')}
</div>
</div>
` : ''}
${data.topics.length > 0 ? `
<div>
<h4 class="font-medium text-gray-700 mb-3">工具分享 (${data.topics.length})</h4>
<div class="space-y-2">
${data.topics.map(t => `
<a href="/topic/${t.id}" class="flex items-center gap-2 p-3 hover:bg-gray-50 rounded-lg">
<span class="text-xl">${t.icon}</span>
<span class="font-medium text-gray-800">${t.name}</span>
</a>
`).join('')}
</div>
</div>
` : ''}
`;
}
document.getElementById('searchModal').classList.remove('hidden');
document.getElementById('searchModal').classList.add('flex');
}
});
function closeSearchModal() {
document.getElementById('searchModal').classList.add('hidden');
document.getElementById('searchModal').classList.remove('flex');
}
</script>
</body>
</html>