Files
my-blog/frontend/index.html
T

1434 lines
91 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 · DevOps</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
brand: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
500: '#0284c7',
600: '#0369a1',
700: '#075985',
}
},
fontFamily: {
sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'Menlo', 'Monaco', 'Consolas', 'monospace']
}
}
}
}
</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; }
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 9999px; }
.dark ::-webkit-scrollbar-thumb { background: #334155; }
.dark ::-webkit-scrollbar-thumb:hover { background: #475569; }
/* 代码块与 Markdown 定制 */
.markdown-body { font-size: 1rem; line-height: 1.85; color: inherit; }
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 {
font-weight: 700;
color: inherit;
scroll-margin-top: 5rem;
}
.markdown-body h1 { font-size: 1.875rem; margin-top: 2rem; margin-bottom: 1rem; border-bottom: 1px solid rgba(148, 163, 184, 0.2); padding-bottom: 0.5rem; }
.markdown-body h2 { font-size: 1.5rem; margin-top: 1.75rem; margin-bottom: 0.75rem; border-bottom: 1px solid rgba(148, 163, 184, 0.15); padding-bottom: 0.4rem; }
.markdown-body h3 { font-size: 1.25rem; margin-top: 1.5rem; margin-bottom: 0.5rem; }
.markdown-body p { margin-bottom: 1.25rem; }
.markdown-body ul, .markdown-body ol { margin-bottom: 1.25rem; padding-left: 1.75rem; }
.markdown-body ul { list-style-type: disc; }
.markdown-body ol { list-style-type: decimal; }
.markdown-body li { margin-bottom: 0.35rem; }
.markdown-body blockquote {
border-left: 4px solid #0284c7;
padding: 0.75rem 1.25rem;
margin: 1.5rem 0;
background: rgba(2, 132, 199, 0.05);
border-radius: 0 0.75rem 0.75rem 0;
color: #475569;
}
.dark .markdown-body blockquote { color: #94a3b8; background: rgba(2, 132, 199, 0.1); }
.markdown-body table { width: 100%; margin: 1.5rem 0; border-collapse: collapse; font-size: 0.875rem; }
.markdown-body th, .markdown-body td { border: 1px solid rgba(148, 163, 184, 0.2); padding: 0.6rem 0.9rem; text-align: left; }
.markdown-body th { background: rgba(148, 163, 184, 0.1); font-weight: 600; }
.markdown-body inline-code, .markdown-body :not(pre) > code {
font-family: ui-monospace, monospace;
background: rgba(148, 163, 184, 0.15);
padding: 0.2rem 0.4rem;
border-radius: 0.375rem;
font-size: 0.875em;
color: #0284c7;
}
.dark .markdown-body :not(pre) > code { color: #38bdf8; background: rgba(148, 163, 184, 0.2); }
.markdown-body img { max-width: 100%; border-radius: 0.75rem; margin: 1.5rem auto; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }
.markdown-body hr { border: none; border-top: 1px dashed rgba(148, 163, 184, 0.3); margin: 2rem 0; }
/* macOS 风格代码高亮块容器 */
.code-block-wrapper {
margin: 1.5rem 0;
border-radius: 0.75rem;
overflow: hidden;
background: #0d1117;
border: 1px solid #30363d;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
}
.code-block-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
background: #161b22;
border-bottom: 1px solid #30363d;
font-size: 0.75rem;
color: #8b949e;
}
.code-block-dots { display: flex; gap: 0.375rem; }
.code-dot { width: 0.65rem; height: 0.65rem; border-radius: 9999px; }
.code-dot.red { background: #ff5f56; }
.code-dot.yellow { background: #ffbd2e; }
.code-dot.green { background: #27c93f; }
.code-block-wrapper pre { margin: 0; padding: 1rem; overflow-x: auto; background: transparent; font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 0.875rem; }
</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-sky-500 selection:text-white">
<div id="app" v-cloak class="flex flex-col min-h-screen">
<!-- 顶部滚动阅读进度条 (仅阅读长文时显示) -->
<div v-show="currentView === 'post'"
class="fixed top-0 left-0 h-1 bg-gradient-to-r from-sky-500 via-indigo-500 to-emerald-400 z-50 transition-all duration-75"
:style="{ width: readingProgress + '%' }">
</div>
<!-- 全局浮动通知 (Toast) -->
<div class="fixed top-5 right-5 z-50 flex flex-col gap-2 pointer-events-none">
<transition-group enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition ease-in duration-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0">
<div v-for="toast in toasts" :key="toast.id"
class="pointer-events-auto px-4 py-3 rounded-xl shadow-lg border backdrop-blur-md flex items-center gap-3 text-sm font-medium"
:class="toast.type === 'success'
? 'bg-emerald-500/90 text-white border-emerald-400'
: toast.type === 'error'
? 'bg-rose-500/90 text-white border-rose-400'
: 'bg-slate-900/90 text-white border-slate-700'">
<span>{{ toast.icon }}</span>
<span>{{ toast.text }}</span>
</div>
</transition-group>
</div>
<!-- 现代化顶部导航栏 -->
<header class="sticky top-0 z-40 backdrop-blur-md bg-white/85 dark:bg-slate-900/85 border-b border-slate-200/80 dark:border-slate-800 transition-colors">
<div class="max-w-6xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
<!-- 品牌 Logo -->
<div class="flex items-center gap-6">
<a href="#/" @click.prevent="navigateView('home')" class="flex items-center gap-3 group">
<div class="relative w-10 h-10 rounded-xl bg-gradient-to-tr from-sky-500 via-indigo-500 to-purple-600 flex items-center justify-center text-white shadow-md shadow-sky-500/25 group-hover:scale-105 transition-transform">
<span class="text-xl font-black">旺</span>
<span class="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-emerald-500 rounded-full border-2 border-white dark:border-slate-900"></span>
</div>
<div class="hidden sm:block">
<div class="font-extrabold text-base tracking-tight flex items-center gap-2">
<span class="bg-gradient-to-r from-slate-900 to-slate-700 dark:from-white dark:to-slate-300 bg-clip-text text-transparent">旺总的技术全栈</span>
<span class="px-2 py-0.5 rounded-full text-[10px] font-semibold bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300 border border-sky-200 dark:border-sky-800">
v2.0
</span>
</div>
<p class="text-[11px] text-slate-400 dark:text-slate-500 font-mono">Full-Stack · Cloud-Native · DevOps</p>
</div>
</a>
<!-- 导航链接 -->
<nav class="hidden md:flex items-center gap-1 text-sm font-medium">
<button @click="navigateView('home')"
:class="currentView === 'home' ? 'text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-950/60' : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'"
class="px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1.5">
<span>📖 文章列表</span>
</button>
<button @click="navigateView('categories')"
:class="currentView === 'categories' ? 'text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-950/60' : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'"
class="px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1.5">
<span>🗂️ 专栏分类</span>
</button>
<button @click="navigateView('archive')"
:class="currentView === 'archive' ? 'text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-950/60' : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'"
class="px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1.5">
<span>⏳ 时光归档</span>
</button>
<button @click="navigateView('about')"
:class="currentView === 'about' ? 'text-sky-600 dark:text-sky-400 bg-sky-50 dark:bg-sky-950/60' : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100 dark:hover:bg-slate-800/60'"
class="px-3 py-1.5 rounded-lg transition-colors flex items-center gap-1.5">
<span>👨‍💻 关于我 & 架构</span>
</button>
</nav>
</div>
<!-- 右侧操作区 -->
<div class="flex items-center gap-2 sm: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 lg:flex items-center gap-1.5 px-3 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 3 + MySQL 8.0 在线' : '本地安全模式' }}</span>
</div>
<!-- 写文章按钮 (唤起现代化 Markdown 写作台) -->
<button @click="openPublishModal"
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-xl text-sm font-semibold bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 text-white shadow-md shadow-sky-600/25 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-xl text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors border border-transparent dark:border-slate-800"
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>
<!-- ======================= 视图 1:主页文章流 (HOME VIEW) ======================= -->
<main v-if="currentView === 'home'" class="flex-1 max-w-6xl w-full mx-auto px-4 sm:px-6 py-8">
<!-- Hero 博主全栈名片与状态展台 -->
<section class="mb-10 p-6 sm:p-8 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm relative overflow-hidden">
<!-- 科技感背景光晕 -->
<div class="absolute -right-20 -top-20 w-72 h-72 bg-gradient-to-br from-sky-400/20 via-indigo-500/15 to-purple-500/10 rounded-full blur-3xl pointer-events-none"></div>
<div class="absolute -left-20 -bottom-20 w-72 h-72 bg-gradient-to-tr from-emerald-400/15 to-sky-500/10 rounded-full blur-3xl pointer-events-none"></div>
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-8 relative z-10">
<div class="flex items-start gap-5">
<!-- 头像 -->
<div class="relative shrink-0">
<div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-slate-900 via-indigo-950 to-slate-800 dark:from-sky-500 dark:to-indigo-600 flex items-center justify-center text-white text-3xl shadow-xl shadow-sky-500/10">
👨‍💻
</div>
<span class="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-emerald-500 border-4 border-white dark:border-slate-900" title="节点在线"></span>
</div>
<!-- 个人介绍与技术主张 -->
<div>
<div class="flex flex-wrap items-center gap-3">
<h1 class="text-2xl sm:text-3xl font-black tracking-tight text-slate-900 dark:text-white">
旺总 <span class="text-slate-400 text-lg font-normal font-mono">(WangZong)</span>
</h1>
<span class="px-2.5 py-0.5 text-xs font-semibold rounded-md bg-gradient-to-r from-sky-500/10 to-indigo-500/10 text-sky-700 dark:text-sky-300 border border-sky-200/60 dark:border-sky-800/60">
🚀 全栈架构师 · DevOps 极客
</span>
</div>
<p class="mt-2 text-sm sm:text-base text-slate-600 dark:text-slate-300 leading-relaxed max-w-2xl">
沉淀高可用企业级工程经验,深耕 <span class="font-semibold text-slate-900 dark:text-white">Spring Boot 3</span>、<span class="font-semibold text-slate-900 dark:text-white">MySQL 8.0</span> 持久化架构与云原生自动化交付;实践 <span class="font-semibold text-slate-900 dark:text-white">DeepSeek R1</span> 等本地开源 AI 大模型的微调与落地。
</p>
<!-- 技术栈徽章胶囊 -->
<div class="mt-4 flex flex-wrap gap-2 text-xs">
<span class="px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 font-mono flex items-center gap-1.5 border border-slate-200/50 dark:border-slate-700/50">
<span>🐬</span> MySQL 8.0
</span>
<span class="px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 font-mono flex items-center gap-1.5 border border-slate-200/50 dark:border-slate-700/50">
<span>🍃</span> Spring Boot 3 + JPA
</span>
<span class="px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 font-mono flex items-center gap-1.5 border border-slate-200/50 dark:border-slate-700/50">
<span>🐳</span> Docker & Compose
</span>
<span class="px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 font-mono flex items-center gap-1.5 border border-slate-200/50 dark:border-slate-700/50">
<span>⚙️</span> Gitea Actions CI/CD
</span>
<span class="px-2.5 py-1 rounded-lg bg-slate-100 dark:bg-slate-800/80 text-slate-700 dark:text-slate-300 font-mono flex items-center gap-1.5 border border-slate-200/50 dark:border-slate-700/50">
<span>🧠</span> DeepSeek R1 本地化
</span>
</div>
</div>
</div>
<!-- 动态核心指标看板 -->
<div class="grid grid-cols-3 gap-3 sm:gap-4 bg-slate-50/90 dark:bg-slate-800/50 p-4 rounded-2xl border border-slate-200/70 dark:border-slate-700/60 shrink-0">
<div class="text-center px-2">
<div class="text-2xl sm:text-3xl font-black text-sky-600 dark:text-sky-400 font-mono">{{ stats.totalPosts }}</div>
<div class="text-[11px] text-slate-500 dark:text-slate-400 font-medium mt-0.5">技术长文</div>
</div>
<div class="text-center px-2 border-x border-slate-200 dark:border-slate-700">
<div class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-slate-200 font-mono">{{ stats.totalViews }}</div>
<div class="text-[11px] text-slate-500 dark:text-slate-400 font-medium mt-0.5">全站阅读</div>
</div>
<div class="text-center px-2">
<div class="text-2xl sm:text-3xl font-black text-rose-500 font-mono">{{ stats.totalLikes }}</div>
<div class="text-[11px] text-slate-500 dark:text-slate-400 font-medium mt-0.5">读者点赞</div>
</div>
</div>
</div>
</section>
<!-- 搜索与分类导航过滤条 -->
<div class="mb-8 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4">
<!-- 分类 Tab 切换 -->
<div class="flex items-center gap-2 overflow-x-auto pb-2 md:pb-0 text-sm no-scrollbar">
<button v-for="cat in categories" :key="cat"
@click="selectCategory(cat)"
:class="selectedCategory === cat
? 'bg-slate-900 text-white dark:bg-sky-600 dark:text-white shadow-sm font-semibold'
: '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/80 dark:border-slate-800'"
class="px-4 py-2 rounded-xl whitespace-nowrap transition-all flex items-center gap-1.5">
<span>{{ getCategoryIcon(cat) }}</span>
<span>{{ cat }}</span>
<span v-if="selectedCategory === cat" class="w-1.5 h-1.5 rounded-full bg-sky-400 dark:bg-white ml-0.5"></span>
</button>
</div>
<!-- 实时快速搜索框 -->
<div class="relative w-full md:w-72">
<input v-model="searchKeyword"
@input="onSearchInput"
type="text"
placeholder="搜索文章、技术栈、关键字..."
class="w-full pl-10 pr-8 py-2 text-sm rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500/40 dark:focus:ring-sky-400/40 transition-all shadow-sm">
<svg class="w-4 h-4 text-slate-400 absolute left-3.5 top-3" 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>
<button v-if="searchKeyword" @click="searchKeyword = ''; fetchPosts();" class="absolute right-3 top-2.5 text-slate-400 hover:text-slate-600">×</button>
</div>
</div>
<!-- 文章卡片网格列表 -->
<div v-if="posts.length > 0" class="space-y-5">
<article v-for="post in posts" :key="post.id"
@click="viewPost(post)"
class="group p-6 sm:p-7 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-xl hover:border-sky-500/40 dark:hover:border-sky-500/40 transition-all duration-300 cursor-pointer relative overflow-hidden">
<!-- 顶部精美标签与时间 -->
<div class="flex flex-wrap items-center justify-between gap-2 mb-3">
<div class="flex items-center gap-2">
<span class="px-2.5 py-1 text-xs font-bold rounded-lg bg-sky-50 text-sky-700 dark:bg-sky-950/80 dark:text-sky-300 border border-sky-100 dark:border-sky-900/60 flex items-center gap-1">
<span>{{ getCategoryIcon(post.categoryName) }}</span>
<span>{{ post.categoryName || '技术架构' }}</span>
</span>
<span class="text-xs text-slate-400">·</span>
<span class="text-xs text-slate-500 dark:text-slate-400 font-mono">📅 {{ formatDate(post.createdAt) }}</span>
</div>
<div class="text-xs text-slate-400 dark:text-slate-500 font-mono flex items-center gap-1">
<span>⏱️ {{ post.readTime || '5 min' }}</span>
</div>
</div>
<!-- 标题 -->
<h2 class="text-xl sm:text-2xl font-black text-slate-900 dark:text-slate-100 group-hover:text-sky-600 dark:group-hover:text-sky-400 transition-colors leading-snug">
{{ post.title }}
</h2>
<!-- 摘要提要 -->
<p class="mt-2.5 text-sm sm:text-base text-slate-600 dark:text-slate-300 line-clamp-2 leading-relaxed">
{{ post.summary || post.content.substring(0, 150) }}
</p>
<!-- 底部标签与互动状态 -->
<div class="mt-5 pt-4 border-t border-slate-100 dark:border-slate-800/80 flex flex-wrap items-center justify-between gap-3 text-xs text-slate-500 dark:text-slate-400">
<!-- 标签群 -->
<div class="flex items-center gap-2 flex-wrap">
<span v-for="tag in parseTags(post.tags)" :key="tag"
class="px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800/80 text-slate-600 dark:text-slate-300 font-mono text-[11px]">
#{{ tag }}
</span>
</div>
<!-- 互动动作 -->
<div class="flex items-center gap-5">
<span class="flex items-center gap-1 font-mono" title="阅读次数">
<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="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 || 0 }}
</span>
<button @click.stop="likePost(post)"
class="flex items-center gap-1.5 hover:text-rose-500 transition-colors active:scale-125 font-mono" title="点赞支持">
<svg class="w-4 h-4 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-bold text-sky-600 dark:text-sky-400 group-hover:translate-x-1 transition-transform">
沉浸阅读 →
</span>
</div>
</div>
</article>
</div>
<!-- 空状态提示 -->
<div v-else class="text-center py-20 bg-white dark:bg-slate-900 rounded-3xl border border-slate-200/80 dark:border-slate-800 shadow-sm">
<div class="text-5xl mb-4">🔍</div>
<h3 class="text-lg font-bold text-slate-800 dark:text-slate-200">没有检索到匹配的文章</h3>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-2 max-w-sm mx-auto">
尝试更换搜索关键词,或者点击右上角“写新文章”向 MySQL 写入一篇吧!
</p>
<button @click="openPublishModal"
class="mt-5 px-5 py-2 rounded-xl text-sm font-semibold bg-sky-600 hover:bg-sky-700 text-white shadow-md shadow-sky-600/30 transition-all">
✍️ 立即发布第一篇
</button>
</div>
</main>
<!-- ======================= 视图 2:沉浸式文章详情阅读页 (POST VIEW) ======================= -->
<main v-if="currentView === 'post' && activePost" class="flex-1 max-w-6xl w-full mx-auto px-4 sm:px-6 py-8">
<!-- 面包屑与顶部返回导航 -->
<div class="mb-6 flex items-center justify-between">
<button @click="navigateView('home')"
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 hover:border-sky-500 text-sm font-medium transition-all shadow-sm">
<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="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
<span>返回文章列表</span>
</button>
<div class="flex items-center gap-2">
<button @click="copyShareLink"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-xs font-medium hover:border-slate-300 dark:hover:border-slate-700 transition-colors">
<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="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"></path></svg>
<span>分享本文</span>
</button>
<button @click="deletePost(activePost)"
class="inline-flex items-center gap-1 px-3 py-1.5 rounded-xl text-xs text-rose-600 hover:bg-rose-50 dark:hover:bg-rose-950/40 border border-rose-200/50 dark:border-rose-900/50 transition-colors">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
<span>删除文章</span>
</button>
</div>
</div>
<!-- 主文章与大纲侧边栏双栏布局 -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- 左侧主文章内容区 (占 3 栏) -->
<div class="lg:col-span-3">
<article class="p-6 sm:p-10 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm">
<!-- 文章元信息头 -->
<header class="mb-8 pb-6 border-b border-slate-100 dark:border-slate-800">
<div class="flex items-center gap-2 mb-3">
<span class="px-2.5 py-1 text-xs font-bold rounded-lg bg-sky-50 text-sky-700 dark:bg-sky-950/80 dark:text-sky-300 border border-sky-100 dark:border-sky-900/60">
{{ activePost.categoryName }}
</span>
<span class="text-xs text-slate-400">·</span>
<span class="text-xs text-slate-500 font-mono">发布于 {{ formatDate(activePost.createdAt) }}</span>
</div>
<h1 class="text-2xl sm:text-4xl font-black text-slate-900 dark:text-white tracking-tight leading-tight mb-4">
{{ activePost.title }}
</h1>
<div class="flex flex-wrap items-center gap-4 text-xs text-slate-500 dark:text-slate-400 font-mono">
<span class="flex items-center gap-1.5">
<span class="w-5 h-5 rounded-full bg-slate-800 text-white flex items-center justify-center text-[10px]">旺</span>
<span>作者: {{ activePost.author || '旺总' }}</span>
</span>
<span>·</span>
<span>⏱️ 预计耗时 {{ activePost.readTime || '5 min' }}</span>
<span>·</span>
<span>👁️ {{ activePost.views }} 次阅读</span>
<span>·</span>
<span class="text-rose-500">❤️ {{ activePost.likes }} 点赞</span>
</div>
<!-- 标签群 -->
<div class="mt-4 flex flex-wrap gap-1.5">
<span v-for="tag in parseTags(activePost.tags)" :key="tag"
class="px-2 py-0.5 rounded-md bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-mono text-xs">
#{{ tag }}
</span>
</div>
</header>
<!-- ✨ 2026 标配:AI 核心要点速读卡片 -->
<div class="mb-8 p-5 rounded-2xl bg-gradient-to-br from-sky-500/5 via-indigo-500/5 to-purple-500/5 border border-sky-500/20 dark:border-sky-500/30">
<div class="flex items-center gap-2 mb-2 text-sky-600 dark:text-sky-400 font-bold text-sm">
<span>✨</span>
<span>AI 智能速读 · 核心要点提炼</span>
</div>
<div class="text-xs sm:text-sm text-slate-700 dark:text-slate-300 leading-relaxed space-y-1.5">
<p>📌 <strong>核心主题</strong>:{{ activePost.summary || activePost.title }}</p>
<p>💡 <strong>实践价值</strong>:基于真实生产架构提炼,避免重复踩坑,代码与配置经过多轮全流程验证。</p>
<p>⚡ <strong>技术归宿</strong>:持久化存储于 MySQL 8.0,支持 Docker 容器全自动持续集成与部署交付。</p>
</div>
</div>
<!-- 正文 Markdown 渲染 -->
<div class="markdown-body" v-html="renderedPostContent"></div>
<!-- 文章底部互动与点赞 -->
<div class="mt-12 pt-8 border-t border-slate-100 dark:border-slate-800 flex flex-col sm:flex-row items-center justify-between gap-4">
<button @click="likePost(activePost)"
class="group inline-flex items-center gap-2 px-6 py-3 rounded-2xl bg-rose-50 hover:bg-rose-100 dark:bg-rose-950/40 dark:hover:bg-rose-950/60 text-rose-600 dark:text-rose-400 border border-rose-200 dark:border-rose-900 transition-all active:scale-95 shadow-sm">
<svg class="w-5 h-5 fill-rose-500 group-hover:scale-125 transition-transform" 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 class="font-bold">点赞支持本文 ({{ activePost.likes }})</span>
</button>
<button @click="copyShareLink"
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 text-xs font-semibold text-slate-700 dark:text-slate-200 transition-colors">
<span>📋 复制文章链接</span>
</button>
</div>
</article>
<!-- 读者互动评论专区 -->
<section class="mt-8 p-6 sm:p-8 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm">
<div class="flex items-center justify-between mb-6">
<h3 class="font-extrabold text-lg text-slate-900 dark:text-white flex items-center gap-2">
<span>💬 读者交流与讨论</span>
<span class="px-2 py-0.5 text-xs font-mono rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500">
{{ comments.length }} 条持久化记录
</span>
</h3>
</div>
<!-- 撰写评论卡片 -->
<div class="bg-slate-50/80 dark:bg-slate-800/40 p-4 sm:p-5 rounded-2xl 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.5 py-1.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs w-48 focus:outline-none focus:ring-2 focus:ring-sky-500">
<span class="text-xs text-slate-400">评论将实时持久化写入 MySQL 8.0</span>
</div>
<textarea v-model="newComment.content" rows="3" placeholder="写下你的技术思考、疑问或独到见解..."
class="w-full px-3.5 py-2.5 rounded-xl bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs sm:text-sm focus:outline-none focus:ring-2 focus:ring-sky-500"></textarea>
<div class="flex justify-end">
<button @click="submitComment" :disabled="submittingComment"
class="px-5 py-2 rounded-xl bg-sky-600 hover:bg-sky-700 disabled:opacity-50 text-white text-xs font-bold shadow-sm transition-all flex items-center gap-1.5">
<span v-if="submittingComment" class="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
<span>{{ submittingComment ? '写入数据库中...' : '提交评论' }}</span>
</button>
</div>
</div>
<!-- 评论展示列表 -->
<div v-if="comments.length > 0" class="space-y-3">
<div v-for="c in comments" :key="c.id"
class="p-4 rounded-2xl bg-slate-50/60 dark:bg-slate-800/30 border border-slate-100 dark:border-slate-800 text-xs sm:text-sm">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-sky-500/20 text-sky-600 dark:text-sky-400 font-bold flex items-center justify-center text-xs">
{{ c.author ? c.author.charAt(0).toUpperCase() : 'U' }}
</div>
<span class="font-bold text-slate-900 dark:text-slate-100">{{ c.author || '匿名访客' }}</span>
</div>
<span class="text-xs text-slate-400 font-mono">{{ formatDate(c.createdAt) }}</span>
</div>
<p class="text-slate-700 dark:text-slate-300 leading-relaxed pl-8">{{ c.content }}</p>
</div>
</div>
<div v-else class="text-center py-8 text-xs text-slate-400">
暂无评论,留下你的第一条精彩见解吧!
</div>
</section>
</div>
<!-- 右侧悬浮目录大纲 (TOC Sidebar) -->
<div class="hidden lg:block lg:col-span-1">
<div class="sticky top-24 p-5 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm">
<div class="flex items-center gap-2 font-bold text-xs uppercase tracking-wider text-slate-400 dark:text-slate-500 mb-4 pb-2 border-b border-slate-100 dark:border-slate-800">
<span>📑 文章大纲目录</span>
</div>
<nav v-if="postToc.length > 0" class="space-y-1.5 text-xs max-h-[70vh] overflow-y-auto pr-1">
<a v-for="item in postToc" :key="item.id"
:href="'#' + item.id"
@click.prevent="scrollToHeading(item.id)"
:class="activeTocId === item.id
? 'text-sky-600 dark:text-sky-400 font-bold bg-sky-50 dark:bg-sky-950/60 rounded-lg px-2'
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white px-2'"
:style="{ paddingLeft: (item.level === 1 ? '0.5rem' : item.level === 2 ? '1.25rem' : '2rem') }"
class="block py-1 rounded transition-colors truncate">
{{ item.text }}
</a>
</nav>
<div v-else class="text-xs text-slate-400">
本文未设置分级标题
</div>
<!-- 快捷工具按钮 -->
<div class="mt-6 pt-4 border-t border-slate-100 dark:border-slate-800 flex flex-col gap-2">
<button @click="scrollToTop" class="w-full py-2 rounded-xl text-xs font-semibold bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 transition-colors">
↑ 回到顶部
</button>
</div>
</div>
</div>
</div>
</main>
<!-- ======================= 视图 3:时光归档 (ARCHIVE VIEW) ======================= -->
<main v-if="currentView === 'archive'" class="flex-1 max-w-4xl w-full mx-auto px-4 sm:px-6 py-8">
<div class="mb-8">
<h1 class="text-3xl font-black text-slate-900 dark:text-white flex items-center gap-3">
<span>⏳</span>
<span>技术成长时光轴</span>
</h1>
<p class="mt-2 text-sm text-slate-500">按发布时间记录每一篇全栈架构与容器化工程实录,共计 {{ posts.length }} 篇。</p>
</div>
<!-- 时间轴主体 -->
<div class="relative pl-6 sm:pl-8 border-l-2 border-slate-200 dark:border-slate-800 space-y-8">
<div v-for="post in posts" :key="post.id" class="relative group cursor-pointer" @click="viewPost(post)">
<!-- 时间轴圆点 -->
<div class="absolute -left-[31px] sm:-left-[39px] top-1.5 w-4 h-4 rounded-full bg-white dark:bg-slate-900 border-4 border-sky-500 group-hover:scale-125 group-hover:border-indigo-500 transition-all"></div>
<!-- 卡片 -->
<div class="p-5 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 group-hover:border-sky-500/40 shadow-sm transition-all">
<div class="flex items-center gap-2 text-xs font-mono text-slate-400 mb-1.5">
<span>📅 {{ formatDate(post.createdAt) }}</span>
<span>·</span>
<span class="text-sky-600 dark:text-sky-400 font-semibold">{{ post.categoryName }}</span>
</div>
<h3 class="text-base sm:text-lg font-bold text-slate-900 dark:text-slate-100 group-hover:text-sky-600 dark:group-hover:text-sky-400 transition-colors">
{{ post.title }}
</h3>
<p class="mt-1 text-xs sm:text-sm text-slate-500 line-clamp-1">
{{ post.summary || post.content.substring(0, 100) }}
</p>
</div>
</div>
</div>
</main>
<!-- ======================= 视图 4:专栏分类 (CATEGORIES VIEW) ======================= -->
<main v-if="currentView === 'categories'" class="flex-1 max-w-5xl w-full mx-auto px-4 sm:px-6 py-8">
<div class="mb-8">
<h1 class="text-3xl font-black text-slate-900 dark:text-white flex items-center gap-3">
<span>🗂️</span>
<span>技术专栏全景</span>
</h1>
<p class="mt-2 text-sm text-slate-500">按照企业级架构体系细分的专业技术领域与专题沉淀。</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div v-for="cat in categoryList" :key="cat.name"
@click="filterByCategory(cat.name)"
class="p-6 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 hover:border-sky-500/50 shadow-sm hover:shadow-lg transition-all cursor-pointer group">
<div class="flex items-center justify-between mb-4">
<div class="text-3xl p-3 rounded-2xl bg-sky-50 dark:bg-sky-950/60 w-fit">
{{ cat.icon }}
</div>
<span class="px-3 py-1 rounded-full text-xs font-mono font-bold bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">
{{ getPostsCountByCategory(cat.name) }} 篇文章
</span>
</div>
<h3 class="text-xl font-bold text-slate-900 dark:text-white group-hover:text-sky-600 dark:group-hover:text-sky-400 transition-colors">
{{ cat.name }}
</h3>
<p class="mt-2 text-xs sm:text-sm text-slate-500 leading-relaxed">
{{ cat.desc }}
</p>
<div class="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800/80 flex items-center justify-between text-xs font-semibold text-sky-600 dark:text-sky-400">
<span>查看该专栏全部文章</span>
<span class="group-hover:translate-x-1 transition-transform">→</span>
</div>
</div>
</div>
</main>
<!-- ======================= 视图 5:关于我与架构全景 (ABOUT VIEW) ======================= -->
<main v-if="currentView === 'about'" class="flex-1 max-w-4xl w-full mx-auto px-4 sm:px-6 py-8">
<article class="p-6 sm:p-10 rounded-3xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm space-y-8">
<div>
<h1 class="text-3xl font-black text-slate-900 dark:text-white flex items-center gap-3">
<span>👨‍💻</span>
<span>关于旺总 (WangZong) 与本站架构</span>
</h1>
<p class="mt-2 text-sm text-slate-500">一个追求工程极致、拥抱云原生与本地大模型的全栈开发者个人空间。</p>
</div>
<!-- 技术栈矩阵 -->
<div class="space-y-4">
<h2 class="text-lg font-bold text-slate-900 dark:text-white border-b border-slate-100 dark:border-slate-800 pb-2">
🛠️ 掌握的技术栈 & 工具链
</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs sm:text-sm">
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200/60 dark:border-slate-700/60">
<div class="font-bold text-sky-600 dark:text-sky-400 mb-2">🍃 后端企业级架构</div>
<p class="text-slate-600 dark:text-slate-300">Java 17 / 21 · Spring Boot 3.2 · Spring Data JPA · RESTful API · 统一异常治理</p>
</div>
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200/60 dark:border-slate-700/60">
<div class="font-bold text-emerald-600 dark:text-emerald-400 mb-2">🐬 数据持久化与存储</div>
<p class="text-slate-600 dark:text-slate-300">MySQL 8.0 性能调优 · Docker 命名卷持久化 · 自动库表结构同步 · 读写连接池</p>
</div>
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200/60 dark:border-slate-700/60">
<div class="font-bold text-indigo-600 dark:text-indigo-400 mb-2">🐳 容器化 & CI/CD 流水线</div>
<p class="text-slate-600 dark:text-slate-300">Docker & Docker Compose · Gitea Actions (Act Runner) · Nginx 反向代理 · 1Panel 运维</p>
</div>
<div class="p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200/60 dark:border-slate-700/60">
<div class="font-bold text-purple-600 dark:text-purple-400 mb-2">🧠 现代 AI & 本地模型工程</div>
<p class="text-slate-600 dark:text-slate-300">Ollama · Open WebUI · DeepSeek R1 性能调优 · CPU 线程释放 · Prompt 预填充优化</p>
</div>
</div>
</div>
<!-- 本站真实系统架构拓扑图 -->
<div class="space-y-4">
<h2 class="text-lg font-bold text-slate-900 dark:text-white border-b border-slate-100 dark:border-slate-800 pb-2">
🏗️ 本站全链路架构拓扑
</h2>
<div class="p-5 rounded-2xl bg-[#0d1117] text-slate-200 font-mono text-xs overflow-x-auto leading-relaxed border border-[#30363d]">
<pre>┌────────────────────────────────────────────────────────────────────────┐
│ Google Cloud 台湾节点 (GCP 4核16G) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ [ 用户访问 http://104.199.128.0:8088 ] │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ Nginx 容器 (前端静态+反代) │ │
│ └────────────┬────────────┘ │
│ │ /api/ 反向代理 │
│ ▼ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Spring Boot 3 容器 (8080) │ ──JPA──▶│ 1Panel MySQL 8.0 容器 │ │
│ └─────────────────────────┘ │ (持久化卷数据隔离保护) │ │
│ ▲ └─────────────────────────┘ │
│ │ 自动重构镜像上线 │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Gitea Runner CI/CD 容器 │ ◀────── │ 私有 Gitea 仓库 (端口3000)│ │
│ └─────────────────────────┘ (git push)└─────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘</pre>
</div>
</div>
<!-- 联系与社交 -->
<div class="pt-4 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between text-xs text-slate-500">
<div>博主邮箱:<span class="font-mono text-sky-600 dark:text-sky-400">j2570852518@gmail.com</span></div>
<div>代码托管:<a href="http://104.199.128.0:3000/admin/my-blog" target="_blank" class="hover:underline text-sky-600">私有 Gitea 仓库</a></div>
</div>
</article>
</main>
<!-- ======================= 写作发布工作室模态框 (双栏实时预览) ======================= -->
<div v-if="showPublishModal"
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/70 backdrop-blur-sm flex items-center justify-center p-3 sm:p-6"
@click.self="showPublishModal = false">
<div class="w-full max-w-4xl bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden flex flex-col max-h-[92vh] 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 shrink-0">
<div class="flex items-center gap-3">
<span class="text-xl">✍️</span>
<div>
<h3 class="font-bold text-base text-slate-900 dark:text-white">Markdown 创作工作室</h3>
<p class="text-xs text-slate-400">直写 1Panel MySQL 8.0 生产库</p>
</div>
</div>
<!-- 模式切换 -->
<div class="flex items-center gap-2">
<div class="hidden sm:flex bg-slate-100 dark:bg-slate-800 p-1 rounded-xl text-xs font-semibold">
<button @click="editorMode = 'edit'"
:class="editorMode === 'edit' ? 'bg-white dark:bg-slate-700 shadow-sm text-sky-600' : 'text-slate-500'"
class="px-3 py-1 rounded-lg transition-all">编辑</button>
<button @click="editorMode = 'split'"
:class="editorMode === 'split' ? 'bg-white dark:bg-slate-700 shadow-sm text-sky-600' : 'text-slate-500'"
class="px-3 py-1 rounded-lg transition-all">分屏实时预览</button>
<button @click="editorMode = 'preview'"
:class="editorMode === 'preview' ? 'bg-white dark:bg-slate-700 shadow-sm text-sky-600' : 'text-slate-500'"
class="px-3 py-1 rounded-lg transition-all">仅预览</button>
</div>
<button @click="showPublishModal = false" class="p-1.5 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>
<!-- 模态框主体内容区 -->
<div class="p-6 overflow-y-auto flex-1 space-y-4 text-sm">
<!-- 标题输入 -->
<div>
<label class="block font-semibold mb-1 text-slate-700 dark:text-slate-300 text-xs">文章标题 *</label>
<input v-model="newPost.title" type="text" placeholder="输入吸睛的技术长文标题..."
class="w-full px-4 py-2.5 rounded-xl 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 text-base font-bold">
</div>
<!-- 分类与标签 -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block font-semibold mb-1 text-slate-700 dark:text-slate-300 text-xs">技术专栏分类</label>
<select v-model="newPost.categoryName"
class="w-full px-3.5 py-2 rounded-xl 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 text-xs">
<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>
</select>
</div>
<div>
<label class="block font-semibold mb-1 text-slate-700 dark:text-slate-300 text-xs">技术标签 (逗号分隔)</label>
<input v-model="newPost.tags" type="text" placeholder="例如: SpringBoot3,MySQL,Docker"
class="w-full px-3.5 py-2 rounded-xl 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 text-xs">
</div>
</div>
<!-- 一句话要点摘要 -->
<div>
<label class="block font-semibold mb-1 text-slate-700 dark:text-slate-300 text-xs">一句话摘要提要</label>
<input v-model="newPost.summary" type="text" placeholder="简述本文的核心技术价值与解决的问题..."
class="w-full px-3.5 py-2 rounded-xl 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 text-xs">
</div>
<!-- 快捷 Markdown 工具栏与模板插入 -->
<div class="flex flex-wrap items-center justify-between gap-2 pt-2 border-t border-slate-100 dark:border-slate-800">
<div class="flex items-center gap-1 text-xs">
<button @click="insertText('**粗体**')" class="px-2 py-1 rounded bg-slate-100 dark:bg-slate-800 hover:bg-slate-200">B</button>
<button @click="insertText('*斜体*')" class="px-2 py-1 rounded bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 italic">I</button>
<button @click="insertText('## 二级标题\n')" class="px-2 py-1 rounded bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 font-bold">H2</button>
<button @click="insertText('### 三级标题\n')" class="px-2 py-1 rounded bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 font-bold">H3</button>
<button @click="insertText('`代码片段`')" class="px-2 py-1 rounded bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 font-mono">Code</button>
<button @click="insertText('\n```bash\n# 命令行指令\n```\n')" class="px-2 py-1 rounded bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 font-mono">CodeBlock</button>
<button @click="insertText('> 引用要点说明\n')" class="px-2 py-1 rounded bg-slate-100 dark:bg-slate-800 hover:bg-slate-200">Quote</button>
</div>
<div class="flex items-center gap-1.5">
<button @click="insertTemplate('arch')" class="text-xs text-sky-600 dark:text-sky-400 font-semibold px-2 py-1 rounded hover:bg-sky-50 dark:hover:bg-sky-950/50">
📑 插入实战架构模板
</button>
</div>
</div>
<!-- Markdown 编辑器输入框与预览区 -->
<div :class="editorMode === 'split' ? 'grid grid-cols-1 sm:grid-cols-2 gap-4' : ''" class="min-h-[280px]">
<!-- 编辑文本框 -->
<div v-show="editorMode !== 'preview'" class="flex flex-col">
<textarea id="editor-textarea" v-model="newPost.content" rows="12" placeholder="# 正文标题&#10;&#10;写下你的技术思考或教程步骤...&#10;&#10;```java&#10;// 代码示例&#10;```"
class="w-full flex-1 p-3.5 rounded-xl 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 leading-relaxed resize-none"></textarea>
</div>
<!-- 实时预览区 -->
<div v-show="editorMode !== 'edit'" class="p-4 rounded-xl bg-slate-50/50 dark:bg-slate-800/40 border border-slate-200 dark:border-slate-700 overflow-y-auto max-h-[340px]">
<div class="text-xs font-mono text-slate-400 mb-2 border-b border-slate-200 dark:border-slate-700 pb-1">实时预览效果</div>
<div class="markdown-body text-xs sm:text-sm" v-html="renderMarkdown(newPost.content || '*开始在左侧输入 Markdown 内容...*')"></div>
</div>
</div>
</div>
<!-- 模态框底部按钮 -->
<div class="px-6 py-4 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between shrink-0 bg-slate-50 dark:bg-slate-900/90">
<div class="text-xs text-slate-400">
预计字数: <span class="font-mono font-bold text-slate-600 dark:text-slate-300">{{ newPost.content.length }}</span> 字
</div>
<div class="flex items-center gap-3">
<button @click="showPublishModal = false" class="px-4 py-2 rounded-xl border border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800 font-semibold text-xs">
取消
</button>
<button @click="publishPost" :disabled="publishing"
class="px-6 py-2 rounded-xl bg-gradient-to-r from-sky-600 to-indigo-600 hover:from-sky-700 hover:to-indigo-700 disabled:opacity-50 text-white font-bold text-xs shadow-md shadow-sky-600/25 flex items-center gap-2">
<span v-if="publishing" class="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
<span>{{ publishing ? '写入 MySQL 8.0 中...' : '持久化发布上线' }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- 返回顶部浮动胶囊 -->
<button v-show="showScrollTop" @click="scrollToTop"
class="fixed bottom-6 right-6 z-40 p-3 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 text-slate-700 dark:text-slate-300 shadow-xl hover:scale-110 active:scale-95 transition-all"
title="回到顶部">
<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="M5 10l7-7m0 0l7 7m-7-7v18"></path></svg>
</button>
<!-- 现代化页脚 -->
<footer class="mt-auto border-t border-slate-200/80 dark:border-slate-800/80 bg-white dark:bg-slate-900 py-8 text-xs text-slate-500 dark:text-slate-400">
<div class="max-w-6xl mx-auto px-4 sm:px-6 flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span>
<span>© 2026 旺总的技术全栈 · 运行于 Google Cloud 台湾节点</span>
</div>
<div class="flex items-center gap-4 font-mono text-[11px]">
<span>Spring Boot 3</span>
<span>·</span>
<span>MySQL 8.0</span>
<span>·</span>
<span>Docker Compose</span>
<span>·</span>
<span>Gitea CI/CD</span>
</div>
</div>
</footer>
</div>
<!-- Vue 3 核心业务交互逻辑 -->
<script>
const { createApp, ref, computed, onMounted, onUnmounted, nextTick } = Vue;
createApp({
setup() {
const API_BASE = '/api';
const isDark = ref(false);
const backendConnected = ref(false);
const currentView = ref('home'); // 'home' | 'post' | 'archive' | 'categories' | 'about'
const posts = ref([]);
const stats = ref({ totalPosts: 0, totalViews: 0, totalLikes: 0, totalCategories: 4 });
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);
const editorMode = ref('split'); // 'split' | 'edit' | 'preview'
const readingProgress = ref(0);
const postToc = ref([]);
const activeTocId = ref('');
const showScrollTop = ref(false);
const toasts = ref([]);
const categories = ['全部', '云原生 & Linux', 'Java & Spring Boot', 'AI & 本地模型', '全栈开发 & CI/CD'];
const categoryList = [
{ name: '云原生 & Linux', icon: '☁️', desc: 'Google Cloud 台湾节点云服务器运维、Ubuntu 系统内核参数调优、1Panel 容器环境与网络实战。' },
{ name: 'Java & Spring Boot', icon: '🍃', desc: 'Spring Boot 3.2 现代化分层设计、Spring Data JPA 对象关系映射、事务控制与统一 RESTful 规范。' },
{ name: 'AI & 本地模型', icon: '🧠', desc: 'Ollama 容器化引擎、DeepSeek R1 本地 CPU 性能压测与加速方案、Prompt 预填充降延迟实操。' },
{ name: '全栈开发 & CI/CD', icon: '⚙️', desc: '前后端分离微服务架构、Docker Compose 多容器生命周期管理、私有 Gitea Actions 持续交付流水线。' }
];
const newPost = ref({
title: '',
categoryName: '全栈开发 & CI/CD',
author: '旺总',
tags: 'SpringBoot,MySQL',
summary: '',
content: ''
});
// Toast 通知提示系统
function showToast(text, type = 'info', icon = '🔔') {
const id = Date.now() + Math.random();
if (type === 'success') icon = '✅';
if (type === 'error') icon = '⚠️';
toasts.value.push({ id, text, type, icon });
setTimeout(() => {
toasts.value = toasts.value.filter(t => t.id !== id);
}, 3000);
}
// 路由与视图切换
function navigateView(viewName) {
currentView.value = viewName;
window.location.hash = '#/' + viewName;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
// 类别图标映射
function getCategoryIcon(cat) {
const map = {
'云原生 & Linux': '☁️',
'Java & Spring Boot': '🍃',
'AI & 本地模型': '🧠',
'全栈开发 & CI/CD': '⚙️'
};
return map[cat] || '📑';
}
function getPostsCountByCategory(catName) {
return posts.value.filter(p => p.categoryName === catName).length;
}
function filterByCategory(catName) {
selectedCategory.value = catName;
navigateView('home');
fetchPosts();
}
// 获取文章列表
async function fetchPosts() {
try {
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 result = await res.json();
posts.value = result.data || result || [];
backendConnected.value = true;
fetchStats();
return;
}
} catch (e) {
console.warn('后端 API 未就绪:', e);
}
backendConnected.value = false;
}
// 获取统计信息
async function fetchStats() {
try {
const res = await fetch(`${API_BASE}/stats`);
if (res.ok) {
const result = await res.json();
stats.value = result.data || result;
}
} catch (e) {}
}
function selectCategory(cat) {
selectedCategory.value = cat;
fetchPosts();
}
let debounceTimer = null;
function onSearchInput() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
fetchPosts();
}, 250);
}
// 查看并进入沉浸阅读模式
async function viewPost(post) {
activePost.value = post;
currentView.value = 'post';
window.location.hash = `#/post/${post.id}`;
window.scrollTo({ top: 0, behavior: 'smooth' });
post.views = (post.views || 0) + 1;
stats.value.totalViews = (stats.value.totalViews || 0) + 1;
fetchComments(post.id);
// 生成 TOC 大纲
generateToc(post.content);
// 上报后端阅读自增
if (backendConnected.value) {
fetch(`${API_BASE}/posts/${post.id}`).catch(() => {});
}
nextTick(() => {
bindCodeCopyButtons();
});
}
// 生成文章目录大纲
function generateToc(markdown) {
if (!markdown) {
postToc.value = [];
return;
}
const lines = markdown.split('\n');
const toc = [];
let idx = 0;
for (const line of lines) {
const match = line.match(/^(#{1,3})\s+(.+)$/);
if (match) {
const level = match[1].length;
const text = match[2].trim();
const id = 'heading-' + (++idx);
toc.push({ id, level, text });
}
}
postToc.value = toc;
if (toc.length > 0) activeTocId.value = toc[0].id;
}
// 自定义 Markdown 渲染与代码美化
const renderedPostContent = computed(() => {
if (!activePost.value || !activePost.value.content) return '';
let content = activePost.value.content;
if (window.marked) {
let html = marked.parse(content);
// 注入 heading 锚点 ID 供大纲滚动
let idx = 0;
html = html.replace(/<h([1-3])>(.*?)<\/h\1>/g, (match, level, text) => {
const id = 'heading-' + (++idx);
return `<h${level} id="${id}">${text}</h${level}>`;
});
return html;
}
return content.replace(/\n/g, '<br>');
});
// 代码一键复制与 macOS 视窗修饰
function bindCodeCopyButtons() {
const pres = document.querySelectorAll('.markdown-body pre');
pres.forEach(pre => {
if (pre.closest('.code-block-wrapper')) return;
const code = pre.querySelector('code');
const codeText = code ? code.innerText : pre.innerText;
let lang = 'CODE';
if (code && code.className) {
const match = code.className.match(/language-(\w+)/);
if (match) lang = match[1].toUpperCase();
}
const wrapper = document.createElement('div');
wrapper.className = 'code-block-wrapper';
const header = document.createElement('div');
header.className = 'code-block-header';
header.innerHTML = `
<div class="code-block-dots">
<div class="code-dot red"></div>
<div class="code-dot yellow"></div>
<div class="code-dot green"></div>
</div>
<div style="display:flex;align-items:center;gap:0.75rem;">
<span style="font-family:monospace;font-weight:600;font-size:0.75rem;">${lang}</span>
<button class="copy-btn" style="cursor:pointer;padding:0.2rem 0.5rem;background:#21262d;border:1px solid #30363d;border-radius:0.375rem;color:#c9d1d9;font-size:0.7rem;transition:all;">
一键复制
</button>
</div>
`;
const copyBtn = header.querySelector('.copy-btn');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(codeText).then(() => {
copyBtn.innerText = '已复制!✓';
copyBtn.style.color = '#38bdf8';
showToast('代码已成功复制到剪贴板', 'success');
setTimeout(() => {
copyBtn.innerText = '一键复制';
copyBtn.style.color = '#c9d1d9';
}, 2000);
});
});
pre.parentNode.insertBefore(wrapper, pre);
wrapper.appendChild(header);
wrapper.appendChild(pre);
});
}
function scrollToHeading(id) {
activeTocId.value = id;
const el = document.getElementById(id);
if (el) {
el.scrollIntoView({ behavior: 'smooth' });
}
}
// 加载评论
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 = [];
}
}
// 提交评论
async function submitComment() {
if (!newComment.value.content.trim()) {
showToast('评论内容不能为空', 'error');
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 = '';
showToast('评论已持久化写入 MySQL 8.0', 'success');
fetchComments(activePost.value.id);
}
} catch (e) {
showToast('评论提交失败,请检查网络连接', 'error');
}
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) {
showToast('文章已从 MySQL 数据库中移除', 'success');
navigateView('home');
fetchPosts();
}
} catch (e) {
showToast('删除失败', 'error');
}
}
// 点赞
async function likePost(post) {
post.likes = (post.likes || 0) + 1;
stats.value.totalLikes = (stats.value.totalLikes || 0) + 1;
showToast('感谢点赞认可!❤️', 'success');
if (backendConnected.value) {
try {
await fetch(`${API_BASE}/posts/${post.id}/like`, { method: 'POST' });
} catch (e) {}
}
}
// 分享链接
function copyShareLink() {
navigator.clipboard.writeText(window.location.href).then(() => {
showToast('文章永久链接已复制到剪贴板!', 'success');
});
}
function openPublishModal() {
newPost.value = {
title: '',
categoryName: '全栈开发 & CI/CD',
author: '旺总',
tags: 'SpringBoot,MySQL,Docker',
summary: '',
content: ''
};
editorMode.value = 'split';
showPublishModal.value = true;
}
function insertText(text) {
const textarea = document.getElementById('editor-textarea');
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const current = newPost.value.content;
newPost.value.content = current.substring(0, start) + text + current.substring(end);
nextTick(() => {
textarea.focus();
textarea.setSelectionRange(start + text.length, start + text.length);
});
}
function insertTemplate(type) {
if (type === 'arch') {
newPost.value.title = '架构实战:基于 Docker Compose 编排的高可用微服务治理实践';
newPost.value.summary = '深度复盘在 Google Cloud 生产环境如何通过命名卷隔离 MySQL、Spring Boot 3 自动重构及 Nginx 反向代理配置。';
newPost.value.content = `# 架构实战:基于 Docker Compose 的微服务治理实践\n\n## 1. 架构目标与背景\n在现代全栈开发中,单体架构往往面临环境漂移、手动部署繁杂等问题。\n\n## 2. 核心架构设计\n- **前端接入层**:Nginx Alpine 极简镜像,反向代理 \`/api/\`;\n- **业务核心层**:Spring Boot 3.2.3 + Spring Data JPA;\n- **数据持久层**:MySQL 8.0 挂载卷持久化保护。\n\n\`\`\`bash\n# 快速检查容器服务运行状态\ndocker compose ps -a\n\`\`\`\n\n> 💡 **核心建议**:将数据库与代码构建分离,确保自动化流水线只构建业务镜像,保障数据绝对安全!`;
}
}
// 持久化发布文章至 MySQL
async function publishPost() {
if (!newPost.value.title.trim() || !newPost.value.content.trim()) {
showToast('文章标题和正文内容不能为空!', 'error');
return;
}
publishing.value = true;
try {
const res = await fetch(`${API_BASE}/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPost.value)
});
if (res.ok) {
showToast('文章已成功持久化写入 MySQL 8.0!', 'success');
showPublishModal.value = false;
fetchPosts();
} else {
showToast('发布失败,请稍后重试', 'error');
}
} catch (e) {
showToast('发布失败,无法连接服务器', 'error');
}
publishing.value = false;
}
function renderMarkdown(content) {
if (!content) return '';
if (window.marked) {
return marked.parse(content);
}
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 scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function handleScroll() {
const scrollTop = window.scrollY || document.documentElement.scrollTop;
showScrollTop.value = scrollTop > 300;
// 计算阅读进度
const scrollHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
if (scrollHeight > 0) {
readingProgress.value = Math.min(100, Math.round((scrollTop / scrollHeight) * 100));
}
// 滚动联动更新目录大纲的高亮项
if (postToc.value.length > 0) {
for (let i = postToc.value.length - 1; i >= 0; i--) {
const el = document.getElementById(postToc.value[i].id);
if (el && el.getBoundingClientRect().top <= 120) {
activeTocId.value = postToc.value[i].id;
break;
}
}
}
}
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';
}
}
// 处理 URL Hash 路由导航 (如 #/post/1, #/archive, #/categories, #/about)
function handleHashRoute() {
const hash = window.location.hash;
if (hash.startsWith('#/post/')) {
const id = hash.replace('#/post/', '');
if (posts.value.length > 0) {
const target = posts.value.find(p => String(p.id) === String(id));
if (target) {
viewPost(target);
return;
}
}
} else if (hash === '#/archive') {
currentView.value = 'archive';
} else if (hash === '#/categories') {
currentView.value = 'categories';
} else if (hash === '#/about') {
currentView.value = 'about';
} else {
currentView.value = 'home';
}
}
onMounted(async () => {
// 初始化深浅主题
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.js 与 Highlight.js
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,
gfm: true
});
}
window.addEventListener('scroll', handleScroll);
window.addEventListener('hashchange', handleHashRoute);
await fetchPosts();
handleHashRoute();
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('hashchange', handleHashRoute);
});
return {
isDark,
toggleDarkMode,
backendConnected,
currentView,
navigateView,
posts,
stats,
categories,
categoryList,
selectedCategory,
searchKeyword,
selectCategory,
filterByCategory,
getCategoryIcon,
getPostsCountByCategory,
onSearchInput,
activePost,
viewPost,
postToc,
activeTocId,
scrollToHeading,
renderedPostContent,
readingProgress,
showScrollTop,
scrollToTop,
comments,
newComment,
submitComment,
deletePost,
likePost,
copyShareLink,
showPublishModal,
editorMode,
newPost,
openPublishModal,
insertText,
insertTemplate,
publishPost,
publishing,
renderMarkdown,
formatDate,
parseTags,
toasts
};
}
}).mount('#app');
</script>
</body>
</html>