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 @@
+
+
+
+
Activity
+ {{ activities.length }} 条记录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ log.nickname || log.username }}
+
+
+
+ {{ formatRelativeTime(log.created_at) }}
+
+
+
+
+
+
+ 暂无活动记录
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ 当前列
+
+ {{ task.column?.name || '加载中...' }}
+
+
+
+
+
+
+
+ 优先级
+
+
+
+
+
+
+
+
执行人
+
+
+
{{ task.assignee.nickname || task.assignee.username }}
+
+
+
+
+
+
+
+
Description
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 创建于
+ {{ formatDateTime(task.created_at) }}
+
+
+ 最后更新
+ {{ formatDateTime(task.updated_at) }}
+
+
+
+
+
+
+
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 @@
+
+
+
+
#{{ taskId }}
+
+
+
+
+
+
+ {{ title || '加载中...' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@