diff --git a/backend/dto/ActivityLogResponse.go b/backend/dto/ActivityLogResponse.go index 2f83785..3a95fb3 100644 --- a/backend/dto/ActivityLogResponse.go +++ b/backend/dto/ActivityLogResponse.go @@ -7,6 +7,8 @@ type ActivityLogResponse struct { ID uint `json:"id"` UserID uint `json:"user_id"` Username string `json:"username"` + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` ActionType string `json:"action_type"` EntityType string `json:"entity_type"` EntityID uint `json:"entity_id"` diff --git a/backend/handlers/activity/board_activities.go b/backend/handlers/activity/board_activities.go index b6f7643..be7b1ac 100644 --- a/backend/handlers/activity/board_activities.go +++ b/backend/handlers/activity/board_activities.go @@ -85,6 +85,7 @@ func (h *BoardActivitiesHandler) GetBoardActivities(c *gin.Context) { // 转换为响应格式 activities := make([]dto.ActivityLogResponse, len(logs)) for i, log := range logs { + // log.User.Nickname 和 log.User.Avatar 应该有值 activities[i] = convertToActivityLogResponse(log) } @@ -107,6 +108,8 @@ func convertToActivityLogResponse(log models.ActivityLog) dto.ActivityLogRespons ID: log.ID, UserID: log.UserID, Username: log.Username, + Nickname: log.User.Nickname, + Avatar: log.User.Avatar, ActionType: log.ActionType, EntityType: log.EntityType, EntityID: log.EntityID, diff --git a/backend/handlers/activity/task_activities.go b/backend/handlers/activity/task_activities.go index 7e2fc1d..c04a75b 100644 --- a/backend/handlers/activity/task_activities.go +++ b/backend/handlers/activity/task_activities.go @@ -84,6 +84,7 @@ func (h *TaskActivitiesHandler) GetTaskActivities(c *gin.Context) { // 转换为响应格式 activities := make([]dto.ActivityLogResponse, len(logs)) for i, log := range logs { + // log.User.Nickname 和 log.User.Avatar 应该有值 activities[i] = convertToActivityLogResponse(log) } diff --git a/backend/handlers/task/task.go b/backend/handlers/task/task.go index d7bb8d0..07bda94 100644 --- a/backend/handlers/task/task.go +++ b/backend/handlers/task/task.go @@ -227,7 +227,10 @@ func (h *TaskHandler) MoveTask(c *gin.Context) { return } - if err := h.taskService.MoveTask(uint(taskID), moveTaskRequest.NewColumnID, moveTaskRequest.NewOrder); err != nil { + userID := c.GetUint("user_id") + username := c.GetString("username") // 假设中间件中设置了username + + if err := h.taskService.MoveTask(uint(taskID), moveTaskRequest.NewColumnID, moveTaskRequest.NewOrder, userID, username); err != nil { if err == services.ErrTaskNotFound { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return diff --git a/backend/handlers/user/profile.go b/backend/handlers/user/profile.go index 35c5bb8..dd8c97d 100644 --- a/backend/handlers/user/profile.go +++ b/backend/handlers/user/profile.go @@ -1,7 +1,11 @@ package user import ( + "fmt" "net/http" + "path/filepath" + "strings" + "time" "progress-wall-backend/services" @@ -47,3 +51,117 @@ func (h *ProfileHandler) GetProfile(c *gin.Context) { "user": user, }) } + +// UploadAvatar 上传头像 +func (h *ProfileHandler) UploadAvatar(c *gin.Context) { + userID := c.GetUint("user_id") + if userID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无法获取用户信息"}) + return + } + + // 获取上传的文件 + file, err := c.FormFile("avatar") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的文件"}) + return + } + + // 检查文件大小 (例如限制为 2MB) + if file.Size > 2*1024*1024 { + c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小不能超过2MB"}) + return + } + + // 检查文件类型 + ext := strings.ToLower(filepath.Ext(file.Filename)) + if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" { + c.JSON(http.StatusBadRequest, gin.H{"error": "只支持 JPG, PNG, GIF 格式的图片"}) + return + } + + // 生成唯一文件名 + filename := fmt.Sprintf("%d_%d%s", userID, time.Now().UnixNano(), ext) + // 确保目录存在 + savePath := filepath.Join("uploads", "avatars", filename) + + // 保存文件 + // c.SaveUploadedFile 会自动打开和关闭文件流,无需手动处理 + if err := c.SaveUploadedFile(file, savePath); err != nil { + // 记录具体错误日志以便排查 + fmt.Printf("File save error: %v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "文件保存失败,请检查服务器存储权限"}) + return + } + + // 生成访问 URL + // 注意:这里假设静态资源通过 /uploads 路径访问 + avatarURL := "/uploads/avatars/" + filename + + // 更新用户信息 + user, err := h.userService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户信息失败"}) + return + } + + user.Avatar = avatarURL + if err := h.userService.UpdateUser(user); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新头像信息失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "上传成功", + "url": avatarURL, + }) +} + +// UpdateProfileRequest 更新用户信息请求结构 +type UpdateProfileRequest struct { + Nickname string `json:"nickname"` + Email string `json:"email"` + Phone string `json:"phone"` +} + +// UpdateProfile 更新用户信息 +func (h *ProfileHandler) UpdateProfile(c *gin.Context) { + userID := c.GetUint("user_id") + if userID == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "无法获取用户信息"}) + return + } + + var req UpdateProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"}) + return + } + + user, err := h.userService.GetUserByID(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户信息失败"}) + return + } + + // 更新字段 + if req.Nickname != "" { + user.Nickname = req.Nickname + } + // Email 更新可能需要验证唯一性等逻辑,这里暂时简化 + if req.Email != "" { + user.Email = req.Email + } + if req.Phone != "" { + user.Phone = req.Phone + } + + if err := h.userService.UpdateUser(user); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户信息失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "user": user, + }) +} diff --git a/backend/routes/routes.go b/backend/routes/routes.go index 0fc5250..0fe7d51 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go @@ -46,6 +46,9 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { r.Use(cors.New(corsConfig)) + // 静态文件服务 + r.Static("/uploads", "./uploads") + permService := services.NewPermissionService(db) rbac := middleware.NewRBACMiddleware(permService, db) @@ -77,6 +80,8 @@ func SetupRoutes(db *gorm.DB, cfg *config.Config) *gin.Engine { { // 用户相关 protected.GET("/user/profile", profileHandler.GetProfile) + protected.PUT("/user/profile", profileHandler.UpdateProfile) + protected.POST("/user/avatar", profileHandler.UploadAvatar) // Team Routes protected.POST("/teams", teamHandler.CreateTeam) diff --git a/backend/services/task_service.go b/backend/services/task_service.go index b504aed..e5adfd3 100644 --- a/backend/services/task_service.go +++ b/backend/services/task_service.go @@ -96,8 +96,10 @@ func (s *TaskService) DeleteTask(taskID uint) error { } // MoveTask 移动任务到新列和新位置 -func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) error { +func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int, userId uint, userName string) error { tx := s.db.Begin() + defer tx.Rollback() + defer func() { if r := recover(); r != nil { tx.Rollback() @@ -106,16 +108,30 @@ func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) erro // 获取任务 var task models.Task - if err := tx.First(&task, taskID).Error; err != nil { - tx.Rollback() + if err := tx.Preload("Column").First(&task, taskID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrTaskNotFound } return fmt.Errorf("查询任务失败: %v", err) } + // 获取看板ID(通过Column) + var column models.Column + if err := tx.First(&column, task.ColumnID).Error; err != nil { + return fmt.Errorf("查询列失败: %v", err) + } + boardID := column.BoardID + oldColumnID := task.ColumnID oldPosition := task.Position + oldColumnName := task.Column.Name + + // 获取新列名称 + var newColumn models.Column + if err := tx.First(&newColumn, newColumnID).Error; err != nil { + return fmt.Errorf("查询新列失败: %v", err) + } + newColumnName := newColumn.Name // 如果移动到不同列,需要更新两个列中的任务位置 if oldColumnID != newColumnID { @@ -123,7 +139,6 @@ func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) erro if err := tx.Model(&models.Task{}). Where("column_id = ? AND position > ?", oldColumnID, oldPosition). Update("position", gorm.Expr("position - 1")).Error; err != nil { - tx.Rollback() return fmt.Errorf("更新旧列任务位置失败: %v", err) } @@ -131,19 +146,34 @@ func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) erro if err := tx.Model(&models.Task{}). Where("column_id = ? AND position >= ?", newColumnID, newOrder). Update("position", gorm.Expr("position + 1")).Error; err != nil { - tx.Rollback() return fmt.Errorf("更新新列任务位置失败: %v", err) } // 更新任务的列ID和位置 - if err := tx.Model(&task). + if err := tx.Model(&models.Task{}).Where("id = ?", task.ID). Updates(map[string]interface{}{ "column_id": newColumnID, "position": newOrder, }).Error; err != nil { - tx.Rollback() return fmt.Errorf("更新任务位置失败: %v", err) } + + // 记录跨列移动日志 + log := models.ActivityLog{ + UserID: userId, + Username: userName, + ActionType: models.ActionMove, + EntityType: models.EntityTask, + EntityID: task.ID, + BoardID: &boardID, + TaskID: &task.ID, + ProjectID: &task.ProjectID, + Description: fmt.Sprintf("moved this task from \"%s\" to \"%s\"", oldColumnName, newColumnName), + } + if err := s.createActivityLog(tx, &log); err != nil { + return fmt.Errorf("创建活动日志失败: %v", err) + } + } else { // 同一列内移动 if oldPosition < newOrder { @@ -151,7 +181,6 @@ func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) erro if err := tx.Model(&models.Task{}). Where("column_id = ? AND position > ? AND position <= ?", newColumnID, oldPosition, newOrder). Update("position", gorm.Expr("position - 1")).Error; err != nil { - tx.Rollback() return fmt.Errorf("更新任务位置失败: %v", err) } } else if oldPosition > newOrder { @@ -159,16 +188,16 @@ func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) erro if err := tx.Model(&models.Task{}). Where("column_id = ? AND position >= ? AND position < ?", newColumnID, newOrder, oldPosition). Update("position", gorm.Expr("position + 1")).Error; err != nil { - tx.Rollback() return fmt.Errorf("更新任务位置失败: %v", err) } } // 更新任务位置 - if err := tx.Model(&task).Update("position", newOrder).Error; err != nil { - tx.Rollback() + if err := tx.Model(&models.Task{}).Where("id = ?", task.ID).Update("position", newOrder).Error; err != nil { return fmt.Errorf("更新任务位置失败: %v", err) } + + // 同列移动暂不记录日志 } // 提交事务并验证 @@ -177,3 +206,8 @@ func (s *TaskService) MoveTask(taskID uint, newColumnID uint, newOrder int) erro } return nil } + +// createActivityLog 创建活动日志的内部辅助方法 +func (s *TaskService) createActivityLog(tx *gorm.DB, log *models.ActivityLog) error { + return tx.Create(log).Error +} diff --git a/backend/services/user_service.go b/backend/services/user_service.go index 077cee6..237755a 100644 --- a/backend/services/user_service.go +++ b/backend/services/user_service.go @@ -36,3 +36,21 @@ func (s *UserService) GetUserByID(userID uint) (*models.User, error) { return &user, nil } + +// UpdateUser 更新用户信息 +// 参数 user: 包含更新信息的User对象,必须包含ID +// 返回 error: 更新失败时返回错误 +func (s *UserService) UpdateUser(user *models.User) error { + if user == nil { + return errors.New("用户信息不能为空") + } + if user.ID == 0 { + return errors.New("用户ID不能为空") + } + + result := s.db.Save(user) + if result.Error != nil { + return errors.New("更新用户失败") + } + return nil +} \ No newline at end of file diff --git a/backend/uploads/avatars/3_1764234613917144000.jpg b/backend/uploads/avatars/3_1764234613917144000.jpg new file mode 100644 index 0000000..1be6a27 Binary files /dev/null and b/backend/uploads/avatars/3_1764234613917144000.jpg differ diff --git a/docs/database/full_er_diagram.drawio b/docs/database/full_er_diagram.drawio new file mode 100644 index 0000000..35c18f0 --- /dev/null +++ b/docs/database/full_er_diagram.drawio @@ -0,0 +1,424 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/design/.$figure4_figure5_diagrams.drawio.bkp b/docs/design/.$figure4_figure5_diagrams.drawio.bkp new file mode 100644 index 0000000..f595fe2 --- /dev/null +++ b/docs/design/.$figure4_figure5_diagrams.drawio.bkp @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/design/.$sequence_diagram_move_task.drawio.bkp b/docs/design/.$sequence_diagram_move_task.drawio.bkp new file mode 100644 index 0000000..2dc5ccf --- /dev/null +++ b/docs/design/.$sequence_diagram_move_task.drawio.bkp @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/design/figure4_figure5_diagrams.drawio b/docs/design/figure4_figure5_diagrams.drawio new file mode 100644 index 0000000..ce7580a --- /dev/null +++ b/docs/design/figure4_figure5_diagrams.drawio @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/design/sequence_diagram_move_task.drawio b/docs/design/sequence_diagram_move_task.drawio new file mode 100644 index 0000000..135f840 --- /dev/null +++ b/docs/design/sequence_diagram_move_task.drawio @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/design/use_case_diagram.drawio b/docs/design/use_case_diagram.drawio new file mode 100644 index 0000000..4030d04 --- /dev/null +++ b/docs/design/use_case_diagram.drawio @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/package.json b/frontend/package.json index 30be192..6c60565 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,11 +9,14 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/typography": "^0.5.19", "@vueuse/core": "14.0.0-alpha.0", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dompurify": "^3.3.0", "lucide-vue-next": "^0.544.0", + "marked": "^17.0.1", "pinia": "^3.0.3", "tailwind-merge": "^3.3.1", "vue": "^3.5.21", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index f709d39..e2882bd 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@3.4.17) '@vueuse/core': specifier: 14.0.0-alpha.0 version: 14.0.0-alpha.0(vue@3.5.22(typescript@5.8.3)) @@ -20,9 +23,15 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dompurify: + specifier: ^3.3.0 + version: 3.3.0 lucide-vue-next: specifier: ^0.544.0 version: 0.544.0(vue@3.5.22(typescript@5.8.3)) + marked: + specifier: ^17.0.1 + version: 17.0.1 pinia: specifier: ^3.0.3 version: 3.0.3(typescript@5.8.3)(vue@3.5.22(typescript@5.8.3)) @@ -295,56 +304,67 @@ packages: resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.3': resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.3': resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.3': resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.3': resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.3': resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.3': resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.3': resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.3': resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.3': resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.3': resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.3': resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} @@ -371,6 +391,11 @@ packages: cpu: [x64] os: [win32] + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -380,6 +405,9 @@ packages: '@types/sortablejs@1.15.8': resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -609,6 +637,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dompurify@3.3.0: + resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -795,6 +826,11 @@ packages: magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + marked@17.0.1: + resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -934,6 +970,10 @@ packages: peerDependencies: postcss: ^8.2.14 + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -1336,6 +1376,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.52.3': optional: true + '@tailwindcss/typography@0.5.19(tailwindcss@3.4.17)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 3.4.17 + '@types/estree@1.0.8': {} '@types/node@24.5.2': @@ -1344,6 +1389,9 @@ snapshots: '@types/sortablejs@1.15.8': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/web-bluetooth@0.0.21': {} '@vitejs/plugin-vue@5.2.4(vite@5.4.20(@types/node@24.5.2))(vue@3.5.22(typescript@5.8.3))': @@ -1599,6 +1647,10 @@ snapshots: dlv@1.1.3: {} + dompurify@3.3.0: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1793,6 +1845,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@17.0.1: {} + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -1892,6 +1946,11 @@ snapshots: postcss: 8.5.6 postcss-selector-parser: 6.1.2 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 diff --git a/frontend/src/components/features/TaskCard.vue b/frontend/src/components/features/TaskCard.vue index 30befa0..a295670 100644 --- a/frontend/src/components/features/TaskCard.vue +++ b/frontend/src/components/features/TaskCard.vue @@ -17,10 +17,6 @@ -

- {{ task.description }} -

-
+
+ + + +
+
+
+
+
+
+ + + + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/components/features/task/TaskActivityLog.vue b/frontend/src/components/features/task/TaskActivityLog.vue new file mode 100644 index 0000000..e0816ae --- /dev/null +++ b/frontend/src/components/features/task/TaskActivityLog.vue @@ -0,0 +1,80 @@ + + + diff --git a/frontend/src/components/features/task/TaskDetailContent.vue b/frontend/src/components/features/task/TaskDetailContent.vue new file mode 100644 index 0000000..2f6844b --- /dev/null +++ b/frontend/src/components/features/task/TaskDetailContent.vue @@ -0,0 +1,115 @@ + + + diff --git a/frontend/src/components/features/task/TaskDetailHeader.vue b/frontend/src/components/features/task/TaskDetailHeader.vue new file mode 100644 index 0000000..824a09b --- /dev/null +++ b/frontend/src/components/features/task/TaskDetailHeader.vue @@ -0,0 +1,58 @@ + + + diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index da30b7e..b0daafb 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -19,13 +19,7 @@ > 首页 - - 看板列表 - + @@ -36,12 +30,12 @@ @@ -69,6 +76,7 @@ import Button from '@/components/ui/Button.vue' import KanbanColumn from '@/components/features/KanbanColumn.vue' import CreateColumnDialog from '@/components/features/CreateColumnDialog.vue' import CreateTaskDialog from '@/components/features/CreateTaskDialog.vue' +import TaskDetailModal from '@/components/features/TaskDetailModal.vue' const route = useRoute() const router = useRouter() @@ -87,6 +95,10 @@ const isCreateTaskDialogOpen = ref(false) const isCreatingTask = ref(false) const currentColumnId = ref(undefined) +// 任务详情模态框状态 +const isTaskDetailModalOpen = ref(false) +const selectedTaskId = ref(undefined) + const loadData = async () => { const boardId = route.params.boardId if (boardId) { @@ -107,7 +119,13 @@ const goBack = () => { } const selectTask = (task: any) => { - router.push(`/tasks/${task.id}`) + selectedTaskId.value = task.id + isTaskDetailModalOpen.value = true +} + +const closeTaskDetailModal = () => { + isTaskDetailModalOpen.value = false + selectedTaskId.value = undefined } const deleteTask = (taskId: number) => { diff --git a/frontend/src/views/user/ProfileView.vue b/frontend/src/views/user/ProfileView.vue index ddcf368..bd12606 100644 --- a/frontend/src/views/user/ProfileView.vue +++ b/frontend/src/views/user/ProfileView.vue @@ -12,13 +12,28 @@
- + +
+ 更换头像 +
+
+ + +

- {{ userStore.currentUser?.name }} + {{ userStore.currentUser?.nickname || userStore.currentUser?.username }}

{{ userStore.currentUser?.email }} @@ -49,10 +64,10 @@

@@ -67,21 +82,19 @@ class="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring" />
+
+ + +
-
- - -
- -
+
@@ -99,8 +112,9 @@