跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Python大前端

使用 FastAPI 和 HTML/CSS/JavaScript 构建博客系统示例

介绍如何使用 FastAPI 作为后端框架,配合 HTML、CSS 和 JavaScript 构建前后端分离的博客系统。项目包含 RESTful API 设计、数据模型管理、响应式界面及暗色模式切换等功能。通过示例代码展示了文章的增删改查(CRUD)逻辑,并提供了运行环境配置说明,适合学习 Web 全栈开发基础架构。

人间失格发布于 2026/3/29更新于 2026/10/25112 浏览
使用 FastAPI 和 HTML/CSS/JavaScript 构建博客系统示例

基于 FastAPI 与 HTML/CSS/JavaScript 的博客系统示例

本项目展示了一个前后端分离的博客系统架构,后端采用 FastAPI 框架,前端使用原生 HTML、CSS 和 JavaScript。

项目结构

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

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

# 创建 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

<!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">
        <!-- 首页内容将通过 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

/* 基础样式 */
: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

// 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

// 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>
            <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');
    }
}

// 添加 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. 启动后端服务

# 进入后端目录
cd backend
# 安装依赖
pip install -r requirements.txt
# 启动后端服务器
python main.py

后端服务将在 http://localhost:8000 启动,可以通过 http://localhost:8000/docs 查看 API 文档。

2. 启动前端服务

由于前端是纯静态文件,可以使用任意 HTTP 服务器。最简单的方法是使用 Python 的内置服务器:

# 进入前端目录
cd frontend
# 使用 Python 启动 HTTP 服务器
python -m http.server 3000

或者使用 Node.js 的 http-server:

# 全局安装 http-server
npm install -g http-server
# 启动服务器
http-server -p 3000

前端服务将在 http://localhost:3000 启动。

3. 访问应用

打开浏览器访问 http://localhost:3000 即可使用博客系统。

主要特性

  1. 前后端分离:清晰的前端和后端架构
  2. RESTful API:标准的 REST API 设计
  3. 响应式设计:适配各种屏幕尺寸
  4. 暗色/亮色主题:支持主题切换
  5. Markdown 支持:文章内容支持 Markdown 格式
  6. 实时交互:无需刷新页面的操作体验
  7. 错误处理:完善的错误处理和用户提示
  8. 加载状态:显示加载动画和状态

标准的 Web 开发架构,便于维护和扩展。

目录

  1. 基于 FastAPI 与 HTML/CSS/JavaScript 的博客系统示例
  2. 项目结构
  3. 后端代码
  4. backend/models.py
  5. backend/database.py
  6. backend/main.py
  7. 创建 FastAPI 应用
  8. 添加 CORS 中间件
  9. 初始化数据库
  10. API 路由
  11. 健康检查端点
  12. backend/requirements.txt
  13. 前端代码
  14. frontend/index.html
  15. frontend/css/style.css
  16. frontend/js/api.js
  17. frontend/js/ui.js
  18. frontend/js/app.js
  19. 运行说明
  20. 1. 启动后端服务
  21. 进入后端目录
  22. 安装依赖
  23. 启动后端服务器
  24. 2. 启动前端服务
  25. 进入前端目录
  26. 使用 Python 启动 HTTP 服务器
  27. 全局安装 http-server
  28. 启动服务器
  29. 3. 访问应用
  30. 主要特性

更多推荐文章

查看全部
  • 《大模型应用开发极简入门》核心技术与实战指南
  • 基于 Unity 2022 LTS 与 UXR 3.0 SDK 开发 Rokid AR 消消乐游戏
  • Java 反射与方法句柄:动态编程深度解析
  • TI AFE5816:16 通道超声波模拟前端 (AFE) 详解
  • OpenClaw 接入 QVeris:为 AI 助手赋予实时数据查询能力
  • RabbitMQ/Spring-AMQP 事务与消息限流高级特性详解
  • Spring 事务管理与传播机制详解
  • 大语言模型 (LLM) 基础:原理、应用与挑战
  • 农业大模型:关键技术、应用场景与未来趋势
  • Web3 核心概念与业务应用场景解析
  • TRO 2026 G3M:基于图生成建模的视频预训练机器人操作
  • ASP.NET Core 主机模型:Host、WebHost 与 WebApplication 怎么选
  • 前端通用 AI Rules 定义:适配 Cursor、Trae 等主流开发工具
  • CherryStudio 使用指南:本地 AI 模型管理与知识库配置
  • 宇树 Unitree 机器人 ROS 2 环境部署指南 (Humble + 真实硬件)
  • SpringBoot 启动引导类的命名约定与核心原理
  • 前端视角的 API 设计最佳实践
  • Java 实现百度地图 SN 权限签名:搜索 2.0 接口实战
  • AI 绘画精讲与 AIGC 时代游戏美术设计:从入门到精通
  • 大模型面试题精选与详细答案解析

相关免费在线工具

  • 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