Hi,我是布兰妮甜 !在当今数字化时代,拥有个人博客系统已成为展示技术能力、分享知识和建立个人品牌的重要方式。本文将详细介绍如何使用
Vue.js框架
配合Element UI
组件库构建一个功能完善的静态个人博客系统
。
文章目录
一、项目概述
在信息爆炸的数字时代,个人博客系统已成为技术从业者不可或缺的数字名片。它不仅是一个知识沉淀的平台,更是展现专业技能、传播思想见解的重要媒介。本项目将基于现代前端技术栈,打造一个兼具美观性与实用性的静态个人博客系统。
1.1 技术选型理由
- Vue.js:轻量级、渐进式框架,学习曲线平缓,适合个人项目
- Element UI:丰富的UI组件,能够快速构建美观的界面
- 静态网页:无需后端服务器,可部署在GitHub Pages等免费平台
- Markdown支持:便于内容创作和管理
1.2 系统功能规划
- 文章列表展示
- 文章详情页
- 分类和标签管理
- 响应式设计
- 简单的搜索功能
- 评论系统(可选,可通过第三方服务集成)
二、环境搭建
2.1 初始化Vue项目
# 使用Vue CLI创建项目
vue create personal-blog
# 进入项目目录
cd personal-blog
2.2 安装Element UI
# 安装Element UI
npm install element-ui -S
# 或者使用yarn
yarn add element-ui
2.3 配置Element UI
在main.js
中引入Element UI:
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
2.4 安装其他必要依赖
npm install vue-router vue-markdown axios
三、项目结构设计
├── public/ # 静态文件
├── src/
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ │ ├── Header.vue # 头部导航
│ │ ├── Footer.vue # 页脚
│ │ └── Sidebar.vue # 侧边栏
│ ├── views/ # 页面组件
│ │ ├── Home.vue # 首页
│ │ ├── BlogList.vue # 博客列表
│ │ ├── BlogDetail.vue # 博客详情
│ │ ├── About.vue # 关于页面
│ │ └── NotFound.vue # 404页面
│ ├── router/ # 路由配置
│ │ └── index.js
│ ├── store/ # Vuex状态管理
│ │ └── index.js
│ ├── styles/ # 全局样式
│ │ └── global.scss
│ ├── utils/ # 工具函数
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── package.json
└── vue.config.js # Vue配置文件
四、核心功能实现
4.1 路由配置
在src/router/index.js
中配置路由:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/views/Home.vue'
import BlogList from '@/views/BlogList.vue'
import BlogDetail from '@/views/BlogDetail.vue'
import About from '@/views/About.vue'
import NotFound from '@/views/NotFound.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/blogs',
name: 'blog-list',
component: BlogList
},
{
path: '/blog/:id',
name: 'blog-detail',
component: BlogDetail,
props: true
},
{
path: '/about',
name: 'about',
component: About
},
{
path: '*',
component: NotFound
}
]
const router = new VueRouter({
routes
})
export default router
4.2 状态管理
在src/store/index.js
中设置Vuex store:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
blogs: [
{
id: 1,
title: 'Vue.js入门指南',
summary: '本文介绍Vue.js的基本概念和使用方法',
content: '...', // 实际内容会更长
date: '2023-05-15',
tags: ['Vue', '前端'],
category: '技术'
},
// 更多博客文章...
],
categories: ['技术', '生活', '旅行'],
tags: ['Vue', '前端', 'JavaScript', 'CSS']
},
getters: {
getBlogById: (state) => (id) => {
return state.blogs.find(blog => blog.id === parseInt(id))
},
getBlogsByCategory: (state) => (category) => {
return state.blogs.filter(blog => blog.category === category)
},
getBlogsByTag: (state) => (tag) => {
return state.blogs.filter(blog => blog.tags.includes(tag))
}
},
mutations: {
// 可以添加修改state的方法
},
actions: {
// 可以添加异步操作
}
})
4.3 头部导航页面
src/components/Header.vue
:
<template>
<el-header class="app-header">
<!-- 导航栏容器 -->
<el-row type="flex" justify="space-between" align="middle" class="header-container">
<!-- 左侧Logo和标题 -->
<el-col :xs="12" :sm="6" class="header-left">
<router-link to="/" class="logo-link">
<img v-if="logo" :src="logo" alt="Logo" class="logo">
<span class="site-title">{{ siteTitle }}</span>
</router-link>
</el-col>
<!-- 中间导航菜单 -->
<el-col :sm="12" class="hidden-xs-only">
<el-menu
:default-active="activeIndex"
mode="horizontal" @select="handleSelect" class="nav-menu" background-color="transparent" text-color="#333" active-text-color="#409EFF">
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
<i v-if="item.icon" :class="item.icon"></i>
{{ item.title }}
</el-menu-item>
</el-menu>
</el-col>
<!-- 右侧功能区域 -->
<el-col :xs="12" :sm="6" class="header-right">
<!-- 搜索框 -->
<el-autocomplete v-model="searchQuery" :fetch-suggestions="querySearch" placeholder="搜索..." @select="handleSearch" class="search-input" size="small">
<template #prefix>
<i class="el-icon-search"></i>
</template>
</el-autocomplete>
<!-- 用户菜单 -->
<el-dropdown @command="handleUserCommand" class="user-dropdown">
<div class="user-info">
<el-avatar :size="36" :src="userAvatar"></el-avatar>
<span class="user-name">{{ userName }}</span>
<i class="el-icon-arrow-down el-icon--right"></i>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">
<i class="el-icon-user"></i> 个人中心
</el-dropdown-item>
<el-dropdown-item command="settings">
<i class="el-icon-setting"></i> 设置
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<i class="el-icon-switch-button"></i> 退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 移动端菜单按钮 -->
<el-button @click="drawerVisible = true" icon="el-icon-menu" circle class="mobile-menu-btn hidden-sm-and-up"></el-button>
</el-col>
</el-row>
<!-- 移动端抽屉菜单 -->
<el-drawer title="菜单导航" :visible.sync="drawerVisible" direction="ttb" size="60%" class="mobile-drawer">
<el-menu :default-active="activeIndex" @select="handleMobileSelect">
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
<i v-if="item.icon" :class="item.icon"></i>
<span slot="title">{{ item.title }}</span>
</el-menu-item>
</el-menu>
</el-drawer>
</el-header>
</template>
<script>
export default {
name: 'AppHeader',
data() {
return {
siteTitle: '我的博客',
logo: require('@/assets/logo.png'), // 假设有logo图片
activeIndex: '/',
searchQuery: '',
drawerVisible: false,
userAvatar: require('@/assets/default-avatar.jpg'),
userName: '用户名',
navItems: [
{ title: '首页', path: '/', icon: 'el-icon-house' },
{ title: '文章', path: '/articles', icon: 'el-icon-notebook-2' },
{ title: '分类', path: '/categories', icon: 'el-icon-collection' },
{ title: '关于', path: '/about', icon: 'el-icon-info' }
],
searchResults: [] // 搜索建议数据
}
},
computed: {
// 从路由获取当前激活的菜单项
currentActiveIndex() {
return this.$route.path
}
},
watch: {
// 监听路由变化更新激活菜单
$route(to) {
this.activeIndex = to.path
}
},
methods: {
// 菜单选择处理
handleSelect(index) {
this.$router.push(index)
},
// 移动端菜单选择处理
handleMobileSelect(index) {
this.$router.push(index)
this.drawerVisible = false
},
// 用户菜单命令处理
handleUserCommand(command) {
switch (command) {
case 'logout':
this.handleLogout()
break
}
},
// 搜索建议查询
querySearch(queryString, cb) {
// 这里应该是API调用,模拟数据
const results = queryString
? this.searchResults.filter(item =>
item.value.toLowerCase().includes(queryString.toLowerCase())
)
: this.searchResults
cb(results)
},
// 搜索处理
handleSearch(item) {
this.$router.push(`/search?q=${item.value}`)
this.searchQuery = ''
},
// 退出登录
handleLogout() {
this.$confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 这里应该是退出登录的逻辑
this.$message.success('已退出登录')
this.$router.push('/login')
})
}
}
}
</script>
<style lang="less" scoped>
.app-header {
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
height: 60px !important;
line-height: 60px;
position: fixed;
width: 100%;
top: 0;
z-index: 1000;
.header-container {
height: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.header-left {
display: flex;
align-items: center;
.logo-link {
display: flex;
align-items: center;
text-decoration: none;
}
.logo {
height: 36px;
margin-right: 10px;
}
.site-title {
font-size: 20px;
font-weight: bold;
color: #333;
white-space: nowrap;
}
}
.nav-menu {
border-bottom: none;
display: flex;
justify-content: center;
.el-menu-item {
height: 60px;
line-height: 60px;
padding: 0 15px;
margin: 0 5px;
&:hover {
background-color: rgba(64, 158, 255, 0.1) !important;
}
}
}
.header-right {
display: flex;
align-items: center;
justify-content: flex-end;
.search-input {
width: 180px;
margin-right: 20px;
/deep/ .el-input__inner {
border-radius: 20px;
}
}
.user-dropdown {
cursor: pointer;
.user-info {
display: flex;
align-items: center;
.user-name {
width: 55px;
margin-left: 8px;
font-size: 14px;
color: #333;
}
}
}
.mobile-menu-btn {
margin-left: 15px;
padding: 8px;
}
}
.mobile-drawer {
/deep/ .el-drawer__header {
margin-bottom: 0;
padding: 0 20px;
height: 60px;
line-height: 60px;
border-bottom: 1px solid #eee;
}
/deep/ .el-menu {
border-right: none;
}
}
}
@media (max-width: 768px) {
.app-header {
.header-left .site-title {
font-size: 16px;
}
.header-right .search-input {
width: 120px;
margin-right: 10px;
}
}
}
</style>
4.4 侧边栏页面
src/components/Sidebar.vue
:
<template>
<div class="sidebar">
<el-card class="search-card">
<el-input placeholder="搜索文章..." v-model="searchQuery" @keyup.enter.native="handleSearch">
<el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
</el-input>
</el-card>
<el-card class="category-card">
<div slot="header" class="card-header">
<span>分类</span>
</div>
<el-tag v-for="category in categories" :key="category" class="category-tag" @click="filterByCategory(category)">
{{ category }}
</el-tag>
</el-card>
<el-card class="tag-card">
<div slot="header" class="card-header">
<span>标签</span>
</div>
<el-tag v-for="tag in tags" :key="tag" class="tag-item" type="info" size="small" @click="filterByTag(tag)">
{{ tag }}
</el-tag>
</el-card>
<el-card class="about-card">
<div slot="header" class="card-header">
<span>关于我</span>
</div>
<div class="about-content">
<p>这里可以写一些个人简介...</p>
<el-button type="text" @click="$router.push({ name: 'about' })">查看更多 →</el-button>
</div>
</el-card>
</div>
</template>
<script>
export default {
name: 'Sidebar',
data() {
return {
searchQuery: ''
}
},
computed: {
categories() {
return this.$store.state.categories
},
tags() {
return this.$store.state.tags
}
},
methods: {
handleSearch() {
// 实现搜索功能
console.log('搜索:', this.searchQuery)
},
filterByCategory(category) {
this.$router.push({
name: 'blog-list',
query: { category }
})
},
filterByTag(tag) {
this.$router.push({
name: 'blog-list',
query: { tag }
})
}
}
}
</script>
<style lang="less" scoped>
.sidebar {
position: sticky;
top: 20px;
.search-card {
margin-bottom: 20px;
}
.category-tag {
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
}
.tag-card {
.tag-item {
margin-right: 5px;
margin-bottom: 5px;
cursor: pointer;
}
}
.about-card {
.about-content {
font-size: 14px;
color: #666;
line-height: 1.6;
}
}
.category-card,
.tag-card,
.about-card {
margin-bottom: 20px;
.card-header {
font-weight: bold;
color: #333;
}
}
}
</style>
4.5 页脚页面
src/components/Footer.vue
:
<template>
<footer class="app-footer">
<!-- <el-divider></el-divider> -->
<el-row class="footer-content" :gutter="20">
<!-- 版权信息 -->
<el-col :xs="24" :sm="12" :md="8">
<div class="footer-section">
<h3 class="footer-title">关于博客</h3>
<p class="footer-text">{{ blogDescription }}</p>
<p class="copyright">
© {{ currentYear }} {{ blogName }}. All Rights Reserved.
</p>
</div>
</el-col>
<!-- 快速链接 -->
<el-col :xs="24" :sm="12" :md="8">
<div class="footer-section">
<h3 class="footer-title">快速链接</h3>
<ul class="footer-links">
<li v-for="link in quickLinks" :key="link.path">
<el-link :underline="false" @click="$router.push(link.path)" class="footer-link">
<i :class="link.icon"></i> {{ link.name }}
</el-link>
</li>
</ul>
</div>
</el-col>
<!-- 联系信息 -->
<el-col :xs="24" :sm="12" :md="8">
<div class="footer-section">
<h3 class="footer-title">联系我</h3>
<ul class="contact-info">
<li v-for="contact in contacts" :key="contact.type">
<i :class="contact.icon"></i>
<span>{{ contact.value }}</span>
</li>
</ul>
<div class="social-media">
<el-link v-for="social in socialMedia" :key="social.name" :href="social.url" target="_blank" :underline="false" class="social-icon">
<i :class="social.icon" :style="{ color: social.color }"></i>
</el-link>
</div>
</div>
</el-col>
</el-row>
<!-- 备案信息 -->
<div v-if="icpInfo" class="icp-info">
<el-link :href="icpInfo.url" target="_blank" :underline="false">
{{ icpInfo.text }}
</el-link>
</div>
</footer>
</template>
<script>
export default {
name: 'Footer',
data() {
return {
blogName: '我的技术博客',
blogDescription: '分享前端开发、后端技术和互联网见闻的个人博客网站',
currentYear: new Date().getFullYear(),
quickLinks: [
{ name: '首页', path: '/', icon: 'el-icon-house' },
{ name: '文章列表', path: '/blogs', icon: 'el-icon-notebook-2' },
{ name: '分类', path: '/blogs?category=技术', icon: 'el-icon-collection-tag' },
{ name: '关于', path: '/about', icon: 'el-icon-user' },
{ name: '留言板', path: '/contact', icon: 'el-icon-chat-line-round' }
],
contacts: [
{ type: 'email', icon: 'el-icon-message', value: 'contact@example.com' },
{ type: 'github', icon: 'el-icon-star-off', value: 'github.com/username' },
{ type: 'location', icon: 'el-icon-location-information', value: '中国 · 北京' }
],
socialMedia: [
{ name: 'GitHub', icon: 'el-icon-star-off', url: 'https://github.com', color: '#333' },
{ name: 'Twitter', icon: 'el-icon-share', url: 'https://twitter.com', color: '#1DA1F2' },
{ name: 'Weibo', icon: 'el-icon-chat-line-square', url: 'https://weibo.com', color: '#E6162D' },
{ name: 'Zhihu', icon: 'el-icon-reading', url: 'https://zhihu.com', color: '#0084FF' }
],
icpInfo: {
text: '京ICP备12345678号-1',
url: 'https://beian.miit.gov.cn'
}
}
}
}
</script>
<style lang="less" scoped>
.app-footer {
background-color: #fff;
padding: 40px 20px 20px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
margin-top: 50px;
.el-divider {
margin: 0 0 30px 0;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
}
.footer-section {
padding: 0 15px;
margin-bottom: 20px;
.footer-title {
color: #333;
font-size: 1.2rem;
margin-bottom: 15px;
position: relative;
padding-left: 10px;
&::before {
content: '';
position: absolute;
left: 0;
top: 5px;
height: 60%;
width: 3px;
background: #409EFF;
border-radius: 3px;
}
}
.footer-text {
color: #666;
line-height: 1.6;
font-size: 0.9rem;
margin-bottom: 15px;
}
.copyright {
color: #999;
font-size: 0.8rem;
margin-top: 20px;
}
.footer-links {
list-style: none;
padding: 0;
li {
margin-bottom: 10px;
}
.footer-link {
color: #666;
transition: all 0.3s;
display: inline-block;
width: 100%;
i {
margin-right: 8px;
color: #409EFF;
}
&:hover {
color: #409EFF;
transform: translateX(5px);
}
}
}
.contact-info {
list-style: none;
padding: 0;
margin-bottom: 20px;
li {
margin-bottom: 12px;
display: flex;
align-items: center;
color: #666;
i {
margin-right: 10px;
color: #409EFF;
font-size: 1.1rem;
}
}
}
.social-media {
display: flex;
gap: 15px;
.social-icon {
font-size: 1.5rem;
transition: transform 0.3s;
&:hover {
transform: translateY(-3px);
}
}
}
}
.icp-info {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
color: #999;
font-size: 0.8rem;
.el-link {
color: #999;
}
}
@media (max-width: 768px) {
.footer-section {
margin-bottom: 30px;
text-align: center;
.footer-title::before {
display: none;
}
.footer-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
li {
margin-bottom: 0;
}
}
.social-media {
justify-content: center;
}
}
}
}
</style>
4.6 首页
src/views/Home.vue
:
<template>
<div class="home-container">
<!-- 欢迎横幅 -->
<el-card class="welcome-banner" shadow="never">
<div class="banner-content">
<h1>欢迎来到我的博客</h1>
<p class="subtitle">分享技术、生活和思考</p>
<el-button type="primary" size="medium" @click="$router.push({ name: 'blog-list' })">浏览文章</el-button>
</div>
</el-card>
<!-- 精选文章 -->
<div class="featured-section">
<h2 class="section-title">精选文章</h2>
<el-row :gutter="20">
<el-col v-for="blog in featuredBlogs" :key="blog.id" :xs="24" :sm="12" :md="8">
<el-card class="featured-card" shadow="hover" @click.native="$router.push({ name: 'blog-detail', params: { id: blog.id } })">
<div class="featured-header">
<h3>{{ blog.title }}</h3>
<div class="featured-meta">
<span class="date">{{ blog.date }}</span>
<el-tag size="mini">{{ blog.category }}</el-tag>
</div>
</div>
<div class="featured-summary">{{ blog.summary }}</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 最新动态 -->
<div class="recent-section">
<h2 class="section-title">最新动态</h2>
<el-timeline>
<el-timeline-item v-for="(activity, index) in activities" :key="index" :timestamp="activity.timestamp">
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
computed: {
featuredBlogs() {
return this.$store.state.blogs.slice(0, 3)
}
},
data() {
return {
activities: [
{
content: '看完了《Vue.js入门指南》',
timestamp: '2025-07-3'
},
{
content: '更新了博客分类系统',
timestamp: '2023-04-28'
},
{
content: '博客系统正式上线',
timestamp: '2023-03-10'
}
]
}
}
}
</script>
<style lang="less" scoped>
.home-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
.welcome-banner {
margin-bottom: 40px;
background: linear-gradient(135deg, #409EFF 0%, #337ecc 100%);
color: white;
border: none;
.banner-content {
padding: 40px 20px;
text-align: center;
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.2rem;
margin-bottom: 20px;
opacity: 0.9;
}
}
}
.featured-section {
.section-title {
font-size: 1.8rem;
margin: 30px 0 20px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.featured-card {
margin-bottom: 20px;
height: 100%;
cursor: pointer;
transition: transform 0.3s;
&:hover {
transform: translateY(-5px);
}
.featured-header {
h3 {
margin: 0 0 10px 0;
color: #409EFF;
}
.featured-meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #999;
}
}
.featured-summary {
margin-top: 15px;
color: #666;
line-height: 1.6;
}
}
}
.recent-section {
margin-top: 40px;
.section-title {
font-size: 1.8rem;
margin: 30px 0 20px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
}
}
</style>
4.7 博客列表页面
src/views/BlogList.vue
:
<template>
<div class="blog-list-container">
<el-row :gutter="20">
<el-col :span="18">
<h2 class="page-title">博客文章</h2>
<el-card v-for="blog in blogs" :key="blog.id" class="blog-card" shadow="hover" @click.native="$router.push({ name: 'blog-detail', params: { id: blog.id } })">
<div slot="header" class="blog-header">
<h3>{{ blog.title }}</h3>
<div class="blog-meta">
<span class="date">{{ blog.date }}</span>
<el-tag v-for="tag in blog.tags" :key="tag" size="mini" class="tag">{{ tag }}</el-tag>
</div>
</div>
<div class="blog-summary">{{ blog.summary }}</div>
</el-card>
<el-pagination background layout="prev, pager, next" :total="totalBlogs" :page-size="pageSize" @current-change="handlePageChange" class="pagination"></el-pagination>
</el-col>
<el-col :span="6">
<sidebar />
</el-col>
</el-row>
</div>
</template>
<script>
import Sidebar from '@/components/Sidebar.vue'
export default {
name: 'BlogList',
components: {
Sidebar
},
data() {
return {
pageSize: 5,
currentPage: 1
}
},
computed: {
blogs() {
const start = (this.currentPage - 1) * this.pageSize
const end = start + this.pageSize
return this.$store.state.blogs.slice(start, end)
},
totalBlogs() {
return this.$store.state.blogs.length
}
},
methods: {
handlePageChange(page) {
this.currentPage = page
}
}
}
</script>
<style lang="less" scoped>
.blog-list-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.page-title {
margin-bottom: 20px;
color: #333;
.blog-card {
margin-bottom: 20px;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.blog-header {
h3 {
margin: 0;
color: #409EFF;
}
.tag {
margin-left: 5px;
}
}
.blog-summary {
color: #666;
line-height: 1.6;
}
}
.pagination {
margin-top: 20px;
text-align: center;
}
}
}
</style>
4.8 博客详情页面
src/views/BlogDetail.vue
:
<template>
<div class="blog-detail-container">
<el-row :gutter="20">
<el-col :span="18">
<el-card class="blog-content-card">
<h1 class="blog-title">{{ blog.title }}</h1>
<div class="blog-meta">
<span class="date">{{ blog.date }}</span>
<el-tag v-for="tag in blog.tags" :key="tag" size="small" class="tag">{{ tag }}</el-tag>
<span class="category">{{ blog.category }}</span>
</div>
<div class="blog-content">
<!-- 这里使用markdown-it或其他Markdown解析器来渲染内容 -->
<vue-markdown :source="blog.content"></vue-markdown>
</div>
</el-card>
<!-- 评论区域 -->
<div class="comment-section">
<h3>评论</h3>
<!-- 这里可以集成第三方评论系统如Disqus -->
</div>
</el-col>
<el-col :span="6">
<sidebar />
</el-col>
</el-row>
</div>
</template>
<script>
import Sidebar from '@/components/Sidebar.vue'
import VueMarkdown from 'vue-markdown'
export default {
name: 'BlogDetail',
components: { Sidebar, VueMarkdown },
props: {
id: {
type: [String, Number],
required: true
}
},
computed: {
blog() {
return this.$store.getters.getBlogById(this.id)
}
},
created() {
if (!this.blog) {
this.$router.push({ name: 'not-found' })
}
}
}
</script>
<style lang="less" scoped>
.blog-detail-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
.blog-content-card {
margin-bottom: 30px;
.blog-title {
margin: 0 0 20px 0;
color: #333;
}
.blog-meta {
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
font-size: 14px;
color: #999;
.tag {
margin-right: 5px;
}
.category {
margin-left: 10px;
color: #409EFF;
}
}
.blog-content {
line-height: 1.8;
font-size: 16px;
}
}
.comment-section {
margin-top: 40px;
padding: 20px;
background: #f9f9f9;
border-radius: 4px;
}
}
</style>
4.9 关于页面
src/Views/About.vue
:
<template>
<div class="sidebar">
<el-card class="search-card">
<el-input placeholder="搜索文章..." v-model="searchQuery" @keyup.enter.native="handleSearch">
<el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
</el-input>
</el-card>
<el-card class="category-card">
<div slot="header" class="card-header">
<span>分类</span>
</div>
<el-tag v-for="category in categories" :key="category" class="category-tag" @click="filterByCategory(category)">
{{ category }}
</el-tag>
</el-card>
<el-card class="tag-card">
<div slot="header" class="card-header">
<span>标签</span>
</div>
<el-tag v-for="tag in tags" :key="tag" class="tag-item" type="info" size="small" @click="filterByTag(tag)">
{{ tag }}
</el-tag>
</el-card>
<el-card class="about-card">
<div slot="header" class="card-header">
<span>关于我</span>
</div>
<div class="about-content">
<p>这里可以写一些个人简介...</p>
<el-button type="text" @click="$router.push({ name: 'about' })">查看更多 →</el-button>
</div>
</el-card>
</div>
</template>
<script>
export default {
name: 'Sidebar',
data() {
return {
searchQuery: ''
}
},
computed: {
categories() {
return this.$store.state.categories
},
tags() {
return this.$store.state.tags
}
},
methods: {
handleSearch() {
// 实现搜索功能
console.log('搜索:', this.searchQuery)
},
filterByCategory(category) {
this.$router.push({
name: 'blog-list',
query: { category }
})
},
filterByTag(tag) {
this.$router.push({
name: 'blog-list',
query: { tag }
})
}
}
}
</script>
<style lang="less" scoped>
.sidebar {
position: sticky;
top: 20px;
.search-card {
margin-bottom: 20px;
}
.category-tag {
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
}
.tag-card {
.tag-item {
margin-right: 5px;
margin-bottom: 5px;
cursor: pointer;
}
}
.about-card {
.about-content {
font-size: 14px;
color: #666;
line-height: 1.6;
}
}
.category-card,
.tag-card,
.about-card {
margin-bottom: 20px;
.card-header {
font-weight: bold;
color: #333;
}
}
}
</style>
4.10 404页面
src/Views/NotFound.vue
:
<template>
<div class="not-found-container">
<el-card class="not-found-card" shadow="never">
<div class="error-content">
<div class="error-img">
<img src="@/assets/404.png" alt="404 Not Found" class="img-fluid">
</div>
<h1 class="error-title">404</h1>
<p class="error-subtitle">页面未找到</p>
<p class="error-text">
您访问的页面不存在或已被移除<br>
请检查URL或返回首页
</p>
<el-button type="primary" size="medium" @click="$router.push({ name: 'home' })">返回首页</el-button>
</div>
</el-card>
</div>
</template>
<script>
export default {
name: 'NotFound'
}
</script>
<style lang="less" scoped>
.not-found-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
padding: 20px;
.not-found-card {
border: none;
text-align: center;
max-width: 600px;
width: 100%;
.error-content {
padding: 40px 20px;
.error-img img {
max-width: 300px;
margin-bottom: 30px;
}
.error-title {
font-size: 5rem;
margin: 0;
color: #409EFF;
line-height: 1;
}
.error-subtitle {
font-size: 1.5rem;
margin: 10px 0;
color: #333;
}
.error-text {
color: #666;
margin-bottom: 30px;
line-height: 1.6;
}
}
}
}
</style>
4.11 App.vue
src/App.vue
:
<template>
<div id="app">
<!-- 引入 Header 组件 -->
<header-component />
<router-view />
<!-- 引入 Footer 组件 -->
<footer-component />
</div>
</template>
<script>
// 引入 Header 组件
import Header from '@/components/Header.vue'
// 引入 Footer 组件
import Footer from '@/components/Footer.vue'
export default {
name: 'App',
components: {
// 注册 Header 组件
'header-component': Header,
// 注册 Footer 组件
'footer-component': Footer
}
}
</script>
<style lang="less">
#app {
padding-top: 60px;
}
router-view {
flex: 1;
}
</style>
4.12 Main.js文件
src/Main.js
:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/styles/global.less'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
五、Markdown支持配置
为了支持Markdown格式的博客内容,我们需要配置webpack来解析Markdown文件。
5.1 修改vue.config.js
module.exports = {
chainWebpack: config => {
config.module
.rule('md')
.test(/\.md$/)
.use('vue-loader')
.loader('vue-loader')
.end()
.use('vue-markdown-loader')
.loader('vue-markdown-loader/lib/markdown-compiler')
.options({
raw: true,
preventExtract: true
})
}
}
5.2 创建Markdown博客内容
在src/assets/blogs
目录下创建Markdown文件,例如vue-intro.md
:
# Vue.js入门指南
本文介绍Vue.js的基本概念和使用方法。
## Vue是什么
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。
## 核心特性
- 响应式数据绑定
- 组件系统
- 指令系统
- 过渡效果
- ...
六、响应式设计
6.1 全局样式调整
在src/styles/global.less
中添加:
// 响应式布局
@media (max-width: 768px) {
.el-col-md-18 {
width: 100%;
}
.el-col-md-6 {
width: 100%;
margin-top: 20px;
}
.blog-list-container,
.blog-detail-container {
padding: 10px;
}
}
// 其他全局样式
body {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
line-height: 1.5;
color: #333;
background-color: #f5f7fa;
margin: 0;
padding: 0;
}
a {
color: #409eff;
text-decoration: none;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 15px;
}
6.2 在main.js中引入全局样式
import '@/styles/global.less'
七、部署静态博客
7.1 构建生产版本
npm run build
这会生成dist
目录,包含所有静态文件。
7.2 部署到GitHub Pages
- 在GitHub上创建新仓库
username.github.io
(username替换为你的GitHub用户名) - 初始化本地git仓库并添加远程仓库
- 创建
deploy.sh
脚本:
#!/usr/bin/env sh
# 确保脚本抛出遇到的错误
set -e
# 生成静态文件
npm run build
# 进入生成的文件夹
cd dist
# 如果是发布到自定义域名
# echo 'www.example.com' > CNAME
git init
git add -A
git commit -m 'deploy'
# 如果发布到 https://<USERNAME>.github.io
git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master
cd -
- 运行部署脚本:
chmod +x deploy.sh
./deploy.sh
八、技术深度增强
8.1 Vuex状态管理优化
// src/store/modules/blog.js (模块化拆分)
export default {
namespaced: true,
state: () => ({
blogs: [],
loading: false
}),
mutations: {
SET_BLOGS(state, blogs) {
state.blogs = blogs
},
SET_LOADING(state, status) {
state.loading = status
}
},
actions: {
async fetchBlogs({ commit }) {
commit('SET_LOADING', true)
try {
// 模拟API请求
const response = await import('@/assets/blogs/data.json')
commit('SET_BLOGS', response.default)
} finally {
commit('SET_LOADING', false)
}
}
}
}
8.2 动态路由加载
// src/router/index.js (懒加载+路由守卫)
const BlogDetail = () => import('@/views/BlogDetail.vue')
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
九、功能扩展
9.1 完整的Markdown解决方案
// 使用marked+highlight.js实现代码高亮
npm install marked highlight.js
<!-- src/components/MarkdownRenderer.vue -->
<template>
<div class="markdown-body" v-html="compiledMarkdown"></div>
</template>
<script>
import marked from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/github.css'
export default {
props: ['content'],
computed: {
compiledMarkdown() {
marked.setOptions({
highlight: (code) => hljs.highlightAuto(code).value
})
return marked(this.content)
}
}
}
</script>
9.2 服务端渲染(SSR)准备
# 添加Nuxt.js支持(如需SEO优化)
npx create-nuxt-app blog-ssr
十、工程化增强
10.1 配置文件分离
// config/blog.config.js
export default {
title: '我的技术博客',
baseURL: process.env.NODE_ENV === 'production'
? 'https://api.yourblog.com'
: 'http://localhost:3000'
}
10.2 自动化测试
// tests/unit/blog.spec.js
import { shallowMount } from '@vue/test-utils'
import BlogList from '@/views/BlogList.vue'
describe('BlogList.vue', () => {
it('渲染分页器当博客数量超过5篇', () => {
const wrapper = shallowMount(BlogList, {
mocks: {
$store: {
state: { blogs: Array(10).fill({/* mock数据 */}) }
}
}
})
expect(wrapper.find('.el-pagination').exists()).toBe(true)
})
})
十一、部署优化方案
11.1 CI/CD集成
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install && npm run build
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
11.2 CDN加速配置
// vue.config.js
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? 'https://cdn.yourdomain.com/blog/'
: '/'
}
十二、安全增强
12.1 CSP策略
<!-- public/index.html -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'unsafe-inline' cdn.example.com;
style-src 'self' 'unsafe-inline' fonts.googleapis.com;">
12.2 XSS防护
// 使用DOMPurify清理Markdown输出
npm install dompurify
<script>
import DOMPurify from 'dompurify'
export default {
methods: {
sanitize(html) {
return DOMPurify.sanitize(html)
}
}
}
</script>
十三、性能优化
13.1 懒加载组件
// src/router/index.js
const BlogDetail = () => import(/* webpackChunkName: "blog" */ '@/views/BlogDetail.vue')
13.2 图片优化策略
<template>
<img
v-lazy="require('@/assets/' + imagePath)"
alt="懒加载图片"
class="responsive-image">
</template>
<script>
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1.3,
attempt: 3
})
</script>
十四、总结
本文详细介绍了如何使用Vue.js和Element UI构建一个静态个人博客系统。通过这个项目,我们实现了:
- 响应式布局设计
- 博客文章的展示和管理
- 分类和标签系统
- Markdown内容支持
- 简单的搜索和过滤功能
这个静态博客系统可以轻松部署在各种静态网站托管服务上,无需服务器维护成本。随着需求的增加,可以逐步添加更多功能,如用户认证、内容管理系统等,将其扩展为更完整的博客平台。
通过这个项目,你不仅能够拥有一个个性化的博客系统,还能深入理解Vue.js和Element UI的实际应用,为更复杂的Web开发项目打下坚实基础。