Files
my-blog/frontend/index.html
T

1563 lines
97 KiB
HTML
Raw Normal View History

<!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; }
.markdown-body pre {
background: #0d1117 !important;
border: 1px solid #30363d !important;
border-radius: 0.75rem;
padding: 1.25rem !important;
overflow-x: auto;
color: #f0f6fc !important;
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
font-size: 0.92rem !important;
line-height: 1.75 !important;
}
.markdown-body pre code {
color: #f0f6fc !important;
background: transparent !important;
font-family: inherit !important;
}
/* macOS 风格代码高亮块容器与自包含超高对比度语法配色 (彻底杜绝发暗看不清) */
.code-block-wrapper {
margin: 1.5rem 0;
border-radius: 0.875rem;
overflow: hidden;
background: #0d1117 !important;
border: 1px solid #30363d !important;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2);
}
.code-block-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1rem;
background: #161b22 !important;
border-bottom: 1px solid #30363d !important;
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
font-size: 0.75rem;
}
.code-block-dots { display: flex; gap: 0.4rem; }
.code-dot { width: 0.7rem; height: 0.7rem; border-radius: 9999px; }
.code-dot.red { background: #ff5f56; }
.code-dot.yellow { background: #ffbd2e; }
.code-dot.green { background: #27c93f; }
.code-block-lang {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
font-weight: 700;
font-size: 0.75rem;
letter-spacing: 0.05em;
color: #58a6ff !important;
text-transform: uppercase;
}
.code-copy-btn {
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.25rem 0.65rem;
background: #21262d;
border: 1px solid #30363d;
border-radius: 0.5rem;
color: #f0f6fc !important;
font-size: 0.72rem;
font-weight: 600;
transition: all 0.15s ease;
}
.code-copy-btn:hover {
background: #30363d;
border-color: #8b949e;
color: #ffffff !important;
}
.code-block-wrapper pre {
margin: 0 !important;
padding: 1.25rem !important;
overflow-x: auto;
background: transparent !important;
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
font-size: 0.92rem !important;
line-height: 1.75 !important;
color: #f0f6fc !important; /* 强制纯正高亮白,杜绝黑色继承 */
}
.code-block-wrapper code,
.code-block-wrapper pre * {
color: #f0f6fc !important; /* 默认保底全为高对比亮白 */
}
/* 纯正 GitHub Dark 顶级语法高亮体系 (100% 独立,不依赖任何第三方未加载 CSS) */
.code-block-wrapper .hljs-comment,
.code-block-wrapper .hljs-quote {
color: #8b949e !important; /* 高亮银灰注释,极易辨认 */
font-style: italic;
}
.code-block-wrapper .hljs-keyword,
.code-block-wrapper .hljs-selector-tag,
.code-block-wrapper .hljs-subst {
color: #ff7b72 !important; /* 珊瑚红关键字 */
font-weight: 600;
}
.code-block-wrapper .hljs-string,
.code-block-wrapper .hljs-title,
.code-block-wrapper .hljs-section,
.code-block-wrapper .hljs-attribute,
.code-block-wrapper .hljs-literal,
.code-block-wrapper .hljs-template-tag,
.code-block-wrapper .hljs-template-variable {
color: #a5d6ff !important; /* 荧光冰蓝文本与字符串 */
}
.code-block-wrapper .hljs-type,
.code-block-wrapper .hljs-class {
color: #d2a8ff !important; /* 浅紫类名与类型 */
font-weight: 600;
}
.code-block-wrapper .hljs-variable {
color: #ffa657 !important; /* 暖橙变量名 */
}
.code-block-wrapper .hljs-number {
color: #79c0ff !important; /* 明亮青蓝数值 */
}
.code-block-wrapper .hljs-built_in,
.code-block-wrapper .hljs-bullet,
.code-block-wrapper .hljs-code {
color: #7ee787 !important; /* 亮绿内置命令与函数 */
}
.code-block-wrapper .hljs-meta {
color: #58a6ff !important; /* 天蓝元数据 */
}
</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">(XiaoDong)</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>关于小东 (XiaoDong) 与本站架构</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 v-for="cat in availableCategories" :key="cat" :value="cat">{{ cat }}</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 = ref(['全部']);
const categoryList = ref([]);
const availableCategories = computed(() => categories.value.filter(c => c !== '全部'));
const newPost = ref({
title: '',
categoryName: '全栈开发 & CI/CD',
author: '小东',
tags: 'SpringBoot,MySQL',
summary: '',
content: ''
});
// 从后端 MySQL 8.0 动态获取分类列表
async function fetchCategories() {
try {
const res = await fetch(`${API_BASE}/categories`);
if (res.ok) {
const result = await res.json();
const list = result.data || result || [];
if (list.length > 0) {
categoryList.value = list.map(c => ({
name: c.name,
icon: getCategoryIcon(c.name),
desc: c.description || '沉淀架构与工程规范'
}));
categories.value = ['全部', ...list.map(c => c.name)];
return;
}
}
} catch (e) {
console.warn('获取分类数据失败,使用基础兜底:', e);
}
categories.value = ['全部', '云原生 & Linux', 'Java & Spring Boot', 'AI & 本地模型', '全栈开发 & CI/CD'];
categoryList.value = [
{ 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 持续交付流水线。' }
];
}
// 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 class="code-block-lang">${lang}</span>
<button class="code-copy-btn">
<svg style="width:0.85rem;height:0.85rem;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
<span>复制代码</span>
</button>
</div>
`;
const copyBtn = header.querySelector('.code-copy-btn');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(codeText).then(() => {
copyBtn.innerHTML = `<span>已复制!✓</span>`;
copyBtn.style.color = '#38bdf8';
copyBtn.style.borderColor = '#38bdf8';
showToast('代码已成功复制到剪贴板', 'success');
setTimeout(() => {
copyBtn.innerHTML = `
<svg style="width:0.85rem;height:0.85rem;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
<span>复制代码</span>
`;
copyBtn.style.color = '#f0f6fc';
copyBtn.style.borderColor = '#30363d';
}, 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: availableCategories.value[0] || '全栈开发 & 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 fetchCategories();
await fetchPosts();
handleHashRoute();
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('hashchange', handleHashRoute);
});
return {
isDark,
toggleDarkMode,
backendConnected,
currentView,
navigateView,
posts,
stats,
categories,
categoryList,
availableCategories,
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>