From affe583db7e319f8369adb4c17e04d4dae5f6ef2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 22 Sep 2026 12:06:10 +0800 Subject: [PATCH 1/4] =?UTF-8?q?refactor(deploy):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=91=98=E8=B4=A6=E5=8F=B7=E5=88=9B=E5=BB=BA?= =?UTF-8?q?/=E9=87=8D=E7=BD=AE=E9=80=BB=E8=BE=91=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=20refactor(seed):=20=E6=94=B9=E8=BF=9B=E7=A7=8D=E5=AD=90?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=EF=BC=8C=E7=BC=BA=E5=A4=B1=E5=8F=98=E9=87=8F?= =?UTF-8?q?=E6=97=B6=E4=BC=98=E9=9B=85=E9=80=80=E5=87=BA=E5=B9=B6=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E6=97=A5=E5=BF=97=20refactor(ArticleList):=20?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=87=E7=AB=A0=E5=88=97=E8=A1=A8=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=A0=B7=E5=BC=8F=EF=BC=8C=E4=BC=98=E5=8C=96=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=BF=A1=E6=81=AF=20refactor(ArticleCard):=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E6=96=87=E7=AB=A0=E5=8D=A1=E7=89=87=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=88=86=E7=B1=BB=E5=92=8C?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E5=BE=BD=E7=AB=A0=20refactor(SidebarCard):?= =?UTF-8?q?=20=E6=9B=B4=E6=96=B0=E4=BE=A7=E6=A0=8F=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=EF=BC=8C=E4=BD=BF=E7=94=A8=E6=9B=B4=E5=9C=86?= =?UTF-8?q?=E6=B6=A6=E7=9A=84=E8=BE=B9=E8=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 20 +++++----- backend/cmd/seed/main.go | 12 +++--- frontend/src/app/(blog)/ArticleList.tsx | 8 ++-- frontend/src/components/blog/ArticleCard.tsx | 40 ++++++++++++-------- frontend/src/components/blog/SidebarCard.tsx | 2 +- 5 files changed, 46 insertions(+), 36 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c27ea03..80f8e7b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -140,19 +140,17 @@ jobs: fi echo ">>> 创建/重置管理员(依赖 .env 中的 SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD)" - # 缺失任一变量则跳过:保留"手工管理管理员"的部署模式不被强制覆盖。 + # 无条件调用 ./seed:seed 命令自身已具备"未设置则优雅跳过"的逻辑, + # 这里不再做 shell 层的 env 检查 —— docker compose --env-file .env + # 只把变量喂给 compose 插值,不会 export 到 SSH shell, + # 此前用 ${SEED_ADMIN_EMAIL:-} 判断会始终为假,导致有 .env 也会被误判。 # 已存在同名账号时 ./seed 仅重置密码 + bump token_version,旧 JWT 立即失效。 - if [ -n "${SEED_ADMIN_EMAIL:-}" ] && [ -n "${SEED_ADMIN_PASSWORD:-}" ]; then - if $COMPOSE run --rm backend /app/seed; then - echo "✅ 管理员账号已同步(创建 / 密码重置 / admin 角色补绑)" - else - echo "::error::管理员种子失败,请看下方日志" - $COMPOSE logs --tail=80 backend - exit 1 - fi + if $COMPOSE run --rm backend /app/seed; then + echo "✅ 管理员账号已同步(创建 / 密码重置 / admin 角色补绑,或因未配置 seed 而跳过)" else - echo "⚠️ SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD 未在 .env 中设置,跳过 ./seed。" - echo " 若 /admin/login 返回\"用户名或密码错误\",请在 .env 中填好这两个变量后重新部署。" + echo "::error::管理员种子失败,请看下方日志" + $COMPOSE logs --tail=80 backend + exit 1 fi echo ">>> 启动 backend / frontend / nginx(强制用新镜像)" diff --git a/backend/cmd/seed/main.go b/backend/cmd/seed/main.go index c6b2b8b..6fd9c84 100644 --- a/backend/cmd/seed/main.go +++ b/backend/cmd/seed/main.go @@ -42,11 +42,13 @@ func main() { email := os.Getenv("SEED_ADMIN_EMAIL") password := os.Getenv("SEED_ADMIN_PASSWORD") - if email == "" { - log.Fatal("SEED_ADMIN_EMAIL 未设置") - } - if password == "" { - log.Fatal("SEED_ADMIN_PASSWORD 未设置") + // 缺失任一变量时优雅退出(exit 0)而非 log.Fatal: + // - 让 CI/CD 流水线可以在不强制配置 seed 的情况下安全地调用 ./seed + // - 仍打印醒目日志,运维一看就知道为什么没建账号 + if email == "" || password == "" { + log.Println("ℹ️ SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD 未设置,跳过 seed。" + + "如需创建/重置管理员,请在 .env 或当前 shell 中设置后重跑。") + return } if len(password) < minPasswordLen { log.Fatalf("SEED_ADMIN_PASSWORD 至少需要 %d 个字符", minPasswordLen) diff --git a/frontend/src/app/(blog)/ArticleList.tsx b/frontend/src/app/(blog)/ArticleList.tsx index ae27e2d..00aa5c6 100644 --- a/frontend/src/app/(blog)/ArticleList.tsx +++ b/frontend/src/app/(blog)/ArticleList.tsx @@ -107,13 +107,13 @@ export default function ArticleList() { const totalPages = Math.ceil(total / PAGE_SIZE); return ( -
+
{/* 主列表 */}
{/* 工具栏 */}

