feat: 个人博客全栈实现(Rust/Axum + React/Tailwind + PostgreSQL)

后端(Axum + sqlx):
- GitHub OAuth 三层权限(管理员/用户/游客),MOCK_OAUTH 本地调试开关
- 文章 CRUD、草稿自动保存、发布/私有分离,发布时渲染 HTML
- 目录(folder 路径分层)、标签、评论、RSS、热门文章(按评论数)
- pages 表驱动动态内容:关于页、首页标题、个人名片
- S3 兼容对象存储图片上传接口(rust-s3,未配置时明确报错)
- 发布时自动提取正文首图作为封面 cover_url
- sqlx migrate 管理 5 个迁移

前端(React + Tailwind):
- 白天 Editorial / 黑夜星空双主题
- 首页(列表 + 热门侧栏 + 可编辑大标题)、文库(目录树+标签+搜索)
- Profile 页(GitHub 风格名片卡,全部字段后台可编辑)
- 后台:文件树文章管理、CodeMirror 编辑器(实时预览/自动保存/传图)
- Markdown 渲染:KaTeX 公式、代码块语言标注 + 双主题语法高亮
- 有封面的文章卡片自动整行展示,全局 ErrorBoundary

部署:
- docker-compose 本地三容器(postgres/backend/frontend)
- k8s manifests(适配 Caddy 终结 TLS → 隧道 → traefik 明文路由)
- 前端静态文件可由 Caddy 直接托管,集群只需 backend + postgres
- 可选 postgres 每日备份到 S3 的 CronJob
This commit is contained in:
2026-07-18 19:15:49 +08:00
commit bfc83a2400
92 changed files with 14221 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
color-scheme: light dark;
}
html {
@apply scroll-smooth;
}
body {
@apply bg-paper text-stone-900 antialiased font-serif;
font-feature-settings: 'cv11', 'ss01';
}
html.dark body {
@apply bg-space-950 text-star font-sans;
}
::selection {
@apply bg-indigo-500/30 text-indigo-900;
}
.dark ::selection {
@apply bg-nebula/40 text-star;
}
}
@layer components {
.stars {
background-image:
radial-gradient(2px 2px at 20px 30px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 40px 70px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 90px 40px, rgba(255,255,255,0.4), transparent),
radial-gradient(2px 2px at 160px 120px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 230px 80px, rgba(255,255,255,0.3), transparent),
radial-gradient(2px 2px at 300px 20px, rgba(255,255,255,0.2), transparent),
radial-gradient(1px 1px at 350px 150px, rgba(255,255,255,0.4), transparent);
background-repeat: repeat;
background-size: 400px 200px;
}
.nebula {
background-image:
radial-gradient(ellipse at 20% 30%, rgba(99,102,241,0.12), transparent 40%),
radial-gradient(ellipse at 80% 70%, rgba(139,92,246,0.08), transparent 40%),
radial-gradient(ellipse at 50% 50%, rgba(34,211,238,0.06), transparent 50%);
}
.glass {
@apply bg-white/70 backdrop-blur-md border border-white/20;
}
.dark .glass {
@apply bg-space-800/60 border-indigo-500/20;
}
.glow-text {
@apply text-transparent bg-clip-text bg-gradient-to-r from-indigo-400 via-purple-400 to-cyan-400;
}
.nav-link {
@apply relative text-stone-600 hover:text-stone-900 transition-colors;
}
.dark .nav-link {
@apply text-star-dim hover:text-star;
}
.nav-link::after {
content: '';
@apply absolute left-0 -bottom-1 w-0 h-0.5 bg-indigo-500 transition-all duration-300;
}
.nav-link:hover::after {
@apply w-full;
}
/* 卡片悬浮光晕 */
.card-glow {
@apply relative overflow-hidden rounded-lg border border-stone-200 bg-white shadow-sm transition-all duration-300;
}
.dark .card-glow {
@apply border-indigo-500/20 bg-space-800/60 shadow-none;
}
.card-glow::before {
content: '';
@apply absolute inset-0 opacity-0 transition-opacity duration-300 pointer-events-none;
background: radial-gradient(600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(99,102,241,0.08), transparent 40%);
}
.dark .card-glow::before {
background: radial-gradient(600px circle at var(--mouse-x, 50%) var(--mouse-y, 50%), rgba(129,140,248,0.12), transparent 40%);
}
.card-glow:hover::before {
@apply opacity-100;
}
/* 发光边框 */
.glow-border {
position: relative;
}
.glow-border::after {
content: '';
@apply absolute inset-0 rounded-2xl opacity-0 transition-opacity duration-300 pointer-events-none;
padding: 1px;
background: linear-gradient(135deg, rgba(99,102,241,0.4), rgba(139,92,246,0.4), rgba(34,211,238,0.4));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
.glow-border:hover::after {
@apply opacity-100;
}
/* 标题动画 */
.title-reveal {
@apply inline-block overflow-hidden;
}
.title-reveal span {
@apply inline-block transform translate-y-full;
animation: titleUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes titleUp {
to {
transform: translateY(0);
}
}
/* 夜间模式正文调白(prose-invert 用的是 invert 变量,得在这里覆盖) */
.dark .prose {
--tw-prose-invert-body: #ffffff;
--tw-prose-invert-headings: #ffffff;
--tw-prose-invert-bold: #ffffff;
--tw-prose-invert-quotes: #ffffff;
--tw-prose-invert-counters: #e0e7ff;
--tw-prose-invert-bullets: #818cf8;
}
/* 代码块:浅色底框 + 顶部语言标注栏(夜间为深空蓝) */
.prose .code-block-wrapper {
@apply relative my-6 overflow-hidden rounded-lg border border-stone-200 bg-[#f6f8fa];
}
.prose .code-block-header {
@apply flex items-center justify-between border-b border-stone-200 bg-stone-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wider text-stone-500;
}
.prose pre.code-block-content {
@apply m-0 overflow-x-auto bg-transparent p-4 font-mono text-sm leading-relaxed text-stone-800;
}
/* hljs 主题自带底色和内边距,去掉以免和容器冲突 */
.prose pre.code-block-content .hljs {
background: transparent;
padding: 0;
}
/* 夜间:深空蓝底 + GitHub Dark 语法配色 */
.dark .prose .code-block-wrapper {
@apply border-indigo-500/20 bg-space-800;
}
.dark .prose .code-block-header {
@apply border-indigo-500/10 bg-space-900 text-star-dim;
}
.dark .prose pre.code-block-content {
@apply text-slate-200;
}
.dark .hljs { color: #c9d1d9; }
.dark .hljs-doctag,
.dark .hljs-keyword,
.dark .hljs-meta .hljs-keyword,
.dark .hljs-template-tag,
.dark .hljs-template-variable,
.dark .hljs-type,
.dark .hljs-variable.language_ { color: #ff7b72; }
.dark .hljs-title,
.dark .hljs-title.class_,
.dark .hljs-title.class_.inherited__,
.dark .hljs-title.function_ { color: #d2a8ff; }
.dark .hljs-attr,
.dark .hljs-attribute,
.dark .hljs-literal,
.dark .hljs-meta,
.dark .hljs-number,
.dark .hljs-operator,
.dark .hljs-variable,
.dark .hljs-selector-attr,
.dark .hljs-selector-class,
.dark .hljs-selector-id { color: #79c0ff; }
.dark .hljs-regexp,
.dark .hljs-string,
.dark .hljs-meta .hljs-string { color: #a5d6ff; }
.dark .hljs-built_in,
.dark .hljs-symbol { color: #ffa657; }
.dark .hljs-comment,
.dark .hljs-code,
.dark .hljs-formula { color: #8b949e; }
.dark .hljs-name,
.dark .hljs-quote,
.dark .hljs-selector-tag,
.dark .hljs-selector-pseudo { color: #7ee787; }
.dark .hljs-subst { color: #c9d1d9; }
.dark .hljs-section { color: #1f6feb; font-weight: bold; }
.dark .hljs-bullet { color: #f2cc60; }
.dark .hljs-emphasis { color: #c9d1d9; font-style: italic; }
.dark .hljs-strong { color: #c9d1d9; font-weight: bold; }
.dark .hljs-addition { color: #aff5b4; background-color: #033a16; }
.dark .hljs-deletion { color: #ffdcd7; background-color: #67060c; }
/* 阅读进度条 */
.progress-bar {
@apply fixed left-0 top-0 h-1 bg-gradient-to-r from-indigo-500 via-purple-500 to-cyan-500 transition-transform origin-left z-50;
}
/* TOC */
.toc-item {
@apply relative pl-4 text-sm text-stone-500 transition-colors hover:text-indigo-600 dark:text-star-dim dark:hover:text-nebula-glow;
}
.toc-item::before {
content: '';
@apply absolute left-0 top-1/2 h-0 w-0.5 -translate-y-1/2 bg-indigo-500 transition-all duration-200;
}
.toc-item.active::before {
@apply h-4;
}
.toc-item.active {
@apply text-indigo-600 font-medium dark:text-nebula-glow;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-full bg-stone-300;
}
.dark ::-webkit-scrollbar-thumb {
@apply bg-space-700;
}
/* 按钮按压效果 */
.btn-press {
@apply active:scale-95 transition-transform;
}
/* 标签云 */
.tag-pill {
@apply inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-all duration-200;
}
/* 输入框聚焦光效 */
.input-glow {
@apply transition-all duration-200;
}
.input-glow:focus {
@apply shadow-[0_0_0_3px_rgba(99,102,241,0.15)];
}
.dark .input-glow:focus {
@apply shadow-[0_0_0_3px_rgba(129,140,248,0.2)];
}
}