构建个人博客系统:基于Vue和Element UI的静态网页实现

发布于:2025-07-05 ⋅ 阅读:(15) ⋅ 点赞:(0)

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">
            &copy; {{ 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

  1. 在GitHub上创建新仓库username.github.io(username替换为你的GitHub用户名)
  2. 初始化本地git仓库并添加远程仓库
  3. 创建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 -
  1. 运行部署脚本:
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构建一个静态个人博客系统。通过这个项目,我们实现了:

  1. 响应式布局设计
  2. 博客文章的展示和管理
  3. 分类和标签系统
  4. Markdown内容支持
  5. 简单的搜索和过滤功能

这个静态博客系统可以轻松部署在各种静态网站托管服务上,无需服务器维护成本。随着需求的增加,可以逐步添加更多功能,如用户认证、内容管理系统等,将其扩展为更完整的博客平台。

通过这个项目,你不仅能够拥有一个个性化的博客系统,还能深入理解Vue.js和Element UI的实际应用,为更复杂的Web开发项目打下坚实基础。


网站公告

今日签到

点亮在社区的每一天
去签到