Nuxt.js基础项目实战

学习目标

通过本章节的学习,你将能够:

  • 了解如何进行项目需求分析
  • 掌握Nuxt.js项目结构设计方法
  • 学习如何开发页面和组件
  • 掌握路由配置技巧
  • 了解静态资源管理方法
  • 学习项目构建和部署流程

项目概述

本实战项目将创建一个简单的个人博客网站,包含以下功能:

  • 首页展示博客文章列表
  • 文章详情页
  • 关于页面
  • 联系页面
  • 导航栏和页脚

项目需求分析

功能需求

  1. 首页

    • 展示博客文章列表
    • 文章标题、发布日期、摘要
    • 分页功能
  2. 文章详情页

    • 展示完整文章内容
    • 文章标题、发布日期、作者
    • 相关文章推荐
  3. 关于页面

    • 个人简介
    • 技能展示
    • 经历介绍
  4. 联系页面

    • 联系表单
    • 联系方式
  5. 通用功能

    • 导航栏(包含 logo、菜单链接)
    • 页脚(包含版权信息、社交媒体链接)
    • 响应式设计

技术需求

  • 使用Nuxt.js框架
  • 使用Vue组件
  • 使用静态资源管理
  • 使用布局系统
  • 使用路由系统

项目结构设计

目录结构

├── assets/            # 静态资源
│   ├── css/           # 样式文件
│   ├── images/        # 图片文件
│   └── fonts/         # 字体文件
├── components/        # 组件
│   ├── common/        # 通用组件
│   │   ├── Header.vue # 头部组件
│   │   └── Footer.vue # 页脚组件
│   └── blog/          # 博客相关组件
│       ├── PostList.vue    # 文章列表组件
│       ├── PostCard.vue    # 文章卡片组件
│       └── Pagination.vue  # 分页组件
├── layouts/           # 布局
│   └── default.vue    # 默认布局
├── pages/             # 页面
│   ├── index.vue      # 首页
│   ├── about.vue      # 关于页面
│   ├── contact.vue    # 联系页面
│   └── blog/          # 博客页面
│       ├── _id.vue    # 文章详情页
│       └── index.vue  # 博客列表页
├── static/            # 静态文件
│   ├── favicon.ico    # 网站图标
│   └── robots.txt     # 爬虫协议
├── nuxt.config.js     # 配置文件
├── package.json       # 项目依赖
└── README.md          # 项目说明

数据结构设计

文章数据结构

{
  id: 1,              // 文章ID
  title: "Nuxt.js入门教程",  // 文章标题
  slug: "nuxtjs-introduction", // 文章slug
  date: "2023-01-01",  // 发布日期
  author: "作者名称",   // 作者
  summary: "这是一篇关于Nuxt.js入门的教程...", // 文章摘要
  content: "# Nuxt.js入门教程\n\n这是一篇关于Nuxt.js入门的教程...", // 文章内容(Markdown格式)
  tags: ["Nuxt.js", "Vue.js"], // 标签
  featuredImage: "/images/nuxtjs-intro.jpg" // 特色图片
}

项目开发

步骤1:创建项目

首先,使用Nuxt.js CLI创建一个新项目:

npx create-nuxt-app blog-project

在创建过程中,选择以下选项:

  • 项目名称:blog-project
  • 包管理器:npm
  • UI框架:None
  • 服务器端框架:None
  • 特性:Axios, ESLint
  • 部署目标:Static (Static/JAMStack hosting)
  • 开发工具:None

步骤2:创建目录结构

按照设计的目录结构创建相应的文件夹:

mkdir -p assets/{css,images,fonts}
mkdir -p components/{common,blog}
mkdir -p pages/blog

步骤3:创建布局文件

默认布局

<!-- layouts/default.vue -->
<template>
  <div class="layout">
    <Header />
    <main class="main-content">
      <nuxt />
    </main>
    <Footer />
  </div>
</template>

<script>
import Header from '~/components/common/Header.vue'
import Footer from '~/components/common/Footer.vue'

export default {
  components: {
    Header,
    Footer
  }
}
</script>

<style>
/* 全局样式 */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  line-height: 1.6;
  color: #333;
  background-color: #f5f5f5;
}

.layout {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.main-content {
  flex: 1;
  padding: 2rem 0;
}

.container {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .main-content {
    padding: 1rem 0;
  }
  
  .container {
    padding: 0 1rem;
  }
}
</style>

步骤4:创建通用组件

头部组件

<!-- components/common/Header.vue -->
<template>
  <header class="header">
    <div class="container">
      <div class="header-content">
        <div class="logo">
          <nuxt-link to="/">
            <h1>个人博客</h1>
          </nuxt-link>
        </div>
        <nav class="nav">
          <ul class="nav-list">
            <li class="nav-item">
              <nuxt-link to="/" exact>首页</nuxt-link>
            </li>
            <li class="nav-item">
              <nuxt-link to="/blog">博客</nuxt-link>
            </li>
            <li class="nav-item">
              <nuxt-link to="/about">关于</nuxt-link>
            </li>
            <li class="nav-item">
              <nuxt-link to="/contact">联系</nuxt-link>
            </li>
          </ul>
        </nav>
      </div>
    </div>
  </header>
</template>

<style scoped>
.header {
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  position: sticky;
  top: 0;
  z-index: 100;
}

.header-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 0;
}

