diff --git a/.gitignore b/.gitignore index 4c4631d..f1195b4 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,7 @@ tmp/ temp/ *.tmp *.temp +reference # Backup files *.bak diff --git a/docs/developer-guide.md b/docs/developer-guide.md index f14c3cf..712bc71 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -79,10 +79,8 @@ NovaBlog/ │ │ ├── FlipCard.tsx │ │ ├── ParticleBackground.tsx │ │ ├── TypewriterText.tsx -│ │ ├── Heatmap.tsx # 热力图组件 -│ │ ├── MicroList.tsx # 微语列表 -│ │ ├── MicroComposer.tsx # 发布微语 -│ │ └── MicroPage.tsx # 微语页面容器 +│ │ ├── MergeTable.tsx +│ │ └── MathFlipCard.tsx │ ├── content/ # 内容集合 │ │ ├── config.ts # 内容配置 │ │ └── blog/ # 博客文章 @@ -92,7 +90,6 @@ NovaBlog/ │ ├── pages/ # 页面路由 │ │ ├── index.astro # 首页 │ │ ├── login.astro # 登录页 -│ │ ├── micro.astro # 微语页 │ │ ├── blog/ # 博客相关页面 │ │ ├── tags/ # 标签页面 │ │ └── categories/ # 分类页面 @@ -111,8 +108,7 @@ NovaBlog/ │ │ ├── handlers/ # HTTP 处理器 │ │ │ ├── auth.go # 认证处理 │ │ │ ├── comment.go # 评论处理 -│ │ │ ├── like.go # 点赞处理 -│ │ │ └── micro.go # 微语处理 +│ │ │ └── like.go # 点赞处理 │ │ ├── middleware/ # 中间件 │ │ ├── models/ # 数据模型 │ │ └── utils/ # 工具函数 @@ -330,171 +326,6 @@ Authorization: Bearer // 可选 **响应**: 同切换接口 -### 微语接口 - -#### 获取微语列表 - -```http -GET /api/micros?page=1&page_size=20&user_id=1 -Authorization: Bearer // 可选 -``` - -**参数**: -- `page` (可选): 页码,默认 1 -- `page_size` (可选): 每页数量,默认 20,最大 50 -- `user_id` (可选): 指定用户的微语 - -**响应**: -```json -{ - "data": [ - { - "id": 1, - "content": "今天天气真好!", - "images": "[]", - "tags": "[\"生活\", \"日常\"]", - "is_public": true, - "created_at": "2024-01-15T10:00:00Z", - "updated_at": "2024-01-15T10:00:00Z", - "user": { - "id": 1, - "username": "testuser", - "nickname": "测试用户", - "avatar": "https://..." - }, - "like_count": 5, - "is_liked": false - } - ], - "pagination": { - "page": 1, - "page_size": 20, - "total": 100, - "total_page": 5 - } -} -``` - -#### 获取单条微语 - -```http -GET /api/micros/:id -Authorization: Bearer // 可选 -``` - -#### 发布微语 - -```http -POST /api/micros -Authorization: Bearer -Content-Type: application/json - -{ - "content": "string", // 必填,最多 2000 字 - "images": ["url1", "url2"], // 可选,图片 URL 数组 - "tags": ["tag1", "tag2"], // 可选,标签数组 - "is_public": true // 可选,默认 true -} -``` - -**响应**: -```json -{ - "id": 1, - "content": "今天天气真好!", - "images": "[]", - "tags": "[\"生活\"]", - "is_public": true, - "created_at": "2024-01-15T10:00:00Z", - "user": {...}, - "like_count": 0, - "is_liked": false -} -``` - -#### 更新微语 - -```http -PUT /api/micros/:id -Authorization: Bearer -Content-Type: application/json - -{ - "content": "string", - "images": ["url1"], - "tags": ["tag1"], - "is_public": true -} -``` - -**权限**: 仅作者可修改 - -#### 删除微语 - -```http -DELETE /api/micros/:id -Authorization: Bearer -``` - -**权限**: 作者或管理员可删除 - -#### 点赞/取消点赞微语 - -```http -POST /api/micros/:id/like -Authorization: Bearer -``` - -**响应**: -```json -{ - "liked": true, - "message": "点赞成功" -} -``` - -#### 获取热力图数据 - -```http -GET /api/micros/heatmap?year=2024&user_id=1 -``` - -**参数**: -- `year` (可选): 年份,默认当前年 -- `user_id` (可选): 指定用户 - -**响应**: -```json -[ - { "date": "2024-01-15", "count": 3 }, - { "date": "2024-01-16", "count": 1 }, - { "date": "2024-01-20", "count": 5 } -] -``` - -#### 获取统计数据 - -```http -GET /api/micros/stats?user_id=1 -``` - -**响应**: -```json -{ - "total_micros": 150, - "total_users": 25, - "top_users": [ - { - "user_id": 1, - "username": "admin", - "nickname": "管理员", - "avatar": "...", - "post_count": 45 - } - ] -} -``` - ### 错误响应格式 ```json @@ -597,46 +428,6 @@ CREATE TABLE post_meta ( CREATE INDEX idx_post_meta_post_id ON post_meta(post_id); ``` -### micro_posts 表 - -```sql -CREATE TABLE micro_posts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at DATETIME, - updated_at DATETIME, - deleted_at DATETIME, -- 软删除 - user_id INTEGER NOT NULL, -- 关联 users.id - content TEXT NOT NULL, -- 微语内容,最多 2000 字 - images TEXT, -- JSON 数组存储图片 URL - tags TEXT, -- JSON 数组存储标签 - is_public BOOLEAN DEFAULT 1, -- 是否公开 - - FOREIGN KEY (user_id) REFERENCES users(id) -); - -CREATE INDEX idx_micro_posts_user_id ON micro_posts(user_id); -CREATE INDEX idx_micro_posts_deleted_at ON micro_posts(deleted_at); -CREATE INDEX idx_micro_posts_created_at ON micro_posts(created_at); -``` - -### micro_post_likes 表 - -```sql -CREATE TABLE micro_post_likes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - created_at DATETIME, - micro_post_id INTEGER NOT NULL, -- 关联 micro_posts.id - user_id INTEGER NOT NULL, -- 关联 users.id - - FOREIGN KEY (micro_post_id) REFERENCES micro_posts(id), - FOREIGN KEY (user_id) REFERENCES users(id) -); - --- 防止同一用户重复点赞同一条微语 -CREATE UNIQUE INDEX idx_micropost_user ON micro_post_likes(micro_post_id, user_id); -CREATE INDEX idx_micropost_likes_user_id ON micro_post_likes(user_id); -``` - --- ## 前端组件开发 diff --git a/docs/user-guide.md b/docs/user-guide.md index e176612..a260a8c 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -12,11 +12,10 @@ 4. [React 动效组件](#react-动效组件) 5. [动效 HTML 块](#动效-html-块) 6. [评论系统](#评论系统) -7. [微语功能](#微语功能) -8. [用户注册与登录](#用户注册与登录) -9. [主题定制](#主题定制) -10. [附件管理](#附件管理) -11. [常见问题](#常见问题) +7. [用户注册与登录](#用户注册与登录) +8. [主题定制](#主题定制) +9. [附件管理](#附件管理) +10. [常见问题](#常见问题) --- @@ -568,64 +567,6 @@ NovaBlog 内置评论系统,支持多级嵌套回复和 Markdown 语法。 --- -## 微语功能 - -微语是一个轻量级的分享空间,让你可以随时记录生活中的点滴、灵感与感悟。类似于社交媒体的动态功能,所有注册用户都可以发布。 - -### 访问微语 - -点击导航栏中的"微语"链接,或访问 `/micro` 页面。 - -### 发布微语 - -1. 登录你的账号 -2. 在微语页面顶部的输入框中输入内容 -3. 可选:添加标签(用逗号或空格分隔) -4. 选择是否公开可见 -5. 点击"发布"按钮 - -**内容限制**: -- 单条微语最多 2000 字 -- 支持多标签 - -### 微语列表 - -- 所有公开的微语都会显示在列表中 -- 支持分页加载更多 -- 显示发布者头像、昵称和发布时间 -- 支持点赞功能 - -### 热力图 - -微语页面右侧显示 GitHub 风格的热力图,展示一年内的发布活动: - -- **颜色深浅**:表示当天发布的微语数量 -- **悬停查看**:鼠标悬停可查看具体日期和数量 -- **年度统计**:显示全年发布的微语总数 - -### 点赞微语 - -1. 登录后可以给微语点赞 -2. 点击心形图标即可点赞或取消点赞 -3. 点赞数会实时更新 - -### 删除微语 - -- 用户可以删除自己发布的微语 -- 管理员可以删除任何微语 - -### 微语与博客的区别 - -| 特性 | 博客文章 | 微语 | -|------|----------|------| -| 内容长度 | 无限制 | 最多 2000 字 | -| 格式支持 | Markdown + MDX | 纯文本 | -| 发布权限 | 管理员 | 所有注册用户 | -| 适用场景 | 长篇教程、技术文章 | 随手记录、灵感分享 | -| 互动功能 | 评论 | 点赞 | - ---- - ## 用户注册与登录 ### 注册账号 diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go index 8e2945a..835b467 100644 --- a/server/cmd/server/main.go +++ b/server/cmd/server/main.go @@ -33,7 +33,6 @@ func main() { authHandler := handlers.NewAuthHandler(jwtManager) commentHandler := handlers.NewCommentHandler() likeHandler := handlers.NewLikeHandler() - microHandler := handlers.NewMicroHandler() // 创建路由 r := gin.New() @@ -55,7 +54,7 @@ func main() { // 健康检查 r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{ - "status": "ok", + "status": "ok", "message": "NovaBlog API is running", }) }) @@ -71,12 +70,6 @@ func main() { api.GET("/likes", likeHandler.GetLikeStatus) api.POST("/likes", likeHandler.ToggleLike) // 允许访客点赞(基于 IP Hash) - // 微语公开接口 - api.GET("/micros", microHandler.GetMicros) - api.GET("/micros/stats", microHandler.GetStats) - api.GET("/micros/heatmap", microHandler.GetHeatmap) - api.GET("/micros/:id", microHandler.GetMicro) - // 需要认证的接口 authGroup := api.Group("") authGroup.Use(middleware.AuthMiddleware(jwtManager)) @@ -88,12 +81,6 @@ func main() { // 评论相关(需要登录才能评论) authGroup.POST("/comments", commentHandler.CreateComment) authGroup.DELETE("/comments/:id", commentHandler.DeleteComment) - - // 微语相关(需要登录) - authGroup.POST("/micros", microHandler.CreateMicro) - authGroup.PUT("/micros/:id", microHandler.UpdateMicro) - authGroup.DELETE("/micros/:id", microHandler.DeleteMicro) - authGroup.POST("/micros/:id/like", microHandler.ToggleLike) } // 管理员接口 @@ -115,4 +102,4 @@ func main() { if err := r.Run(":" + port); err != nil { log.Fatalf("Failed to start server: %v", err) } -} \ No newline at end of file +} diff --git a/server/internal/database/database.go b/server/internal/database/database.go index 0491d04..c7da001 100644 --- a/server/internal/database/database.go +++ b/server/internal/database/database.go @@ -54,8 +54,6 @@ func autoMigrate() error { &models.Like{}, &models.LikeCount{}, &models.PostMeta{}, - &models.MicroPost{}, - &models.MicroPostLike{}, ) } diff --git a/server/internal/handlers/micro.go b/server/internal/handlers/micro.go deleted file mode 100644 index 7832a40..0000000 --- a/server/internal/handlers/micro.go +++ /dev/null @@ -1,411 +0,0 @@ -package handlers - -import ( - "encoding/json" - "net/http" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/novablog/server/internal/database" - "github.com/novablog/server/internal/middleware" - "github.com/novablog/server/internal/models" - "gorm.io/gorm" -) - -func mustMarshal(v interface{}) []byte { - data, _ := json.Marshal(v) - return data -} - -type MicroHandler struct{} - -func NewMicroHandler() *MicroHandler { - return &MicroHandler{} -} - -type CreateMicroRequest struct { - Content string `json:"content" binding:"required,max=2000"` - Images []string `json:"images"` - Tags []string `json:"tags"` - IsPublic bool `json:"is_public"` -} - -type UpdateMicroRequest struct { - Content string `json:"content" binding:"required,max=2000"` - Images []string `json:"images"` - Tags []string `json:"tags"` - IsPublic bool `json:"is_public"` -} - -type MicroResponse struct { - models.MicroPost - LikeCount int `json:"like_count"` - IsLiked bool `json:"is_liked"` -} - -func (h *MicroHandler) CreateMicro(c *gin.Context) { - userID, ok := middleware.GetUserID(c) - if !ok || userID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"error": "请登录后再发布"}) - return - } - - var req CreateMicroRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - imagesJSON := "[]" - if len(req.Images) > 0 { - imagesJSON = string(mustMarshal(req.Images)) - } - - tagsJSON := "[]" - if len(req.Tags) > 0 { - tagsJSON = string(mustMarshal(req.Tags)) - } - - micro := models.MicroPost{ - UserID: userID, - Content: req.Content, - Images: imagesJSON, - Tags: tagsJSON, - IsPublic: req.IsPublic, - } - - if err := database.DB.Create(µ).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "发布失败"}) - return - } - - database.DB.Preload("User").First(µ, micro.ID) - - c.JSON(http.StatusCreated, MicroResponse{ - MicroPost: micro, - LikeCount: 0, - IsLiked: false, - }) -} - -func (h *MicroHandler) GetMicros(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - userIDQuery := c.Query("user_id") - - if page < 1 { - page = 1 - } - if pageSize < 1 || pageSize > 50 { - pageSize = 20 - } - - var micros []models.MicroPost - var total int64 - - query := database.DB.Model(&models.MicroPost{}).Where("is_public = ?", true) - - if userIDQuery != "" { - query = query.Where("user_id = ?", userIDQuery) - } - - query.Count(&total) - - if err := query. - Preload("User"). - Order("created_at DESC"). - Offset((page - 1) * pageSize). - Limit(pageSize). - Find(µs).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) - return - } - - currentUserID, _ := middleware.GetUserID(c) - - responses := make([]MicroResponse, len(micros)) - for i, micro := range micros { - var likeCount int64 - database.DB.Model(&models.MicroPostLike{}).Where("micro_post_id = ?", micro.ID).Count(&likeCount) - - isLiked := false - if currentUserID > 0 { - var count int64 - database.DB.Model(&models.MicroPostLike{}). - Where("micro_post_id = ? AND user_id = ?", micro.ID, currentUserID). - Count(&count) - isLiked = count > 0 - } - - responses[i] = MicroResponse{ - MicroPost: micro, - LikeCount: int(likeCount), - IsLiked: isLiked, - } - } - - c.JSON(http.StatusOK, gin.H{ - "data": responses, - "pagination": gin.H{ - "page": page, - "page_size": pageSize, - "total": total, - "total_page": (total + int64(pageSize) - 1) / int64(pageSize), - }, - }) -} - -func (h *MicroHandler) GetMicro(c *gin.Context) { - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) - return - } - - var micro models.MicroPost - if err := database.DB.Preload("User").First(µ, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) - return - } - - var likeCount int64 - database.DB.Model(&models.MicroPostLike{}).Where("micro_post_id = ?", micro.ID).Count(&likeCount) - - currentUserID, _ := middleware.GetUserID(c) - isLiked := false - if currentUserID > 0 { - var count int64 - database.DB.Model(&models.MicroPostLike{}). - Where("micro_post_id = ? AND user_id = ?", micro.ID, currentUserID). - Count(&count) - isLiked = count > 0 - } - - c.JSON(http.StatusOK, MicroResponse{ - MicroPost: micro, - LikeCount: int(likeCount), - IsLiked: isLiked, - }) -} - -func (h *MicroHandler) UpdateMicro(c *gin.Context) { - userID, _ := middleware.GetUserID(c) - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) - return - } - - var micro models.MicroPost - if err := database.DB.First(µ, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) - return - } - - if micro.UserID != userID { - c.JSON(http.StatusForbidden, gin.H{"error": "无权修改"}) - return - } - - var req UpdateMicroRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - imagesJSON := "[]" - if len(req.Images) > 0 { - imagesJSON = string(mustMarshal(req.Images)) - } - - tagsJSON := "[]" - if len(req.Tags) > 0 { - tagsJSON = string(mustMarshal(req.Tags)) - } - - updates := map[string]interface{}{ - "content": req.Content, - "images": imagesJSON, - "tags": tagsJSON, - "is_public": req.IsPublic, - } - - if err := database.DB.Model(µ).Updates(updates).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"}) - return - } - - database.DB.Preload("User").First(µ, micro.ID) - - c.JSON(http.StatusOK, micro) -} - -func (h *MicroHandler) DeleteMicro(c *gin.Context) { - userID, _ := middleware.GetUserID(c) - role, _ := c.Get("role") - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) - return - } - - var micro models.MicroPost - if err := database.DB.First(µ, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) - return - } - - if micro.UserID != userID && role != "admin" { - c.JSON(http.StatusForbidden, gin.H{"error": "无权删除"}) - return - } - - if err := database.DB.Delete(µ).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"}) - return - } - - c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) -} - -func (h *MicroHandler) ToggleLike(c *gin.Context) { - userID, ok := middleware.GetUserID(c) - if !ok || userID == 0 { - c.JSON(http.StatusUnauthorized, gin.H{"error": "请登录后再点赞"}) - return - } - - idStr := c.Param("id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) - return - } - - var micro models.MicroPost - if err := database.DB.First(µ, id).Error; err != nil { - if err == gorm.ErrRecordNotFound { - c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) - return - } - c.JSON(http.StatusInternalServerError, gin.H{"error": "database error"}) - return - } - - var existingLike models.MicroPostLike - result := database.DB.Where("micro_post_id = ? AND user_id = ?", id, userID).First(&existingLike) - - if result.Error == nil { - database.DB.Delete(&existingLike) - c.JSON(http.StatusOK, gin.H{"liked": false, "message": "取消点赞"}) - return - } - - like := models.MicroPostLike{ - MicroPostID: uint(id), - UserID: userID, - } - - if err := database.DB.Create(&like).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "点赞失败"}) - return - } - - c.JSON(http.StatusOK, gin.H{"liked": true, "message": "点赞成功"}) -} - -func (h *MicroHandler) GetHeatmap(c *gin.Context) { - userIDQuery := c.Query("user_id") - yearStr := c.DefaultQuery("year", strconv.Itoa(time.Now().Year())) - year, _ := strconv.Atoi(yearStr) - - query := database.DB.Model(&models.MicroPost{}). - Select("DATE(created_at) as date, COUNT(*) as count"). - Where("is_public = ?", true). - Group("DATE(created_at)") - - if userIDQuery != "" { - query = query.Where("user_id = ?", userIDQuery) - } - - if year > 0 { - startDate := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) - endDate := time.Date(year+1, 1, 1, 0, 0, 0, 0, time.UTC) - query = query.Where("created_at >= ? AND created_at < ?", startDate, endDate) - } - - var results []struct { - Date string `json:"date"` - Count int `json:"count"` - } - - if err := query.Scan(&results).Error; err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "获取失败"}) - return - } - - c.JSON(http.StatusOK, results) -} - -func (h *MicroHandler) GetStats(c *gin.Context) { - userIDQuery := c.Query("user_id") - - var totalMicros int64 - var totalUsers int64 - - query := database.DB.Model(&models.MicroPost{}).Where("is_public = ?", true) - if userIDQuery != "" { - query = query.Where("user_id = ?", userIDQuery) - } - query.Count(&totalMicros) - - database.DB.Model(&models.User{}).Count(&totalUsers) - - var topUsers []struct { - UserID uint `json:"user_id"` - Username string `json:"username"` - Nickname string `json:"nickname"` - Avatar string `json:"avatar"` - PostCount int `json:"post_count"` - } - - database.DB.Model(&models.MicroPost{}). - Select("user_id, COUNT(*) as post_count"). - Where("is_public = ?", true). - Group("user_id"). - Order("post_count DESC"). - Limit(10). - Scan(&topUsers) - - for i := range topUsers { - var user models.User - if err := database.DB.First(&user, topUsers[i].UserID).Error; err == nil { - topUsers[i].Username = user.Username - topUsers[i].Nickname = user.Nickname - topUsers[i].Avatar = user.Avatar - } - } - - c.JSON(http.StatusOK, gin.H{ - "total_micros": totalMicros, - "total_users": totalUsers, - "top_users": topUsers, - }) -} diff --git a/server/internal/models/models.go b/server/internal/models/models.go index 1ddd513..87829e1 100644 --- a/server/internal/models/models.go +++ b/server/internal/models/models.go @@ -55,32 +55,10 @@ type LikeCount struct { // PostMeta 文章元数据(可选,用于存储文章额外信息) type PostMeta struct { - ID uint `json:"id" gorm:"primaryKey"` - PostID string `json:"post_id" gorm:"uniqueIndex;size:100;not null"` - ViewCount int `json:"view_count" gorm:"default:0"` - LikeCount int `json:"like_count" gorm:"default:0"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `json:"id" gorm:"primaryKey"` + PostID string `json:"post_id" gorm:"uniqueIndex;size:100;not null"` + ViewCount int `json:"view_count" gorm:"default:0"` + LikeCount int `json:"like_count" gorm:"default:0"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } - -// MicroPost 微语模型 -type MicroPost struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` - UserID uint `json:"user_id" gorm:"index;not null"` - Content string `json:"content" gorm:"type:text;not null"` - Images string `json:"images" gorm:"type:text"` // JSON 数组存储图片 URL - Tags string `json:"tags" gorm:"type:text"` // JSON 数组存储标签 - IsPublic bool `json:"is_public" gorm:"default:true"` // 是否公开 - User User `json:"user" gorm:"foreignKey:UserID"` -} - -// MicroPostLike 微语点赞 -type MicroPostLike struct { - ID uint `json:"id" gorm:"primaryKey"` - CreatedAt time.Time `json:"created_at"` - MicroPostID uint `json:"micro_post_id" gorm:"uniqueIndex:idx_micropost_user;not null"` - UserID uint `json:"user_id" gorm:"uniqueIndex:idx_micropost_user;not null"` -} \ No newline at end of file diff --git a/src/components/react/Heatmap.tsx b/src/components/react/Heatmap.tsx deleted file mode 100644 index ff92147..0000000 --- a/src/components/react/Heatmap.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { useState, useEffect } from 'react'; - -interface HeatmapData { - date: string; - count: number; -} - -interface HeatmapProps { - userId?: string; - year?: number; - apiBaseUrl?: string; -} - -const API_BASE = typeof window !== 'undefined' - ? (import.meta.env.VITE_API_BASE || 'http://localhost:8080/api') - : 'http://localhost:8080/api'; - -const COLORS = [ - 'bg-primary-100 dark:bg-primary-900/30', - 'bg-primary-200 dark:bg-primary-800/40', - 'bg-primary-300 dark:bg-primary-700/50', - 'bg-primary-400 dark:bg-primary-600/60', - 'bg-primary-500 dark:bg-primary-500/70', -]; - -const MONTHS = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']; -const DAYS = ['日', '一', '二', '三', '四', '五', '六']; - -export default function Heatmap({ userId, year = new Date().getFullYear(), apiBaseUrl }: HeatmapProps) { - const baseUrl = apiBaseUrl || API_BASE; - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [hoveredCell, setHoveredCell] = useState<{ date: string; count: number } | null>(null); - - useEffect(() => { - const fetchHeatmap = async () => { - try { - const params = new URLSearchParams({ year: year.toString() }); - if (userId) params.append('user_id', userId); - - const response = await fetch(`${baseUrl}/micros/heatmap?${params}`); - if (response.ok) { - const result = await response.json(); - setData(result); - } - } catch (error) { - console.error('Failed to fetch heatmap:', error); - } finally { - setLoading(false); - } - }; - - fetchHeatmap(); - }, [userId, year, baseUrl]); - - const getDaysInYear = (year: number) => { - const days: Date[] = []; - const startDate = new Date(year, 0, 1); - const endDate = new Date(year, 11, 31); - - for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { - days.push(new Date(d)); - } - return days; - }; - - const getCountForDate = (date: Date): number => { - const dateStr = date.toISOString().split('T')[0]; - const item = data.find(d => d.date === dateStr); - return item ? item.count : 0; - }; - - const getColorClass = (count: number): string => { - if (count === 0) return 'bg-muted dark:bg-muted/50'; - if (count <= 2) return COLORS[0]; - if (count <= 4) return COLORS[1]; - if (count <= 6) return COLORS[2]; - if (count <= 8) return COLORS[3]; - return COLORS[4]; - }; - - const formatDisplayDate = (date: Date): string => { - return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`; - }; - - const getWeeksInYear = (year: number) => { - const days = getDaysInYear(year); - const weeks: Date[][] = []; - let currentWeek: Date[] = []; - - const firstDay = days[0]; - const firstDayOfWeek = firstDay.getDay(); - for (let i = 0; i < firstDayOfWeek; i++) { - currentWeek.push(new Date(year, 0, 1 - firstDayOfWeek + i)); - } - - days.forEach(day => { - if (day.getDay() === 0 && currentWeek.length > 0) { - weeks.push(currentWeek); - currentWeek = []; - } - currentWeek.push(day); - }); - - if (currentWeek.length > 0) { - weeks.push(currentWeek); - } - - return weeks; - }; - - const weeks = getWeeksInYear(year); - const totalCount = data.reduce((sum, item) => sum + item.count, 0); - - if (loading) { - return ( -
-
-
- ); - } - - return ( -
-
-
-

{year} 年微语热力图

- 共 {totalCount} 条 -
-
- - {COLORS.map((color, i) => ( -
- ))} - -
-
- -
-
-
- {DAYS.map((day, i) => ( -
{i % 2 === 1 ? day : ''}
- ))} -
- - {weeks.map((week, weekIndex) => ( -
- {DAYS.map((_, dayIndex) => { - const day = week[dayIndex]; - if (!day || day.getFullYear() !== year) { - return
; - } - - const count = getCountForDate(day); - const colorClass = getColorClass(count); - - return ( -
setHoveredCell({ date: formatDisplayDate(day), count })} - onMouseLeave={() => setHoveredCell(null)} - /> - ); - })} -
- ))} -
-
- - {hoveredCell && ( -
- {hoveredCell.date}:{hoveredCell.count} 条微语 -
- )} - -
- {MONTHS.map((month, i) => ( - {month} - ))} -
-
- ); -} diff --git a/src/components/react/MicroComposer.tsx b/src/components/react/MicroComposer.tsx deleted file mode 100644 index cf3968b..0000000 --- a/src/components/react/MicroComposer.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useState } from 'react'; - -interface MicroComposerProps { - onClose?: () => void; - onSuccess?: () => void; - apiBaseUrl?: string; -} - -const API_BASE = typeof window !== 'undefined' - ? (import.meta.env.VITE_API_BASE || 'http://localhost:8080/api') - : 'http://localhost:8080/api'; - -export default function MicroComposer({ onClose, onSuccess, apiBaseUrl }: MicroComposerProps) { - const baseUrl = apiBaseUrl || API_BASE; - const [content, setContent] = useState(''); - const [tags, setTags] = useState(''); - const [isPublic, setIsPublic] = useState(true); - const [submitting, setSubmitting] = useState(false); - - const handleSubmit = async () => { - if (!content.trim()) { - alert('请输入内容'); - return; - } - - const token = localStorage.getItem('token'); - if (!token) { - alert('请登录后再发布'); - return; - } - - setSubmitting(true); - try { - const tagList = tags - .split(/[,,\s]+/) - .map(t => t.trim()) - .filter(t => t.length > 0); - - const response = await fetch(`${baseUrl}/micros`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ - content: content.trim(), - tags: tagList, - is_public: isPublic, - images: [], - }), - }); - - if (response.ok) { - setContent(''); - setTags(''); - setIsPublic(true); - onSuccess?.(); - onClose?.(); - } else { - const error = await response.json(); - alert(error.error || '发布失败'); - } - } catch (error) { - console.error('Failed to post:', error); - alert('发布失败,请重试'); - } finally { - setSubmitting(false); - } - }; - - const remainingChars = 2000 - content.length; - - return ( -
-
-