- {hasFilter ? "文章筛选" : "最新推荐"} + {hasFilter ? "文章筛选" : "最新文章"}

{/* 图片开关 */} @@ -202,7 +202,6 @@ export default function ArticleList() { {/* 侧栏 */} {/* 移动端:筛选在底部 */}
); diff --git a/frontend/src/components/blog/ArticleCard.tsx b/frontend/src/components/blog/ArticleCard.tsx index b2e662c..248b825 100644 --- a/frontend/src/components/blog/ArticleCard.tsx +++ b/frontend/src/components/blog/ArticleCard.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import Image from "next/image"; -import { Calendar, Eye, ChevronRight } from "lucide-react"; +import { Calendar, Eye, FolderTree, Tag as TagIcon, ChevronRight } from "lucide-react"; import { cn, formatViews, safeTruncate, stripMarkdown } from "@/lib/utils"; export interface ArticleCardData { @@ -98,11 +98,21 @@ export function ArticleCard({ {summary}

- {/* 标签 */} - {article.tags && article.tags.length > 0 && ( -
- {article.tags.slice(0, 3).map((t) => ( - + {/* 分类 + 标签 徽章 */} + {(article.categoryName || (article.tags && article.tags.length > 0)) && ( +
+ {article.categoryName && ( + + + {article.categoryName} + + )} + {article.tags?.slice(0, 2).map((t) => ( + + {t.name} ))} @@ -111,36 +121,36 @@ export function ArticleCard({ {/* meta */}
-
+
{article.author?.avatar ? ( {article.author.name} ) : ( -
+
{article.author?.name?.[0] || "U"}
)} -
+
{article.author?.name && ( - + {article.author.name} )} {article.publishTime && ( - - - {article.publishTime} + + + {article.publishTime} )}
{article.viewCount !== undefined && ( - + {formatViews(article.viewCount)} diff --git a/frontend/src/components/blog/SidebarCard.tsx b/frontend/src/components/blog/SidebarCard.tsx index 598101a..2ed1f6c 100644 --- a/frontend/src/components/blog/SidebarCard.tsx +++ b/frontend/src/components/blog/SidebarCard.tsx @@ -18,7 +18,7 @@ export function SidebarCard({ return (
From c1eed7f38d72e9b3b247e39f5cccf609b15d6eff Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 22 Sep 2026 12:29:03 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor(blog):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E5=B8=83=E5=B1=80=E5=92=8C=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=AE=BD=E9=93=BA=E6=BB=A1?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/(blog)/page.tsx | 24 +++++++++++++---------- frontend/src/app/globals.css | 8 ++++++++ frontend/src/components/blog/Hero.tsx | 6 +++--- frontend/src/components/blog/Sidebars.tsx | 7 +------ 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/(blog)/page.tsx b/frontend/src/app/(blog)/page.tsx index 307c885..5f3430d 100644 --- a/frontend/src/app/(blog)/page.tsx +++ b/frontend/src/app/(blog)/page.tsx @@ -16,7 +16,8 @@ export default async function HomePage({ searchParams }: HomePageProps) { : await Promise.all([fetchSiteConfig(), fetchFeatured()]); return ( -
+
+ {/* 图片 div:全宽铺满视口 */} - - 加载中... -
- } - > - - + {/* 内容 div:居中对齐浏览器 */} +
+ + 加载中... +
+ } + > + + +
); } diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 036f73d..d7df2b6 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -214,6 +214,14 @@ scrollbar-width: none; -ms-overflow-style: none; } + +/* ===== 全宽铺满(突破父级 max-width 限制) ===== */ +.full-bleed { + width: 100vw; + margin-left: calc(50% - 50vw); + margin-right: calc(50% - 50vw); + position: relative; +} .hide-scrollbar::-webkit-scrollbar { display: none; } diff --git a/frontend/src/components/blog/Hero.tsx b/frontend/src/components/blog/Hero.tsx index 74e3c40..584fb6c 100644 --- a/frontend/src/components/blog/Hero.tsx +++ b/frontend/src/components/blog/Hero.tsx @@ -39,19 +39,19 @@ export function Hero({ const bgCover = "/Hero.jpg"; return ( -
+
{title}
-
+
{pinned?.categoryName && ( diff --git a/frontend/src/components/blog/Sidebars.tsx b/frontend/src/components/blog/Sidebars.tsx index a2c4050..c79d3b6 100644 --- a/frontend/src/components/blog/Sidebars.tsx +++ b/frontend/src/components/blog/Sidebars.tsx @@ -44,12 +44,7 @@ export function CategorySidebar({ -

- {articleId ? "编辑文章" : "发布文章"} -

-
-
- - +
+ {/* 顶部工具栏:标题 + 同步滚动 + 取消/保存 */} +
+ + updateField("articleTitle", e.target.value)} + onBlur={() => validateField("articleTitle")} + className={`h-10 flex-1 border-transparent bg-transparent text-lg font-semibold shadow-none focus-visible:border-input focus-visible:ring-0 ${ + errors.articleTitle ? "border-destructive" : "" + }`} + /> +
+ +
+ +
+ {/* 错误提示 */} + {errors.articleTitle && ( +

{errors.articleTitle}

+ )} + {/* Draft Recovery Dialog */} {showDraftDialog && (
@@ -337,22 +383,6 @@ function EditorContent() {
)} - {/* Title */} -
- - updateField("articleTitle", e.target.value)} - onBlur={() => validateField("articleTitle")} - className={errors.articleTitle ? "border-destructive" : ""} - /> - {errors.articleTitle && ( -

{errors.articleTitle}

- )} -
- {/* Category & Type & Status */}
@@ -451,7 +481,9 @@ function EditorContent() { onChange={(val) => { updateField("articleContent", val || ""); }} - height={500} + height={typeof window !== "undefined" && window.innerWidth < 768 ? 420 : 620} + preview={syncScroll ? "live" : "edit"} + visibleDragbar={false} />
{errors.articleContent && ( diff --git a/frontend/src/app/(admin)/admin/articles/page.tsx b/frontend/src/app/(admin)/admin/articles/page.tsx index c6bb5dd..61f25e8 100644 --- a/frontend/src/app/(admin)/admin/articles/page.tsx +++ b/frontend/src/app/(admin)/admin/articles/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback, useMemo } from "react"; +import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useRouter } from "next/navigation"; import { type ColumnDef } from "@tanstack/react-table"; import { api } from "@/lib/api"; @@ -8,7 +8,7 @@ import type { PageResult } from "@/lib/types"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; -import { Pencil, Pin, Trash2, Plus, Filter } from "lucide-react"; +import { Pencil, Pin, Trash2, Plus, Filter, Upload, FileText } from "lucide-react"; import { Dialog, DialogContent, @@ -403,14 +403,59 @@ export default function ArticlesPage() { [handleDelete], ); + // 导入 .md 文件 + const fileInputRef = useRef(null); + const handleImportClick = () => fileInputRef.current?.click(); + const handleImportFile = useCallback( + async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + // 清空 input,允许重复导入同一文件 + e.target.value = ""; + if (!file) return; + if (!file.name.toLowerCase().endsWith(".md") && !file.name.toLowerCase().endsWith(".markdown")) { + toast.error("请选择 Markdown 文件 (.md)"); + return; + } + try { + const text = await file.text(); + // 通过 sessionStorage 传递给编辑器(带文件名做标题兜底) + sessionStorage.setItem( + "import_article", + JSON.stringify({ + fileName: file.name.replace(/\.(md|markdown)$/i, ""), + content: text, + }), + ); + router.push("/admin/articles/editor"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "读取文件失败"); + } + }, + [router], + ); + return (

文章列表

- +
+ {/* 隐藏的 file input */} + + + +
{loading ? ( From 53b9ca0f45c582d0ea8748f546c65d2b832a9815 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 23 Sep 2026 10:26:16 +0800 Subject: [PATCH 4/4] =?UTF-8?q?refactor(article):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E5=B0=81=E9=9D=A2=E6=A0=B7=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=B8=83=E5=B1=80=E5=92=8C=E9=97=B4=E8=B7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(blog)/articles/[id]/page.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/(blog)/articles/[id]/page.tsx b/frontend/src/app/(blog)/articles/[id]/page.tsx index 77e3b08..60f9981 100644 --- a/frontend/src/app/(blog)/articles/[id]/page.tsx +++ b/frontend/src/app/(blog)/articles/[id]/page.tsx @@ -191,9 +191,9 @@ export default function ArticleDetailPage() { return (
- {/* 封面(spec §4.6) */} + {/* 封面(full-bleed 全宽铺满视口) */} {article.articleCover && ( -
+
{article.articleTitle}
-
-
+
+
{article.categoryName && ( {article.articleTitle} -
+
{article.createTime} @@ -233,13 +233,8 @@ export default function ArticleDetailPage() {
)} - {/* 三栏布局 */} -
+ {/* 三栏布局:自然衔接封面,不做 -mt-20 覆盖以免 TOC 卡粘性时盖住标题 */} +
{/* 左侧 TOC */}