🚀 从零打造现代化个人博客: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?
- Composition API:代码组织更清晰,逻辑复用更方便
- TypeScript 支持:原生 TS 支持,类型安全有保障
- 性能提升:响应式系统重构,运行效率大幅提升
- 生态完善:周边工具链成熟,开发体验优秀
构建工具
选择 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/ # 数据文件
核心设计理念
- 模块化设计:每个功能模块独立,便于维护
- 组件复用:公共组件抽离,避免重复代码
- 组合式 API:逻辑复用,代码更简洁
- 类型安全:全程 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

\n`, '')
}
reader.readAsDataURL(file)
}
input.click()
}
watch(() => props.modelValue, (val) => {
content.value = val
})
</script>
特性说明:

- 实时预览:使用
marked库解析 Markdown,实时渲染 HTML - 图片上传:支持拖拽和点击上传,自动转换为 Base64
- 快捷键:Ctrl+B 加粗、Ctrl+I 斜体、Ctrl+S 保存
- 自动保存:2秒防抖,避免频繁保存
- 状态显示:底部显示字数、行数、保存状态
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>
图表特点:
- 渐变效果:折线图和柱状图使用渐变色,视觉效果更佳
- 响应式:图表自适应容器大小
- 交互提示:鼠标悬停显示详细数据
- 动画效果:数据更新时有平滑过渡动画
4. AI 辅助功能
文章自动摘要
// 本地摘要生成算法
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
}
评论情感分析
// 本地情感分析
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 看板娘集成
<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>
效果:
- 可爱的二次元角色
- 支持鼠标拖动互动
- 响应式显示,移动端自适应
- 支持多种模型切换
性能优化
1. 代码分割
// 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
更多页面展示
项目总结
技术亮点
- 现代化技术栈:Vue 3 + TypeScript + Vite,开发体验优秀
- 组件化设计:高度复用的组件,代码可维护性强
- 性能优化:首屏加载快,用户体验流畅
- 功能丰富:数据可视化、AI辅助、Live2D 等特色功能
- SEO 友好:完善的 SEO 配置,利于搜索引擎收录
收获与体会
- Vue 3 Composition API 的优势明显,逻辑复用更方便
- TypeScript 能够有效减少运行时错误,提升代码质量
- Element Plus 组件库设计精美,开发效率高
- 性能优化 是持续的过程,需要不断测试和调整
- 用户体验 至关重要,细节决定成败
后续规划
- 后端集成:接入 Node.js 后端,实现真正的 CMS
- 数据库集成:使用 PostgreSQL 或 MongoDB 存储数据
- 评论系统:集成 Giscus 或 Waline
- 国际化:支持多语言
- PWA:实现离线访问
参考资源
结语
从零构建这个博客系统是一个充满挑战和收获的过程。希望本文能够帮助到正在学习 Vue 3 和前端开发的同学们。如果你有任何问题或建议,欢迎在评论区交流!
项目地址: github.com/zxgzs/fined…
作者: 晴天 日期: 2026年4月 标签: #Vue3 #TypeScript #ElementPlus #前端开发 #博客系统