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:
@@ -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)];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user