From d73ed493be283c2aed15b253aab8234068fb861c Mon Sep 17 00:00:00 2001 From: Snowykami Date: Mon, 28 Jul 2025 06:22:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=AF=84=E8=AE=BA?= =?UTF-8?q?=E5=92=8C=E5=B8=96=E5=AD=90=E6=A8=A1=E5=9E=8B=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF=E5=92=8C=E5=8E=9F?= =?UTF-8?q?=E5=88=9B=E6=A0=87=E8=AF=86=EF=BC=8C=E4=BC=98=E5=8C=96API?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E5=92=8C=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/controller/v1/comment.go | 15 +- internal/controller/v1/post.go | 2 - internal/dto/post.go | 2 + internal/dto/user.go | 20 +-- internal/model/post.go | 34 +++-- internal/model/user.go | 4 +- internal/service/comment.go | 24 ++-- internal/service/post.go | 17 +-- web/src/api/client.ts | 4 +- web/src/api/post.ts | 3 +- web/src/app/(main)/layout.tsx | 36 ++--- web/src/app/layout.tsx | 10 +- web/src/components/blog-home.tsx | 5 +- web/src/components/blog-post.tsx | 176 ++++++++++++------------ web/src/components/navbar.tsx | 12 +- web/src/config.ts | 4 +- web/src/contexts/background-context.tsx | 60 ++++++++ web/src/contexts/device-context.tsx | 7 +- web/src/i18n/request.ts | 3 +- web/src/models/post.ts | 3 + 20 files changed, 260 insertions(+), 181 deletions(-) create mode 100644 web/src/contexts/background-context.tsx diff --git a/internal/controller/v1/comment.go b/internal/controller/v1/comment.go index 3f78e5a..2eb8150 100644 --- a/internal/controller/v1/comment.go +++ b/internal/controller/v1/comment.go @@ -102,13 +102,12 @@ func (cc *CommentController) GetCommentList(ctx context.Context, c *app.RequestC resps.BadRequest(c, "无效的 target_id") return } - req := dto.GetCommentListReq{ - Desc: pagination.Desc, - OrderBy: pagination.OrderBy, - Page: pagination.Page, - Size: pagination.Size, - TargetID: uint(targetID), + Desc: pagination.Desc, + OrderBy: pagination.OrderBy, + Page: pagination.Page, + Size: pagination.Size, + TargetID: uint(targetID), TargetType: c.Query("target_type"), } resp, err := cc.service.GetCommentList(ctx, &req) @@ -120,4 +119,6 @@ func (cc *CommentController) GetCommentList(ctx context.Context, c *app.RequestC resps.Ok(c, resps.Success, resp) } -func (cc *CommentController) ReactComment(ctx context.Context, c *app.RequestContext) {} +func (cc *CommentController) ReactComment(ctx context.Context, c *app.RequestContext) { + +} diff --git a/internal/controller/v1/post.go b/internal/controller/v1/post.go index c7d6779..bcb8e24 100644 --- a/internal/controller/v1/post.go +++ b/internal/controller/v1/post.go @@ -2,7 +2,6 @@ package v1 import ( "context" - "fmt" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/common/utils" "github.com/snowykami/neo-blog/internal/ctxutils" @@ -95,7 +94,6 @@ func (p *PostController) Update(ctx context.Context, c *app.RequestContext) { func (p *PostController) List(ctx context.Context, c *app.RequestContext) { pagination := ctxutils.GetPaginationParams(c) - fmt.Println(pagination) if pagination.OrderBy == "" { pagination.OrderBy = constant.OrderByUpdatedAt } diff --git a/internal/dto/post.go b/internal/dto/post.go index 5f7ab0d..a816c69 100644 --- a/internal/dto/post.go +++ b/internal/dto/post.go @@ -5,11 +5,13 @@ import "time" type PostDto struct { ID uint `json:"id"` // 帖子ID UserID uint `json:"user_id"` // 发布者的用户ID + User UserDto `json:"user"` // 发布者信息 Title string `json:"title"` // 帖子标题 Content string `json:"content"` Cover string `json:"cover"` // 帖子封面图 Type string `json:"type"` // 帖子类型 markdown / html / text Labels []LabelDto `json:"labels"` // 关联的标签 + IsOriginal bool `json:"is_original"` // 是否为原创帖子 IsPrivate bool `json:"is_private"` // 是否为私密帖子 LikeCount uint64 `json:"like_count"` // 点赞数 CommentCount uint64 `json:"comment_count"` // 评论数 diff --git a/internal/dto/user.go b/internal/dto/user.go index 99a0d91..8ac83e2 100644 --- a/internal/dto/user.go +++ b/internal/dto/user.go @@ -23,9 +23,9 @@ type UserLoginReq struct { } type UserLoginResp struct { - Token string `json:"token"` - RefreshToken string `json:"refresh_token"` - User *UserDto `json:"user"` + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` + User UserDto `json:"user"` } type UserRegisterReq struct { @@ -37,9 +37,9 @@ type UserRegisterReq struct { } type UserRegisterResp struct { - Token string `json:"token"` // 访问令牌 - RefreshToken string `json:"refresh_token"` // 刷新令牌 - User *UserDto `json:"user"` // 用户信息 + Token string `json:"token"` // 访问令牌 + RefreshToken string `json:"refresh_token"` // 刷新令牌 + User UserDto `json:"user"` // 用户信息 } type VerifyEmailReq struct { @@ -57,9 +57,9 @@ type OidcLoginReq struct { } type OidcLoginResp struct { - Token string `json:"token"` - RefreshToken string `json:"refresh_token"` - User *UserDto `json:"user"` + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` + User UserDto `json:"user"` } type ListOidcConfigResp struct { @@ -71,7 +71,7 @@ type GetUserReq struct { } type GetUserResp struct { - User *UserDto `json:"user"` // 用户信息 + User UserDto `json:"user"` // 用户信息 } type UpdateUserReq struct { diff --git a/internal/model/post.go b/internal/model/post.go index 5d37c8d..cb2ad02 100644 --- a/internal/model/post.go +++ b/internal/model/post.go @@ -1,6 +1,7 @@ package model import ( + "fmt" "github.com/snowykami/neo-blog/internal/dto" "github.com/snowykami/neo-blog/pkg/constant" "gorm.io/gorm" @@ -13,10 +14,11 @@ type Post struct { Title string `gorm:"type:text;not null"` // 帖子标题 Cover string `gorm:"type:text"` // 帖子封面图 Content string `gorm:"type:text;not null"` // 帖子内容 - Type string `gorm:"type:text;default:markdown"` // markdown类型,支持markdown或html + Type string `gorm:"type:text;default:markdown"` // markdown类型,支持markdown或html或txt CategoryID uint `gorm:"index"` // 帖子分类ID Category Category `gorm:"foreignKey:CategoryID;references:ID"` // 关联的分类 Labels []Label `gorm:"many2many:post_labels;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // 关联的标签 + IsOriginal bool `gorm:"default:true"` // 是否为原创帖子 IsPrivate bool `gorm:"default:false"` LikeCount uint64 CommentCount uint64 @@ -44,26 +46,36 @@ func (p *Post) AfterUpdate(tx *gorm.DB) (err error) { return nil } -func (p *Post) ToDto() dto.PostDto { - return dto.PostDto{ - ID: p.ID, - UserID: p.UserID, - Title: p.Title, - Content: p.Content, - Cover: p.Cover, - Type: p.Type, - IsPrivate: p.IsPrivate, +func (p *Post) ToDto() *dto.PostDto { + fmt.Println("User", p.User) + return &dto.PostDto{ + ID: p.ID, + UserID: p.UserID, + Title: p.Title, + Content: p.Content, + Cover: p.Cover, + Type: p.Type, + IsOriginal: p.IsOriginal, + IsPrivate: p.IsPrivate, + Labels: func() []dto.LabelDto { + labelDtos := make([]dto.LabelDto, len(p.Labels)) + for i, label := range p.Labels { + labelDtos[i] = label.ToDto() + } + return labelDtos + }(), LikeCount: p.LikeCount, CommentCount: p.CommentCount, ViewCount: p.ViewCount, Heat: p.Heat, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, + User: p.User.ToDto(), } } // ToDtoWithShortContent 返回一个简化的 DTO,内容可以根据需要截断 -func (p *Post) ToDtoWithShortContent(contentLength int) dto.PostDto { +func (p *Post) ToDtoWithShortContent(contentLength int) *dto.PostDto { dtoPost := p.ToDto() if len(p.Content) > contentLength { dtoPost.Content = p.Content[:contentLength] + "..." diff --git a/internal/model/user.go b/internal/model/user.go index ba2e0da..66de9d1 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -25,8 +25,8 @@ type UserOpenID struct { Sub string `gorm:"index"` // OIDC Sub openid } -func (user *User) ToDto() *dto.UserDto { - return &dto.UserDto{ +func (user *User) ToDto() dto.UserDto { + return dto.UserDto{ ID: user.ID, Username: user.Username, Nickname: user.Nickname, diff --git a/internal/service/comment.go b/internal/service/comment.go index 1839bca..2dbeb97 100644 --- a/internal/service/comment.go +++ b/internal/service/comment.go @@ -31,7 +31,7 @@ func (cs *CommentService) CreateComment(ctx context.Context, req *dto.CreateComm UserID: currentUser.ID, IsPrivate: req.IsPrivate, } - + err := repo.Comment.CreateComment(comment) if err != nil { @@ -63,7 +63,7 @@ func (cs *CommentService) UpdateComment(ctx context.Context, req *dto.UpdateComm if err != nil { return err } - + return nil } @@ -100,15 +100,15 @@ func (cs *CommentService) GetComment(ctx context.Context, commentID string) (*dt } commentDto := dto.CommentDto{ - ID: comment.ID, - TargetID: comment.TargetID, + ID: comment.ID, + TargetID: comment.TargetID, TargetType: comment.TargetType, - Content: comment.Content, - ReplyID: comment.ReplyID, - Depth: comment.Depth, - CreatedAt: comment.CreatedAt.String(), - UpdatedAt: comment.UpdatedAt.String(), - User: *comment.User.ToDto(), + Content: comment.Content, + ReplyID: comment.ReplyID, + Depth: comment.Depth, + CreatedAt: comment.CreatedAt.String(), + UpdatedAt: comment.UpdatedAt.String(), + User: comment.User.ToDto(), } return &commentDto, err @@ -132,8 +132,8 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen TargetType: comment.TargetType, CreatedAt: comment.CreatedAt.String(), UpdatedAt: comment.UpdatedAt.String(), - Depth: comment.Depth, - User: *comment.User.ToDto(), + Depth: comment.Depth, + User: comment.User.ToDto(), } commentDtos = append(commentDtos, commentDto) } diff --git a/internal/service/post.go b/internal/service/post.go index 2d6526b..44465fb 100644 --- a/internal/service/post.go +++ b/internal/service/post.go @@ -76,18 +76,7 @@ func (p *PostService) GetPost(ctx context.Context, id string) (*dto.PostDto, err if post.IsPrivate && (!ok || post.UserID != currentUser.ID) { return nil, errs.ErrForbidden } - return &dto.PostDto{ - UserID: post.UserID, - Title: post.Title, - Content: post.Content, - Labels: func() []dto.LabelDto { - labelDtos := make([]dto.LabelDto, 0) - for _, label := range post.Labels { - labelDtos = append(labelDtos, label.ToDto()) - } - return labelDtos - }(), - }, nil + return post.ToDto(), nil } func (p *PostService) UpdatePost(ctx context.Context, id string, req *dto.CreateOrUpdatePostReq) (uint, error) { @@ -124,8 +113,8 @@ func (p *PostService) UpdatePost(ctx context.Context, id string, req *dto.Create return post.ID, nil } -func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) ([]dto.PostDto, error) { - postDtos := make([]dto.PostDto, 0) +func (p *PostService) ListPosts(ctx context.Context, req *dto.ListPostReq) ([]*dto.PostDto, error) { + postDtos := make([]*dto.PostDto, 0) currentUserID, _ := ctxutils.GetCurrentUserID(ctx) posts, err := repo.Post.ListPosts(currentUserID, req.Keywords, req.Page, req.Size, req.OrderBy, req.Desc) if err != nil { diff --git a/web/src/api/client.ts b/web/src/api/client.ts index db9afcb..cf8a730 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -1,9 +1,9 @@ import axios from 'axios' import { camelToSnakeObj, snakeToCamelObj } from 'field-conv' -export const BACKEND_URL = process.env.BACKEND_URL || 'http://neo-blog-backend:8888' +export const BACKEND_URL = process.env.BACKEND_URL || (process.env.NODE_ENV == "production" ? 'http://neo-blog-backend:8888' : 'http://localhost:8888') -console.info(`Using backend URL: ${BACKEND_URL}`) +console.info(`Using ${process.env.NODE_ENV} backend URL: ${BACKEND_URL}`) const isServer = typeof window === 'undefined' diff --git a/web/src/api/post.ts b/web/src/api/post.ts index 77697b1..f18c255 100644 --- a/web/src/api/post.ts +++ b/web/src/api/post.ts @@ -11,9 +11,8 @@ interface ListPostsParams { } export async function getPostById(id: string): Promise { - console.log('Fetching post by ID:', id) try { - const res = await axiosClient.get>(`/post/p/19`) + const res = await axiosClient.get>(`/post/p/${id}`) return res.data.data } catch (error) { diff --git a/web/src/app/(main)/layout.tsx b/web/src/app/(main)/layout.tsx index 009a2f2..ffb7cd9 100644 --- a/web/src/app/(main)/layout.tsx +++ b/web/src/app/(main)/layout.tsx @@ -1,8 +1,9 @@ 'use client' -import { AnimatePresence, motion } from 'framer-motion' +import { motion } from 'framer-motion' import { usePathname } from 'next/navigation' import { Navbar } from '@/components/navbar' +import { BackgroundProvider } from '@/contexts/background-context' export default function RootLayout({ children, @@ -15,22 +16,21 @@ export default function RootLayout({
- - - {children} - - + + +
{children}
+
+
) -} +} \ No newline at end of file diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index a7eb03c..4c6e7b1 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -3,6 +3,8 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { DeviceProvider } from "@/contexts/device-context"; import { NextIntlClientProvider } from 'next-intl'; +import config from "@/config"; +import { getUserLocales } from "@/i18n/request"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -15,17 +17,17 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: config.metadata.name, + description: config.metadata.description, }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( - + diff --git a/web/src/components/blog-home.tsx b/web/src/components/blog-home.tsx index 0ad6044..8202a12 100644 --- a/web/src/components/blog-home.tsx +++ b/web/src/components/blog-home.tsx @@ -9,10 +9,11 @@ import type { Label } from "@/models/label"; import type { Post } from "@/models/post"; import { listPosts } from "@/api/post"; -import { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useStoredState } from '@/hooks/use-storage-state'; import { listLabels } from "@/api/label"; import { POST_SORT_TYPE } from "@/localstore"; +import Image from "next/image"; // 定义排序类型 type SortType = 'latest' | 'popular'; @@ -84,7 +85,7 @@ export default function BlogHome() { {/* 主内容区域 */}
{/* 容器 - 关键布局 */} -
+
{/* 主要内容区域 */}
diff --git a/web/src/components/blog-post.tsx b/web/src/components/blog-post.tsx index f183ba4..5201358 100644 --- a/web/src/components/blog-post.tsx +++ b/web/src/components/blog-post.tsx @@ -1,94 +1,99 @@ "use client"; import { useEffect } from "react"; -import Image from "next/image"; import type { Post } from "@/models/post"; +import { Calendar, Clock, FileText, Flame, Heart, MessageCircle, PenLine, SquarePen } from "lucide-react"; -function WaveHeader({ title }: { title: string }) { +function PostMeta({ post }: { post: Post }) { + // 假设 post 结构包含这些字段 + // post.author, post.wordCount, post.readMinutes, post.createdAt, post.isOriginal, post.viewCount, post.commentCount return ( -
- {/* 波浪SVG,半透明悬浮 */} -
- - - - - - - - - - - -
- {/* 标题 */} -

- {title} -

-
- ); -} - -function BlogMeta({ post }: { post: Post }) { - return ( -
-
- {post.labels?.map(label => ( - - {label.key} - - ))} -
-
- 发表于 {new Date(post.createdAt).toLocaleDateString()} - 👁️ {post.viewCount} - 💬 {post.commentCount} - 🔥 {post.heat} -
-
- ); -} - -function BlogContent({ post }: { post: Post }) { - return ( -
- {post.cover && ( - cover + console.log(post), +
+ {/* 作者 */} + + + {post.user.nickname || "未知作者"} + + {/* 字数 */} + + + {post.content.length || 0} + + {/* 阅读时间 */} + + + {post.content.length / 100 || 1} 分钟 + + {/* 发布时间 */} + + + {post.createdAt ? new Date(post.createdAt).toLocaleDateString("zh-CN") : ""} + + {/* 最后编辑时间,如果和发布时间不一样 */} + {post.updatedAt && post.createdAt !== post.updatedAt && ( + + + {new Date(post.updatedAt).toLocaleDateString("zh-CN")} + )} -
-
+ {/* 浏览数 */} + + + {post.viewCount || 0} + + {/* 点赞数 */} + + + {post.likeCount || 0} + + {/* 评论数 */} + + + {post.commentCount || 0} + + {/* 热度 */} + + + {post.heat || 0} + +
); } +function PostHeader({ post }: { post: Post }) { + // 三排 标签/标题/一些元数据 + return ( +
+ { + post.labels && post.labels.length > 0 && ( +
+ {post.labels.map(label => ( + + {label.key} + + ))} +
+ ) + } +

{post.title}

+ {/* 元数据区 */} +
+ +
+
+ ); +} + +function PostContent({ post }: { post: Post }) { + return ( +
+
+
+ ) +} + + function BlogPost({ post }: { post: Post }) { useEffect(() => { if (typeof window !== "undefined") { @@ -96,11 +101,10 @@ function BlogPost({ post }: { post: Post }) { } }, []); return ( -
- -
- - +
+ +
+
); diff --git a/web/src/components/navbar.tsx b/web/src/components/navbar.tsx index 56dac13..ce08a4b 100644 --- a/web/src/components/navbar.tsx +++ b/web/src/components/navbar.tsx @@ -44,8 +44,9 @@ const navbarMenuComponents = [ ] export function Navbar() { + const { navbarAdditionalClassName } = useDevice() return ( -