用FastAPI 后端 和 HTML/CSS/JavaScript 前端写一个博客系统 例

博客系统用 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 即可使用博客系统。

主要特性

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

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

相关推荐
名字越长技术越强14 小时前
html\css\js(一)
javascript·css·html
hunter145014 小时前
2026.1.4 html简单制作
java·前端·笔记·html
鹏程十八少14 小时前
Android 深入剖析Android内存泄漏:ViewPager2与Fragment的生命周期陷阱
android·前端·app
李少兄14 小时前
深入理解 CSS opacity 属性
前端·css
幺零九零零14 小时前
前端测试·1
前端
1024小神14 小时前
uniapp项目中使用vue3和小程序组件父子通信
前端·小程序·uni-app
Knight_AL14 小时前
Vue + Spring Boot 项目添加 /wvp 前缀的完整链路解析(从浏览器到静态资源)
前端·vue.js·spring boot
粟悟饭&龟波功14 小时前
【软考系统架构设计师】九、架构演化与维护
前端·后端·架构·系统架构·软件工程
广州华水科技14 小时前
单北斗GNSS的变形监测应用是什么?主要用于大坝的安全监测吗?
前端