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

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

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

系统功能如下:
- 渐变背景:紫色渐变营造现代感
- 分类浏览:按类别查看网站
- 搜索功能:快速查找网站
- 收藏系统:收藏喜欢的网站
- 点击统计:记录网站访问次数
- 随机推荐:发现新的网站
- 热门排行:展示热门网站
一、环境搭建
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.py 和 insert_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