.logo h1 {
  font-size: 1.5rem;
  font-weight: bold;
  margin: 0;
}

.logo a {
  color: #333;
  text-decoration: none;
}

.nav-list {
  display: flex;
  list-style: none;
  gap: 1.5rem;
}

.nav-item a {
  color: #333;
  text-decoration: none;
  font-weight: 500;
  transition: color 0.3s;
}

.nav-item a:hover {
  color: #007bff;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .header-content {
    flex-direction: column;
    gap: 1rem;
    padding: 1rem 0;
  }
  
  .nav-list {
    gap: 1rem;
  }
}
</style>

页脚组件

<!-- components/common/Footer.vue -->
<template>
  <footer class="footer">
    <div class="container">
      <div class="footer-content">
        <div class="footer-info">
          <h3>个人博客</h3>
          <p>分享技术与生活的点滴</p>
        </div>
        <div class="footer-links">
          <h4>快速链接</h4>
          <ul>
            <li><nuxt-link to="/">首页</nuxt-link></li>
            <li><nuxt-link to="/blog">博客</nuxt-link></li>
            <li><nuxt-link to="/about">关于</nuxt-link></li>
            <li><nuxt-link to="/contact">联系</nuxt-link></li>
          </ul>
        </div>
        <div class="footer-social">
          <h4>社交媒体</h4>
          <ul class="social-list">
            <li><a href="#" target="_blank">GitHub</a></li>
            <li><a href="#" target="_blank">Twitter</a></li>
            <li><a href="#" target="_blank">LinkedIn</a></li>
          </ul>
        </div>
      </div>
      <div class="footer-bottom">
        <p>&copy; {{ new Date().getFullYear() }} 个人博客. All rights reserved.</p>
      </div>
    </div>
  </footer>
</template>

<style scoped>
.footer {
  background-color: #333;
  color: #fff;
  padding: 2rem 0;
  margin-top: 2rem;
}

.footer-content {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 2rem;
  margin-bottom: 2rem;
}

.footer-info h3 {
  font-size: 1.2rem;
  margin-bottom: 0.5rem;
}

.footer-links h4,
.footer-social h4 {
  font-size: 1rem;
  margin-bottom: 1rem;
}

.footer-links ul,
.social-list {
  list-style: none;
}

.footer-links li,
.social-list li {
  margin-bottom: 0.5rem;
}

.footer-links a,
.social-list a {
  color: #ccc;
  text-decoration: none;
  transition: color 0.3s;
}

.footer-links a:hover,
.social-list a:hover {
  color: #fff;
}

.footer-bottom {
  border-top: 1px solid #444;
  padding-top: 1rem;
  text-align: center;
  font-size: 0.9rem;
  color: #ccc;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .footer-content {
    grid-template-columns: 1fr;
    gap: 1.5rem;
  }
}
</style>

步骤5:创建博客相关组件

文章卡片组件

<!-- components/blog/PostCard.vue -->
<template>
  <div class="post-card">
    <div class="post-card-image" v-if="post.featuredImage">
      <img :src="post.featuredImage" :alt="post.title">
    </div>
    <div class="post-card-content">
      <div class="post-card-meta">
        <span class="post-date">{{ formatDate(post.date) }}</span>
        <span class="post-author">{{ post.author }}</span>
      </div>
      <h3 class="post-card-title">
        <nuxt-link :to="`/blog/${post.slug}`">{{ post.title }}</nuxt-link>
      </h3>
      <p class="post-card-excerpt">{{ post.summary }}</p>
      <div class="post-card-footer">
        <div class="post-tags">
          <span 
            v-for="tag in post.tags" 
            :key="tag"
            class="post-tag"
          >
            {{ tag }}
          </span>
        </div>
        <nuxt-link :to="`/blog/${post.slug}`" class="post-read-more">
          阅读更多 →
        </nuxt-link>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    post: {
      type: Object,
      required: true
    }
  },
  methods: {
    formatDate(date) {
      return new Date(date).toLocaleDateString('zh-CN', {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
      })
    }
  }
}
</script>

<style scoped>
.post-card {
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  margin-bottom: 2rem;
  transition: transform 0.3s, box-shadow 0.3s;
}

.post-card:hover {
  transform: translateY(-5px);
  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.post-card-image {
  height: 200px;
  overflow: hidden;
}

.post-card-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.3s;
}

.post-card:hover .post-card-image img {
  transform: scale(1.05);
}

.post-card-content {
  padding: 1.5rem;
}

