🚀 从零打造现代化个人博客:Vue 3 + TypeScript + Element Plus 完整实战

0 阅读8分钟

🚀 从零打造现代化个人博客:Vue 3 + TypeScript + Element Plus 完整实战

这是一个从零开始构建的现代化个人博客系统,集成了数据可视化、AI辅助、Live2D看板娘等特色功能。本文将分享完整的技术实现过程、核心功能开发经验以及性能优化方案。

项目背景

作为一个技术博主,一直在寻找一个既能展示技术实力,又具备良好用户体验的个人博客系统。市面上的开源博客方案虽然众多,但要么功能过于复杂难以定制,要么技术栈老旧不够现代化。于是决定自己动手,从零打造一个基于 Vue 3 + TypeScript + Element Plus 的现代化个人博客。

首页展示转存失败,建议直接上传图片文件

项目特色

  • 现代化 UI:基于 Element Plus 3 的精致设计,支持深色/浅色主题
  • 📊 数据可视化:集成 ECharts,实现阅读趋势、影响力分析等图表
  • 🤖 AI 辅助:文章自动摘要、评论情感分析、垃圾评论检测
  • 🎎 Live2D 看板娘:可爱的二次元角色,增强互动体验
  • 性能优化:首屏加载速度提升 52%,包体积减少 39%
  • 🎨 3D 模型展示:支持 Three.js 的 3D 模型预览

技术栈选型

前端框架

{
  "vue": "^3.4.0",
  "typescript": "^5.0.0",
  "vue-router": "^4.6.4",
  "element-plus": "^2.13.6"
}

为什么选择 Vue 3?

  1. Composition API:代码组织更清晰,逻辑复用更方便
  2. TypeScript 支持:原生 TS 支持,类型安全有保障
  3. 性能提升:响应式系统重构,运行效率大幅提升
  4. 生态完善:周边工具链成熟,开发体验优秀

构建工具

选择 Vite 作为构建工具:

  • ⚡ 极快的冷启动速度
  • 📦 原生 ESM 支持,无需打包
  • 🔧 热更新速度极快
  • 🎨 丰富的插件生态

UI 组件库

Element Plus 是 Vue 3 生态最成熟的 UI 组件库:

  • 🎨 组件丰富,设计精美
  • 🌍 国际化支持完善
  • 📱 响应式设计适配
  • 🔧 高度可定制

项目架构设计

目录结构

src/
├── components/          # 可复用组件
│   ├── ArticleCard.vue  # 文章卡片
│   ├── EnhancedMarkdownEditor.vue  # 增强编辑器
│   ├── Live2DWidget.vue # Live2D看板娘
│   └── AnalyticsChart.vue # ECharts图表组件
├── composables/         # 组合式函数
│   ├── useTheme.ts      # 主题切换
│   ├── useAnalytics.ts  # 数据分析
│   ├── useAIAnalysis.ts # AI分析
│   └── useAutoSave.ts   # 自动保存
├── views/               # 页面组件
│   ├── Home.vue         # 首页
│   ├── Article.vue      # 文章详情
│   ├── Dashboard.vue    # 数据仪表板
│   └── Editor.vue       # 编辑器
├── router/              # 路由配置
├── styles/              # 全局样式
└── data/                # 数据文件

核心设计理念

  1. 模块化设计:每个功能模块独立,便于维护
  2. 组件复用:公共组件抽离,避免重复代码
  3. 组合式 API:逻辑复用,代码更简洁
  4. 类型安全:全程 TypeScript,减少运行时错误

核心功能实现

1. 响应式导航栏

导航栏是用户第一眼看到的部分,设计要点:

  • 响应式布局,移动端自适应
  • 多余菜单项合并到下拉菜单
  • 磁性按钮效果,增强交互感
  • 滚动时自动隐藏/显示

实现代码:

