第 6 章:文章列表功能
约 2987 字大约 10 分钟
2025-12-19
本章学习目标
- 理解 Nuxt 文件路由系统
- 创建后端文章列表 API
- 前端展示文章列表
- 学习组件拆分与复用
- 🎉 成果:博客文章列表页!
个人主页做好了,但博客没有文章怎么行?这一章我们来实现文章列表功能!
本章结束后你会得到什么
一个文章列表页,可以看到所有文章的标题、摘要和发布时间——博客的雏形出现了!
6.1 Nuxt 文件路由系统
6.1.1 什么是文件路由?
在传统的 Web 开发中,你需要手动配置路由:
// 传统方式:手动配置每个路由
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/blog', component: Blog },
]但在 Nuxt 中,文件就是路由!你只需要在 pages 目录下创建文件,Nuxt 会自动生成对应的路由。
pages/
├── index.vue → /
├── about.vue → /about
└── blog/
├── index.vue → /blog
└── [id].vue → /blog/123, /blog/456, ...约定优于配置
这就是 Nuxt 的"约定优于配置"理念:遵循约定,少写配置。
文件名就是路由路径,简单直观!
6.1.2 路由类型速查
| 文件路径 | 生成的路由 | 说明 |
|---|---|---|
pages/index.vue | / | 首页 |
pages/about.vue | /about | 普通页面 |
pages/blog/index.vue | /blog | 目录首页 |
pages/blog/[id].vue | /blog/:id | 动态路由(下章详讲) |
pages/blog/[...slug].vue | /blog/* | 捕获所有路由 |
6.1.3 我们的博客路由规划
pages/
├── index.vue → / 首页(个人主页)
├── about.vue → /about 关于页面
└── blog/
├── index.vue → /blog 文章列表 ← 本章要做的!
└── [id].vue → /blog/123 文章详情 ← 下章要做的6.2 创建博客列表页面
6.2.1 创建 blog 目录和页面
在 frontend/app/pages/ 目录下,创建 blog 文件夹,然后创建 index.vue:
<!-- frontend/app/pages/blog/index.vue -->
<template>
<div>
<h1 class="text-3xl font-bold text-gray-800 mb-8">
📝 博客文章
</h1>
<p class="text-gray-600">
这里将显示文章列表...
</p>
</div>
</template>保存后,访问 http://localhost:3000/blog,你应该能看到这个页面了!
看到页面了吗?
如果看到了"博客文章"标题,说明路由已经自动生成。
如果看到 404,检查一下:
- 文件路径是否正确:
app/pages/blog/index.vue - 开发服务器是否在运行
6.2.2 更新导航链接
之前我们在 Header 里写了 /blog 链接,现在它终于有页面了!
点击导航栏的"博客",应该能跳转到博客列表页。
6.3 后端文章列表 API
6.3.1 设计文章数据结构
一篇文章需要哪些信息?
{
"id": 1, # 文章 ID
"title": "我的第一篇博客", # 标题
"summary": "这是文章的摘要...", # 摘要
"content": "这是文章的正文...", # 正文(列表页不需要)
"author": "张三", # 作者
"created_at": "2025-01-15", # 创建时间
"tags": ["Vue", "学习笔记"] # 标签
}6.3.2 创建文章列表 API
修改 backend/src/backend/__init__.py,添加文章相关的 API:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import random
from datetime import datetime, timedelta
app = FastAPI()
# 配置跨域
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 问候语列表(之前的代码保留)
greetings = [
"你好,这是来自后端的问候!",
"欢迎来到全栈开发的世界!",
"今天也要加油写代码哦!",
"休息一下,喝杯水吧~",
"代码写累了?出去走走!",
"你已经很棒了,继续保持!",
]
# 模拟文章数据 #
articles = [
{
"id": 1,
"title": "我的第一篇博客",
"summary": "这是我学习全栈开发的第一篇博客,记录了从零开始搭建项目的过程。",
"author": "博主",
"created_at": "2025-01-15",
"tags": ["入门", "全栈"]
},
{
"id": 2,
"title": "Vue 3 组合式 API 学习笔记",
"summary": "深入理解 Vue 3 的 Composition API,包括 ref、reactive、computed 等核心概念。",
"author": "博主",
"created_at": "2025-01-18",
"tags": ["Vue", "前端"]
},
{
"id": 3,
"title": "FastAPI 快速入门指南",
"summary": "FastAPI 是一个现代、快速的 Python Web 框架,本文介绍其基本用法和最佳实践。",
"author": "博主",
"created_at": "2025-01-20",
"tags": ["Python", "后端", "FastAPI"]
},
{
"id": 4,
"title": "Tailwind CSS 实战技巧",
"summary": "分享一些 Tailwind CSS 的实用技巧,让你的页面开发更高效。",
"author": "博主",
"created_at": "2025-01-22",
"tags": ["CSS", "前端", "Tailwind"]
},
{
"id": 5,
"title": "全栈项目部署实战",
"summary": "从本地开发到线上部署,完整记录一个全栈项目的部署过程。",
"author": "博主",
"created_at": "2025-01-25",
"tags": ["部署", "Docker", "DevOps"]
},
]
@app.get("/")
async def root():
return {"message": "Hello World!"}
@app.get("/api/greeting")
async def greeting():
text = random.choice(greetings)
return {"text": text}
@app.get("/api/hello/{name}")
async def hello(name: str):
return {"message": f"你好,{name}!欢迎来到全栈世界!"}
@app.get("/api/greet")
async def greet(name: str = "朋友", mood: str = "开心"):
moods = {
"开心": "😄",
"难过": "😢",
"生气": "😠",
"惊讶": "😲",
}
emoji = moods.get(mood, "😊")
return {"message": f"{emoji} 你好,{name}!今天{mood}吗?"}
# 获取文章列表 #
@app.get("/api/articles")
async def get_articles():
return {"articles": articles, "total": len(articles)}
# 获取单篇文章(下章会用到) #
@app.get("/api/articles/{article_id}")
async def get_article(article_id: int):
for article in articles:
if article["id"] == article_id:
return article
return {"error": "文章不存在"}6.3.3 测试 API
保存后,uvicorn 会自动重新加载。打开浏览器测试:
http://localhost:8000/api/articles- 应该看到文章列表http://localhost:8000/api/articles/1- 应该看到第一篇文章http://localhost:8000/api/articles/999- 应该看到错误信息
使用 FastAPI 文档
访问 http://localhost:8000/docs,你可以在交互式文档中测试这些 API!
6.4 前端对接 API
6.4.1 在博客页面获取数据
现在修改 frontend/app/pages/blog/index.vue,调用后端 API:
<!-- frontend/app/pages/blog/index.vue -->
<template>
<div>
<h1 class="text-3xl font-bold text-gray-800 mb-2">
📝 博客文章
</h1>
<p class="text-gray-500 mb-8">
共 {{ data?.total || 0 }} 篇文章
</p>
<!-- 加载状态 -->
<div v-if="pending" class="text-center py-12">
<p class="text-gray-500">⏳ 加载中...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="text-center py-12">
<p class="text-red-500">❌ 加载失败:{{ error.message }}</p>
<button
@click="refresh"
class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
重试
</button>
</div>
<!-- 文章列表 -->
<div v-else class="space-y-6">
<div
v-for="article in data?.articles"
:key="article.id"
class="bg-white rounded-xl shadow-md p-6 hover:shadow-lg transition-shadow"
>
<!-- 文章标题 -->
<h2 class="text-xl font-bold text-gray-800 mb-2 hover:text-indigo-600">
<NuxtLink :to="`/blog/${article.id}`">
{{ article.title }}
</NuxtLink>
</h2>
<!-- 文章摘要 -->
<p class="text-gray-600 mb-4">
{{ article.summary }}
</p>
<!-- 文章元信息 -->
<div class="flex items-center justify-between text-sm text-gray-500">
<div class="flex items-center gap-4">
<span>👤 {{ article.author }}</span>
<span>📅 {{ article.created_at }}</span>
</div>
<!-- 标签 -->
<div class="flex gap-2">
<span
v-for="tag in article.tags"
:key="tag"
class="px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs"
>
{{ tag }}
</span>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!pending && !error && data?.articles?.length === 0" class="text-center py-12">
<p class="text-gray-500">📭 暂无文章</p>
</div>
</div>
</template>
<script setup>
// 获取文章列表
const { data, pending, error, refresh } = await useFetch('http://localhost:8000/api/articles')
</script>6.4.2 查看效果
保存后访问 http://localhost:3000/blog,你应该能看到:
- 5 篇文章的列表
- 每篇文章显示标题、摘要、作者、日期和标签
- 鼠标悬停时有阴影效果
- 点击标题可以跳转(虽然详情页还没做)
🎉 看到文章列表了吗?
如果你看到了 5 篇文章,恭喜!前后端已经成功对接了!
这就是全栈开发的魅力:后端提供数据,前端展示数据,通过 API 连接起来。
6.5 文章卡片组件
6.5.1 为什么要拆分组件?
现在文章卡片的代码都写在页面里,有几个问题:
- 代码太长:页面文件变得臃肿
- 无法复用:如果首页也想显示文章,得复制代码
- 难以维护:修改卡片样式要在页面里找
让我们把文章卡片拆分成独立组件!
6.5.2 创建 ArticleCard 组件
在 frontend/app/components/ 目录下创建 ArticleCard.vue:
<!-- frontend/app/components/ArticleCard.vue -->
<template>
<article class="article-card">
<!-- 文章标题 -->
<h2 class="card-title">
<NuxtLink :to="`/blog/${article.id}`">
{{ article.title }}
</NuxtLink>
</h2>
<!-- 文章摘要 -->
<p class="card-summary">
{{ article.summary }}
</p>
<!-- 文章元信息 -->
<div class="card-meta">
<div class="meta-left">
<span class="meta-item">👤 {{ article.author }}</span>
<span class="meta-item">📅 {{ article.created_at }}</span>
</div>
<!-- 标签 -->
<div class="tags">
<span
v-for="tag in article.tags"
:key="tag"
class="tag"
>
{{ tag }}
</span>
</div>
</div>
</article>
</template>
<script setup>
// 定义组件的 props
defineProps({
article: {
type: Object,
required: true
}
})
</script>
<style scoped>
.article-card {
background: white;
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
padding: 1.5rem;
transition: box-shadow 0.2s;
}
.article-card:hover {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
.card-title {
font-size: 1.25rem;
font-weight: bold;
color: #1f2937;
margin-bottom: 0.5rem;
}
.card-title a {
text-decoration: none;
color: inherit;
transition: color 0.2s;
}
.card-title a:hover {
color: #4f46e5;
}
.card-summary {
color: #4b5563;
margin-bottom: 1rem;
line-height: 1.6;
}
.card-meta {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.875rem;
color: #6b7280;
}
.meta-left {
display: flex;
gap: 1rem;
}
.tags {
display: flex;
gap: 0.5rem;
}
.tag {
padding: 0.25rem 0.5rem;
background: #f3f4f6;
color: #4b5563;
border-radius: 0.25rem;
font-size: 0.75rem;
}
</style>为什么这里用原生 CSS?
还记得第 5 章讲的吗?可复用的组件样式用原生 CSS。
ArticleCard 是一个会被多处使用的组件,用原生 CSS 更容易维护和统一风格。
6.5.3 使用 ArticleCard 组件
现在简化 frontend/app/pages/blog/index.vue:
<!-- frontend/app/pages/blog/index.vue -->
<template>
<div>
<h1 class="text-3xl font-bold text-gray-800 mb-2">
📝 博客文章
</h1>
<p class="text-gray-500 mb-8">
共 {{ data?.total || 0 }} 篇文章
</p>
<!-- 加载状态 -->
<div v-if="pending" class="text-center py-12">
<p class="text-gray-500">⏳ 加载中...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="text-center py-12">
<p class="text-red-500">❌ 加载失败:{{ error.message }}</p>
<button
@click="refresh"
class="mt-4 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
重试
</button>
</div>
<!-- 文章列表 -->
<div v-else class="space-y-6">
<ArticleCard <!-- [!code focus] -->
v-for="article in data?.articles"
:key="article.id"
:article="article"
/>
</div>
<!-- 空状态 -->
<div v-if="!pending && !error && data?.articles?.length === 0" class="text-center py-12">
<p class="text-gray-500">📭 暂无文章</p>
</div>
</div>
</template>
<script setup>
// 获取文章列表
const { data, pending, error, refresh } = await useFetch('http://localhost:8000/api/articles')
</script>看!文章列表部分从几十行变成了 5 行,清爽多了!
6.5.4 在首页展示最新文章
组件化的好处:现在可以轻松在首页也展示文章了!
修改 frontend/app/pages/index.vue,在项目展示,ProjectsSection后面添加最新文章:
<!-- 在 ProjectsSection 后面添加 -->
<!-- 分割线 -->
<hr class="border-gray-200 my-8" />
<!-- 最新文章 -->
<section class="py-12">
<h2 class="text-2xl font-bold text-gray-800 text-center mb-8">
📝 最新文章
</h2>
<div v-if="articlesPending" class="text-center">
<p class="text-gray-500">加载中...</p>
</div>
<div v-else class="space-y-4">
<ArticleCard
v-for="article in latestArticles"
:key="article.id"
:article="article"
/>
</div>
<div class="text-center mt-8">
<NuxtLink
to="/blog"
class="inline-block px-6 py-2 border-2 border-indigo-600 text-indigo-600 rounded-lg hover:bg-indigo-600 hover:text-white transition"
>
查看全部文章 →
</NuxtLink>
</div>
</section>同时在 <script setup> 中添加:
<script setup>
// ... 其他代码 ...
// 获取最新文章(只取前 3 篇)
const { data: articlesData, pending: articlesPending } = await useFetch('http://localhost:8000/api/articles')
const latestArticles = computed(() => articlesData.value?.articles?.slice(0, 3) || [])
</script>组件复用的力量
同一个 ArticleCard 组件,现在在两个地方使用:
/blog页面:显示所有文章/首页:显示最新 3 篇
如果要修改卡片样式,只需要改 ArticleCard.vue 一个文件!
6.6 小结
本章回顾
- ✅ 理解了 Nuxt 文件路由系统(文件即路由)
- ✅ 创建了
/blog博客列表页面 - ✅ 后端实现了
/api/articles文章列表 API - ✅ 前端使用
useFetch获取并展示文章 - ✅ 将文章卡片拆分为可复用的
ArticleCard组件 - ✅ 在首页复用组件展示最新文章
动手练习
试着扩展一下:
- 添加更多文章:在后端
articles列表中添加几篇新文章 - 美化卡片:给
ArticleCard添加封面图片(可以用占位图) - 添加分类筛选:在博客页面添加按标签筛选的功能
- 显示阅读时间:根据文章长度计算预估阅读时间
这些练习能帮你更好地理解组件化和 API 设计。
下一章预告
文章列表有了,但点进去看不到内容?而且数据都是"假的",后台服务器一重启就没了。
下一章我们要:
- 引入数据库,让文章数据持久化
- 实现文章详情页面,点击可以查看完整内容
准备好让你的博客"真正"存储数据了吗?🗄️