700 lines
42 KiB
HTML
700 lines
42 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN" class="scroll-smooth">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>旺总的技术全栈博客 | Full-Stack & Cloud-Native</title>
|
|
<!-- Tailwind CSS CDN -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
brand: {
|
|
50: '#f0f9ff',
|
|
100: '#e0f2fe',
|
|
500: '#0284c7',
|
|
600: '#0369a1',
|
|
700: '#075985',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<!-- Vue 3 -->
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
|
|
<!-- Marked.js for Markdown Rendering -->
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<!-- Highlight.js for Code Highlighting -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
|
|
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
|
<style>
|
|
[v-cloak] { display: none; }
|
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
|
|
.dark ::-webkit-scrollbar-thumb { background: #334155; }
|
|
.markdown-body pre { background: #0d1117; border-radius: 0.75rem; padding: 1rem; overflow-x: auto; margin: 1rem 0; }
|
|
.markdown-body code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
|
.markdown-body h1, .markdown-body h2, .markdown-body h3 { font-weight: 700; margin-top: 1.5rem; margin-bottom: 0.75rem; color: inherit; }
|
|
.markdown-body h1 { font-size: 1.75rem; }
|
|
.markdown-body h2 { font-size: 1.4rem; border-bottom: 1px solid rgba(156, 163, 175, 0.2); padding-bottom: 0.3rem; }
|
|
.markdown-body h3 { font-size: 1.15rem; }
|
|
.markdown-body p { line-height: 1.75; margin-bottom: 1rem; }
|
|
.markdown-body blockquote { border-left: 4px solid #0284c7; padding-left: 1rem; color: #64748b; font-style: italic; margin: 1rem 0; }
|
|
.dark .markdown-body blockquote { color: #94a3b8; }
|
|
.markdown-body ul, .markdown-body ol { padding-left: 1.5rem; margin-bottom: 1rem; list-style-type: disc; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-slate-50 dark:bg-slate-950 text-slate-800 dark:text-slate-100 min-h-screen transition-colors duration-200 antialiased font-sans selection:bg-brand-500 selection:text-white">
|
|
|
|
<div id="app" v-cloak class="flex flex-col min-h-screen">
|
|
|
|
<!-- 顶部导航栏 -->
|
|
<header class="sticky top-0 z-40 backdrop-blur-md bg-white/80 dark:bg-slate-900/80 border-b border-slate-200/80 dark:border-slate-800 transition-colors">
|
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between">
|
|
<!-- 品牌 Logo -->
|
|
<a href="#" class="flex items-center gap-3 group">
|
|
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-sky-500 to-indigo-600 flex items-center justify-center text-white shadow-md shadow-sky-500/20 group-hover:scale-105 transition-transform">
|
|
<span class="text-xl font-black">旺</span>
|
|
</div>
|
|
<div>
|
|
<div class="font-bold text-base tracking-tight flex items-center gap-2">
|
|
<span>旺总的技术全栈博客</span>
|
|
<span class="hidden sm:inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-800 dark:bg-sky-950/80 dark:text-sky-300 border border-sky-200 dark:border-sky-800">
|
|
SpringBoot 3 + MySQL 8.0
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400 hidden sm:block">Full-Stack · Cloud-Native · DevOps</p>
|
|
</div>
|
|
</a>
|
|
|
|
<!-- 右侧操作区 -->
|
|
<div class="flex items-center gap-3">
|
|
<!-- 数据库与后端连接指示器 -->
|
|
<div :class="backendConnected ? 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-400 dark:border-emerald-800' : 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/40 dark:text-amber-400 dark:border-amber-800'"
|
|
class="hidden md:flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs border font-medium">
|
|
<span class="w-2 h-2 rounded-full" :class="backendConnected ? 'bg-emerald-500 animate-pulse' : 'bg-amber-500'"></span>
|
|
<span>{{ backendConnected ? 'MySQL 8.0 + SpringBoot 3 已联通' : '离线预览模式' }}</span>
|
|
</div>
|
|
|
|
<!-- 写文章按钮 -->
|
|
<button @click="openPublishModal"
|
|
class="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg text-sm font-semibold bg-sky-600 hover:bg-sky-700 text-white shadow-sm shadow-sky-600/30 transition-all hover:shadow active:scale-95">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
|
<span class="hidden sm:inline">发布文章</span>
|
|
</button>
|
|
|
|
<!-- 深色/浅色模式切换 -->
|
|
<button @click="toggleDarkMode"
|
|
class="p-2 rounded-lg text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
|
title="切换明暗模式">
|
|
<svg v-if="isDark" class="w-5 h-5 text-amber-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"></path></svg>
|
|
<svg v-else class="w-5 h-5 text-slate-600" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- 主体容器 -->
|
|
<main class="flex-1 max-w-5xl w-full mx-auto px-4 sm:px-6 py-8">
|
|
|
|
<!-- Hero 博主名片区域 -->
|
|
<section class="mb-10 p-6 sm:p-8 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm relative overflow-hidden">
|
|
<div class="absolute -right-16 -top-16 w-56 h-56 bg-gradient-to-br from-sky-400/20 to-indigo-500/10 rounded-full blur-2xl pointer-events-none"></div>
|
|
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-6 relative z-10">
|
|
<div class="flex items-start gap-4">
|
|
<div class="relative">
|
|
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-slate-800 to-slate-950 dark:from-sky-500 dark:to-indigo-600 flex items-center justify-center text-white text-2xl font-bold shadow-lg">
|
|
👨💻
|
|
</div>
|
|
<span class="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-emerald-500 border-2 border-white dark:border-slate-900" title="在线"></span>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-3">
|
|
<h1 class="text-2xl sm:text-3xl font-extrabold tracking-tight">旺总 (WangZong)</h1>
|
|
<span class="px-2.5 py-0.5 text-xs font-semibold rounded-md bg-indigo-50 text-indigo-700 dark:bg-indigo-950/60 dark:text-indigo-300 border border-indigo-200/60 dark:border-indigo-800">全栈工程师</span>
|
|
</div>
|
|
<p class="mt-1 text-sm sm:text-base text-slate-600 dark:text-slate-300">
|
|
专注 Spring Boot 3 企业级架构 · MySQL 8.0 持久化 · Docker / K8s 容器编排 · CI/CD 自动化流水线
|
|
</p>
|
|
<!-- 技术栈徽章 -->
|
|
<div class="mt-3 flex flex-wrap gap-2 text-xs">
|
|
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🐬 MySQL 8.0 持久化</span>
|
|
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">⚡ Spring Boot 3 + JPA</span>
|
|
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🐳 Docker Compose</span>
|
|
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🚀 Gitea Actions CI/CD</span>
|
|
<span class="px-2 py-0.5 rounded bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 font-mono">🤖 DeepSeek R1 本地化</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 数据统计面板卡片 -->
|
|
<div class="flex items-center gap-4 sm:gap-6 bg-slate-50 dark:bg-slate-800/60 p-3 sm:p-4 rounded-xl border border-slate-200/60 dark:border-slate-700/60 self-start md:self-auto">
|
|
<div class="text-center">
|
|
<div class="text-xl sm:text-2xl font-black text-sky-600 dark:text-sky-400">{{ stats.totalPosts }}</div>
|
|
<div class="text-xs text-slate-500 dark:text-slate-400">文章总数</div>
|
|
</div>
|
|
<div class="w-px h-8 bg-slate-200 dark:bg-slate-700"></div>
|
|
<div class="text-center">
|
|
<div class="text-xl sm:text-2xl font-black text-slate-800 dark:text-slate-200">{{ stats.totalViews }}</div>
|
|
<div class="text-xs text-slate-500 dark:text-slate-400">累计浏览</div>
|
|
</div>
|
|
<div class="w-px h-8 bg-slate-200 dark:bg-slate-700"></div>
|
|
<div class="text-center">
|
|
<div class="text-xl sm:text-2xl font-black text-rose-500">{{ stats.totalLikes }}</div>
|
|
<div class="text-xs text-slate-500 dark:text-slate-400">点赞认可</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 筛选与搜索控制栏 -->
|
|
<div class="mb-6 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-4">
|
|
<!-- 分类选项卡 -->
|
|
<div class="flex items-center gap-1.5 overflow-x-auto pb-2 sm:pb-0 text-sm no-scrollbar">
|
|
<button v-for="cat in categories" :key="cat"
|
|
@click="selectCategory(cat)"
|
|
:class="selectedCategory === cat
|
|
? 'bg-slate-900 text-white dark:bg-sky-600 dark:text-white shadow-sm'
|
|
: 'bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 border border-slate-200 dark:border-slate-800'"
|
|
class="px-3.5 py-1.5 rounded-lg whitespace-nowrap font-medium transition-all">
|
|
{{ cat }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 搜索框 -->
|
|
<div class="relative w-full sm:w-64">
|
|
<input v-model="searchKeyword"
|
|
@input="onSearchInput"
|
|
type="text"
|
|
placeholder="搜索文章标题或关键词..."
|
|
class="w-full pl-9 pr-4 py-1.5 text-sm rounded-lg bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500/50 dark:focus:ring-sky-400/50 transition-all">
|
|
<svg class="w-4 h-4 text-slate-400 absolute left-3 top-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 文章列表区域 -->
|
|
<div v-if="posts.length > 0" class="space-y-4">
|
|
<article v-for="post in posts" :key="post.id"
|
|
class="group p-5 sm:p-6 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/80 dark:border-slate-800 shadow-sm hover:shadow-md hover:border-sky-500/40 dark:hover:border-sky-500/40 transition-all cursor-pointer"
|
|
@click="openPostDetail(post)">
|
|
<div class="flex flex-col sm:flex-row sm:items-baseline sm:justify-between gap-1 mb-2">
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-2 py-0.5 text-xs font-semibold rounded bg-sky-50 text-sky-700 dark:bg-sky-950/70 dark:text-sky-300 border border-sky-100 dark:border-sky-900/50">
|
|
{{ post.categoryName || '技术杂谈' }}
|
|
</span>
|
|
<span class="text-xs text-slate-400">·</span>
|
|
<span class="text-xs text-slate-500 dark:text-slate-400">📅 {{ formatDate(post.createdAt) }}</span>
|
|
<span class="text-xs text-slate-400">·</span>
|
|
<span class="text-xs text-slate-500 dark:text-slate-400">⏱️ {{ post.readTime || '3 min' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 标题 -->
|
|
<h2 class="text-lg sm:text-xl font-bold text-slate-900 dark:text-slate-100 group-hover:text-sky-600 dark:group-hover:text-sky-400 transition-colors">
|
|
{{ post.title }}
|
|
</h2>
|
|
|
|
<!-- 摘要 -->
|
|
<p class="mt-2 text-sm text-slate-600 dark:text-slate-300 line-clamp-2 leading-relaxed">
|
|
{{ post.summary || post.content.substring(0, 140) }}
|
|
</p>
|
|
|
|
<!-- 底部标签与互动 -->
|
|
<div class="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800/80 flex items-center justify-between text-xs text-slate-500 dark:text-slate-400">
|
|
<!-- 标签群 -->
|
|
<div class="flex items-center gap-1.5 flex-wrap">
|
|
<span v-for="tag in parseTags(post.tags)" :key="tag"
|
|
class="text-slate-500 dark:text-slate-400 hover:text-sky-500">
|
|
#{{ tag }}
|
|
</span>
|
|
</div>
|
|
|
|
<!-- 互动按钮 -->
|
|
<div class="flex items-center gap-4">
|
|
<span class="flex items-center gap-1">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
|
|
{{ post.views || 0 }}
|
|
</span>
|
|
|
|
<button @click.stop="likePost(post)"
|
|
class="flex items-center gap-1 hover:text-rose-500 transition-colors active:scale-125">
|
|
<svg class="w-3.5 h-3.5 text-rose-500 fill-rose-500" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd"></path></svg>
|
|
<span>{{ post.likes || 0 }}</span>
|
|
</button>
|
|
|
|
<span class="inline-flex items-center gap-1 font-semibold text-sky-600 dark:text-sky-400 group-hover:translate-x-0.5 transition-transform">
|
|
阅读全文 →
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<!-- 空状态 -->
|
|
<div v-else class="text-center py-16 bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800">
|
|
<div class="text-4xl mb-3">🔍</div>
|
|
<h3 class="text-base font-semibold text-slate-800 dark:text-slate-200">没有找到相关文章</h3>
|
|
<p class="text-xs text-slate-500 mt-1">换个搜索词或点击右上方“发布文章”向 MySQL 写入一篇吧!</p>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- 文章详情阅读模态框 -->
|
|
<div v-if="activePost"
|
|
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4 sm:p-6"
|
|
@click.self="activePost = null">
|
|
<div class="w-full max-w-3xl bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 max-h-[90vh] flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
|
<!-- 模态框顶部 -->
|
|
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-2 py-0.5 text-xs font-semibold rounded bg-sky-50 text-sky-700 dark:bg-sky-950/70 dark:text-sky-300">
|
|
{{ activePost.categoryName }}
|
|
</span>
|
|
<span class="text-xs text-slate-400">·</span>
|
|
<span class="text-xs text-slate-500">📅 {{ formatDate(activePost.createdAt) }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button @click="deletePost(activePost)" class="text-xs text-red-500 hover:text-red-700 px-2 py-1 rounded hover:bg-red-50 dark:hover:bg-red-950/40 transition-colors" title="删除文章">
|
|
删除
|
|
</button>
|
|
<button @click="activePost = null" class="p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 rounded-lg">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 模态框正文 -->
|
|
<div class="p-6 sm:p-8 overflow-y-auto flex-1">
|
|
<h1 class="text-2xl sm:text-3xl font-extrabold text-slate-900 dark:text-slate-100 mb-4 leading-tight">
|
|
{{ activePost.title }}
|
|
</h1>
|
|
<div class="flex items-center gap-3 text-xs text-slate-500 mb-6 pb-4 border-b border-slate-100 dark:border-slate-800">
|
|
<span>👤 作者: {{ activePost.author || '旺总' }}</span>
|
|
<span>·</span>
|
|
<span>⏱️ 预计阅读: {{ activePost.readTime || '3 min' }}</span>
|
|
<span>·</span>
|
|
<span>👁️ {{ activePost.views }} 浏览</span>
|
|
</div>
|
|
|
|
<!-- Markdown 内容渲染区 -->
|
|
<div class="markdown-body prose dark:prose-invert max-w-none text-slate-800 dark:text-slate-200 text-sm sm:text-base"
|
|
v-html="renderMarkdown(activePost.content)">
|
|
</div>
|
|
|
|
<!-- 底部互动点赞 -->
|
|
<div class="mt-8 pt-6 border-t border-slate-100 dark:border-slate-800 flex items-center justify-between">
|
|
<button @click="likePost(activePost)"
|
|
class="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-rose-50 text-rose-600 dark:bg-rose-950/40 dark:text-rose-400 border border-rose-200 dark:border-rose-900 hover:scale-105 active:scale-95 transition-transform font-medium text-sm">
|
|
<svg class="w-4 h-4 fill-rose-500" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd"></path></svg>
|
|
<span>点赞支持 ({{ activePost.likes }})</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- 评论专区 (连接 MySQL comment 表) -->
|
|
<div class="mt-10 pt-6 border-t border-slate-200 dark:border-slate-800">
|
|
<h3 class="text-base font-bold mb-4 flex items-center gap-2">
|
|
<span>💬 读者交流区</span>
|
|
<span class="text-xs text-slate-500 font-normal">({{ comments.length }} 条持久化评论)</span>
|
|
</h3>
|
|
|
|
<!-- 发表评论表单 -->
|
|
<div class="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200/60 dark:border-slate-700/60 mb-6 space-y-3">
|
|
<div class="flex items-center gap-3">
|
|
<input v-model="newComment.author" type="text" placeholder="你的昵称 (默认: 访客)"
|
|
class="px-3 py-1.5 rounded-lg bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs w-48 focus:outline-none focus:ring-1 focus:ring-sky-500">
|
|
</div>
|
|
<textarea v-model="newComment.content" rows="2" placeholder="写下你的技术见解或想法..."
|
|
class="w-full px-3 py-2 rounded-lg bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 text-xs focus:outline-none focus:ring-1 focus:ring-sky-500"></textarea>
|
|
<div class="flex justify-end">
|
|
<button @click="submitComment" :disabled="submittingComment"
|
|
class="px-3.5 py-1.5 rounded-lg bg-sky-600 hover:bg-sky-700 text-white text-xs font-semibold shadow-sm transition-all">
|
|
{{ submittingComment ? '提交中...' : '发表评论' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 评论列表 -->
|
|
<div v-if="comments.length > 0" class="space-y-3">
|
|
<div v-for="c in comments" :key="c.id"
|
|
class="p-3.5 rounded-xl bg-slate-50/80 dark:bg-slate-800/40 border border-slate-100 dark:border-slate-800 text-xs">
|
|
<div class="flex items-center justify-between mb-1.5">
|
|
<span class="font-bold text-slate-900 dark:text-slate-100">👤 {{ c.author }}</span>
|
|
<span class="text-slate-400">{{ formatDate(c.createdAt) }}</span>
|
|
</div>
|
|
<p class="text-slate-700 dark:text-slate-300 leading-relaxed">{{ c.content }}</p>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center py-6 text-xs text-slate-400">
|
|
暂无评论,快来抢沙发吧!
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 发布文章写作工作室模态框 (直写 MySQL) -->
|
|
<div v-if="showPublishModal"
|
|
class="fixed inset-0 z-50 overflow-y-auto bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4 sm:p-6"
|
|
@click.self="showPublishModal = false">
|
|
<div class="w-full max-w-2xl bg-white dark:bg-slate-900 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 overflow-hidden animate-in fade-in zoom-in-95 duration-200">
|
|
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-800 flex items-center justify-between">
|
|
<h3 class="font-bold text-lg text-slate-900 dark:text-slate-100 flex items-center gap-2">
|
|
<span>✍️ 撰写并持久化发布到 MySQL</span>
|
|
</h3>
|
|
<button @click="showPublishModal = false" class="text-slate-400 hover:text-slate-600">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6 space-y-4 text-sm">
|
|
<div>
|
|
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">文章标题 *</label>
|
|
<input v-model="newPost.title" type="text" placeholder="例如: 探索 Kubernetes 核心架构与 Pod 调度机制"
|
|
class="w-full px-3.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">技术分类</label>
|
|
<select v-model="newPost.categoryName"
|
|
class="w-full px-3.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
|
<option value="云原生 & Linux">云原生 & Linux</option>
|
|
<option value="Java & Spring Boot">Java & Spring Boot</option>
|
|
<option value="AI & 本地模型">AI & 本地模型</option>
|
|
<option value="全栈开发 & CI/CD">全栈开发 & CI/CD</option>
|
|
<option value="前端与工程化">前端与工程化</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">标签 (逗号分隔)</label>
|
|
<input v-model="newPost.tags" type="text" placeholder="例如: SpringBoot,MySQL,Docker"
|
|
class="w-full px-3.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block font-medium mb-1 text-slate-700 dark:text-slate-300">一句话摘要</label>
|
|
<input v-model="newPost.summary" type="text" placeholder="简述本文的核心要点与阅读价值..."
|
|
class="w-full px-3.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none">
|
|
</div>
|
|
|
|
<div>
|
|
<div class="flex items-center justify-between mb-1">
|
|
<label class="font-medium text-slate-700 dark:text-slate-300">文章内容 (支持 Markdown) *</label>
|
|
<span class="text-xs text-slate-400">支持代码块高亮及标题格式</span>
|
|
</div>
|
|
<textarea v-model="newPost.content" rows="7" placeholder="# 正文标题 写下你的技术思考或教程步骤... ```java // 代码示例 ```"
|
|
class="w-full px-3.5 py-2 rounded-lg bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 focus:ring-2 focus:ring-sky-500 focus:outline-none font-mono text-xs sm:text-sm"></textarea>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-end gap-3 pt-3 border-t border-slate-100 dark:border-slate-800">
|
|
<button @click="showPublishModal = false" class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800 font-medium">
|
|
取消
|
|
</button>
|
|
<button @click="publishPost" :disabled="publishing"
|
|
class="px-5 py-2 rounded-lg bg-sky-600 hover:bg-sky-700 disabled:opacity-50 text-white font-semibold shadow-md shadow-sky-600/30 flex items-center gap-2">
|
|
<span v-if="publishing" class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
|
|
<span>{{ publishing ? '写入 MySQL 中...' : '持久化发布' }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 页脚 -->
|
|
<footer class="mt-auto border-t border-slate-200 dark:border-slate-800/80 bg-white dark:bg-slate-900 py-8 text-center text-xs text-slate-500 dark:text-slate-400">
|
|
<div class="max-w-5xl mx-auto px-4 flex flex-col sm:flex-row items-center justify-between gap-4">
|
|
<div>
|
|
© 2026 旺总的技术全栈博客 · 驱动于
|
|
<span class="text-sky-600 dark:text-sky-400 font-mono">Google Cloud 台湾节点</span>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<span>Spring Boot 3 + JPA</span>
|
|
<span>·</span>
|
|
<span>MySQL 8.0</span>
|
|
<span>·</span>
|
|
<span>Gitea CI/CD</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
</div>
|
|
|
|
<!-- Vue 业务脚本 -->
|
|
<script>
|
|
const { createApp, ref, computed, onMounted } = Vue;
|
|
|
|
createApp({
|
|
setup() {
|
|
const API_BASE = '/api';
|
|
const isDark = ref(false);
|
|
const backendConnected = ref(false);
|
|
const posts = ref([]);
|
|
const stats = ref({ totalPosts: 0, totalViews: 0, totalLikes: 0, totalCategories: 0 });
|
|
const selectedCategory = ref('全部');
|
|
const searchKeyword = ref('');
|
|
const activePost = ref(null);
|
|
const comments = ref([]);
|
|
const newComment = ref({ author: '访客', content: '' });
|
|
const submittingComment = ref(false);
|
|
const showPublishModal = ref(false);
|
|
const publishing = ref(false);
|
|
|
|
const categories = ['全部', '云原生 & Linux', 'Java & Spring Boot', 'AI & 本地模型', '全栈开发 & CI/CD'];
|
|
|
|
const newPost = ref({
|
|
title: '',
|
|
categoryName: '全栈开发 & CI/CD',
|
|
author: '旺总',
|
|
tags: 'SpringBoot,MySQL',
|
|
summary: '',
|
|
content: ''
|
|
});
|
|
|
|
// 请求后端文章列表
|
|
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();
|
|
}, 300);
|
|
}
|
|
|
|
// 查看文章详情并加载评论
|
|
async function openPostDetail(post) {
|
|
activePost.value = post;
|
|
post.views = (post.views || 0) + 1;
|
|
fetchComments(post.id);
|
|
if (backendConnected.value) {
|
|
fetch(`${API_BASE}/posts/${post.id}`).catch(() => {});
|
|
}
|
|
}
|
|
|
|
// 加载该文章的评论
|
|
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()) {
|
|
alert('评论内容不能为空!');
|
|
return;
|
|
}
|
|
submittingComment.value = true;
|
|
try {
|
|
const res = await fetch(`${API_BASE}/posts/${activePost.value.id}/comments`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(newComment.value)
|
|
});
|
|
if (res.ok) {
|
|
newComment.value.content = '';
|
|
fetchComments(activePost.value.id);
|
|
}
|
|
} catch (e) {
|
|
alert('提交评论失败,请检查网络');
|
|
}
|
|
submittingComment.value = false;
|
|
}
|
|
|
|
// 删除文章
|
|
async function deletePost(post) {
|
|
if (!confirm(`确定要删除文章《${post.title}》吗?`)) return;
|
|
try {
|
|
const res = await fetch(`${API_BASE}/posts/${post.id}`, { method: 'DELETE' });
|
|
if (res.ok) {
|
|
activePost.value = null;
|
|
fetchPosts();
|
|
}
|
|
} catch (e) {
|
|
alert('删除失败');
|
|
}
|
|
}
|
|
|
|
// 点赞
|
|
async function likePost(post) {
|
|
post.likes = (post.likes || 0) + 1;
|
|
stats.value.totalLikes = (stats.value.totalLikes || 0) + 1;
|
|
if (backendConnected.value) {
|
|
try {
|
|
await fetch(`${API_BASE}/posts/${post.id}/like`, { method: 'POST' });
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
function openPublishModal() {
|
|
newPost.value = {
|
|
title: '',
|
|
categoryName: '全栈开发 & CI/CD',
|
|
author: '旺总',
|
|
tags: 'SpringBoot,MySQL',
|
|
summary: '',
|
|
content: ''
|
|
};
|
|
showPublishModal.value = true;
|
|
}
|
|
|
|
// 发布新文章至 MySQL
|
|
async function publishPost() {
|
|
if (!newPost.value.title.trim() || !newPost.value.content.trim()) {
|
|
alert('请至少填写文章标题和文章正文内容!');
|
|
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) {
|
|
showPublishModal.value = false;
|
|
fetchPosts();
|
|
} else {
|
|
alert('发布失败,请稍后重试');
|
|
}
|
|
} catch (e) {
|
|
alert('发布失败,无法连接服务器');
|
|
}
|
|
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 toggleDarkMode() {
|
|
isDark.value = !isDark.value;
|
|
if (isDark.value) {
|
|
document.documentElement.classList.add('dark');
|
|
localStorage.theme = 'dark';
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
localStorage.theme = 'light';
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
isDark.value = true;
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
isDark.value = false;
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
|
|
if (window.marked && window.hljs) {
|
|
marked.setOptions({
|
|
highlight: function(code, lang) {
|
|
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
return hljs.highlight(code, { language }).value;
|
|
},
|
|
breaks: true
|
|
});
|
|
}
|
|
|
|
fetchPosts();
|
|
});
|
|
|
|
return {
|
|
isDark,
|
|
toggleDarkMode,
|
|
backendConnected,
|
|
posts,
|
|
stats,
|
|
categories,
|
|
selectedCategory,
|
|
searchKeyword,
|
|
selectCategory,
|
|
onSearchInput,
|
|
activePost,
|
|
openPostDetail,
|
|
comments,
|
|
newComment,
|
|
submitComment,
|
|
deletePost,
|
|
likePost,
|
|
showPublishModal,
|
|
newPost,
|
|
openPublishModal,
|
|
publishPost,
|
|
publishing,
|
|
renderMarkdown,
|
|
formatDate,
|
|
parseTags
|
|
};
|
|
}
|
|
}).mount('#app');
|
|
</script>
|
|
</body>
|
|
</html>
|