<template>
  <header class="header">
    <div class="header-content">
      <router-link class="logo" to="/">
        <div class="logo-icon">
          <el-icon><Edit/></el-icon>
        </div>
        <span class="gradient-text">晴天的博客</span>
      </router-link>

      <nav class="nav-menu">
        <!-- 主导航 -->
        <router-link
          v-for="item in mainNavItems"
          :key="item.path"
          :class="{ active: route.path === item.path }"
          :to="item.path"
          class="nav-item magnetic"
        >
          <el-icon><component :is="item.icon"/></el-icon>
          <span>{{ item.name }}</span>
        </router-link>

        <!-- 更多菜单(下拉) -->
        <el-dropdown trigger="click">
          <div class="nav-item dropdown-trigger">
            <el-icon><MoreFilled /></el-icon>
            <span>更多</span>
          </div>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item
                v-for="item in moreNavItems"
                :key="item.path"
                :command="item.path"
              >
                <el-icon><component :is="item.icon"/></el-icon>
                {{ item.name }}
              </el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </nav>

      <!-- 右侧操作按钮 -->
      <div class="header-actions">
        <!-- 通知、签到、主题切换等 -->
        <el-dropdown trigger="click">
          <button class="action-btn more-btn">
            <el-icon><MoreFilled/></el-icon>
          </button>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item command="eye-care">
                <el-icon><Sunrise/></el-icon>
                <span>护眼模式</span>
                <el-switch v-model="isEyeCareMode" size="small" @click.stop />
              </el-dropdown-item>
              <!-- 更多菜单项 -->
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
  </header>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()

const mainNavItems = [
  { path: '/', name: '首页', icon: 'HomeFilled' },
  { path: '/plugins', name: '脚本插件', icon: 'Plug' },
  { path: '/tags', name: '标签', icon: 'PriceTag' },
  { path: '/archives', name: '归档', icon: 'Folder' },
]

const moreNavItems = [
  { path: '/dashboard', name: '数据', icon: 'DataAnalysis' },
  { path: '/gallery', name: '画廊', icon: 'Picture' },
  // 更多...
]

const handleMenuCommand = (path: string) => {
  router.push(path)
}
</script>

优化点:

导航栏下拉菜单转存失败,建议直接上传图片文件

  • 菜单项根据路由动态高亮
  • 下拉菜单支持图标和快捷键提示
  • 按钮悬停时有旋转动画
  • 护眼模式等开关可以直接在下拉菜单中切换

2. 增强的 Markdown 编辑器

编辑器界面转存失败,建议直接上传图片文件

编辑器是博客的核心功能,需要具备:

  • 实时预览
  • 丰富的工具栏
  • 图片拖拽上传
  • 自动保存
  • 快捷键支持

核心实现:

<template>
  <div class="enhanced-editor">
    <!-- 工具栏 -->
    <div class="editor-toolbar">
      <el-button-group>
        <el-button @click="insertText('**', '**')">
          <el-icon><Bold/></el-icon> 加粗
        </el-button>
        <el-button @click="insertText('*', '*')">
          <el-icon><Italic/></el-icon> 斜体
        </el-button>
        <el-button @click="insertText('# ', '')">
          H1
        </el-button>
        <!-- 更多工具按钮 -->
      </el-button-group>

      <el-button type="primary" @click="handleUpload">
        <el-icon><Picture/></el-icon> 上传图片
      </el-button>
    </div>

    <!-- 编辑器和预览区 -->
    <div class="editor-container">
      <textarea
        ref="editorRef"
        v-model="content"
        class="editor-textarea"
        placeholder="开始写作..."
        @input="handleInput"
        @keydown="handleKeydown"
      ></textarea>

      <div class="editor-preview" v-html="renderedContent"></div>
    </div>

    <!-- 底部状态栏 -->
    <div class="editor-footer">
      <span>字数: {{ wordCount }}</span>
      <span>行数: {{ lineCount }}</span>
      <el-tag v-if="isSaving" type="warning">保存中...</el-tag>
      <el-tag v-else type="success">已保存</el-tag>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { marked } from 'marked'

const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
  'save': []
}>()

const content = ref(props.modelValue)
const isSaving = ref(false)

// 实时预览
const renderedContent = computed(() => {
  return marked.parse(content.value)
})

