feat(fullstack): 接入 MySQL 8.0 持久化与 Spring Boot 3 分层企业级架构,支持文章与评论完整 CRUD

This commit is contained in:
2026-10-22 15:29:11 +08:00
parent 3c00b65c08
commit 572c23b60f
23 changed files with 1111 additions and 383 deletions
+214 -162
View File
@@ -33,7 +33,6 @@
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
<style>
[v-cloak] { display: none; }
/* Custom scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
@@ -54,7 +53,7 @@
<div id="app" v-cloak class="flex flex-col min-h-screen">
<!-- 顶部导航栏 (借鉴 Tailwind Next.js Starter Blog 的轻量极简毛玻璃质感) -->
<!-- 顶部导航栏 -->
<header class="sticky top-0 z-40 backdrop-blur-md bg-white/80 dark:bg-slate-900/80 border-b border-slate-200/80 dark:border-slate-800 transition-colors">
<div class="max-w-5xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
<!-- 品牌 Logo -->
@@ -66,7 +65,7 @@
<div class="font-bold text-base tracking-tight flex items-center gap-2">
<span>旺总的技术全栈博客</span>
<span class="hidden sm:inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-800 dark:bg-sky-950/80 dark:text-sky-300 border border-sky-200 dark:border-sky-800">
Pro
SpringBoot 3 + MySQL 8.0
</span>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 hidden sm:block">Full-Stack · Cloud-Native · DevOps</p>
@@ -75,11 +74,11 @@
<!-- 右侧操作区 -->
<div class="flex items-center gap-3">
<!-- 后端连接状态小药丸 -->
<!-- 数据库与后端连接指示器 -->
<div :class="backendConnected ? 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-400 dark:border-emerald-800' : 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/40 dark:text-amber-400 dark:border-amber-800'"
class="hidden md:flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs border font-medium">
<span class="w-2 h-2 rounded-full" :class="backendConnected ? 'bg-emerald-500 animate-pulse' : 'bg-amber-500'"></span>
<span>{{ backendConnected ? 'Spring Boot 容器已联通' : '离线预览模式' }}</span>
<span>{{ backendConnected ? 'MySQL 8.0 + SpringBoot 3 已联通' : '离线预览模式' }}</span>
</div>
<!-- 写文章按钮 -->
@@ -103,7 +102,7 @@
<!-- 主体容器 -->
<main class="flex-1 max-w-5xl w-full mx-auto px-4 sm:px-6 py-8">
<!-- Hero 博主名片区域 (借鉴 AstroPaper & GitHub Profile 设计) -->
<!-- Hero 博主名片区域 -->
<section class="mb-10 p-6 sm:p-8 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm relative overflow-hidden">
<div class="absolute -right-16 -top-16 w-56 h-56 bg-gradient-to-br from-sky-400/20 to-indigo-500/10 rounded-full blur-2xl pointer-events-none"></div>
@@ -121,15 +120,15 @@
<span class="px-2.5 py-0.5 text-xs font-semibold rounded-md bg-indigo-50 text-indigo-700 dark:bg-indigo-950/60 dark:text-indigo-300 border border-indigo-200/60 dark:border-indigo-800">全栈工程师</span>
</div>
<p class="mt-1 text-sm sm:text-base text-slate-600 dark:text-slate-300">
专注 Spring Boot 微服务架构 · Docker / K8s 云原生容器化 · CI/CD 自动化交付
专注 Spring Boot 3 企业级架构 · MySQL 8.0 持久化 · Docker / K8s 容器编排 · CI/CD 自动化流水线
</p>
<!-- 技术栈徽章 -->
<div class="mt-3 flex flex-wrap gap-2 text-xs">
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">⚡ Spring Boot 3</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🐬 MySQL 8.0 持久化</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">⚡ Spring Boot 3 + JPA</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🐳 Docker Compose</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🌿 Vue 3 / Vite</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🚀 Gitea Actions</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🤖 DeepSeek R1</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🚀 Gitea Actions CI/CD</span>
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🤖 DeepSeek R1 本地化</span>
</div>
</div>
</div>
@@ -137,29 +136,29 @@
<!-- 数据统计面板卡片 -->
<div class="flex items-center gap-4 sm:gap-6 bg-slate-50 dark:bg-slate-800/60 p-3 sm:p-4 rounded-xl border border-slate-200/60 dark:border-slate-700/60 self-start md:self-auto">
<div class="text-center">
<div class="text-xl sm:text-2xl font-black text-sky-600 dark:text-sky-400">{{ stats.totalPosts || posts.length }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">公开文章</div>
<div class="text-xl sm:text-2xl font-black text-sky-600 dark:text-sky-400">{{ stats.totalPosts }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">文章总数</div>
</div>
<div class="w-px h-8 bg-slate-200 dark:bg-slate-700"></div>
<div class="text-center">
<div class="text-xl sm:text-2xl font-black text-slate-800 dark:text-slate-200">{{ stats.totalViews || 2061 }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">阅读总次</div>
<div class="text-xl sm:text-2xl font-black text-slate-800 dark:text-slate-200">{{ stats.totalViews }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">累计浏览</div>
</div>
<div class="w-px h-8 bg-slate-200 dark:bg-slate-700"></div>
<div class="text-center">
<div class="text-xl sm:text-2xl font-black text-rose-500">{{ stats.totalLikes || 165 }}</div>
<div class="text-xl sm:text-2xl font-black text-rose-500">{{ stats.totalLikes }}</div>
<div class="text-xs text-slate-500 dark:text-slate-400">点赞认可</div>
</div>
</div>
</div>
</section>
<!-- 筛选与搜索控制栏 (借鉴 Halo & Fluid 主题工具条) -->
<!-- 筛选与搜索控制栏 -->
<div class="mb-6 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4">
<!-- 分类选项卡 -->
<div class="flex items-center gap-1.5 overflow-x-auto pb-2 sm:pb-0 text-sm no-scrollbar">
<button v-for="cat in categories" :key="cat"
@click="selectedCategory = cat"
@click="selectCategory(cat)"
:class="selectedCategory === cat
? 'bg-slate-900 text-white dark:bg-sky-600 dark:text-white shadow-sm'
: 'bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200 dark:border-slate-800'"
@@ -171,6 +170,7 @@
<!-- 搜索框 -->
<div class="relative w-full sm:w-64">
<input v-model="searchKeyword"
@input="onSearchInput"
type="text"
placeholder="搜索文章标题或关键词..."
class="w-full pl-9 pr-4 py-1.5 text-sm rounded-lg bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500/50 dark:focus:ring-sky-400/50 transition-all">
@@ -178,18 +178,18 @@
</div>
</div>
<!-- 文章列表区域 (模仿 GitHub Tailwind Starter Blog 经典卡片流) -->
<div v-if="filteredPosts.length > 0" class="space-y-4">
<article v-for="post in filteredPosts" :key="post.id"
<!-- 文章列表区域 -->
<div v-if="posts.length > 0" class="space-y-4">
<article v-for="post in posts" :key="post.id"
class="group p-5 sm:p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-md hover:border-sky-500/40 dark:hover:border-sky-500/40 transition-all cursor-pointer"
@click="openPostDetail(post)">
<div class="flex flex-col sm:flex-row sm:items-baseline sm:justify-between gap-1 mb-2">
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 text-xs font-semibold rounded bg-sky-50 text-sky-700 dark:bg-sky-950/70 dark:text-sky-300 border border-sky-100 dark:border-sky-900/50">
{{ post.category || '技术' }}
{{ post.categoryName || '技术杂谈' }}
</span>
<span class="text-xs text-slate-400">·</span>
<span class="text-xs text-slate-500 dark:text-slate-400">📅 {{ post.date }}</span>
<span class="text-xs text-slate-500 dark:text-slate-400">📅 {{ formatDate(post.createdAt) }}</span>
<span class="text-xs text-slate-400">·</span>
<span class="text-xs text-slate-500 dark:text-slate-400">⏱️ {{ post.readTime || '3 min' }}</span>
</div>
@@ -202,14 +202,14 @@
<!-- 摘要 -->
<p class="mt-2 text-sm text-slate-600 dark:text-slate-300 line-clamp-2 leading-relaxed">
{{ post.summary || post.content.substring(0, 140) }}...
{{ post.summary || post.content.substring(0, 140) }}
</p>
<!-- 底部标签与互动 -->
<div class="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800/80 flex items-center justify-between text-xs text-slate-500 dark:text-slate-400">
<!-- 标签群 -->
<div class="flex items-center gap-1.5 flex-wrap">
<span v-for="tag in (post.tags || [])" :key="tag"
<span v-for="tag in parseTags(post.tags)" :key="tag"
class="text-slate-500 dark:text-slate-400 hover:text-sky-500">
#{{ tag }}
</span>
@@ -219,7 +219,7 @@
<div class="flex items-center gap-4">
<span class="flex items-center gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
{{ post.views || 1 }}
{{ post.views || 0 }}
</span>
<button @click.stop="likePost(post)"
@@ -240,11 +240,11 @@
<div v-else class="text-center py-16 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800">
<div class="text-4xl mb-3">🔍</div>
<h3 class="text-base font-semibold text-slate-800 dark:text-slate-200">没有找到相关文章</h3>
<p class="text-xs text-slate-500 mt-1">换个搜索词或点击右上方“发布文章”添加一篇吧!</p>
<p class="text-xs text-slate-500 mt-1">换个搜索词或点击右上方“发布文章”向 MySQL 写入一篇吧!</p>
</div>
</main>
<!-- 文章详情阅读模态框 (模仿 AstroPaper 干净排版) -->
<!-- 文章详情阅读模态框 -->
<div v-if="activePost"
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4 sm:p-6"
@click.self="activePost = null">
@@ -253,14 +253,19 @@
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 text-xs font-semibold rounded bg-sky-50 text-sky-700 dark:bg-sky-950/70 dark:text-sky-300">
{{ activePost.category }}
{{ activePost.categoryName }}
</span>
<span class="text-xs text-slate-400">·</span>
<span class="text-xs text-slate-500">📅 {{ activePost.date }}</span>
<span class="text-xs text-slate-500">📅 {{ formatDate(activePost.createdAt) }}</span>
</div>
<div class="flex items-center gap-2">
<button @click="deletePost(activePost)" class="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded hover:bg-red-50 dark:hover:bg-red-950/40 transition-colors" title="删除文章">
删除
</button>
<button @click="activePost = null" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<button @click="activePost = null" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<!-- 模态框正文 -->
@@ -286,24 +291,60 @@
<button @click="likePost(activePost)"
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-rose-50 text-rose-600 dark:bg-rose-950/40 dark:text-rose-400 border border-rose-200 dark:border-rose-900 hover:scale-105 active:scale-95 transition-transform font-medium text-sm">
<svg class="w-4 h-4 fill-rose-500" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd"></path></svg>
<span>给文章点赞 ({{ activePost.likes }})</span>
</button>
<button @click="activePost = null" class="text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300">
返回文章列表
<span>点赞支持 ({{ activePost.likes }})</span>
</button>
</div>
<!-- 评论专区 (连接 MySQL comment 表) -->
<div class="mt-10 pt-6 border-t border-slate-200 dark:border-slate-800">
<h3 class="text-base font-bold mb-4 flex items-center gap-2">
<span>💬 读者交流区</span>
<span class="text-xs text-slate-500 font-normal">({{ comments.length }} 条持久化评论)</span>
</h3>
<!-- 发表评论表单 -->
<div class="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200/60 dark:border-slate-700/60 mb-6 space-y-3">
<div class="flex items-center gap-3">
<input v-model="newComment.author" type="text" placeholder="你的昵称 (默认: 访客)"
class="px-3 py-1.5 rounded-lg bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs w-48 focus:outline-none focus:ring-1 focus:ring-sky-500">
</div>
<textarea v-model="newComment.content" rows="2" placeholder="写下你的技术见解或想法..."
class="w-full px-3 py-2 rounded-lg bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs focus:outline-none focus:ring-1 focus:ring-sky-500"></textarea>
<div class="flex justify-end">
<button @click="submitComment" :disabled="submittingComment"
class="px-3.5 py-1.5 rounded-lg bg-sky-600 hover:bg-sky-700 text-white text-xs font-semibold shadow-sm transition-all">
{{ submittingComment ? '提交中...' : '发表评论' }}
</button>
</div>
</div>
<!-- 评论列表 -->
<div v-if="comments.length > 0" class="space-y-3">
<div v-for="c in comments" :key="c.id"
class="p-3.5 rounded-xl bg-slate-50/80 dark:bg-slate-800/40 border border-slate-100 dark:border-slate-800 text-xs">
<div class="flex items-center justify-between mb-1.5">
<span class="font-bold text-slate-900 dark:text-slate-100">👤 {{ c.author }}</span>
<span class="text-slate-400">{{ formatDate(c.createdAt) }}</span>
</div>
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">{{ c.content }}</p>
</div>
</div>
<div v-else class="text-center py-6 text-xs text-slate-400">
暂无评论,快来抢沙发吧!
</div>
</div>
</div>
</div>
</div>
<!-- 发布文章写作工作室模态框 (模仿 Ghost / Notion 极简写作台) -->
<!-- 发布文章写作工作室模态框 (直写 MySQL) -->
<div v-if="showPublishModal"
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4 sm:p-6"
@click.self="showPublishModal = false">
<div class="w-full max-w-2xl bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between">
<h3 class="font-bold text-lg text-slate-900 dark:text-slate-100 flex items-center gap-2">
<span>✍️ 撰写并发布新博客</span>
<span>✍️ 撰写并持久化发布到 MySQL</span>
</h3>
<button @click="showPublishModal = false" class="text-slate-400 hover:text-slate-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
@@ -320,7 +361,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">技术分类</label>
<select v-model="newPost.category"
<select v-model="newPost.categoryName"
class="w-full px-3.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
<option value="云原生 & Linux">云原生 & Linux</option>
<option value="Java & Spring Boot">Java & Spring Boot</option>
@@ -330,21 +371,21 @@
</select>
</div>
<div>
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">作者签名</label>
<input v-model="newPost.author" type="text" placeholder="默认: 旺总"
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">标签 (逗号分隔)</label>
<input v-model="newPost.tags" type="text" placeholder="例如: SpringBoot,MySQL,Docker"
class="w-full px-3.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
</div>
</div>
<div>
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">一句话简要摘要</label>
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">一句话摘要</label>
<input v-model="newPost.summary" type="text" placeholder="简述本文的核心要点与阅读价值..."
class="w-full px-3.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
</div>
<div>
<div class="flex items-center justify-between mb-1">
<label class="font-medium text-slate-700 dark:text-slate-300">文章内容 (支持标准 Markdown 语法) *</label>
<label class="font-medium text-slate-700 dark:text-slate-300">文章内容 (支持 Markdown) *</label>
<span class="text-xs text-slate-400">支持代码块高亮及标题格式</span>
</div>
<textarea v-model="newPost.content" rows="7" placeholder="# 正文标题&#10;&#10;写下你的技术思考或教程步骤...&#10;&#10;```java&#10;// 代码示例&#10;```"
@@ -358,7 +399,7 @@
<button @click="publishPost" :disabled="publishing"
class="px-5 py-2 rounded-lg bg-sky-600 hover:bg-sky-700 disabled:opacity-50 text-white font-semibold shadow-md shadow-sky-600/30 flex items-center gap-2">
<span v-if="publishing" class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
<span>{{ publishing ? '发布中...' : '立即发布到线上' }}</span>
<span>{{ publishing ? '写入 MySQL 中...' : '持久化发布' }}</span>
</button>
</div>
</div>
@@ -373,9 +414,9 @@
<span class="text-sky-600 dark:text-sky-400 font-mono">Google Cloud 台湾节点</span>
</div>
<div class="flex items-center gap-4">
<span>Spring Boot 3</span>
<span>Spring Boot 3 + JPA</span>
<span>·</span>
<span>Docker Compose</span>
<span>MySQL 8.0</span>
<span>·</span>
<span>Gitea CI/CD</span>
</div>
@@ -394,10 +435,13 @@
const isDark = ref(false);
const backendConnected = ref(false);
const posts = ref([]);
const stats = ref({ totalPosts: 0, totalViews: 0, totalLikes: 0 });
const stats = ref({ totalPosts: 0, totalViews: 0, totalLikes: 0, totalCategories: 0 });
const selectedCategory = ref('全部');
const searchKeyword = ref('');
const activePost = ref(null);
const comments = ref([]);
const newComment = ref({ author: '访客', content: '' });
const submittingComment = ref(false);
const showPublishModal = ref(false);
const publishing = ref(false);
@@ -405,104 +449,119 @@
const newPost = ref({
title: '',
category: '全栈开发 & CI/CD',
categoryName: '全栈开发 & CI/CD',
author: '旺总',
tags: 'SpringBoot,MySQL',
summary: '',
content: ''
});
// 本地优雅备用数据(即使脱机双击打开 HTML,也永远不会出现丑陋的红字报错)
const fallbackPosts = [
{
id: '1',
title: '云原生实践:在 Google Cloud 台湾节点搭建高性能 Ubuntu 与 1Panel',
summary: '本文记录了如何通过 GCP 赠金在亚洲低延迟机房部署 4核16G 高性能云服务器,并使用现代化容器面板 1Panel 进行运维管理实录。',
content: '# 云原生实践:在 Google Cloud 台湾节点搭建高性能服务器\n\n在挑选云服务器的过程中,综合对比了各大厂商的免费策略与性能表现,最终选择了 **Google Cloud 台湾节点 (`asia-east1`)**。\n\n## 核心硬件与网络配置\n- **规格**:`e2-standard-4`(4 vCPU,16 GB 内存)\n- **系统**:Ubuntu 22.04 LTS (x86_64)\n- **延迟**:到中国大陆仅 30~50ms,操作极为丝滑。\n\n## 为什么采用 1Panel 面板?\n相比传统臃肿的面料,1Panel 深度拥抱 **Docker 容器化**,所有应用(MySQL、OpenResty、Gitea)完全容器隔离,真正做到“环境随用随建,数据挂载持久化”。\n\n```bash\n# 快速检查 Docker 运行状态\ndocker ps --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}"\n```\n\n> 💡 **运维心得**:永远在云平台控制台统一放行防火墙端口,切勿盲目在系统内部启用 UFW,避免失联风险。',
author: '旺总',
date: '2026-10-22',
category: '云原生 & Linux',
tags: ['GCP', 'Ubuntu', '1Panel', 'Linux'],
readTime: '4 min',
views: 528,
likes: 42
},
{
id: '2',
title: '探索本地大模型:从 Ollama 运行 DeepSeek R1 到 CPU 推理性能调优',
summary: '深度解析在纯 CPU 服务器上运行 DeepSeek R1 和通义千问 2.5 的真实表现,揭开 2050 Token 预填充延迟背后的机制。',
content: '# 探索本地大模型:从 Ollama 到 Open WebUI 性能调优实战\n\n近期 DeepSeek R1 推理思考模型火遍全球,我们在 4核16G 的服务器上进行了本地大模型的全链路压测。\n\n## 1. 架构搭建\n- **模型发动机**:`Ollama` 容器\n- **交互界面**:`Open WebUI` 容器\n- **网络通信**:通过 Docker 网关 `http://172.17.0.1:11434` 极速直连\n\n## 2. 性能排查与调优秘籍\n测试过程中遇到了“每次提问卡顿 50 秒”的问题,经过 `top` 分析和指标剖析:\n1. **解除容器 CPU 配额**:确保 4 线程全开(`eval rate` 提升至 14 tokens/s);\n2. **内存永久常驻**:注入 `OLLAMA_KEEP_ALIVE=-1`,避免 5 分钟超时后重新读盘;\n3. **清空全局默认 Prompt**:消除了默认附带的 2050 历史 Token,读题耗时从 60s 骤降至 0.2s!\n\n```bash\n# 测试模型运行性能\ndocker exec -it 1Panel-ollama-nzre ollama run qwen2.5:1.5b --verbose\n```\n\n> 纯 CPU 跑 7B 参数是物理带宽瓶颈,日常开发推荐搭配 1.5B~3B 模型,速度与逻辑达到最佳平衡!',
author: '旺总',
date: '2026-10-22',
category: 'AI & 本地模型',
tags: ['DeepSeek', 'Ollama', 'OpenWebUI', '调优'],
readTime: '6 min',
views: 892,
likes: 68
},
{
id: '3',
title: '全栈工程师实战:Spring Boot 3 + Vue 3 博客与 Gitea CI/CD 自动化流水线',
summary: '从零手写前后端分离架构,配合私有 Gitea 仓库与自动化部署脚本,实现 push 代码后全自动打包上线。',
content: '# 全栈工程化实践:Spring Boot 3 + Vue 3 + CI/CD 自动化\n\n现代化开发的精髓在于**“把重复劳动交给自动化”**。我们搭建了一套小而美但功能极其完备的企业级全栈流水线。\n\n## 全栈架构一览\n```text\n[开发者本地 IDEA] ──git push──▶ [私有 Gitea 仓库]\n │\n (Webhook 触发流水线)\n ▼\n [deploy.sh 自动化脚本]\n │\n ┌─────────────┴─────────────┐\n ▼ ▼\n [Spring Boot 3 容器] [Nginx 静态反代容器]\n```\n\n## Nginx 反向代理配置关键点\n前端容器中通过反代 `/api/` 到后端服务容器,天然解决跨域和外部端口分散问题:\n\n```nginx\nlocation /api/ {\n proxy_pass http://backend:8080/api/;\n proxy_set_header Host $host;\n}\n```\n\n从此,改完代码只需一条 `git push`,剩下的事情交给服务器自动化即可!',
author: '旺总',
date: '2026-10-22',
category: '全栈开发 & CI/CD',
tags: ['Spring Boot', 'Vue 3', 'CI/CD', 'Docker Compose'],
readTime: '5 min',
views: 641,
likes: 55
}
];
// 获取后端数据
// 请求后端文章列表
async function fetchPosts() {
try {
const res = await fetch(`${API_BASE}/posts`);
const params = new URLSearchParams();
if (selectedCategory.value && selectedCategory.value !== '全部') {
params.append('category', selectedCategory.value);
}
if (searchKeyword.value.trim()) {
params.append('search', searchKeyword.value.trim());
}
const res = await fetch(`${API_BASE}/posts?${params.toString()}`);
if (res.ok) {
const data = await res.json();
posts.value = data;
const result = await res.json();
posts.value = result.data || result;
backendConnected.value = true;
fetchStats();
return;
}
} catch (e) {
console.warn('后端 API 未就绪,切换到高仿真离线模式:', e);
console.warn('后端 API 未就绪:', e);
}
posts.value = fallbackPosts;
backendConnected.value = false;
}
// 获取统计信息
// 获取全栈统计数据
async function fetchStats() {
try {
const res = await fetch(`${API_BASE}/stats`);
if (res.ok) {
stats.value = await res.json();
const result = await res.json();
stats.value = result.data || result;
}
} catch (e) {
// ignore
} catch (e) {}
}
function selectCategory(cat) {
selectedCategory.value = cat;
fetchPosts();
}
let debounceTimer = null;
function onSearchInput() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fetchPosts();
}, 300);
}
// 查看文章详情并加载评论
async function openPostDetail(post) {
activePost.value = post;
post.views = (post.views || 0) + 1;
fetchComments(post.id);
if (backendConnected.value) {
fetch(`${API_BASE}/posts/${post.id}`).catch(() => {});
}
}
// 筛选文章
const filteredPosts = computed(() => {
return posts.value.filter(p => {
const matchCat = selectedCategory.value === '全部' || p.category === selectedCategory.value;
const keyword = searchKeyword.value.trim().toLowerCase();
const matchSearch = !keyword ||
p.title.toLowerCase().includes(keyword) ||
(p.summary && p.summary.toLowerCase().includes(keyword)) ||
(p.content && p.content.toLowerCase().includes(keyword));
return matchCat && matchSearch;
});
});
// 加载该文章的评论
async function fetchComments(postId) {
try {
const res = await fetch(`${API_BASE}/posts/${postId}/comments`);
if (res.ok) {
const result = await res.json();
comments.value = result.data || [];
}
} catch (e) {
comments.value = [];
}
}
// 阅读文章
function openPostDetail(post) {
activePost.value = post;
post.views = (post.views || 0) + 1;
if (backendConnected.value) {
fetch(`${API_BASE}/posts/${post.id}`).catch(() => {});
// 提交评论
async function submitComment() {
if (!newComment.value.content.trim()) {
alert('评论内容不能为空!');
return;
}
submittingComment.value = true;
try {
const res = await fetch(`${API_BASE}/posts/${activePost.value.id}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newComment.value)
});
if (res.ok) {
newComment.value.content = '';
fetchComments(activePost.value.id);
}
} catch (e) {
alert('提交评论失败,请检查网络');
}
submittingComment.value = false;
}
// 删除文章
async function deletePost(post) {
if (!confirm(`确定要删除文章《${post.title}》吗?`)) return;
try {
const res = await fetch(`${API_BASE}/posts/${post.id}`, { method: 'DELETE' });
if (res.ok) {
activePost.value = null;
fetchPosts();
}
} catch (e) {
alert('删除失败');
}
}
@@ -517,19 +576,19 @@
}
}
// 打开写作框
function openPublishModal() {
newPost.value = {
title: '',
category: '全栈开发 & CI/CD',
categoryName: '全栈开发 & CI/CD',
author: '旺总',
tags: 'SpringBoot,MySQL',
summary: '',
content: ''
};
showPublishModal.value = true;
}
// 发布新文章
// 发布新文章至 MySQL
async function publishPost() {
if (!newPost.value.title.trim() || !newPost.value.content.trim()) {
alert('请至少填写文章标题和文章正文内容!');
@@ -537,45 +596,24 @@
}
publishing.value = true;
const createdPost = {
id: String(Date.now()),
title: newPost.value.title,
summary: newPost.value.summary || newPost.value.content.substring(0, 100),
content: newPost.value.content,
author: newPost.value.author || '旺总',
date: new Date().toISOString().split('T')[0],
category: newPost.value.category,
tags: [newPost.value.category.split(' ')[0]],
readTime: `${Math.max(2, Math.round(newPost.value.content.length / 300))} min`,
views: 1,
likes: 0
};
if (backendConnected.value) {
try {
const res = await fetch(`${API_BASE}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(createdPost)
});
if (res.ok) {
const saved = await res.json();
posts.value.unshift(saved);
} else {
posts.value.unshift(createdPost);
}
} catch (e) {
posts.value.unshift(createdPost);
try {
const res = await fetch(`${API_BASE}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost.value)
});
if (res.ok) {
showPublishModal.value = false;
fetchPosts();
} else {
alert('发布失败,请稍后重试');
}
} else {
posts.value.unshift(createdPost);
} catch (e) {
alert('发布失败,无法连接服务器');
}
publishing.value = false;
showPublishModal.value = false;
}
// Markdown 渲染
function renderMarkdown(content) {
if (!content) return '';
if (window.marked) {
@@ -584,7 +622,16 @@
return content.replace(/\n/g, '<br>');
}
// 明暗模式切换
function formatDate(dt) {
if (!dt) return '刚刚';
return String(dt).split('T')[0];
}
function parseTags(tags) {
if (!tags) return [];
return String(tags).split(',').map(t => t.trim()).filter(Boolean);
}
function toggleDarkMode() {
isDark.value = !isDark.value;
if (isDark.value) {
@@ -597,7 +644,6 @@
}
onMounted(() => {
// 初始化主题
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
isDark.value = true;
document.documentElement.classList.add('dark');
@@ -606,7 +652,6 @@
document.documentElement.classList.remove('dark');
}
// 配置 Marked 与代码高亮
if (window.marked && window.hljs) {
marked.setOptions({
highlight: function(code, lang) {
@@ -629,16 +674,23 @@
categories,
selectedCategory,
searchKeyword,
filteredPosts,
selectCategory,
onSearchInput,
activePost,
openPostDetail,
comments,
newComment,
submitComment,
deletePost,
likePost,
showPublishModal,
newPost,
openPublishModal,
publishPost,
publishing,
renderMarkdown
renderMarkdown,
formatDate,
parseTags
};
}
}).mount('#app');