博客系统用 HTML/CSS/JavaScript 前端和 FastAPI 后端的写分离架构。

项目结构
blog_system/
├── backend/
│ ├── main.py # FastAPI 后端
│ ├── models.py # 数据模型
│ ├── database.py # 数据库/状态管理
│ └── requirements.txt
├── frontend/
│ ├── index.html # 主页面
│ ├── css/
│ │ └── style.css # 样式文件
│ └── js/
│ ├── app.js # 主应用逻辑
│ ├── api.js # API 调用封装
│ └── ui.js # UI 组件
后端代码
backend/models.py
python
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
class BlogPost(BaseModel):
id: Optional[int] = None
title: str
content: str
author: str
created_at: Optional[str] = None
updated_at: Optional[str] = None
backend/database.py
python
from datetime import datetime
from typing import List, Dict, Optional
from models import BlogPost
class BlogDatabase:
def __init__(self):
self.posts = []
self.post_id_counter = 1
self.init_sample_data()
def init_sample_data(self):
"""初始化示例数据"""
if not self.posts:
sample_posts = [
BlogPost(
id=1,
title="欢迎来到我的博客",
content="这是我的第一篇博客文章,欢迎阅读!",
author="管理员",
created_at="2024-01-01 10:00:00",
updated_at="2024-01-01 10:00:00"
),
BlogPost(
id=2,
title="NiceGUI 3.X 新特性介绍",
content="NiceGUI 3.X 带来了很多令人兴奋的新特性...",
author="技术达人",
created_at="2024-01-02 14:30:00",
updated_at="2024-01-02 14:30:00"
),
BlogPost(
id=3,
title="Python Web开发趋势",
content="近年来,Python在Web开发领域发展迅速...",
author="Python爱好者",
created_at="2024-01-03 09:15:00",
updated_at="2024-01-03 09:15:00"
)
]
for post in sample_posts:
self.posts.append(post.dict())
self.post_id_counter = 4
def get_all_posts(self) -> List[Dict]:
"""获取所有文章"""
return self.posts
def get_post(self, post_id: int) -> Optional[Dict]:
"""根据ID获取文章"""
for post in self.posts:
if post["id"] == post_id:
return post
return None
def create_post(self, post_data: Dict) -> Dict:
"""创建新文章"""
post = BlogPost(**post_data)
post.id = self.post_id_counter
post.created_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
post.updated_at = post.created_at
post_dict = post.dict()
self.posts.append(post_dict)
self.post_id_counter += 1
return post_dict
def update_post(self, post_id: int, post_data: Dict) -> Optional[Dict]:
"""更新文章"""
for i, existing_post in enumerate(self.posts):
if existing_post["id"] == post_id:
post_data["id"] = post_id
post_data["created_at"] = existing_post["created_at"]
post_data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.posts[i] = post_data
return post_data
return None
def delete_post(self, post_id: int) -> bool:
"""删除文章"""
for i, post in enumerate(self.posts):
if post["id"] == post_id:
self.posts.pop(i)
return True
return False
backend/main.py
python
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import List
import uvicorn
from models import BlogPost
from database import BlogDatabase
# 创建 FastAPI 应用
app = FastAPI(
title="博客系统 API",
description="一个简单的博客系统后端",
version="1.0.0"
)
# 添加 CORS 中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 在生产环境中应该指定具体域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 初始化数据库
db = BlogDatabase()
# API 路由
@app.get("/")
async def root():
return {
"message": "欢迎访问博客系统 API",
"docs": "/docs",
"endpoints": {
"获取所有文章": "GET /api/posts",
"获取单篇文章": "GET /api/posts/{id}",
"创建文章": "POST /api/posts",
"更新文章": "PUT /api/posts/{id}",
"删除文章": "DELETE /api/posts/{id}"
}
}
@app.get("/api/posts", response_model=List[BlogPost])
async def get_all_posts():
"""获取所有博客文章"""
return db.get_all_posts()
@app.get("/api/posts/{post_id}", response_model=BlogPost)
async def get_post(post_id: int):
"""根据ID获取博客文章"""
post = db.get_post(post_id)
if not post:
raise HTTPException(status_code=404, detail="文章不存在")
return post
@app.post("/api/posts", response_model=BlogPost)
async def create_post(post: BlogPost):
"""创建新博客文章"""
return db.create_post(post.dict())
@app.put("/api/posts/{post_id}", response_model=BlogPost)
async def update_post(post_id: int, post: BlogPost):
"""更新博客文章"""
updated = db.update_post(post_id, post.dict())
if not updated:
raise HTTPException(status_code=404, detail="文章不存在")
return updated
@app.delete("/api/posts/{post_id}")
async def delete_post(post_id: int):
"""删除博客文章"""
if db.delete_post(post_id):
return {"message": "文章删除成功"}
raise HTTPException(status_code=404, detail="文章不存在")
# 健康检查端点
@app.get("/health")
async def health_check():
return {"status": "healthy", "posts_count": len(db.get_all_posts())}
if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True
)
backend/requirements.txt
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0
python-multipart==0.0.6
前端代码
frontend/index.html
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>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-container">
<div class="nav-brand">
<i class="fas fa-blog"></i>
<span>博客系统</span>
</div>
<div class="nav-links">
<a href="#" onclick="showPage('home')">
<i class="fas fa-home"></i> 首页
</a>
<a href="#" onclick="showPage('manage')">
<i class="fas fa-tasks"></i> 管理
</a>
<button class="btn btn-success" onclick="showPage('create')">
<i class="fas fa-plus"></i> 写文章
</button>
<button class="btn-toggle-theme" onclick="toggleTheme()">
<i class="fas fa-moon"></i>
</button>
</div>
</div>
</nav>
<!-- 主内容区域 -->
<main class="container" id="main-content">
<!-- 首页内容将通过JavaScript动态加载 -->
</main>
<!-- 模态框容器 -->
<div id="modal-container"></div>
<!-- 加载指示器 -->
<div class="loading-overlay" id="loading-overlay">
<div class="spinner"></div>
</div>
<!-- JavaScript文件 -->
<script src="js/api.js"></script>
<script src="js/ui.js"></script>
<script src="js/app.js"></script>
<script>
// 初始化应用
document.addEventListener('DOMContentLoaded', function() {
initApp();
});
</script>
</body>
</html>
frontend/css/style.css
css
/* 基础样式 */
:root {
--primary-color: #3b82f6;
--primary-dark: #2563eb;
--secondary-color: #6b7280;
--success-color: #10b981;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--light-color: #f9fafb;
--dark-color: #1f2937;
--border-color: #e5e7eb;
--shadow-color: rgba(0, 0, 0, 0.1);
--card-bg: #ffffff;
--body-bg: #f3f4f6;
--text-color: #374151;
}
[data-theme="dark"] {
--primary-color: #60a5fa;
--primary-dark: #3b82f6;
--secondary-color: #9ca3af;
--success-color: #34d399;
--danger-color: #f87171;
--warning-color: #fbbf24;
--light-color: #374151;
--dark-color: #111827;
--border-color: #4b5563;
--shadow-color: rgba(0, 0, 0, 0.3);
--card-bg: #1f2937;
--body-bg: #111827;
--text-color: #f3f4f6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--body-bg);
color: var(--text-color);
line-height: 1.6;
transition: background-color 0.3s, color 0.3s;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 导航栏 */
.navbar {
background-color: var(--primary-color);
color: white;
padding: 1rem 0;
box-shadow: 0 2px 10px var(--shadow-color);
position: sticky;
top: 0;
z-index: 1000;
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-brand {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.5rem;
font-weight: bold;
}
.nav-brand i {
font-size: 1.8rem;
}
.nav-links {
display: flex;
align-items: center;
gap: 15px;
}
.nav-links a {
color: white;
text-decoration: none;
padding: 8px 16px;
border-radius: 6px;
transition: background-color 0.3s;
}
.nav-links a:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* 按钮样式 */
.btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.3s;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow-color);
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
}
.btn-success {
background-color: var(--success-color);
color: white;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
}
.btn-outline {
background-color: transparent;
border: 2px solid var(--primary-color);
color: var(--primary-color);
}
.btn-outline:hover {
background-color: var(--primary-color);
color: white;
}
.btn-toggle-theme {
background: none;
border: none;
color: white;
font-size: 1.2rem;
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background-color 0.3s;
}
.btn-toggle-theme:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* 卡片样式 */
.card {
background-color: var(--card-bg);
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 4px 20px var(--shadow-color);
transition: transform 0.3s, box-shadow 0.3s;
border: 1px solid var(--border-color);
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px var(--shadow-color);
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-color);
}
.form-control {
width: 100%;
padding: 12px;
border: 2px solid var(--border-color);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s;
background-color: var(--card-bg);
color: var(--text-color);
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
}
textarea.form-control {
min-height: 200px;
resize: vertical;
font-family: 'Noto Sans SC', sans-serif;
}
/* 徽章 */
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.badge-primary {
background-color: var(--primary-color);
color: white;
}
.badge-success {
background-color: var(--success-color);
color: white;
}
/* 加载动画 */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 2000;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid var(--border-color);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 模态框 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 1001;
padding: 20px;
}
.modal-content {
background-color: var(--card-bg);
border-radius: 12px;
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 1.5rem;
font-weight: bold;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--secondary-color);
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 文章列表 */
.post-list {
display: grid;
gap: 20px;
}
.post-card {
background-color: var(--card-bg);
border-radius: 12px;
padding: 24px;
border: 1px solid var(--border-color);
transition: all 0.3s;
}
.post-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px var(--shadow-color);
}
.post-title {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 10px;
color: var(--text-color);
}
.post-meta {
display: flex;
gap: 15px;
margin-bottom: 15px;
color: var(--secondary-color);
font-size: 0.9rem;
}
.post-content-preview {
color: var(--text-color);
line-height: 1.6;
margin-bottom: 15px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.nav-container {
flex-direction: column;
gap: 15px;
}
.nav-links {
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
.container {
padding: 10px;
}
.post-meta {
flex-direction: column;
gap: 5px;
}
}
frontend/js/api.js
javascript
// API基础配置
const API_BASE_URL = 'http://localhost:8000';
const API_ENDPOINTS = {
POSTS: '/api/posts',
POST: (id) => `/api/posts/${id}`
};
// 显示/隐藏加载指示器
function showLoading() {
document.getElementById('loading-overlay').style.display = 'flex';
}
function hideLoading() {
document.getElementById('loading-overlay').style.display = 'none';
}
// 统一的错误处理
function handleApiError(error) {
console.error('API Error:', error);
let message = '网络错误,请稍后重试';
if (error.response) {
message = error.response.data?.detail || `服务器错误: ${error.response.status}`;
} else if (error.request) {
message = '无法连接到服务器,请检查网络连接';
}
alert(message);
throw error;
}
// 博客API
const BlogAPI = {
// 获取所有文章
async getAllPosts() {
try {
showLoading();
const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.POSTS}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
return data;
} catch (error) {
handleApiError(error);
} finally {
hideLoading();
}
},
// 获取单篇文章
async getPost(id) {
try {
showLoading();
const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.POST(id)}`);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
return data;
} catch (error) {
handleApiError(error);
} finally {
hideLoading();
}
},
// 创建文章
async createPost(postData) {
try {
showLoading();
const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.POSTS}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData)
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
return data;
} catch (error) {
handleApiError(error);
} finally {
hideLoading();
}
},
// 更新文章
async updatePost(id, postData) {
try {
showLoading();
const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.POST(id)}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData)
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
return data;
} catch (error) {
handleApiError(error);
} finally {
hideLoading();
}
},
// 删除文章
async deletePost(id) {
try {
showLoading();
const response = await fetch(`${API_BASE_URL}${API_ENDPOINTS.POST(id)}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
return data;
} catch (error) {
handleApiError(error);
} finally {
hideLoading();
}
}
};
frontend/js/ui.js
javascript
// UI组件库
// 显示通知
function showNotification(message, type = 'info') {
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6'
};
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
background-color: ${colors[type]};
color: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
z-index: 1002;
animation: slideIn 0.3s ease-out;
max-width: 400px;
`;
notification.innerHTML = `
<div style="display: flex; align-items: center; gap: 10px;">
<i class="fas ${type === 'success' ? 'fa-check-circle' :
type === 'error' ? 'fa-exclamation-circle' :
type === 'warning' ? 'fa-exclamation-triangle' :
'fa-info-circle'}"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// 自动消失
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease-out forwards';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// 创建模态框
function createModal(options) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">${options.title || ''}</h3>
<button class="modal-close" onclick="this.closest('.modal').remove()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="modal-body">
${options.body || ''}
</div>
${options.footer ? `
<div class="modal-footer">
${options.footer}
</div>
` : ''}
</div>
`;
document.getElementById('modal-container').appendChild(modal);
// 点击背景关闭
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
return modal;
}
// 确认对话框
function showConfirmDialog(message, onConfirm, onCancel = null) {
const modal = createModal({
title: '确认操作',
body: message,
footer: `
<button class="btn btn-secondary" onclick="this.closest('.modal').remove(); ${onCancel ? 'onCancel()' : ''}">
取消
</button>
<button class="btn btn-danger" onclick="this.closest('.modal').remove(); onConfirm()">
确认
</button>
`
});
return modal;
}
// 渲染文章卡片
function renderPostCard(post) {
const contentPreview = post.content.length > 150
? post.content.substring(0, 150) + '...'
: post.content;
return `
<div class="post-card">
<div class="post-title">${post.title}</div>
<div class="post-meta">
<span><i class="fas fa-user"></i> ${post.author}</span>
<span><i class="fas fa-calendar"></i> ${post.created_at}</span>
</div>
<div class="post-content-preview">${contentPreview}</div>
<div class="post-actions">
<button class="btn btn-primary" onclick="viewPost(${post.id})">
<i class="fas fa-eye"></i> 查看
</button>
<button class="btn btn-secondary" onclick="editPost(${post.id})">
<i class="fas fa-edit"></i> 编辑
</button>
<button class="btn btn-danger" onclick="deletePost(${post.id})">
<i class="fas fa-trash"></i> 删除
</button>
</div>
</div>
`;
}
// 渲染文章详情
function renderPostDetail(post) {
const cleanContent = DOMPurify.sanitize(marked.parse(post.content));
return `
<div class="card">
<div style="margin-bottom: 20px;">
<button class="btn btn-secondary" onclick="showPage('home')">
<i class="fas fa-arrow-left"></i> 返回
</button>
</div>
<h1 class="post-title" style="font-size: 2rem; margin-bottom: 20px;">
${post.title}
</h1>
<div class="post-meta" style="margin-bottom: 30px;">
<span class="badge badge-primary">
<i class="fas fa-user"></i> ${post.author}
</span>
<span><i class="fas fa-calendar"></i> 创建: ${post.created_at}</span>
${post.updated_at !== post.created_at ?
`<span><i class="fas fa-history"></i> 更新: ${post.updated_at}</span>` : ''}
</div>
<div class="post-content" style="font-size: 1.1rem; line-height: 1.8;">
${cleanContent}
</div>
<div class="post-actions" style="margin-top: 30px;">
<button class="btn btn-secondary" onclick="editPost(${post.id})">
<i class="fas fa-edit"></i> 编辑文章
</button>
<button class="btn btn-danger" onclick="deletePost(${post.id})">
<i class="fas fa-trash"></i> 删除文章
</button>
</div>
</div>
`;
}
// 渲染文章表单
function renderPostForm(post = null) {
const isEdit = !!post;
return `
<div class="card">
<div style="margin-bottom: 20px;">
<button class="btn btn-secondary" onclick="showPage('${isEdit ? `view/${post.id}` : 'home'}')">
<i class="fas fa-arrow-left"></i> 返回
</button>
</div>
<h2 style="margin-bottom: 30px; color: var(--text-color);">
${isEdit ? '编辑文章' : '写新文章'}
</h2>
<form id="postForm" onsubmit="handleSubmitPost(event, ${isEdit ? post.id : 'null'})">
<div class="form-group">
<label class="form-label">标题</label>
<input type="text" class="form-control" id="postTitle"
value="${post ? post.title : ''}" required>
</div>
<div class="form-group">
<label class="form-label">作者</label>
<input type="text" class="form-control" id="postAuthor"
value="${post ? post.author : '管理员'}" required>
</div>
<div class="form-group">
<label class="form-label">内容</label>
<textarea class="form-control" id="postContent"
rows="10" required>${post ? post.content : ''}</textarea>
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;">
<button type="button" class="btn btn-secondary"
onclick="showPage('${isEdit ? `view/${post.id}` : 'home'}')">
取消
</button>
<button type="submit" class="btn btn-success">
<i class="fas fa-paper-plane"></i>
${isEdit ? '更新文章' : '发布文章'}
</button>
</div>
</form>
</div>
`;
}
// 渲染文章管理表格
function renderPostTable(posts) {
if (posts.length === 0) {
return `
<div class="card" style="text-align: center; padding: 40px;">
<i class="fas fa-inbox" style="font-size: 3rem; color: var(--secondary-color); margin-bottom: 20px;"></i>
<h3 style="color: var(--text-color); margin-bottom: 10px;">暂无文章</h3>
<p style="color: var(--secondary-color);">点击"写文章"按钮开始创作</p>
</div>
`;
}
const rows = posts.map(post => `
<tr>
<td>${post.id}</td>
<td>${post.title}</td>
<td>${post.author}</td>
<td>${post.created_at}</td>
<td>
<button class="btn btn-primary btn-sm" onclick="viewPost(${post.id})">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-secondary btn-sm" onclick="editPost(${post.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-danger btn-sm" onclick="deletePost(${post.id})">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`).join('');
return `
<div class="card">
<h2 style="margin-bottom: 20px; color: var(--text-color);">文章管理</h2>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background-color: var(--light-color);">
<th style="padding: 12px; text-align: left;">ID</th>
<th style="padding: 12px; text-align: left;">标题</th>
<th style="padding: 12px; text-align: left;">作者</th>
<th style="padding: 12px; text-align: left;">创建时间</th>
<th style="padding: 12px; text-align: left;">操作</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
</div>
`;
}
frontend/js/app.js
javascript
// 应用主逻辑
let currentPage = 'home';
// 初始化应用
async function initApp() {
// 设置主题
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
// 加载首页
await showPage('home');
}
// 切换主题
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// 更新按钮图标
const themeButton = document.querySelector('.btn-toggle-theme i');
themeButton.className = newTheme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
// 显示页面
async function showPage(page) {
currentPage = page;
const mainContent = document.getElementById('main-content');
try {
if (page === 'home') {
await loadHomePage();
} else if (page === 'manage') {
await loadManagePage();
} else if (page === 'create') {
loadCreatePage();
} else if (page.startsWith('view/')) {
const postId = parseInt(page.split('/')[1]);
await loadPostPage(postId);
} else if (page.startsWith('edit/')) {
const postId = parseInt(page.split('/')[1]);
await loadEditPage(postId);
}
} catch (error) {
console.error('Error loading page:', error);
mainContent.innerHTML = `
<div class="card" style="text-align: center; padding: 40px;">
<i class="fas fa-exclamation-triangle" style="font-size: 3rem; color: var(--danger-color); margin-bottom: 20px;"></i>
<h3 style="color: var(--text-color); margin-bottom: 10px;">加载失败</h3>
<p style="color: var(--secondary-color);">${error.message}</p>
<button class="btn btn-primary" onclick="showPage('home')" style="margin-top: 20px;">
返回首页
</button>
</div>
`;
}
}
// 加载首页
async function loadHomePage() {
const mainContent = document.getElementById('main-content');
mainContent.innerHTML = `
<div style="margin-bottom: 30px;">
<h1 style="color: var(--text-color); margin-bottom: 10px;">📚 最新文章</h1>
<p style="color: var(--secondary-color);">欢迎阅读我们的博客文章</p>
</div>
<div id="posts-container" style="min-height: 200px;">
<div style="text-align: center; padding: 40px;">
<div class="spinner" style="margin: 0 auto;"></div>
<p style="margin-top: 20px; color: var(--secondary-color);">加载中...</p>
</div>
</div>
`;
try {
const posts = await BlogAPI.getAllPosts();
if (posts.length === 0) {
document.getElementById('posts-container').innerHTML = `
<div class="card" style="text-align: center; padding: 40px;">
<i class="fas fa-feather-alt" style="font-size: 3rem; color: var(--secondary-color); margin-bottom: 20px;"></i>
<h3 style="color: var(--text-color); margin-bottom: 10px;">暂无文章</h3>
<p style="color: var(--secondary-color); margin-bottom: 20px;">点击"写文章"按钮开始创作</p>
<button class="btn btn-success" onclick="showPage('create')">
<i class="fas fa-plus"></i> 写文章
</button>
</div>
`;
} else {
const postsHTML = posts
.slice() // 创建副本避免修改原数组
.reverse() // 最新的在前
.map(post => renderPostCard(post))
.join('');
document.getElementById('posts-container').innerHTML = `
<div class="post-list">
${postsHTML}
</div>
`;
}
} catch (error) {
document.getElementById('posts-container').innerHTML = `
<div class="card" style="text-align: center; padding: 40px; color: var(--danger-color);">
<i class="fas fa-exclamation-circle" style="font-size: 3rem; margin-bottom: 20px;"></i>
<p>加载文章失败,请刷新页面重试</p>
</div>
`;
}
}
// 加载文章详情页
async function loadPostPage(postId) {
const mainContent = document.getElementById('main-content');
mainContent.innerHTML = `
<div style="text-align: center; padding: 40px;">
<div class="spinner" style="margin: 0 auto;"></div>
<p style="margin-top: 20px; color: var(--secondary-color);">加载中...</p>
</div>
`;
try {
const post = await BlogAPI.getPost(postId);
mainContent.innerHTML = renderPostDetail(post);
} catch (error) {
mainContent.innerHTML = `
<div class="card" style="text-align: center; padding: 40px;">
<i class="fas fa-exclamation-triangle" style="font-size: 3rem; color: var(--danger-color); margin-bottom: 20px;"></i>
<h3 style="color: var(--text-color); margin-bottom: 10px;">文章不存在</h3>
<p style="color: var(--secondary-color);">该文章可能已被删除</p>
<button class="btn btn-primary" onclick="showPage('home')" style="margin-top: 20px;">
返回首页
</button>
</div>
`;
}
}
// 加载创建文章页
function loadCreatePage() {
const mainContent = document.getElementById('main-content');
mainContent.innerHTML = renderPostForm();
}
// 加载编辑文章页
async function loadEditPage(postId) {
const mainContent = document.getElementById('main-content');
mainContent.innerHTML = `
<div style="text-align: center; padding: 40px;">
<div class="spinner" style="margin: 0 auto;"></div>
<p style="margin-top: 20px; color: var(--secondary-color);">加载中...</p>
</div>
`;
try {
const post = await BlogAPI.getPost(postId);
mainContent.innerHTML = renderPostForm(post);
} catch (error) {
mainContent.innerHTML = `
<div class="card" style="text-align: center; padding: 40px;">
<i class="fas fa-exclamation-triangle" style="font-size: 3rem; color: var(--danger-color); margin-bottom: 20px;"></i>
<h3 style="color: var(--text-color); margin-bottom: 10px;">文章不存在</h3>
<p style="color: var(--secondary-color);">无法编辑不存在的文章</p>
<button class="btn btn-primary" onclick="showPage('home')" style="margin-top: 20px;">
返回首页
</button>
</div>
`;
}
}
// 加载管理页面
async function loadManagePage() {
const mainContent = document.getElementById('main-content');
mainContent.innerHTML = `
<div style="margin-bottom: 30px;">
<h1 style="color: var(--text-color); margin-bottom: 10px;">📋 文章管理</h1>
<p style="color: var(--secondary-color);">管理所有博客文章</p>
</div>
<div id="manage-container" style="min-height: 200px;">
<div style="text-align: center; padding: 40px;">
<div class="spinner" style="margin: 0 auto;"></div>
<p style="margin-top: 20px; color: var(--secondary-color);">加载中...</p>
</div>
</div>
`;
try {
const posts = await BlogAPI.getAllPosts();
document.getElementById('manage-container').innerHTML = renderPostTable(posts);
} catch (error) {
document.getElementById('manage-container').innerHTML = `
<div class="card" style="text-align: center; padding: 40px; color: var(--danger-color);">
<i class="fas fa-exclamation-circle" style="font-size: 3rem; margin-bottom: 20px;"></i>
<p>加载失败,请刷新页面重试</p>
</div>
`;
}
}
// 查看文章
function viewPost(postId) {
showPage(`view/${postId}`);
}
// 编辑文章
function editPost(postId) {
showPage(`edit/${postId}`);
}
// 删除文章
async function deletePost(postId) {
showConfirmDialog(
'确定要删除这篇文章吗?删除后无法恢复。',
async () => {
try {
await BlogAPI.deletePost(postId);
showNotification('文章删除成功', 'success');
// 根据当前页面刷新内容
if (currentPage === 'manage') {
await loadManagePage();
} else if (currentPage === 'home') {
await loadHomePage();
} else {
showPage('home');
}
} catch (error) {
showNotification('删除失败: ' + error.message, 'error');
}
}
);
}
// 处理文章提交
async function handleSubmitPost(event, postId = null) {
event.preventDefault();
const title = document.getElementById('postTitle').value.trim();
const author = document.getElementById('postAuthor').value.trim();
const content = document.getElementById('postContent').value.trim();
if (!title || !author || !content) {
showNotification('请填写所有必填字段', 'warning');
return;
}
const postData = {
title,
author,
content
};
try {
if (postId) {
// 更新文章
await BlogAPI.updatePost(postId, postData);
showNotification('文章更新成功', 'success');
showPage(`view/${postId}`);
} else {
// 创建新文章
const newPost = await BlogAPI.createPost(postData);
showNotification('文章发布成功', 'success');
showPage(`view/${newPost.id}`);
}
} catch (error) {
showNotification('操作失败: ' + error.message, 'error');
}
}
// 添加CSS动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.btn-sm {
padding: 4px 8px;
font-size: 0.8rem;
}
`;
document.head.appendChild(style);
运行说明
1. 启动后端服务
bash
# 进入后端目录
cd backend
# 安装依赖
pip install -r requirements.txt
# 启动后端服务器
python main.py
后端服务将在 http://localhost:8000 启动,可以通过 http://localhost:8000/docs 查看 API 文档。
2. 启动前端服务
由于前端是纯静态文件,可以使用任意 HTTP 服务器。最简单的方法是使用 Python 的内置服务器:
bash
# 进入前端目录
cd frontend
# 使用Python启动HTTP服务器
python -m http.server 3000
或者使用 Node.js 的 http-server:
bash
# 全局安装 http-server
npm install -g http-server
# 启动服务器
http-server -p 3000
前端服务将在 http://localhost:3000 启动。
3. 访问应用
打开浏览器访问 http://localhost:3000 即可使用博客系统。
主要特性
- 前后端分离:清晰的前端和后端架构
- RESTful API:标准的 REST API 设计
- 响应式设计:适配各种屏幕尺寸
- 暗色/亮色主题:支持主题切换
- Markdown 支持:文章内容支持 Markdown 格式
- 实时交互:无需刷新页面的操作体验
- 错误处理:完善的错误处理和用户提示
- 加载状态:显示加载动画和状态
标准的 Web 开发架构,便于维护和扩展。