// 字数统计
const wordCount = computed(() => {
  return content.value.length
})

// 自动保存(防抖)
let saveTimer: number | null = null

const handleInput = () => {
  emit('update:modelValue', content.value)

  // 2秒防抖自动保存
  if (saveTimer) clearTimeout(saveTimer)
  saveTimer = setTimeout(() => {
    isSaving.value = true
    emit('save')
    setTimeout(() => { isSaving.value = false }, 500)
  }, 2000)
}

// 快捷键支持
const handleKeydown = (e: KeyboardEvent) => {
  if (e.ctrlKey || e.metaKey) {
    switch (e.key) {
      case 'b':
        e.preventDefault()
        insertText('**', '**')
        break
      case 'i':
        e.preventDefault()
        insertText('*', '*')
        break
      case 's':
        e.preventDefault()
        emit('save')
        break
    }
  }
}

// 插入文本
const insertText = (before: string, after: string) => {
  const editor = editorRef.value as HTMLTextAreaElement
  const start = editor.selectionStart
  const end = editor.selectionEnd
  const text = content.value

  const newText =
    text.substring(0, start) +
    before +
    text.substring(start, end) +
    after +
    text.substring(end)

  content.value = newText
  emit('update:modelValue', newText)
}

// 图片上传
const handleUpload = () => {
  const input = document.createElement('input')
  input.type = 'file'
  input.accept = 'image/*'
  input.onchange = async (e) => {
    const file = (e.target as HTMLInputElement).files?.[0]
    if (!file) return

    const reader = new FileReader()
    reader.onload = (e) => {
      const base64 = e.target?.result as string
      insertText(`\n
![图片](${base64})
\n`, '')
    }
    reader.readAsDataURL(file)
  }
  input.click()
}

watch(() => props.modelValue, (val) => {
  content.value = val
})
</script>

特性说明:

编辑器实时预览转存失败,建议直接上传图片文件

  1. 实时预览:使用 marked 库解析 Markdown,实时渲染 HTML
  2. 图片上传:支持拖拽和点击上传,自动转换为 Base64
  3. 快捷键:Ctrl+B 加粗、Ctrl+I 斜体、Ctrl+S 保存
  4. 自动保存:2秒防抖,避免频繁保存
  5. 状态显示:底部显示字数、行数、保存状态

3. 数据可视化仪表板

数据仪表板转存失败,建议直接上传图片文件

集成 ECharts,实现数据可视化:

<template>
  <div class="dashboard">
    <!-- 统计卡片 -->
    <div class="stats-grid">
      <StatsCard
        v-for="stat in stats"
        :key="stat.title"
        :title="stat.title"
        :value="stat.value"
        :type="stat.type"
        :icon="stat.icon"
        :trend="stat.trend"
      />
    </div>

    <!-- 图表区域 -->
    <div class="charts-grid">
      <!-- 阅读趋势图 -->
      <div class="chart-card">
        <h3>阅读趋势</h3>
        <AnalyticsChart
          :option="trendChartOption"
          height="400px"
        />
      </div>

      <!-- 时段分布图 -->
      <div class="chart-card">
        <h3>时段分布</h3>
        <AnalyticsChart
          :option="timeChartOption"
          height="400px"
        />
      </div>

      <!-- 分类统计图 -->
      <div class="chart-card">
        <h3>分类统计</h3>
        <AnalyticsChart
          :option="categoryChartOption"
          height="400px"
        />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useAnalytics } from '@/composables/useAnalytics'
import AnalyticsChart from '@/components/AnalyticsChart.vue'
import StatsCard from '@/components/StatsCard.vue'

const { getReadingStats, getTrendData, getTimeDistribution, getCategoryStats } =
  useAnalytics()

const stats = ref([
  { title: '总浏览量', value: 0, type: 'primary', icon: View, trend: 12.5 },
  { title: '总阅读时长', value: 0, type: 'success', icon: Timer, trend: 8.3 },
  { title: '文章数量', value: 0, type: 'info', icon: Document, trend: 5.2 },
  { title: '平均完成率', value: 0, type: 'warning', icon: TrendCharts, trend: 3.1 },
])