.post-card-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.9rem;
  color: #666;
  margin-bottom: 1rem;
}

.post-card-title {
  font-size: 1.3rem;
  margin-bottom: 1rem;
}

.post-card-title a {
  color: #333;
  text-decoration: none;
  transition: color 0.3s;
}

.post-card-title a:hover {
  color: #007bff;
}

.post-card-excerpt {
  color: #666;
  margin-bottom: 1.5rem;
  line-height: 1.6;
}

.post-card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.post-tags {
  display: flex;
  gap: 0.5rem;
}

.post-tag {
  background-color: #f0f0f0;
  padding: 0.25rem 0.75rem;
  border-radius: 20px;
  font-size: 0.8rem;
  color: #666;
}

.post-read-more {
  color: #007bff;
  text-decoration: none;
  font-weight: 500;
  transition: color 0.3s;
}

.post-read-more:hover {
  color: #0056b3;
  text-decoration: underline;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .post-card-footer {
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
  }
}
</style>

文章列表组件

<!-- components/blog/PostList.vue -->
<template>
  <div class="post-list">
    <PostCard 
      v-for="post in posts" 
      :key="post.id"
      :post="post"
    />
    <div class="pagination" v-if="totalPages > 1">
      <button 
        class="pagination-btn"
        :disabled="currentPage === 1"
        @click="$emit('pageChange', currentPage - 1)"
      >
        上一页
      </button>
      <span class="pagination-info">
        第 {{ currentPage }} 页,共 {{ totalPages }} 页
      </span>
      <button 
        class="pagination-btn"
        :disabled="currentPage === totalPages"
        @click="$emit('pageChange', currentPage + 1)"
      >
        下一页
      </button>
    </div>
  </div>
</template>

<script>
import PostCard from './PostCard.vue'

export default {
  components: {
    PostCard
  },
  props: {
    posts: {
      type: Array,
      required: true
    },
    currentPage: {
      type: Number,
      default: 1
    },
    totalPages: {
      type: Number,
      default: 1
    }
  }
}
</script>

<style scoped>
.post-list {
  margin-bottom: 2rem;
}

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  margin-top: 2rem;
}

