FastAPI项目:从零到一搭建一个网站导航系统

文章目录

    • 前言
    • 一、环境搭建
      • [1.1 项目结构](#1.1 项目结构)
      • [1.2 安装依赖](#1.2 安装依赖)
      • [1.3 修改数据库配置](#1.3 修改数据库配置)
    • 二、完整代码
      • [2.1 main.py - 后端代码](#2.1 main.py - 后端代码)
      • [2.2 insert_data.py - 数据插入脚本](#2.2 insert_data.py - 数据插入脚本)
      • [2.3 templates/index.html - 前端代码](#2.3 templates/index.html - 前端代码)
      • [2.4 运行项目](#2.4 运行项目)

前言

本文讲解从零到一搭建一个网站导航系统,包含分类管理、搜索、收藏等功能。网站首页打开是网站分类页面,截图如下:

分类页面点开是网站详情页面:

同时可以对网站进行收藏:

系统功能如下:

  • 渐变背景:紫色渐变营造现代感
  • 分类浏览:按类别查看网站
  • 搜索功能:快速查找网站
  • 收藏系统:收藏喜欢的网站
  • 点击统计:记录网站访问次数
  • 随机推荐:发现新的网站
  • 热门排行:展示热门网站

一、环境搭建

1.1 项目结构

复制代码
website_navigator/
├── main.py
├── insert_data.py
├── templates/
│   └── index.html
└── requirements.txt

1.2 安装依赖

bash 复制代码
pip install fastapi uvicorn jinja2 sqlalchemy python-multipart

1.3 修改数据库配置

main.pyinsert_data.py 中修改数据库连接信息:

python 复制代码
DATABASE_URL = "mysql+pymysql://用户名:密码@localhost/website_navigator"

二、完整代码

2.1 main.py - 后端代码

python 复制代码
from fastapi import FastAPI, Request, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from datetime import datetime
from typing import List, Optional
import random

app = FastAPI(title="网站导航系统")
templates = Jinja2Templates(directory="templates")
# 数据库配置
DATABASE_URL = "mysql+pymysql://root:password@localhost/website_navigator"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()


# 数据库模型
class Category(Base):
    __tablename__ = "categories"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(50), unique=True, nullable=False)
    description = Column(String(200))
    icon = Column(String(50))
    color = Column(String(20), default="#6c5ce7")
    created_at = Column(DateTime, default=datetime.utcnow)
    websites = relationship("Website", back_populates="category")


class Website(Base):
    __tablename__ = "websites"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(100), nullable=False)
    url = Column(String(500), nullable=False)
    description = Column(Text)
    category_id = Column(Integer, ForeignKey("categories.id"))
    icon_url = Column(String(500))
    is_favorite = Column(Boolean, default=False)
    click_count = Column(Integer, default=0)
    created_at = Column(DateTime, default=datetime.utcnow)
    category = relationship("Category", back_populates="websites")


# 创建数据库表
Base.metadata.create_all(bind=engine)


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


# 路由定义
@app.get("/", response_class=HTMLResponse)
async def home(request: Request, category: Optional[str] = None, search: Optional[str] = None):
    db = next(get_db())

    # 获取所有分类
    categories = db.query(Category).all()

    # 获取网站列表
    query = db.query(Website)

    if category:
        query = query.filter(Website.category_id == category)

    if search:
        query = query.filter(
            Website.name.contains(search) |
            Website.description.contains(search)
        )

    websites = query.order_by(Website.click_count.desc()).all()

    # 获取热门网站
    popular_websites = db.query(Website).order_by(Website.click_count.desc()).limit(6).all()

    return templates.TemplateResponse("index.html", {
        "request": request,
        "page": "home",
        "categories": categories,
        "websites": websites,
        "popular_websites": popular_websites,
        "selected_category": category,
        "search_query": search
    })


@app.get("/category/{category_id}", response_class=HTMLResponse)
async def category_detail(request: Request, category_id: int):
    db = next(get_db())

    category = db.query(Category).filter(Category.id == category_id).first()
    if not category:
        raise HTTPException(status_code=404, detail="分类不存在")

    websites = db.query(Website).filter(Website.category_id == category_id).all()
    all_categories = db.query(Category).all()

    return templates.TemplateResponse("index.html", {
        "request": request,
        "page": "category",
        "category": category,
        "websites": websites,
        "categories": all_categories
    })


@app.get("/favorites", response_class=HTMLResponse)
async def favorites(request: Request):
    db = next(get_db())

    websites = db.query(Website).filter(Website.is_favorite == True).all()
    categories = db.query(Category).all()

    return templates.TemplateResponse("index.html", {
        "request": request,
        "page": "favorites",
        "websites": websites,
        "categories": categories
    })


@app.post("/api/website/{website_id}/click")
async def click_website(website_id: int):
    db = next(get_db())
    website = db.query(Website).filter(Website.id == website_id).first()

    if website:
        website.click_count += 1
        db.commit()
        return JSONResponse({"success": True, "url": website.url})

    return JSONResponse({"success": False}, status_code=404)


@app.post("/api/website/{website_id}/favorite")
async def toggle_favorite(website_id: int):
    db = next(get_db())
    website = db.query(Website).filter(Website.id == website_id).first()

    if website:
        website.is_favorite = not website.is_favorite
        db.commit()
        return JSONResponse({
            "success": True,
            "is_favorite": website.is_favorite
        })

    return JSONResponse({"success": False}, status_code=404)


@app.get("/api/random")
async def get_random_website():
    db = next(get_db())
    websites = db.query(Website).all()

    if websites:
        website = random.choice(websites)
        return JSONResponse({
            "id": website.id,
            "name": website.name,
            "url": website.url,
            "description": website.description,
            "category": website.category.name if website.category else "未分类"
        })

    return JSONResponse({"error": "No websites found"}, status_code=404)


@app.get("/search", response_class=HTMLResponse)
async def search_page(request: Request, q: str = ""):
    db = next(get_db())

    websites = []
    if q:
        websites = db.query(Website).filter(
            Website.name.contains(q) |
            Website.description.contains(q)
        ).order_by(Website.click_count.desc()).all()

    categories = db.query(Category).all()

    return templates.TemplateResponse("index.html", {
        "request": request,
        "page": "search",
        "websites": websites,
        "categories": categories,
        "search_query": q
    })


if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)

2.2 insert_data.py - 数据插入脚本

python 复制代码
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, Boolean, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from datetime import datetime
import random
# 数据库配置 - 请根据你的实际情况修改
DATABASE_URL = "mysql+pymysql://root:password@localhost/website_navigator"
# 创建数据库引擎
engine = create_engine(DATABASE_URL, echo=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# 定义数据模型(与main.py保持一致)
class Category(Base):
    __tablename__ = "categories"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(50), unique=True, nullable=False)
    description = Column(String(200))
    icon = Column(String(50))
    color = Column(String(20), default="#6c5ce7")
    created_at = Column(DateTime, default=datetime.utcnow)
    websites = relationship("Website", back_populates="category")
class Website(Base):
    __tablename__ = "websites"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(100), nullable=False)
    url = Column(String(500), nullable=False)
    description = Column(Text)
    category_id = Column(Integer, ForeignKey("categories.id"))
    icon_url = Column(String(500))
    is_favorite = Column(Boolean, default=False)
    click_count = Column(Integer, default=0)
    created_at = Column(DateTime, default=datetime.utcnow)
    category = relationship("Category", back_populates="websites")
def insert_sample_data():
    print("开始插入样例数据...(请确保已修改DATABASE_URL中的数据库连接配置)")
    print("-" * 60)
    
    db = SessionLocal()
    
    try:
        # 1. 清空现有数据(可选,如果需要保留现有数据请注释掉这部分)
        print("清空现有数据...")
        db.query(Website).delete()
        db.query(Category).delete()
        db.commit()
        
        # 2. 插入分类数据
        print("插入分类数据...")
        categories_data = [
            Category(
                name="搜索引擎",
                description="查找信息,探索世界的入口",
                icon="fa-search",
                color="#3498db"
            ),
            Category(
                name="社交媒体",
                description="连接朋友,分享生活的平台",
                icon="fa-users",
                color="#e74c3c"
            ),
            Category(
                name="视频娱乐",
                description="观看视频,享受娱乐时光",
                icon="fa-play-circle",
                color="#e67e22"
            ),
            Category(
                name="开发工具",
                description="程序开发,技术学习的必备工具",
                icon="fa-code",
                color="#2ecc71"
            ),
            Category(
                name="设计资源",
                description="设计灵感,创意素材的宝库",
                icon="fa-palette",
                color="#9b59b6"
            ),
            Category(
                name="新闻资讯",
                description="获取最新资讯,了解世界动态",
                icon="fa-newspaper",
                color="#34495e"
            ),
            Category(
                name="学习教育",
                description="在线学习,知识提升的平台",
                icon="fa-graduation-cap",
                color="#16a085"
            ),
            Category(
                name="购物电商",
                description="在线购物,享受便捷生活",
                icon="fa-shopping-cart",
                color="#f39c12"
            ),
            Category(
                name="音乐流媒体",
                description="聆听音乐,感受旋律之美",
                icon="fa-music",
                color="#8e44ad"
            ),
            Category(
                name="云存储",
                description="文件存储,数据备份的云端服务",
                icon="fa-cloud",
                color="#2980b9"
            )
        ]
        
        db.add_all(categories_data)
        db.commit()
        
        # 刷新以获取ID
        for category in categories_data:
            db.refresh(category)
        
        # 创建分类名称到ID的映射
        category_map = {cat.name: cat.id for cat in categories_data}
        print(f"成功创建 {len(categories_data)} 个分类")
        
        # 3. 插入网站数据
        print("\n插入网站数据...")
        websites_data = [
            # 搜索引擎
            Website(
                name="Google",
                url="https://www.google.com",
                description="全球最大的搜索引擎,提供网页搜索、图片搜索、学术搜索等服务",
                category_id=category_map["搜索引擎"],
                icon_url="https://www.google.com/favicon.ico",
                click_count=random.randint(1000, 5000)
            ),
            Website(
                name="百度",
                url="https://www.baidu.com",
                description="中文搜索引擎,提供网页搜索、新闻、图片、地图等服务",
                category_id=category_map["搜索引擎"],
                icon_url="https://www.baidu.com/favicon.ico",
                click_count=random.randint(800, 4000)
            ),
            Website(
                name="Bing",
                url="https://www.bing.com",
                description="微软推出的搜索引擎,以精美的背景图片著称",
                category_id=category_map["搜索引擎"],
                icon_url="https://www.bing.com/favicon.ico",
                click_count=random.randint(500, 2000)
            ),
            Website(
                name="DuckDuckGo",
                url="https://duckduckgo.com",
                description="注重隐私保护的搜索引擎,不追踪用户搜索记录",
                category_id=category_map["搜索引擎"],
                icon_url="https://duckduckgo.com/favicon.ico",
                click_count=random.randint(300, 1500)
            ),
            
            # 社交媒体
            Website(
                name="Facebook",
                url="https://www.facebook.com",
                description="全球最大的社交网络平台,连接朋友和家人",
                category_id=category_map["社交媒体"],
                icon_url="https://www.facebook.com/favicon.ico",
                click_count=random.randint(2000, 8000)
            ),
            Website(
                name="Twitter",
                url="https://twitter.com",
                description="实时信息分享平台,关注热点话题和动态",
                category_id=category_map["社交媒体"],
                icon_url="https://twitter.com/favicon.ico",
                click_count=random.randint(1500, 6000)
            ),
            Website(
                name="Instagram",
                url="https://www.instagram.com",
                description="图片和短视频分享社交平台,记录美好生活",
                category_id=category_map["社交媒体"],
                icon_url="https://www.instagram.com/favicon.ico",
                click_count=random.randint(1800, 7000)
            ),
            Website(
                name="LinkedIn",
                url="https://www.linkedin.com",
                description="职业社交平台,建立职业人脉,寻找工作机会",
                category_id=category_map["社交媒体"],
                icon_url="https://www.linkedin.com/favicon.ico",
                click_count=random.randint(800, 3000)
            ),
            
            # 视频娱乐
            Website(
                name="YouTube",
                url="https://www.youtube.com",
                description="全球最大的视频分享平台,观看和上传视频内容",
                category_id=category_map["视频娱乐"],
                icon_url="https://www.youtube.com/favicon.ico",
                click_count=random.randint(3000, 10000)
            ),
            Website(
                name="Bilibili",
                url="https://www.bilibili.com",
                description="中国领先的弹幕视频网站,动漫、游戏、科技内容丰富",
                category_id=category_map["视频娱乐"],
                icon_url="https://www.bilibili.com/favicon.ico",
                click_count=random.randint(2500, 8000)
            ),
            Website(
                name="Netflix",
                url="https://www.netflix.com",
                description="全球领先的流媒体平台,观看电影、电视剧和原创内容",
                category_id=category_map["视频娱乐"],
                icon_url="https://www.netflix.com/favicon.ico",
                click_count=random.randint(1500, 5000)
            ),
            Website(
                name="Twitch",
                url="https://www.twitch.tv",
                description="游戏直播平台,观看游戏直播和电竞比赛",
                category_id=category_map["视频娱乐"],
                icon_url="https://www.twitch.tv/favicon.ico",
                click_count=random.randint(1000, 4000)
            ),
            
            # 开发工具
            Website(
                name="GitHub",
                url="https://github.com",
                description="全球最大的代码托管平台,开源项目的聚集地",
                category_id=category_map["开发工具"],
                icon_url="https://github.com/favicon.ico",
                click_count=random.randint(2000, 7000)
            ),
            Website(
                name="Stack Overflow",
                url="https://stackoverflow.com",
                description="程序员问答社区,解决编程问题的最佳平台",
                category_id=category_map["开发工具"],
                icon_url="https://stackoverflow.com/favicon.ico",
                click_count=random.randint(1800, 6000)
            ),
            Website(
                name="CodePen",
                url="https://codepen.io",
                description="前端代码在线编辑器,展示和分享前端作品",
                category_id=category_map["开发工具"],
                icon_url="https://codepen.io/favicon.ico",
                click_count=random.randint(800, 3000)
            ),
            Website(
                name="MDN Web Docs",
                url="https://developer.mozilla.org",
                description="Mozilla开发者网络,Web技术文档和学习资源",
                category_id=category_map["开发工具"],
                icon_url="https://developer.mozilla.org/favicon.ico",
                click_count=random.randint(1200, 4000)
            ),
            
            # 设计资源
            Website(
                name="Behance",
                url="https://www.behance.net",
                description="Adobe旗下创意作品展示平台,发现优秀设计作品",
                category_id=category_map["设计资源"],
                icon_url="https://www.behance.net/favicon.ico",
                click_count=random.randint(1000, 3500)
            ),
            Website(
                name="Dribbble",
                url="https://dribbble.com",
                description="设计师社区,展示设计作品和寻找设计灵感",
                category_id=category_map["设计资源"],
                icon_url="https://dribbble.com/favicon.ico",
                click_count=random.randint(900, 3000)
            ),
            Website(
                name="Unsplash",
                url="https://unsplash.com",
                description="高质量免费图片素材库,摄影作品的聚集地",
                category_id=category_map["设计资源"],
                icon_url="https://unsplash.com/favicon.ico",
                click_count=random.randint(1500, 5000)
            ),
            Website(
                name="Figma",
                url="https://www.figma.com",
                description="协作式界面设计工具,团队设计协作的首选",
                category_id=category_map["设计资源"],
                icon_url="https://www.figma.com/favicon.ico",
                click_count=random.randint(1100, 4000)
            ),
            
            # 新闻资讯
            Website(
                name="BBC News",
                url="https://www.bbc.com/news",
                description="英国广播公司新闻,提供全球新闻和深度报道",
                category_id=category_map["新闻资讯"],
                icon_url="https://www.bbc.com/favicon.ico",
                click_count=random.randint(1200, 4000)
            ),
            Website(
                name="CNN",
                url="https://www.cnn.com",
                description="美国有线电视新闻网,24小时新闻报道",
                category_id=category_map["新闻资讯"],
                icon_url="https://www.cnn.com/favicon.ico",
                click_count=random.randint(1000, 3500)
            ),
            Website(
                name="Reddit",
                url="https://www.reddit.com",
                description="社交新闻聚合平台,发现热门话题和讨论",
                category_id=category_map["新闻资讯"],
                icon_url="https://www.reddit.com/favicon.ico",
                click_count=random.randint(2000, 7000)
            ),
            Website(
                name="Hacker News",
                url="https://news.ycombinator.com",
                description="技术新闻和讨论社区,程序员必看",
                category_id=category_map["新闻资讯"],
                icon_url="https://news.ycombinator.com/favicon.ico",
                click_count=random.randint(800, 2500)
            ),
            
            # 学习教育
            Website(
                name="Coursera",
                url="https://www.coursera.org",
                description="在线教育平台,提供大学课程和专业证书",
                category_id=category_map["学习教育"],
                icon_url="https://www.coursera.org/favicon.ico",
                click_count=random.randint(1000, 3500)
            ),
            Website(
                name="Khan Academy",
                url="https://www.khanacademy.org",
                description="免费在线学习平台,涵盖各种学科",
                category_id=category_map["学习教育"],
                icon_url="https://www.khanacademy.org/favicon.ico",
                click_count=random.randint(800, 3000)
            ),
            Website(
                name="Duolingo",
                url="https://www.duolingo.com",
                description="免费语言学习平台,游戏化学习体验",
                category_id=category_map["学习教育"],
                icon_url="https://www.duolingo.com/favicon.ico",
                click_count=random.randint(1500, 5000)
            ),
            Website(
                name="edX",
                url="https://www.edx.org",
                description="由哈佛和MIT创建的在线学习平台",
                category_id=category_map["学习教育"],
                icon_url="https://www.edx.org/favicon.ico",
                click_count=random.randint(700, 2500)
            ),
            
            # 购物电商
            Website(
                name="Amazon",
                url="https://www.amazon.com",
                description="全球最大的电商平台,商品种类丰富",
                category_id=category_map["购物电商"],
                icon_url="https://www.amazon.com/favicon.ico",
                click_count=random.randint(3000, 10000)
            ),
            Website(
                name="Taobao",
                url="https://www.taobao.com",
                description="中国最大的C2C购物平台,商品应有尽有",
                category_id=category_map["购物电商"],
                icon_url="https://www.taobao.com/favicon.ico",
                click_count=random.randint(2500, 8000)
            ),
            Website(
                name="eBay",
                url="https://www.ebay.com",
                description="全球在线拍卖和购物网站",
                category_id=category_map["购物电商"],
                icon_url="https://www.ebay.com/favicon.ico",
                click_count=random.randint(1200, 4000)
            ),
            Website(
                name="JD.com",
                url="https://www.jd.com",
                description="中国领先的综合电商平台,正品保障",
                category_id=category_map["购物电商"],
                icon_url="https://www.jd.com/favicon.ico",
                click_count=random.randint(2000, 6000)
            ),
            
            # 音乐流媒体
            Website(
                name="Spotify",
                url="https://www.spotify.com",
                description="全球领先的音乐流媒体服务",
                category_id=category_map["音乐流媒体"],
                icon_url="https://www.spotify.com/favicon.ico",
                click_count=random.randint(2000, 7000)
            ),
            Website(
                name="Apple Music",
                url="https://music.apple.com",
                description="苹果公司的音乐流媒体服务",
                category_id=category_map["音乐流媒体"],
                icon_url="https://music.apple.com/favicon.ico",
                click_count=random.randint(1500, 5000)
            ),
            Website(
                name="NetEase Cloud Music",
                url="https://music.163.com",
                description="网易云音乐,发现好音乐",
                category_id=category_map["音乐流媒体"],
                icon_url="https://music.163.com/favicon.ico",
                click_count=random.randint(1800, 6000)
            ),
            Website(
                name="SoundCloud",
                url="https://soundcloud.com",
                description="音频分享平台,独立音乐人的聚集地",
                category_id=category_map["音乐流媒体"],
                icon_url="https://soundcloud.com/favicon.ico",
                click_count=random.randint(800, 3000)
            ),
            
            # 云存储
            Website(
                name="Google Drive",
                url="https://drive.google.com",
                description="谷歌云存储服务,文件同步和共享",
                category_id=category_map["云存储"],
                icon_url="https://drive.google.com/favicon.ico",
                click_count=random.randint(2500, 8000)
            ),
            Website(
                name="Dropbox",
                url="https://www.dropbox.com",
                description="文件同步和云存储服务",
                category_id=category_map["云存储"],
                icon_url="https://www.dropbox.com/favicon.ico",
                click_count=random.randint(1200, 4000)
            ),
            Website(
                name="OneDrive",
                url="https://onedrive.live.com",
                description="微软云存储服务,与Office深度集成",
                category_id=category_map["云存储"],
                icon_url="https://onedrive.live.com/favicon.ico",
                click_count=random.randint(1500, 5000)
            ),
            Website(
                name="iCloud",
                url="https://www.icloud.com",
                description="苹果云存储服务,苹果设备间的数据同步",
                category_id=category_map["云存储"],
                icon_url="https://www.icloud.com/favicon.ico",
                click_count=random.randint(1800, 6000)
            )
        ]
        
        db.add_all(websites_data)
        db.commit()
        print(f"成功插入 {len(websites_data)} 个网站")
        
        # 4. 显示统计信息
        print("\n" + "="*60)
        print("数据插入完成!统计信息:")
        print(f"   分类数量: {len(categories_data)}")
        print(f"   网站数量: {len(websites_data)}")
        print("="*60)
        
    except Exception as e:
        print(f"插入数据时发生错误: {e}")
        db.rollback()
        raise
    finally:
        db.close()
        print("\n数据库连接已关闭")
if __name__ == "__main__":
    insert_sample_data()

2.3 templates/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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    <style>
        :root {
            --primary-color: #6c5ce7;
            --secondary-color: #a29bfe;
            --accent-color: #fd79a8;
            --text-dark: #2d3436;
            --text-light: #636e72;
            --bg-light: #f8f9fa;
            --bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        }
        
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Inter', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            position: relative;
        }
        
        body::before {
            content: '';
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23ffffff" fill-opacity="0.05" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,122.7C672,117,768,139,864,133.3C960,128,1056,96,1152,90.7C1248,85,1344,107,1392,117.3L1440,128L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>') no-repeat bottom;
            background-size: cover;
            z-index: -1;
        }
        
        .navbar {
            background: rgba(255, 255, 255, 0.95) !important;
            backdrop-filter: blur(10px);
            box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
        }
        
        .navbar-brand {
            font-weight: 700;
            font-size: 1.5rem;
            background: var(--bg-gradient);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }
        
        .main-container {
            max-width: 1400px;
            margin: 0 auto;
            padding: 20px;
        }
        
        .hero-section {
            text-align: center;
            padding: 60px 0;
            color: white;
        }
        
        .hero-title {
            font-size: 3.5rem;
            font-weight: 800;
            margin-bottom: 20px;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
            animation: fadeInDown 1s ease;
        }
        
        .hero-subtitle {
            font-size: 1.3rem;
            opacity: 0.9;
            animation: fadeInUp 1s ease 0.3s both;
        }
        
        .search-section {
            max-width: 600px;
            margin: -30px auto 40px;
            position: relative;
            z-index: 10;
        }
        
        .search-box {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 50px;
            padding: 20px 30px;
            box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
            display: flex;
            align-items: center;
            gap: 15px;
        }
        
        .search-input {
            flex: 1;
            border: none;
            outline: none;
            font-size: 1.1rem;
            background: transparent;
        }
        
        .search-btn {
            background: var(--bg-gradient);
            border: none;
            color: white;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .search-btn:hover {
            transform: scale(1.1);
        }
        
        .category-card {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 30px;
            text-align: center;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
            transition: all 0.3s ease;
            cursor: pointer;
            border: 3px solid transparent;
            height: 100%;
        }
        
        .category-card:hover {
            transform: translateY(-10px);
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
        }
        
        .category-card.active {
            border-color: var(--primary-color);
            transform: translateY(-5px);
        }
        
        .category-icon {
            width: 80px;
            height: 80px;
            border-radius: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 0 auto 20px;
            font-size: 2rem;
            color: white;
            transition: all 0.3s ease;
        }
        
        .category-card:hover .category-icon {
            transform: scale(1.1) rotate(5deg);
        }
        
        .category-name {
            font-size: 1.2rem;
            font-weight: 600;
            margin-bottom: 10px;
            color: var(--text-dark);
        }
        
        .category-description {
            color: var(--text-light);
            font-size: 0.9rem;
        }
        
        .website-card {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 20px;
            padding: 25px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
            transition: all 0.3s ease;
            height: 100%;
            position: relative;
            overflow: hidden;
        }
        
        .website-card::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 4px;
            background: var(--bg-gradient);
            transform: scaleX(0);
            transition: transform 0.3s ease;
        }
        
        .website-card:hover::before {
            transform: scaleX(1);
        }
        
        .website-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
        }
        
        .website-header {
            display: flex;
            align-items: center;
            gap: 15px;
            margin-bottom: 15px;
        }
        
        .website-icon {
            width: 50px;
            height: 50px;
            border-radius: 12px;
            object-fit: cover;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
        }
        
        .website-info {
            flex: 1;
        }
        
        .website-name {
            font-size: 1.2rem;
            font-weight: 600;
            color: var(--text-dark);
            margin-bottom: 5px;
            text-decoration: none;
            display: block;
        }
        
        .website-name:hover {
            color: var(--primary-color);
        }
        
        .website-url {
            color: var(--text-light);
            font-size: 0.85rem;
            text-decoration: none;
        }
        
        .website-description {
            color: var(--text-light);
            line-height: 1.6;
            margin-bottom: 20px;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
            overflow: hidden;
        }
        
        .website-actions {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .website-stats {
            display: flex;
            gap: 15px;
            font-size: 0.85rem;
            color: var(--text-light);
        }
        
        .website-buttons {
            display: flex;
            gap: 10px;
        }
        
        .action-btn {
            background: none;
            border: none;
            color: var(--text-light);
            cursor: pointer;
            transition: all 0.3s ease;
            font-size: 1.1rem;
            padding: 8px;
            border-radius: 8px;
        }
        
        .action-btn:hover {
            background: var(--bg-light);
            color: var(--primary-color);
            transform: scale(1.1);
        }
        
        .action-btn.favorited {
            color: #f39c12;
        }
        
        .popular-section {
            margin-bottom: 50px;
        }
        
        .section-title {
            font-size: 2rem;
            font-weight: 700;
            color: white;
            margin-bottom: 30px;
            text-align: center;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
        }
        
        .floating-btn {
            position: fixed;
            bottom: 30px;
            right: 30px;
            width: 60px;
            height: 60px;
            background: var(--bg-gradient);
            border: none;
            border-radius: 50%;
            color: white;
            font-size: 1.5rem;
            cursor: pointer;
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
            transition: all 0.3s ease;
            z-index: 1000;
        }
        
        .floating-btn:hover {
            transform: scale(1.1) rotate(180deg);
        }
        
        .random-modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.8);
            z-index: 2000;
            animation: fadeIn 0.3s ease;
        }
        
        .modal-content {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            border-radius: 20px;
            padding: 40px;
            max-width: 500px;
            width: 90%;
            animation: slideInUp 0.3s ease;
        }
        
        .close-modal {
            position: absolute;
            top: 20px;
            right: 20px;
            background: none;
            border: none;
            font-size: 1.5rem;
            cursor: pointer;
            color: var(--text-light);
        }
        
        .empty-state {
            text-align: center;
            padding: 60px 20px;
            color: white;
        }
        
        .empty-state i {
            font-size: 4rem;
            margin-bottom: 20px;
            opacity: 0.7;
        }
        
        @keyframes fadeInDown {
            from {
                opacity: 0;
                transform: translateY(-30px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
        
        @keyframes fadeInUp {
            from {
                opacity: 0;
                transform: translateY(30px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
        
        @keyframes slideInUp {
            from {
                opacity: 0;
                transform: translate(-50%, -40%);
            }
            to {
                opacity: 1;
                transform: translate(-50%, -50%);
            }
        }
        
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
        
        @media (max-width: 768px) {
            .hero-title {
                font-size: 2.5rem;
            }
            
            .category-card {
                margin-bottom: 20px;
            }
            
            .website-card {
                margin-bottom: 20px;
            }
        }
    </style>
</head>
<body>
    <!-- 导航栏 -->
    <nav class="navbar navbar-expand-lg navbar-light sticky-top">
        <div class="container">
            <a class="navbar-brand" href="/">
                <i class="fas fa-compass"></i> 网站导航
            </a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarNav">
                <ul class="navbar-nav ms-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="/">首页</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/favorites">收藏</a>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
    <div class="main-container">
        <!-- 首页 -->
        {% if page == "home" %}
        <div class="hero-section">
            <h1 class="hero-title">发现精彩网站</h1>
            <p class="hero-subtitle">精选优质网站,让你的网络生活更精彩</p>
        </div>
        
        <!-- 搜索框 -->
        <div class="search-section">
            <div class="search-box">
                <i class="fas fa-search text-muted"></i>
                <input type="text" class="search-input" placeholder="搜索网站名称或描述..." id="searchInput">
                <button class="search-btn" onclick="performSearch()">
                    <i class="fas fa-arrow-right"></i>
                </button>
            </div>
        </div>
        
        <!-- 分类导航 -->
        <div class="mb-5">
            <h2 class="section-title">网站分类</h2>
            <div class="row g-4">
                {% for category in categories %}
                <div class="col-lg-3 col-md-4 col-sm-6">
                    <div class="category-card {% if selected_category == category.id|string %}active{% endif %}" 
                         onclick="goToCategory({{ category.id }})">
                        <div class="category-icon" style="background: {{ category.color }};">
                            <i class="fas {{ category.icon }}"></i>
                        </div>
                        <div class="category-name">{{ category.name }}</div>
                        <div class="category-description">{{ category.description }}</div>
                    </div>
                </div>
                {% endfor %}
            </div>
        </div>
        
        <!-- 热门网站 -->
        {% if not selected_category and not search_query %}
        <div class="popular-section">
            <h2 class="section-title">🔥 热门网站</h2>
            <div class="row g-4">
                {% for website in popular_websites %}
                <div class="col-lg-3 col-md-4 col-sm-6">
                    <div class="website-card">
                        <div class="website-header">
                            <img src="{{ website.icon_url or '/static/default-icon.png' }}" 
                                 alt="{{ website.name }}" class="website-icon">
                            <div class="website-info">
                                <a href="{{ website.url }}" target="_blank" class="website-name" 
                                   onclick="trackClick({{ website.id }})">{{ website.name }}</a>
                                <div class="website-url">{{ website.url }}</div>
                            </div>
                        </div>
                        <div class="website-description">{{ website.description }}</div>
                        <div class="website-actions">
                            <div class="website-stats">
                                <span><i class="fas fa-mouse-pointer"></i> {{ website.click_count }}</span>
                            </div>
                            <div class="website-buttons">
                                <button class="action-btn favorite-btn" 
                                        onclick="toggleFavorite({{ website.id }})"
                                        {% if website.is_favorite %}class="action-btn favorited"{% endif %}>
                                    <i class="fas fa-star"></i>
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
                {% endfor %}
            </div>
        </div>
        {% endif %}
        
        <!-- 网站列表 -->
        {% if websites %}
        <div>
            <h2 class="section-title">
                {% if selected_category %}
                    {% for category in categories %}
                        {% if category.id == selected_category|int %}
                            {{ category.name }}
                        {% endif %}
                    {% endfor %}
                {% elif search_query %}
                    搜索结果
                {% else %}
                    所有网站
                {% endif %}
            </h2>
            <div class="row g-4">
                {% for website in websites %}
                <div class="col-lg-3 col-md-4 col-sm-6">
                    <div class="website-card">
                        <div class="website-header">
                            <img src="{{ website.icon_url or '/static/default-icon.png' }}" 
                                 alt="{{ website.name }}" class="website-icon">
                            <div class="website-info">
                                <a href="{{ website.url }}" target="_blank" class="website-name" 
                                   onclick="trackClick({{ website.id }})">{{ website.name }}</a>
                                <div class="website-url">{{ website.url }}</div>
                            </div>
                        </div>
                        <div class="website-description">{{ website.description }}</div>
                        <div class="website-actions">
                            <div class="website-stats">
                                <span><i class="fas fa-mouse-pointer"></i> {{ website.click_count }}</span>
                            </div>
                            <div class="website-buttons">
                                <button class="action-btn favorite-btn {% if website.is_favorite %}favorited{% endif %}" 
                                        onclick="toggleFavorite({{ website.id }})">
                                    <i class="fas fa-star"></i>
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
                {% endfor %}
            </div>
        </div>
        {% endif %}
        <!-- 分类详情页 -->
        {% elif page == "category" %}
        <div class="hero-section">
            <h1 class="hero-title">{{ category.name }}</h1>
            <p class="hero-subtitle">{{ category.description }}</p>
        </div>
        
        <div class="row g-4">
            {% for website in websites %}
            <div class="col-lg-3 col-md-4 col-sm-6">
                <div class="website-card">
                    <div class="website-header">
                        <img src="{{ website.icon_url or '/static/default-icon.png' }}" 
                             alt="{{ website.name }}" class="website-icon">
                        <div class="website-info">
                            <a href="{{ website.url }}" target="_blank" class="website-name" 
                               onclick="trackClick({{ website.id }})">{{ website.name }}</a>
                            <div class="website-url">{{ website.url }}</div>
                        </div>
                    </div>
                    <div class="website-description">{{ website.description }}</div>
                    <div class="website-actions">
                        <div class="website-stats">
                            <span><i class="fas fa-mouse-pointer"></i> {{ website.click_count }}</span>
                        </div>
                        <div class="website-buttons">
                            <button class="action-btn favorite-btn {% if website.is_favorite %}favorited{% endif %}" 
                                    onclick="toggleFavorite({{ website.id }})">
                                <i class="fas fa-star"></i>
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
        <!-- 收藏页 -->
        {% elif page == "favorites" %}
        <div class="hero-section">
            <h1 class="hero-title">我的收藏</h1>
            <p class="hero-subtitle">你收藏的精彩网站</p>
        </div>
        
        {% if websites %}
        <div class="row g-4">
            {% for website in websites %}
            <div class="col-lg-3 col-md-4 col-sm-6">
                <div class="website-card">
                    <div class="website-header">
                        <img src="{{ website.icon_url or '/static/default-icon.png' }}" 
                             alt="{{ website.name }}" class="website-icon">
                        <div class="website-info">
                            <a href="{{ website.url }}" target="_blank" class="website-name" 
                               onclick="trackClick({{ website.id }})">{{ website.name }}</a>
                            <div class="website-url">{{ website.url }}</div>
                        </div>
                    </div>
                    <div class="website-description">{{ website.description }}</div>
                    <div class="website-actions">
                        <div class="website-stats">
                            <span><i class="fas fa-mouse-pointer"></i> {{ website.click_count }}</span>
                        </div>
                        <div class="website-buttons">
                            <button class="action-btn favorite-btn favorited" 
                                    onclick="toggleFavorite({{ website.id }})">
                                <i class="fas fa-star"></i>
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
        {% else %}
        <div class="empty-state">
            <i class="fas fa-star"></i>
            <h3>还没有收藏的网站</h3>
            <p>点击网站旁的星星图标,收藏你喜欢的网站吧!</p>
            <a href="/" class="btn btn-light">去发现网站</a>
        </div>
        {% endif %}
        <!-- 搜索页 -->
        {% elif page == "search" %}
        <div class="hero-section">
            <h1 class="hero-title">搜索结果</h1>
            <p class="hero-subtitle">
                {% if search_query %}
                关键词:"{{ search_query }}"
                {% else %}
                请输入搜索关键词
                {% endif %}
            </p>
        </div>
        
        {% if websites %}
        <div class="row g-4">
            {% for website in websites %}
            <div class="col-lg-3 col-md-4 col-sm-6">
                <div class="website-card">
                    <div class="website-header">
                        <img src="{{ website.icon_url or '/static/default-icon.png' }}" 
                             alt="{{ website.name }}" class="website-icon">
                        <div class="website-info">
                            <a href="{{ website.url }}" target="_blank" class="website-name" 
                               onclick="trackClick({{ website.id }})">{{ website.name }}</a>
                            <div class="website-url">{{ website.url }}</div>
                        </div>
                    </div>
                    <div class="website-description">{{ website.description }}</div>
                    <div class="website-actions">
                        <div class="website-stats">
                            <span><i class="fas fa-mouse-pointer"></i> {{ website.click_count }}</span>
                        </div>
                        <div class="website-buttons">
                            <button class="action-btn favorite-btn {% if website.is_favorite %}favorited{% endif %}" 
                                    onclick="toggleFavorite({{ website.id }})">
                                <i class="fas fa-star"></i>
                            </button>
                        </div>
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
        {% else %}
        <div class="empty-state">
            <i class="fas fa-search"></i>
            <h3>没有找到相关网站</h3>
            <p>试试其他关键词吧</p>
            <a href="/" class="btn btn-light">返回首页</a>
        </div>
        {% endif %}
        {% endif %}
    </div>
    <!-- 随机网站按钮 -->
    <button class="floating-btn" onclick="showRandomWebsite()">
        <i class="fas fa-dice"></i>
    </button>
    <!-- 随机网站弹窗 -->
    <div class="random-modal" id="randomModal">
        <div class="modal-content">
            <button class="close-modal" onclick="closeRandomModal()">
                <i class="fas fa-times"></i>
            </button>
            <div id="randomWebsiteContent">
                <div class="text-center">
                    <i class="fas fa-spinner fa-spin fa-3x"></i>
                    <p class="mt-3">正在获取随机网站...</p>
                </div>
            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script>
        // 跟踪点击次数
        async function trackClick(websiteId) {
            try {
                const response = await fetch(`/api/website/${websiteId}/click`, {
                    method: 'POST'
                });
                const data = await response.json();
                
                if (data.success) {
                    // 可以在这里添加成功提示
                    console.log('点击记录成功');
                }
            } catch (error) {
                console.error('点击记录失败:', error);
            }
        }
        
        // 切换收藏状态
        async function toggleFavorite(websiteId) {
            try {
                const response = await fetch(`/api/website/${websiteId}/favorite`, {
                    method: 'POST'
                });
                const data = await response.json();
                
                if (data.success) {
                    const favoriteBtn = document.querySelector(`.favorite-btn[onclick="toggleFavorite(${websiteId})"]`);
                    if (data.is_favorite) {
                        favoriteBtn.classList.add('favorited');
                    } else {
                        favoriteBtn.classList.remove('favorited');
                    }
                }
            } catch (error) {
                console.error('收藏操作失败:', error);
            }
        }
        
        // 显示随机网站
        async function showRandomWebsite() {
            document.getElementById('randomModal').style.display = 'block';
            
            try {
                const response = await fetch('/api/random');
                const data = await response.json();
                
                if (data.id) {
                    document.getElementById('randomWebsiteContent').innerHTML = `
                        <div class="text-center mb-4">
                            <img src="${data.icon_url || '/static/default-icon.png'}" 
                                 alt="${data.name}" class="website-icon" style="width: 80px; height: 80px;">
                        </div>
                        <h3 class="text-center mb-3">${data.name}</h3>
                        <p class="text-muted text-center mb-3">${data.description}</p>
                        <p class="text-center mb-4">
                            <small class="text-muted">${data.category}</small>
                        </p>
                        <div class="text-center">
                            <a href="${data.url}" target="_blank" class="btn btn-primary me-2" onclick="trackClick(${data.id})">
                                <i class="fas fa-external-link-alt"></i> 访问网站
                            </a>
                            <button class="btn btn-outline-secondary" onclick="closeRandomModal()">
                                关闭
                            </button>
                        </div>
                    `;
                }
            } catch (error) {
                document.getElementById('randomWebsiteContent').innerHTML = `
                    <div class="text-center text-danger">
                        <i class="fas fa-exclamation-triangle fa-3x"></i>
                        <p class="mt-3">获取随机网站失败</p>
                    </div>
                `;
            }
        }
        
        // 关闭随机网站弹窗
        function closeRandomModal() {
            document.getElementById('randomModal').style.display = 'none';
        }
        
        // 点击弹窗外部关闭
        document.getElementById('randomModal').addEventListener('click', function(e) {
            if (e.target === this) {
                closeRandomModal();
            }
        });
        
        // 搜索功能
        function performSearch() {
            const query = document.getElementById('searchInput').value.trim();
            if (query) {
                window.location.href = `/search?q=${encodeURIComponent(query)}`;
            }
        }
        
        // 回车搜索
        document.getElementById('searchInput')?.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                performSearch();
            }
        });
        
        // 分类跳转
        function goToCategory(categoryId) {
            window.location.href = `/category/${categoryId}`;
        }
    </script>
</body>
</html>

2.4 运行项目

1、插入样例数据

bash 复制代码
python insert_data.py

2、启动项目

bash 复制代码
python main.py

3、访问系统

打开浏览器访问 http://localhost:8000

相关推荐
程序员爱钓鱼2 小时前
Python 编程实战 · 进阶与职业发展:数据分析与 AI(Pandas、NumPy、Scikit-learn)
后端·python·trae
软件开发技术深度爱好者2 小时前
Python库/包/模块管理工具
开发语言·python
Leon-Ning Liu2 小时前
MySQL 5.7大表索引优化实战:108GB数据建索引效率提升50%
运维·数据库·mysql
程序员爱钓鱼2 小时前
Python 编程实战 · 进阶与职业发展:Web 全栈(Django / FastAPI)
后端·python·trae
千寻技术帮2 小时前
50030_基于微信小程序的生鲜配送系统
mysql·微信小程序·源码·安装·文档·ppt·答疑
Wang's Blog2 小时前
MySQL: 数据库监控核心要素与实施策略
数据库·mysql
郝学胜-神的一滴2 小时前
Python中一切皆对象:深入理解Python的对象模型
开发语言·python·程序人生·个人开发
烤汉堡3 小时前
Python入门到实战:post请求和响应
python·html
海奥华23 小时前
分库分表技术详解:从入门到实践
数据库·后端·mysql·golang