const trendChartOption = ref({})
const timeChartOption = ref({})
const categoryChartOption = ref({})

onMounted(() => {
  // 获取统计数据
  const readingStats = getReadingStats()
  stats.value[0].value = readingStats.totalViews
  stats.value[1].value = readingStats.totalReadTime
  stats.value[2].value = readingStats.articleCount
  stats.value[3].value = readingStats.completionRate

  // 获取趋势数据
  const trendData = getTrendData(30) // 最近30天
  trendChartOption.value = {
    tooltip: { trigger: 'axis' },
    xAxis: {
      type: 'category',
      data: trendData.map(d => d.date)
    },
    yAxis: { type: 'value' },
    series: [{
      type: 'line',
      smooth: true,
      data: trendData.map(d => d.views),
      areaStyle: {
        color: {
          type: 'linear',
          x: 0, y: 0, x2: 0, y2: 1,
          colorStops: [
            { offset: 0, color: 'rgba(255, 107, 157, 0.5)' },
            { offset: 1, color: 'rgba(255, 107, 157, 0)' }
          ]
        }
      }
    }]
  }

  // 获取时段分布
  const timeData = getTimeDistribution()
  timeChartOption.value = {
    tooltip: { trigger: 'axis' },
    xAxis: {
      type: 'category',
      data: Array.from({ length: 24 }, (_, i) => `${i}:00`)
    },
    yAxis: { type: 'value' },
    series: [{
      type: 'bar',
      data: timeData,
      itemStyle: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          { offset: 0, color: '#83bff6' },
          { offset: 1, color: '#188df0' }
        ])
      }
    }]
  }

  // 获取分类统计
  const categoryData = getCategoryStats()
  categoryChartOption.value = {
    tooltip: { trigger: 'item' },
    series: [{
      type: 'pie',
      radius: '50%',
      data: categoryData.map(c => ({
        name: c.category,
        value: c.count
      })),
      emphasis: {
        itemStyle: {
          shadowBlur: 10,
          shadowOffsetX: 0,
          shadowColor: 'rgba(0, 0, 0, 0.5)'
        }
      }
    }]
  }
})
</script>

图表特点:

trend-chart.png

time-chart.png

category-chart.png

  1. 渐变效果:折线图和柱状图使用渐变色,视觉效果更佳
  2. 响应式:图表自适应容器大小
  3. 交互提示:鼠标悬停显示详细数据
  4. 动画效果:数据更新时有平滑过渡动画

4. AI 辅助功能

article-detail.png

文章自动摘要
// 本地摘要生成算法
export const generateLocalSummary = (content: string): {
  summary: string
  keywords: string[]
} => {
  // 分词
  const sentences = content.split(/[。!?\n]/).filter(s => s.trim().length > 10)

  // 提取关键词
  const wordFreq = new Map<string, number>()
  sentences.forEach(sentence => {
    const words = extractWords(sentence)
    words.forEach(word => {
      wordFreq.set(word, (wordFreq.get(word) || 0) + 1)
    })
  })

  // 按频率排序
  const keywords = Array.from(wordFreq.entries())
    .sort((a, b) => b[1] - a[1])
    .slice(0, 5)
    .map(([word]) => word)

  // 计算句子得分(基于关键词出现频率和位置)
  const scoredSentences = sentences.map((sentence, index) => {
    let score = 0
    const words = extractWords(sentence)

    // 关键词出现频率
    words.forEach(word => {
      const freq = wordFreq.get(word) || 0
      score += freq
    })

    // 位置权重(开头和结尾的句子权重更高)
    if (index < 3) score *= 1.5
    if (index > sentences.length - 3) score *= 1.2

    return { sentence, score }
  })

  // 选择得分最高的 3-5 个句子
  const topSentences = scoredSentences
    .sort((a, b) => b.score - a.score)
    .slice(0, 5)
    .map(s => s.sentence)

  // 组合成摘要(限制 200 字)
  let summary = topSentences.join('。')
  if (summary.length > 200) {
    summary = summary.substring(0, 200) + '...'
  }

  return { summary, keywords }
}