.pagination-btn {
  background-color: #007bff;
  color: #fff;
  border: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.pagination-btn:hover:not(:disabled) {
  background-color: #0056b3;
}

.pagination-btn:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.pagination-info {
  font-size: 0.9rem;
  color: #666;
}
</style>

步骤6:创建页面

首页

<!-- pages/index.vue -->
<template>
  <div class="home-page">
    <div class="container">
      <section class="hero">
        <h1>欢迎来到我的个人博客</h1>
        <p>分享技术与生活的点滴,记录成长的历程</p>
        <nuxt-link to="/blog" class="btn-primary">
          浏览博客
        </nuxt-link>
      </section>
      
      <section class="latest-posts">
        <h2>最新文章</h2>
        <PostList 
          :posts="latestPosts" 
          :currentPage="1"
          :totalPages="1"
        />
      </section>
    </div>
  </div>
</template>

<script>
import PostList from '~/components/blog/PostList.vue'

export default {
  components: {
    PostList
  },
  data() {
    return {
      latestPosts: [
        {
          id: 1,
          title: "Nuxt.js入门教程",
          slug: "nuxtjs-introduction",
          date: "2023-01-15",
          author: "张三",
          summary: "这是一篇关于Nuxt.js入门的教程,介绍了Nuxt.js的基本概念、特点和使用方法。",
          content: "# Nuxt.js入门教程\n\n这是一篇关于Nuxt.js入门的教程...",
          tags: ["Nuxt.js", "Vue.js"],
          featuredImage: "/images/nuxtjs-intro.jpg"
        },
        {
          id: 2,
          title: "Vue 3组合式API详解",
          slug: "vue3-composition-api",
          date: "2023-01-10",
          author: "张三",
          summary: "本文详细介绍了Vue 3的组合式API,包括setup函数、响应式API、生命周期钩子等内容。",
          content: "# Vue 3组合式API详解\n\n本文详细介绍了Vue 3的组合式API...",
          tags: ["Vue.js", "前端"],
          featuredImage: "/images/vue3-composition.jpg"
        }
      ]
    }
  }
}
</script>

<style scoped>
.home-page {
  padding: 2rem 0;
}

.hero {
  text-align: center;
  padding: 4rem 0;
  background-color: #f8f9fa;
  border-radius: 8px;
  margin-bottom: 3rem;
}

.hero h1 {
  font-size: 2.5rem;
  margin-bottom: 1rem;
  color: #333;
}

.hero p {
  font-size: 1.2rem;
  margin-bottom: 2rem;
  color: #666;
}

.btn-primary {
  display: inline-block;
  background-color: #007bff;
  color: #fff;
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  text-decoration: none;
  font-weight: 500;
  transition: background-color 0.3s;
}

.btn-primary:hover {
  background-color: #0056b3;
}

.latest-posts h2 {
  font-size: 1.8rem;
  margin-bottom: 2rem;
  color: #333;
  border-bottom: 2px solid #007bff;
  padding-bottom: 0.5rem;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .hero h1 {
    font-size: 2rem;
  }
  
  .hero p {
    font-size: 1rem;
  }
  
  .latest-posts h2 {
    font-size: 1.5rem;
  }
}
</style>

博客列表页

<!-- pages/blog/index.vue -->
<template>
  <div class="blog-page">
    <div class="container">
      <h1>博客文章</h1>
      <PostList 
        :posts="posts"
        :currentPage="currentPage"
        :totalPages="totalPages"
        @pageChange="handlePageChange"
      />
    </div>
  </div>
</template>

<script>
import PostList from '~/components/blog/PostList.vue'

export default {
  components: {
    PostList
  },
  data() {
    return {
      posts: [
        {
          id: 1,
          title: "Nuxt.js入门教程",
          slug: "nuxtjs-introduction",
          date: "2023-01-15",
          author: "张三",
          summary: "这是一篇关于Nuxt.js入门的教程,介绍了Nuxt.js的基本概念、特点和使用方法。",
          content: "# Nuxt.js入门教程\n\n这是一篇关于Nuxt.js入门的教程...",
          tags: ["Nuxt.js", "Vue.js"],
          featuredImage: "/images/nuxtjs-intro.jpg"
        },
        {
          id: 2,
          title: "Vue 3组合式API详解",
          slug: "vue3-composition-api",
          date: "2023-01-10",
          author: "张三",
          summary: "本文详细介绍了Vue 3的组合式API,包括setup函数、响应式API、生命周期钩子等内容。",
          content: "# Vue 3组合式API详解\n\n本文详细介绍了Vue 3的组合式API...",
          tags: ["Vue.js", "前端"],
          featuredImage: "/images/vue3-composition.jpg"
        },
        {
          id: 3,
          title: "前端性能优化技巧",
          slug: "frontend-performance-optimization",
          date: "2023-01-05",
          author: "张三",
          summary: "本文介绍了前端性能优化的各种技巧,包括资源压缩、代码分割、缓存策略等内容。",
          content: "# 前端性能优化技巧\n\n本文介绍了前端性能优化的各种技巧...",
          tags: ["前端", "性能优化"],
          featuredImage: "/images/performance-optimization.jpg"
        }
      ],
      currentPage: 1,
      totalPages: 1,
      postsPerPage: 2
    }
  },
  computed: {
    paginatedPosts() {
      const start = (this.currentPage - 1) * this.postsPerPage
      const end = start + this.postsPerPage
      return this.posts.slice(start, end)
    }
  },
  methods: {
    handlePageChange(page) {
      this.currentPage = page
    }
  }
}
</script>

<style scoped>
.blog-page {
  padding: 2rem 0;
}

.blog-page h1 {
  font-size: 2rem;
  margin-bottom: 2rem;
  color: #333;
  border-bottom: 2px solid #007bff;
  padding-bottom: 0.5rem;
}
</style>

文章详情页

<!-- pages/blog/_slug.vue -->
<template>
  <div class="post-detail-page">
    <div class="container">
      <div v-if="post" class="post-detail">
        <div class="post-detail-header">
          <div class="post-meta">
            <span class="post-date">{{ formatDate(post.date) }}</span>
            <span class="post-author">{{ post.author }}</span>
          </div>
          <h1 class="post-title">{{ post.title }}</h1>
          <div class="post-tags">
            <span 
              v-for="tag in post.tags" 
              :key="tag"
              class="post-tag"
            >
              {{ tag }}
            </span>
          </div>
        </div>
        
        <div class="post-detail-image" v-if="post.featuredImage">
          <img :src="post.featuredImage" :alt="post.title">
        </div>
        
        <div class="post-detail-content">
          <div v-html="renderedContent"></div>
        </div>
        
        <div class="post-detail-footer">
          <div class="post-navigation">
            <div class="post-nav-prev" v-if="prevPost">
              <span>上一篇</span>
              <nuxt-link :to="`/blog/${prevPost.slug}`">{{ prevPost.title }}</nuxt-link>
            </div>
            <div class="post-nav-next" v-if="nextPost">
              <span>下一篇</span>
              <nuxt-link :to="`/blog/${nextPost.slug}`">{{ nextPost.title }}</nuxt-link>
            </div>
          </div>
        </div>
      </div>
      <div v-else class="post-not-found">
        <h2>文章不存在</h2>
        <p>抱歉,您访问的文章不存在。</p>
        <nuxt-link to="/blog" class="btn-primary">
          返回博客首页
        </nuxt-link>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      posts: [
        {
          id: 1,
          title: "Nuxt.js入门教程",
          slug: "nuxtjs-introduction",
          date: "2023-01-15",
          author: "张三",
          summary: "这是一篇关于Nuxt.js入门的教程,介绍了Nuxt.js的基本概念、特点和使用方法。",
          content: "# Nuxt.js入门教程\n\n## 什么是Nuxt.js\n\nNuxt.js是一个基于Vue.js的通用应用框架,它提供了服务端渲染、静态站点生成等功能,帮助开发者构建高性能的Web应用。\n\n## Nuxt.js的特点\n\n- 服务端渲染\n- 静态站点生成\n- 自动路由生成\n- 代码分割\n- 热更新\n\n## 开始使用Nuxt.js\n\n要开始使用Nuxt.js,你可以使用以下命令创建一个新项目:\n\n```bash\nnpx create-nuxt-app my-project\n```",
          tags: ["Nuxt.js", "Vue.js"],
          featuredImage: "/images/nuxtjs-intro.jpg"
        },
        {
          id: 2,
          title: "Vue 3组合式API详解",
          slug: "vue3-composition-api",
          date: "2023-01-10",
          author: "张三",
          summary: "本文详细介绍了Vue 3的组合式API,包括setup函数、响应式API、生命周期钩子等内容。",
          content: "# Vue 3组合式API详解\n\n## 什么是组合式API\n\n组合式API是Vue 3中引入的一种新的API风格,它允许开发者按照逻辑功能组织代码,而不是按照选项类型。\n\n## setup函数\n\nsetup函数是组合式API的入口点,它在组件创建之前执行。\n\n```javascript\nexport default {\n  setup() {\n    // 组合式API代码\n  }\n}\n```",
          tags: ["Vue.js", "前端"],
          featuredImage: "/images/vue3-composition.jpg"
        },
        {
          id: 3,
          title: "前端性能优化技巧",
          slug: "frontend-performance-optimization",
          date: "2023-01-05",
          author: "张三",
          summary: "本文介绍了前端性能优化的各种技巧,包括资源压缩、代码分割、缓存策略等内容。",
          content: "# 前端性能优化技巧\n\n## 资源压缩\n\n压缩HTML、CSS和JavaScript文件可以减少文件大小,提高加载速度。\n\n## 代码分割\n\n代码分割可以将代码分成多个小块,按需加载,减少初始加载时间。\n\n## 缓存策略\n\n合理的缓存策略可以减少重复请求,提高页面加载速度。",
          tags: ["前端", "性能优化"],
          featuredImage: "/images/performance-optimization.jpg"
        }
      ],
      post: null,
      prevPost: null,
      nextPost: null
    }
  },
  computed: {
    renderedContent() {
      if (!this.post) return ''
      // 简单的Markdown渲染(实际项目中可以使用marked等库)
      return this.post.content
        .replace(/^# (.*$)/gm, '<h1>$1</h1>')
        .replace(/^## (.*$)/gm, '<h2>$1</h2>')
        .replace(/^### (.*$)/gm, '<h3>$1</h3>')
        .replace(/\`\`\`(.*?)\`\`\`/gs, '<pre><code>$1</code></pre>')
        .replace(/\*(.*?)\*/g, '<em>$1</em>')
        .replace(/\n/g, '<br>')
    }
  },
  asyncData({ params }) {
    // 在实际项目中,这里会从API获取数据
    return {}
  },
  mounted() {
    // 查找当前文章
    const slug = this.$route.params.slug
    const postIndex = this.posts.findIndex(post => post.slug === slug)
    
    if (postIndex !== -1) {
      this.post = this.posts[postIndex]
      // 查找上一篇和下一篇文章
      this.prevPost = postIndex > 0 ? this.posts[postIndex - 1] : null
      this.nextPost = postIndex < this.posts.length - 1 ? this.posts[postIndex + 1] : null
    }
  },
  methods: {
    formatDate(date) {
      return new Date(date).toLocaleDateString('zh-CN', {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
      })
    }
  }
}
</script>

<style scoped>
.post-detail-page {
  padding: 2rem 0;
}

.post-detail {
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 2rem;
}

.post-detail-header {
  margin-bottom: 2rem;
}

.post-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.9rem;
  color: #666;
  margin-bottom: 1rem;
}

.post-title {
  font-size: 2rem;
  margin-bottom: 1rem;
  color: #333;
}

.post-tags {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.post-tag {
  background-color: #f0f0f0;
  padding: 0.25rem 0.75rem;
  border-radius: 20px;
  font-size: 0.8rem;
  color: #666;
}

.post-detail-image {
  margin-bottom: 2rem;
}

.post-detail-image img {
  width: 100%;
  height: auto;
  border-radius: 8px;
}

.post-detail-content {
  margin-bottom: 2rem;
  line-height: 1.8;
}

.post-detail-content h1,
.post-detail-content h2,
.post-detail-content h3 {
  margin-top: 2rem;
  margin-bottom: 1rem;
  color: #333;
}

.post-detail-content p {
  margin-bottom: 1rem;
}

.post-detail-content pre {
  background-color: #f8f9fa;
  padding: 1rem;
  border-radius: 4px;
  overflow-x: auto;
  margin-bottom: 1rem;
}

.post-detail-content code {
  font-family: 'Courier New', Courier, monospace;
  background-color: #f0f0f0;
  padding: 0.2rem 0.4rem;
  border-radius: 3px;
}

.post-detail-content pre code {
  background-color: transparent;
  padding: 0;
}

.post-detail-footer {
  margin-top: 3rem;
  padding-top: 2rem;
  border-top: 1px solid #e0e0e0;
}

.post-navigation {
  display: flex;
  justify-content: space-between;
  gap: 2rem;
}

.post-nav-prev,
.post-nav-next {
  flex: 1;
}

.post-nav-prev span,
.post-nav-next span {
  display: block;
  font-size: 0.9rem;
  color: #666;
  margin-bottom: 0.5rem;
}

.post-nav-prev a,
.post-nav-next a {
  color: #007bff;
  text-decoration: none;
  transition: color 0.3s;
}

.post-nav-prev a:hover,
.post-nav-next a:hover {
  color: #0056b3;
  text-decoration: underline;
}

.post-not-found {
  text-align: center;
  padding: 4rem 0;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.post-not-found h2 {
  font-size: 1.8rem;
  margin-bottom: 1rem;
  color: #333;
}

.post-not-found p {
  margin-bottom: 2rem;
  color: #666;
}

.btn-primary {
  display: inline-block;
  background-color: #007bff;
  color: #fff;
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  text-decoration: none;
  font-weight: 500;
  transition: background-color 0.3s;
}

.btn-primary:hover {
  background-color: #0056b3;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .post-detail {
    padding: 1.5rem;
  }
  
  .post-title {
    font-size: 1.8rem;
  }
  
  .post-navigation {
    flex-direction: column;
    align-items: flex-start;
    gap: 1.5rem;
  }
}
</style>

关于页面

<!-- pages/about.vue -->
<template>
  <div class="about-page">
    <div class="container">
      <h1>关于我</h1>
      
      <section class="about-profile">
        <div class="about-profile-image">
          <img src="/images/profile.jpg" alt="个人头像">
        </div>
        <div class="about-profile-content">
          <h2>张三</h2>
          <p class="about-job-title">前端开发工程师</p>
          <p class="about-description">
            我是一名热爱技术的前端开发工程师,专注于Web前端开发,拥有多年的开发经验。
            我喜欢学习新技术,分享技术知识,希望通过博客记录自己的成长历程,同时帮助更多的人。
          </p>
          <div class="about-social">
            <a href="#" target="_blank" class="social-link">GitHub</a>
            <a href="#" target="_blank" class="social-link">Twitter</a>
            <a href="#" target="_blank" class="social-link">LinkedIn</a>
          </div>
        </div>
      </section>
      
      <section class="about-skills">
        <h2>我的技能</h2>
        <div class="skills-list">
          <div class="skill-item">
            <h3>前端技术</h3>
            <ul>
              <li>HTML5 / CSS3</li>
              <li>JavaScript / TypeScript</li>
              <li>Vue.js / Nuxt.js</li>
              <li>React / Next.js</li>
              <li>Webpack / Vite</li>
            </ul>
          </div>
          <div class="skill-item">
            <h3>后端技术</h3>
            <ul>
              <li>Node.js</li>
              <li>Express</li>
              <li>MongoDB</li>
              <li>MySQL</li>
            </ul>
          </div>
          <div class="skill-item">
            <h3>其他技能</h3>
            <ul>
              <li>Git</li>
              <li>Docker</li>
              <li>Linux</li>
              <li>UI/UX设计基础</li>
            </ul>
          </div>
        </div>
      </section>
      
      <section class="about-experience">
        <h2>工作经历</h2>
        <div class="experience-list">
          <div class="experience-item">
            <div class="experience-date">2020 - 至今</div>
            <div class="experience-content">
              <h3>前端开发工程师</h3>
              <p class="experience-company">ABC科技有限公司</p>
              <p class="experience-description">
                负责公司产品的前端开发,使用Vue.js技术栈构建单页应用,参与产品需求分析和UI设计讨论。
              </p>
            </div>
          </div>
          <div class="experience-item">
            <div class="experience-date">2018 - 2020</div>
            <div class="experience-content">
              <h3>初级前端开发工程师</h3>
              <p class="experience-company">XYZ互联网公司</p>
              <p class="experience-description">
                参与公司网站和Web应用的开发,使用HTML、CSS、JavaScript和jQuery构建前端页面。
              </p>
            </div>
          </div>
        </div>
      </section>
    </div>
  </div>
</template>

<style scoped>
.about-page {
  padding: 2rem 0;
}

.about-page h1 {
  font-size: 2rem;
  margin-bottom: 3rem;
  color: #333;
  border-bottom: 2px solid #007bff;
  padding-bottom: 0.5rem;
}

.about-profile {
  display: flex;
  gap: 3rem;
  margin-bottom: 3rem;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 2rem;
}

.about-profile-image {
  flex: 0 0 200px;
}

.about-profile-image img {
  width: 100%;
  height: auto;
  border-radius: 50%;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}

.about-profile-content {
  flex: 1;
}

.about-profile-content h2 {
  font-size: 1.8rem;
  margin-bottom: 0.5rem;
  color: #333;
}

.about-job-title {
  font-size: 1.1rem;
  color: #007bff;
  margin-bottom: 1rem;
  font-weight: 500;
}

.about-description {
  margin-bottom: 2rem;
  line-height: 1.6;
  color: #666;
}

.about-social {
  display: flex;
  gap: 1rem;
}

.social-link {
  display: inline-block;
  background-color: #f0f0f0;
  color: #333;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  text-decoration: none;
  transition: background-color 0.3s, color 0.3s;
}

.social-link:hover {
  background-color: #007bff;
  color: #fff;
}

.about-skills {
  margin-bottom: 3rem;
}

.about-skills h2 {
  font-size: 1.5rem;
  margin-bottom: 2rem;
  color: #333;
  border-bottom: 1px solid #e0e0e0;
  padding-bottom: 0.5rem;
}

.skills-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 2rem;
}

.skill-item {
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 1.5rem;
}

.skill-item h3 {
  font-size: 1.2rem;
  margin-bottom: 1rem;
  color: #333;
}

.skill-item ul {
  list-style: none;
}

.skill-item li {
  margin-bottom: 0.5rem;
  color: #666;
  position: relative;
  padding-left: 1.5rem;
}

.skill-item li::before {
  content: "•";
  color: #007bff;
  font-weight: bold;
  position: absolute;
  left: 0;
}

.about-experience h2 {
  font-size: 1.5rem;
  margin-bottom: 2rem;
  color: #333;
  border-bottom: 1px solid #e0e0e0;
  padding-bottom: 0.5rem;
}

.experience-list {
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.experience-item {
  display: flex;
  gap: 2rem;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  padding: 1.5rem;
}

.experience-date {
  flex: 0 0 150px;
  font-weight: 500;
  color: #007bff;
  border-right: 2px solid #007bff;
  padding-right: 1.5rem;
}

.experience-content h3 {
  font-size: 1.2rem;
  margin-bottom: 0.25rem;
  color: #333;
}

.experience-company {
  font-size: 0.9rem;
  color: #666;
  margin-bottom: 0.5rem;
}

.experience-description {
  line-height: 1.6;
  color: #666;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .about-profile {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }
  
  .about-profile-image {
    flex: 0 0 150px;
  }
  
  .skills-list {
    grid-template-columns: 1fr;
  }
  
  .experience-item {
    flex-direction: column;
    gap: 1rem;
  }
  
  .experience-date {
    flex: none;
    border-right: none;
    border-bottom: 1px solid #e0e0e0;
    padding-right: 0;
    padding-bottom: 0.5rem;
  }
}
</style>

联系页面

<!-- pages/contact.vue -->
<template>
  <div class="contact-page">
    <div class="container">
      <h1>联系我</h1>
      
      <div class="contact-content">
        <div class="contact-form">
          <h2>发送消息</h2>
          <form @submit.prevent="handleSubmit">
            <div class="form-group">
              <label for="name">姓名</label>
              <input 
                type="text" 
                id="name" 
                v-model="form.name" 
                required
              >
            </div>
            <div class="form-group">
              <label for="email">邮箱</label>
              <input 
                type="email" 
                id="email" 
                v-model="form.email" 
                required
              >
            </div>
            <div class="form-group">
              <label for="subject">主题</label>
              <input 
                type="text" 
                id="subject" 
                v-model="form.subject" 
                required
              >
            </div>
            <div class="form-group">
              <label for="message">消息</label>
              <textarea 
                id="message" 
                v-model="form.message" 
                rows="5" 
                required
              ></textarea>
            </div>
            <button type="submit" class="btn-primary">发送消息</button>
          </form>
        </div>
        
        <div class="contact-info">
          <h2>联系方式</h2>
          <div class="contact-info-item">
            <h3>邮箱</h3>
            <p>zhangsan@example.com</p>
          </div>
          <div class="contact-info-item">
            <h3>电话</h3>
            <p>138-0000-0000</p>
          </div>
          <div class="contact-info-item">
            <h3>地址</h3>
            <p>北京市朝阳区</p>
          </div>
          <div class="contact-info-item">
            <h3>社交媒体</h3>
            <div class="social-links">
              <a href="#" target="_blank" class="social-link">GitHub</a>
              <a href="#" target="_blank" class="social-link">Twitter</a>
              <a href="#" target="_blank" class="social-link">LinkedIn</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        name: '',
        email: '',
        subject: '',
        message: ''
      }
    }
  },
  methods: {
    handleSubmit() {
      // 在实际项目中,这里会发送表单数据到服务器
      console.log('表单数据:', this.form)
      alert('消息发送成功!')
      // 重置表单
      this.form = {
        name: '',
        email: '',
        subject: '',
        message: ''
      }
    }
  }
}
</script>

<style scoped>
.contact-page {
  padding: 2rem 0;
}

.contact-page h1 {
  font-size: 2rem;
  margin-bottom: 3rem;
  color: #333;
  border-bottom: 2px solid #007bff;
  padding-bottom: 0.5rem;
}

.contact-content {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 3rem;
}

.contact-form h2,
.contact-info h2 {
  font-size: 1.5rem;
  margin-bottom: 2rem;
  color: #333;
  border-bottom: 1px solid #e0e0e0;
  padding-bottom: 0.5rem;
}

.form-group {
  margin-bottom: 1.5rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
  color: #333;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  font-size: 1rem;
  transition: border-color 0.3s;
}

.form-group input:focus,
.form-group textarea:focus {
  outline: none;
  border-color: #007bff;
  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

.btn-primary {
  display: inline-block;
  background-color: #007bff;
  color: #fff;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  font-size: 1rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.3s;
}

.btn-primary:hover {
  background-color: #0056b3;
}

.contact-info-item {
  margin-bottom: 2rem;
}

.contact-info-item h3 {
  font-size: 1.1rem;
  margin-bottom: 0.5rem;
  color: #333;
}

.contact-info-item p {
  color: #666;
  margin-bottom: 1rem;
}

.social-links {
  display: flex;
  gap: 1rem;
}

.social-link {
  display: inline-block;
  background-color: #f0f0f0;
  color: #333;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  text-decoration: none;
  transition: background-color 0.3s, color 0.3s;
}

.social-link:hover {
  background-color: #007bff;
  color: #fff;
}

/* 响应式设计 */
@media (max-width: 768px) {
  .contact-content {
    grid-template-columns: 1fr;
    gap: 2rem;
  }
}
</style>

步骤7:添加静态资源

static/images目录中添加以下图片文件:

  • profile.jpg:个人头像
  • nuxtjs-intro.jpg:Nuxt.js入门教程图片
  • vue3-composition.jpg:Vue 3组合式API图片
  • performance-optimization.jpg:前端性能优化图片

步骤8:配置项目

修改nuxt.config.js文件,添加以下配置:

// nuxt.config.js
module.exports = {
  mode: 'universal',
  /*
  ** Headers of the page
  */
  head: {
    title: '个人博客',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '个人博客网站' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress-bar color
  */
  loading: {
    color: '#007bff'
  },
  /*
  ** Global CSS
  */
  css: [],
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [],
  /*
  ** Nuxt.js dev-modules
  */
  buildModules: [],
  /*
  ** Nuxt.js modules
  */
  modules: [
    '@nuxtjs/axios'
  ],
  /*
  ** Axios module configuration
  ** See https://axios.nuxtjs.org/options
  */
  axios: {},
  /*
  ** Build configuration
  */
  build: {
    /*
    ** You can extend webpack config here
    */
    extend(config, ctx) {}
  }
}

项目构建和部署

构建项目

在项目根目录下运行以下命令构建项目:

npm run build

生成静态文件

运行以下命令生成静态文件(适用于静态部署):

npm run generate

生成的静态文件将存放在dist目录中。

部署项目

可以将生成的dist目录部署到任何静态网站托管服务,如:

  • GitHub Pages
  • Netlify
  • Vercel
  • 阿里云OSS
  • 腾讯云COS

以GitHub Pages为例,部署步骤如下:

  1. 创建一个GitHub仓库
  2. 将生成的dist目录内容推送到仓库的gh-pages分支
  3. 在仓库设置中启用GitHub Pages,选择gh-pages分支

总结

本实战项目通过创建一个简单的个人博客网站,巩固了Nuxt.js的基础知识,包括:

  1. 项目需求分析:明确了网站的功能需求和技术需求
  2. 项目结构设计:创建了合理的目录结构和文件组织
  3. 页面和组件开发:开发了首页、博客列表页、文章详情页、关于页面和联系页面,以及相关组件
  4. 路由配置:使用了Nuxt.js的自动路由系统,实现了页面之间的导航
  5. 静态资源管理:添加了图片等静态资源
  6. 项目构建和部署:学习了如何构建项目和部署到静态网站托管服务

通过这个项目,你应该已经掌握了Nuxt.js的基本使用方法,能够独立开发简单的Nuxt.js应用。在实际开发中,你可以根据具体需求扩展功能,如添加后端API集成、使用数据库存储数据、实现用户认证等。

练习

  1. 扩展博客功能,添加评论系统
  2. 实现深色模式切换
  3. 添加文章搜索功能
  4. 集成第三方Markdown编辑器
  5. 实现文章标签分类功能

拓展阅读

« 上一篇 Nuxt.js中间件使用 下一篇 » Nuxt.js 数据获取方法