Nuxt.js基础项目实战
学习目标
通过本章节的学习,你将能够:
- 了解如何进行项目需求分析
- 掌握Nuxt.js项目结构设计方法
- 学习如何开发页面和组件
- 掌握路由配置技巧
- 了解静态资源管理方法
- 学习项目构建和部署流程
项目概述
本实战项目将创建一个简单的个人博客网站,包含以下功能:
- 首页展示博客文章列表
- 文章详情页
- 关于页面
- 联系页面
- 导航栏和页脚
项目需求分析
功能需求
首页:
- 展示博客文章列表
- 文章标题、发布日期、摘要
- 分页功能
文章详情页:
- 展示完整文章内容
- 文章标题、发布日期、作者
- 相关文章推荐
关于页面:
- 个人简介
- 技能展示
- 经历介绍
联系页面:
- 联系表单
- 联系方式
通用功能:
- 导航栏(包含 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>© {{ 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为例,部署步骤如下:
- 创建一个GitHub仓库
- 将生成的
dist目录内容推送到仓库的gh-pages分支 - 在仓库设置中启用GitHub Pages,选择
gh-pages分支
总结
本实战项目通过创建一个简单的个人博客网站,巩固了Nuxt.js的基础知识,包括:
- 项目需求分析:明确了网站的功能需求和技术需求
- 项目结构设计:创建了合理的目录结构和文件组织
- 页面和组件开发:开发了首页、博客列表页、文章详情页、关于页面和联系页面,以及相关组件
- 路由配置:使用了Nuxt.js的自动路由系统,实现了页面之间的导航
- 静态资源管理:添加了图片等静态资源
- 项目构建和部署:学习了如何构建项目和部署到静态网站托管服务
通过这个项目,你应该已经掌握了Nuxt.js的基本使用方法,能够独立开发简单的Nuxt.js应用。在实际开发中,你可以根据具体需求扩展功能,如添加后端API集成、使用数据库存储数据、实现用户认证等。
练习
- 扩展博客功能,添加评论系统
- 实现深色模式切换
- 添加文章搜索功能
- 集成第三方Markdown编辑器
- 实现文章标签分类功能