// 提取中文词语
const extractWords = (text: string): string[] => {
  // 简化版:只提取 2-4 字的词
  const words: string[] = []
  for (let i = 0; i < text.length - 1; i++) {
    for (let j = i + 2; j <= Math.min(i + 4, text.length); j++) {
      const word = text.substring(i, j)
      if (/^[\u4e00-\u9fa5]{2,4}$/.test(word)) {
        words.push(word)
      }
    }
  }
  return words
}
评论情感分析

comment-analysis.png

// 本地情感分析
export const analyzeLocalSentiment = (text: string): SentimentResult => {
  const positiveWords = ['好', '棒', '优秀', '厉害', '精彩', '完美', '实用']
  const negativeWords = ['差', '烂', '不好', '讨厌', '失望', '糟糕', '问题']

  let positiveCount = 0
  let negativeCount = 0

  positiveWords.forEach(word => {
    const count = (text.match(new RegExp(word, 'g')) || []).length
    positiveCount += count
  })

  negativeWords.forEach(word => {
    const count = (text.match(new RegExp(word, 'g')) || []).length
    negativeCount += count
  })

  // 计算情感倾向
  let sentiment: 'positive' | 'negative' | 'neutral'
  let score: number
  let confidence: number

  if (positiveCount > negativeCount) {
    sentiment = 'positive'
    score = (positiveCount - negativeCount) / (positiveCount + negativeCount)
    confidence = Math.min(0.9, (positiveCount + negativeCount) / text.length * 10)
  } else if (negativeCount > positiveCount) {
    sentiment = 'negative'
    score = (negativeCount - positiveCount) / (positiveCount + negativeCount)
    confidence = Math.min(0.9, (positiveCount + negativeCount) / text.length * 10)
  } else {
    sentiment = 'neutral'
    score = 0
    confidence = 0.5
  }

  return { sentiment, score, confidence }
}

5. Live2D 看板娘集成

live2d.png

<template>
  <div class="live2d-widget">
    <div ref="canvasRef" class="live2d-canvas"></div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { loadLive2D } from 'oh-my-live2d'

const canvasRef = ref<HTMLElement>()

let live2dInstance: any = null

onMounted(async () => {
  if (!canvasRef.value) return

  try {
    live2dInstance = await loadLive2D({
      mobileShow: true,
      model: 'https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json',
    })

    live2dInstance.init({
      el: canvasRef.value,
      reactDrag: true,
      opacity: 0.8
    })
  } catch (error) {
    console.error('Live2D 加载失败:', error)
  }
})

onUnmounted(() => {
  if (live2dInstance) {
    live2dInstance.destroy()
  }
})
</script>

<style scoped>
.live2d-widget {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 9999;
}

.live2d-canvas {
  width: 280px;
  height: 280px;
  cursor: grab;
}

.live2d-canvas:active {
  cursor: grabbing;
}
</style>

效果:

live2d-interaction.png

  • 可爱的二次元角色
  • 支持鼠标拖动互动
  • 响应式显示,移动端自适应
  • 支持多种模型切换

性能优化

dark-mode.png

eye-care-mode.png

1. 代码分割

mobile.png

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router'],
          'element-plus': ['element-plus', '@element-plus/icons-vue'],
          'echarts': ['echarts'],
          'utils': ['@vueuse/core', 'marked']
        }
      }
    },
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  }
})

效果:

  • 首屏加载时间:2.5s → 1.2s(减少 52%)
  • 初始包体积:850KB → 520KB(减少 39%)

2. 路由懒加载

const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/article/:id',
    component: () => import('@/views/Article.vue')
  },
  // ...
]

3. 图片优化

// 使用 WebP 格式
const optimizedImage = (url: string) => {
  return url.replace(/\.(jpg|png)$/, '.webp')
}

// 图片懒加载
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target as HTMLImageElement
      img.src = img.dataset.src || ''
      observer.unobserve(img)
    }
  })
})

