Files
my-blog/frontend/index.html
T

648 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN" class="scroll-smooth">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>旺总的技术全栈博客 | Full-Stack & Cloud-Native</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#f0f9ff',
100: '#e0f2fe',
500: '#0284c7',
600: '#0369a1',
700: '#075985',
}
}
}
}
}
</script>
<!-- Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<!-- Marked.js for Markdown Rendering -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Highlight.js for Code Highlighting -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
<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; }
.dark ::-webkit-scrollbar-thumb { background: #334155; }
.markdown-body pre { background: #0d1117; border-radius: 0.75rem; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
.markdown-body code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
.markdown-body h1, .markdown-body h2, .markdown-body h3 { font-weight: 700; margin-top: 1.5rem; margin-bottom: 0.75rem; color: inherit; }
.markdown-body h1 { font-size: 1.75rem; }
.markdown-body h2 { font-size: 1.4rem; border-bottom: 1px solid rgba(156, 163, 175, 0.2); padding-bottom: 0.3rem; }
.markdown-body h3 { font-size: 1.15rem; }
.markdown-body p { line-height: 1.75; margin-bottom: 1rem; }
.markdown-body blockquote { border-left: 4px solid #0284c7; padding-left: 1rem; color: #64748b; font-style: italic; margin: 1rem 0; }
.dark .markdown-body blockquote { color: #94a3b8; }
.markdown-body ul, .markdown-body ol { padding-left: 1.5rem; margin-bottom: 1rem; list-style-type: disc; }
</style>
</head>
<body class="bg-slate-50 dark:bg-slate-950 text-slate-800 dark:text-slate-100 min-h-screen transition-colors duration-200 antialiased font-sans selection:bg-brand-500 selection:text-white">
<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 -->
<a href="#" class="flex items-center gap-3 group">
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-sky-500 to-indigo-600 flex items-center justify-center text-white shadow-md shadow-sky-500/20 group-hover:scale-105 transition-transform">
<span class="text-xl font-black">旺</span>
</div>
<div>
<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
</span>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400 hidden sm:block">Full-Stack · Cloud-Native · DevOps</p>
</div>
</a>
<!-- 右侧操作区 -->
<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>
</div>
<!-- 写文章按钮 -->
<button @click="openPublishModal"
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-sm font-semibold bg-sky-600 hover:bg-sky-700 text-white shadow-sm shadow-sky-600/30 transition-all hover:shadow active:scale-95">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
<span class="hidden sm:inline">发布文章</span>
</button>
<!-- 深色/浅色模式切换 -->
<button @click="toggleDarkMode"
class="p-2 rounded-lg text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
title="切换明暗模式">
<svg v-if="isDark" class="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path></svg>
<svg v-else class="w-5 h-5 text-slate-600" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
</button>
</div>
</div>
</header>
<!-- 主体容器 -->
<main class="flex-1 max-w-5xl w-full mx-auto px-4 sm:px-6 py-8">
<!-- Hero 博主名片区域 (借鉴 AstroPaper & GitHub Profile 设计) -->
<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>
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10">
<div class="flex items-start gap-4">
<div class="relative">
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-slate-800 to-slate-950 dark:from-sky-500 dark:to-indigo-600 flex items-center justify-center text-white text-2xl font-bold shadow-lg">
👨‍💻
</div>
<span class="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-emerald-500 border-2 border-white dark:border-slate-900" title="在线"></span>
</div>
<div>
<div class="flex items-center gap-3">
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight">旺总 (WangZong)</h1>
<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 自动化交付
</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">🐳 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>
</div>
</div>
</div>
<!-- 数据统计面板卡片 -->
<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>
<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>
<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-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"
: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'"
class="px-3.5 py-1.5 rounded-lg whitespace-nowrap font-medium transition-all">
{{ cat }}
</button>
</div>
<!-- 搜索框 -->
<div class="relative w-full sm:w-64">
<input v-model="searchKeyword"
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">
<svg class="w-4 h-4 text-slate-400 absolute left-3 top-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</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"
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 || '技术' }}
</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-400">·</span>
<span class="text-xs text-slate-500 dark:text-slate-400">⏱️ {{ post.readTime || '3 min' }}</span>
</div>
</div>
<!-- 标题 -->
<h2 class="text-lg sm:text-xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-sky-600 dark:group-hover:text-sky-400 transition-colors">
{{ post.title }}
</h2>
<!-- 摘要 -->
<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) }}...
</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"
class="text-slate-500 dark:text-slate-400 hover:text-sky-500">
#{{ tag }}
</span>
</div>
<!-- 互动按钮 -->
<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 }}
</span>
<button @click.stop="likePost(post)"
class="flex items-center gap-1 hover:text-rose-500 transition-colors active:scale-125">
<svg class="w-3.5 h-3.5 text-rose-500 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>{{ post.likes || 0 }}</span>
</button>
<span class="inline-flex items-center gap-1 font-semibold text-sky-600 dark:text-sky-400 group-hover:translate-x-0.5 transition-transform">
阅读全文 →
</span>
</div>
</div>
</article>
</div>
<!-- 空状态 -->
<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>
</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">
<div class="w-full max-w-3xl bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 max-h-[90vh] flex flex-col 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">
<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 }}
</span>
<span class="text-xs text-slate-400">·</span>
<span class="text-xs text-slate-500">📅 {{ activePost.date }}</span>
</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>
<!-- 模态框正文 -->
<div class="p-6 sm:p-8 overflow-y-auto flex-1">
<h1 class="text-2xl sm:text-3xl font-extrabold text-slate-900 dark:text-slate-100 mb-4 leading-tight">
{{ activePost.title }}
</h1>
<div class="flex items-center gap-3 text-xs text-slate-500 mb-6 pb-4 border-b border-slate-100 dark:border-slate-800">
<span>👤 作者: {{ activePost.author || '旺总' }}</span>
<span>·</span>
<span>⏱️ 预计阅读: {{ activePost.readTime || '3 min' }}</span>
<span>·</span>
<span>👁️ {{ activePost.views }} 浏览</span>
</div>
<!-- Markdown 内容渲染区 -->
<div class="markdown-body prose dark:prose-invert max-w-none text-slate-800 dark:text-slate-200 text-sm sm:text-base"
v-html="renderMarkdown(activePost.content)">
</div>
<!-- 底部互动点赞 -->
<div class="mt-8 pt-6 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between">
<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">
返回文章列表
</button>
</div>
</div>
</div>
</div>
<!-- 发布文章写作工作室模态框 (模仿 Ghost / Notion 极简写作台) -->
<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>
</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>
</button>
</div>
<div class="p-6 space-y-4 text-sm">
<div>
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">文章标题 *</label>
<input v-model="newPost.title" type="text" placeholder="例如: 探索 Kubernetes 核心架构与 Pod 调度机制"
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 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"
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>
<option value="AI & 本地模型">AI & 本地模型</option>
<option value="全栈开发 & CI/CD">全栈开发 & CI/CD</option>
<option value="前端与工程化">前端与工程化</option>
</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="默认: 旺总"
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>
<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>
<span class="text-xs text-slate-400">支持代码块高亮及标题格式</span>
</div>
<textarea v-model="newPost.content" rows="7" placeholder="# 正文标题&#10;&#10;写下你的技术思考或教程步骤...&#10;&#10;```java&#10;// 代码示例&#10;```"
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 font-mono text-xs sm:text-sm"></textarea>
</div>
<div class="flex items-center justify-end gap-3 pt-3 border-t border-slate-100 dark:border-slate-800">
<button @click="showPublishModal = false" class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800 font-medium">
取消
</button>
<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>
</button>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="mt-auto border-t border-slate-200 dark:border-slate-800/80 bg-white dark:bg-slate-900 py-8 text-center text-xs text-slate-500 dark:text-slate-400">
<div class="max-w-5xl mx-auto px-4 flex flex-col sm:flex-row items-center justify-between gap-4">
<div>
© 2026 旺总的技术全栈博客 · 驱动于
<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>·</span>
<span>Docker Compose</span>
<span>·</span>
<span>Gitea CI/CD</span>
</div>
</div>
</footer>
</div>
<!-- Vue 业务脚本 -->
<script>
const { createApp, ref, computed, onMounted } = Vue;
createApp({
setup() {
const API_BASE = '/api';
const isDark = ref(false);
const backendConnected = ref(false);
const posts = ref([]);
const stats = ref({ totalPosts: 0, totalViews: 0, totalLikes: 0 });
const selectedCategory = ref('全部');
const searchKeyword = ref('');
const activePost = ref(null);
const showPublishModal = ref(false);
const publishing = ref(false);
const categories = ['全部', '云原生 & Linux', 'Java & Spring Boot', 'AI & 本地模型', '全栈开发 & CI/CD'];
const newPost = ref({
title: '',
category: '全栈开发 & CI/CD',
author: '旺总',
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`);
if (res.ok) {
const data = await res.json();
posts.value = data;
backendConnected.value = true;
fetchStats();
return;
}
} catch (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();
}
} catch (e) {
// ignore
}
}
// 筛选文章
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;
});
});
// 阅读文章
function openPostDetail(post) {
activePost.value = post;
post.views = (post.views || 0) + 1;
if (backendConnected.value) {
fetch(`${API_BASE}/posts/${post.id}`).catch(() => {});
}
}
// 点赞
async function likePost(post) {
post.likes = (post.likes || 0) + 1;
stats.value.totalLikes = (stats.value.totalLikes || 0) + 1;
if (backendConnected.value) {
try {
await fetch(`${API_BASE}/posts/${post.id}/like`, { method: 'POST' });
} catch (e) {}
}
}
// 打开写作框
function openPublishModal() {
newPost.value = {
title: '',
category: '全栈开发 & CI/CD',
author: '旺总',
summary: '',
content: ''
};
showPublishModal.value = true;
}
// 发布新文章
async function publishPost() {
if (!newPost.value.title.trim() || !newPost.value.content.trim()) {
alert('请至少填写文章标题和文章正文内容!');
return;
}
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);
}
} else {
posts.value.unshift(createdPost);
}
publishing.value = false;
showPublishModal.value = false;
}
// Markdown 渲染
function renderMarkdown(content) {
if (!content) return '';
if (window.marked) {
return marked.parse(content);
}
return content.replace(/\n/g, '<br>');
}
// 明暗模式切换
function toggleDarkMode() {
isDark.value = !isDark.value;
if (isDark.value) {
document.documentElement.classList.add('dark');
localStorage.theme = 'dark';
} else {
document.documentElement.classList.remove('dark');
localStorage.theme = 'light';
}
}
onMounted(() => {
// 初始化主题
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
isDark.value = true;
document.documentElement.classList.add('dark');
} else {
isDark.value = false;
document.documentElement.classList.remove('dark');
}
// 配置 Marked 与代码高亮
if (window.marked && window.hljs) {
marked.setOptions({
highlight: function(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
breaks: true
});
}
fetchPosts();
});
return {
isDark,
toggleDarkMode,
backendConnected,
posts,
stats,
categories,
selectedCategory,
searchKeyword,
filteredPosts,
activePost,
openPostDetail,
likePost,
showPublishModal,
newPost,
openPublishModal,
publishPost,
publishing,
renderMarkdown
};
}
}).mount('#app');
</script>
</body>
</html>