跳到主要内容使用 FastAPI 和 HTML/CSS/JavaScript 构建博客系统示例 | 极客日志Python大前端
使用 FastAPI 和 HTML/CSS/JavaScript 构建博客系统示例
综述由AI生成介绍如何使用 FastAPI 作为后端框架,配合 HTML、CSS 和 JavaScript 构建前后端分离的博客系统。项目包含 RESTful API 设计、数据模型管理、响应式界面及暗色模式切换等功能。通过示例代码展示了文章的增删改查(CRUD)逻辑,并提供了运行环境配置说明,适合学习 Web 全栈开发基础架构。
人间失格27 浏览 基于 FastAPI 与 HTML/CSS/JavaScript 的博客系统示例
本项目展示了一个前后端分离的博客系统架构,后端采用 FastAPI 框架,前端使用原生 HTML、CSS 和 JavaScript。
项目结构
blog_system/
├── backend/
│ ├── main.py
│ ├── models.py
│ ├── database.py
│ └── requirements.txt
├── frontend/
│ ├── index.html
│ ├── css/
│ │ └── style.css
│ └── js/
│ ├── app.js
│ ├── api.js
│ └── ui.js
后端代码
backend/models.py
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
from datetime import datetime
from typing import , ,
models BlogPost
:
():
.posts = []
.post_id_counter =
.init_sample_data()
():
.posts:
sample_posts = [
BlogPost(=, title=, content=, author=, created_at=, updated_at=),
BlogPost(=, title=, content=, author=, created_at=, updated_at=),
BlogPost(=, title=, content=, author=, created_at=, updated_at=)
]
post sample_posts:
.posts.append(post.())
.post_id_counter =
() -> []:
.posts
() -> []:
post .posts:
post[] == post_id:
post
() -> :
post = BlogPost(**post_data)
post. = .post_id_counter
post.created_at = datetime.now().strftime()
post.updated_at = post.created_at
post_dict = post.()
.posts.append(post_dict)
.post_id_counter +=
post_dict
() -> []:
i, existing_post (.posts):
existing_post[] == post_id:
post_data[] = post_id
post_data[] = existing_post[]
post_data[] = datetime.now().strftime()
.posts[i] = post_data
post_data
() -> :
i, post (.posts):
post[] == post_id:
.posts.pop(i)
List
Dict
Optional
from
import
class
BlogDatabase
def
__init__
self
self
self
1
self
def
init_sample_data
self
"""初始化示例数据"""
if
not
self
id
1
"欢迎来到我的博客"
"这是我的第一篇博客文章,欢迎阅读!"
"管理员"
"2024-01-01 10:00:00"
"2024-01-01 10:00:00"
id
2
"NiceGUI 3.X 新特性介绍"
"NiceGUI 3.X 带来了很多令人兴奋的新特性..."
"技术达人"
"2024-01-02 14:30:00"
"2024-01-02 14:30:00"
id
3
"Python Web 开发趋势"
"近年来,Python 在 Web 开发领域发展迅速..."
"Python 爱好者"
"2024-01-03 09:15:00"
"2024-01-03 09:15:00"
for
in
self
dict
self
4
def
get_all_posts
self
List
Dict
"""获取所有文章"""
return
self
def
get_post
self, post_id: int
Optional
Dict
"""根据 ID 获取文章"""
for
in
self
if
"id"
return
return
None
def
create_post
self, post_data: Dict
Dict
"""创建新文章"""
id
self
"%Y-%m-%d %H:%M:%S"
dict
self
self
1
return
def
update_post
self, post_id: int, post_data: Dict
Optional
Dict
"""更新文章"""
for
in
enumerate
self
if
"id"
"id"
"created_at"
"created_at"
"updated_at"
"%Y-%m-%d %H:%M:%S"
self
return
return
None
def
delete_post
self, post_id: int
bool
"""删除文章"""
for
in
enumerate
self
if
"id"
self
return
True
return
False
backend/main.py
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
app = FastAPI(
title="博客系统 API",
description="一个简单的博客系统后端",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
db = BlogDatabase()
@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
<!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/[email protected]/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">
</main>
<div id="modal-container"></div>
<div class="loading-overlay" id="loading-overlay">
<div class="spinner"></div>
</div>
<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
: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
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;
}
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
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>
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : type === 'warning' ? 'exclamation-triangle' : '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>${options.title || ''}</h3>
<button 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 onclick="this.closest('.modal').remove(); ${onCancel ? 'onCancel()' : ''}">取消</button>
<button 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-clock"></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 class="post-actions">
<button class="btn btn-secondary" onclick="showPage('home')"><i class="fas fa-arrow-left"></i> 返回</button>
</div>
<h1>${post.title}</h1>
<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>
${post.updated_at !== post.created_at ? `<span><i class="fas fa-sync"></i> 更新:${post.updated_at}</span>` : ''}
</div>
<div class="post-content">${cleanContent}</div>
<div class="post-actions">
<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 class="post-actions">
<button class="btn btn-secondary" onclick="showPage('${isEdit ? `view/${post.id}` : 'home'}')"><i class="fas fa-arrow-left"></i> 返回</button>
</div>
<h2>${isEdit ? '编辑文章' : '写新文章'}</h2>
<form onsubmit="handleSubmitPost(event, ${isEdit ? post.id : 'null'})">
<div class="form-group">
<label class="form-label">标题</label>
<input type="text" class="form-control" value="${post ? post.title : ''}" required>
</div>
<div class="form-group">
<label class="form-label">作者</label>
<input type="text" class="form-control" value="${post ? post.author : '管理员'}" required>
</div>
<div class="form-group">
<label class="form-label">内容</label>
<textarea class="form-control" rows="10" required>${post ? post.content : ''}</textarea>
</div>
<div class="post-actions">
<button type="button" class="btn btn-secondary" onclick="showPage('${isEdit ? `view/${post.id}` : 'home'}')">取消</button>
<button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> ${isEdit ? '更新文章' : '发布文章'}</button>
</div>
</form>
</div>
`;
}
function renderPostTable(posts) {
if (posts.length === 0) {
return `
<div class="card">
<i class="fas fa-inbox"></i>
<h3>暂无文章</h3>
<p>点击"写文章"按钮开始创作</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-sm btn-primary" onclick="viewPost(${post.id})"><i class="fas fa-eye"></i></button>
<button class="btn btn-sm btn-secondary" onclick="editPost(${post.id})"><i class="fas fa-edit"></i></button>
<button class="btn btn-sm btn-danger" onclick="deletePost(${post.id})"><i class="fas fa-trash"></i></button>
</td>
</tr>
`).join('');
return `
<div class="card">
<h2>文章管理</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>作者</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
`;
}
frontend/js/app.js
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">
<i class="fas fa-exclamation-triangle"></i>
<h3>加载失败</h3>
<p>${error.message}</p>
<button onclick="showPage('home')">返回首页</button>
</div>
`;
}
}
async function loadHomePage() {
const mainContent = document.getElementById('main-content');
mainContent.innerHTML = `
<div class="card">
<h1>📚 最新文章</h1>
<p>欢迎阅读我们的博客文章</p>
</div>
<div id="posts-container">
<div class="card">
<div class="spinner"></div>
<p>加载中...</p>
</div>
</div>
`;
try {
const posts = await BlogAPI.getAllPosts();
if (posts.length === 0) {
document.getElementById('posts-container').innerHTML = `
<div class="card">
<i class="fas fa-inbox"></i>
<h3>暂无文章</h3>
<p>点击"写文章"按钮开始创作</p>
<button class="btn btn-primary" 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">
<i class="fas fa-exclamation-circle"></i>
<p>加载文章失败,请刷新页面重试</p>
</div>
`;
}
}
async function loadPostPage(postId) {
const mainContent = document.getElementById('main-content');
mainContent.innerHTML = `
<div class="card">
<div class="spinner"></div>
<p>加载中...</p>
</div>
`;
try {
const post = await BlogAPI.getPost(postId);
mainContent.innerHTML = renderPostDetail(post);
} catch (error) {
mainContent.innerHTML = `
<div class="card">
<i class="fas fa-exclamation-circle"></i>
<h3>文章不存在</h3>
<p>该文章可能已被删除</p>
<button onclick="showPage('home')">返回首页</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 class="card">
<div class="spinner"></div>
<p>加载中...</p>
</div>
`;
try {
const post = await BlogAPI.getPost(postId);
mainContent.innerHTML = renderPostForm(post);
} catch (error) {
mainContent.innerHTML = `
<div class="card">
<i class="fas fa-exclamation-circle"></i>
<h3>文章不存在</h3>
<p>无法编辑不存在的文章</p>
<button onclick="showPage('home')">返回首页</button>
</div>
`;
}
}
async function loadManagePage() {
const mainContent = document.getElementById('main-content');
mainContent.innerHTML = `
<div class="card">
<h1>📋 文章管理</h1>
<p>管理所有博客文章</p>
</div>
<div id="manage-container">
<div class="card">
<div class="spinner"></div>
<p>加载中...</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">
<i class="fas fa-exclamation-circle"></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.querySelector('input[type="text"]').value.trim();
const author = document.querySelectorAll('input[type="text"]')[1].value.trim();
const content = document.querySelector('textarea').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');
}
}
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. 启动后端服务
cd backend
pip install -r requirements.txt
python main.py
后端服务将在 http://localhost:8000 启动,可以通过 http://localhost:8000/docs 查看 API 文档。
2. 启动前端服务
由于前端是纯静态文件,可以使用任意 HTTP 服务器。最简单的方法是使用 Python 的内置服务器:
cd frontend
python -m http.server 3000
或者使用 Node.js 的 http-server:
npm install -g http-server
http-server -p 3000
前端服务将在 http://localhost:3000 启动。
3. 访问应用
打开浏览器访问 http://localhost:3000 即可使用博客系统。
主要特性
- 前后端分离:清晰的前端和后端架构
- RESTful API:标准的 REST API 设计
- 响应式设计:适配各种屏幕尺寸
- 暗色/亮色主题:支持主题切换
- Markdown 支持:文章内容支持 Markdown 格式
- 实时交互:无需刷新页面的操作体验
- 错误处理:完善的错误处理和用户提示
- 加载状态:显示加载动画和状态
相关免费在线工具
- curl 转代码
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online