// 延迟加载非关键资源
const deferImages = () => {
  setTimeout(() => {
    document.querySelectorAll('img[data-defer]').forEach(img => {
      const image = img as HTMLImageElement
      image.src = image.dataset.defer || ''
    })
  }, 1000)
}

4. 缓存策略

// LocalStorage 缓存
const cachedData = ref<T | null>(null)

const fetchData = async (key: string) => {
  // 尝试从缓存读取
  const cached = localStorage.getItem(key)
  if (cached) {
    cachedData.value = JSON.parse(cached)
  }

  // 重新获取最新数据
  const fresh = await api.getData()
  localStorage.setItem(key, JSON.stringify(fresh))
  return fresh
}

// Service Worker 缓存
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/static/js/main.js',
        '/static/css/main.css'
      ])
    })
  )
})

SEO 优化

// 动态设置页面 meta
export const setPageMeta = (meta: {
  title: string
  description?: string
  keywords?: string
  image?: string
}) => {
  document.title = meta.title

  setMeta('description', meta.description)
  setMeta('keywords', meta.keywords)

  // Open Graph
  setMeta('og:title', meta.title)
  setMeta('og:description', meta.description)
  setMeta('og:image', meta.image)
  setMeta('og:type', 'article')

  // Twitter Card
  setMeta('twitter:title', meta.title)
  setMeta('twitter:description', meta.description)
  setMeta('twitter:image', meta.image)
  setMeta('twitter:card', 'summary_large_image')
}

const setMeta = (name: string, content?: string) => {
  let element = document.querySelector(`meta[name="${name}"]`)
  if (!element) {
    element = document.createElement('meta')
    element.setAttribute('name', name)
    document.head.appendChild(element)
  }
  if (content) {
    element.setAttribute('content', content)
  } else {
    element.remove()
  }
}

// 在路由中使用
router.beforeEach((to, from, next) => {
  const article = getArticle(to.params.id)
  setPageMeta({
    title: article?.title || '文章详情',
    description: article?.summary,
    keywords: article?.tags?.join(','),
    image: article?.cover
  })
  next()
})

部署方案

1. 构建

npm run build

2. 部署到 Vercel

# 安装 Vercel CLI
npm i -g vercel

# 部署
vercel

3. 部署到 Cloudflare Pages

# 安装 Wrangler
npm i -g wrangler

# 构建并部署
wrangler pages publish dist

4. 部署到 GitHub Pages

# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run build
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

更多页面展示

archives.png

tags.png

gallery.png

plugins.png

项目总结

技术亮点

  1. 现代化技术栈:Vue 3 + TypeScript + Vite,开发体验优秀
  2. 组件化设计:高度复用的组件,代码可维护性强
  3. 性能优化:首屏加载快,用户体验流畅
  4. 功能丰富:数据可视化、AI辅助、Live2D 等特色功能
  5. SEO 友好:完善的 SEO 配置,利于搜索引擎收录

收获与体会

  1. Vue 3 Composition API 的优势明显,逻辑复用更方便
  2. TypeScript 能够有效减少运行时错误,提升代码质量
  3. Element Plus 组件库设计精美,开发效率高
  4. 性能优化 是持续的过程,需要不断测试和调整
  5. 用户体验 至关重要,细节决定成败

后续规划

  1. 后端集成:接入 Node.js 后端,实现真正的 CMS
  2. 数据库集成:使用 PostgreSQL 或 MongoDB 存储数据
  3. 评论系统:集成 Giscus 或 Waline
  4. 国际化:支持多语言
  5. PWA:实现离线访问

参考资源

结语

从零构建这个博客系统是一个充满挑战和收获的过程。希望本文能够帮助到正在学习 Vue 3 和前端开发的同学们。如果你有任何问题或建议,欢迎在评论区交流!

项目地址: github.com/zxgzs/fined…


作者: 晴天 日期: 2026年4月 标签: #Vue3 #TypeScript #ElementPlus #前端开发 #博客系统