dark 7 months ago
parent
commit
99755b216f
  1. 193
      backend/internal/handler/statistics/get_overview.go
  2. 336
      backend/internal/handler/statistics/get_task_statistics.go
  3. 447
      backend/internal/handler/statistics/get_user_statistics.go
  4. 79
      backend/internal/handler/task/add_task_attachment.go
  5. 72
      backend/internal/handler/task/add_task_comment.go
  6. 100
      backend/internal/handler/task/create_task.go
  7. 59
      backend/internal/handler/task/delete_task.go
  8. 62
      backend/internal/handler/task/delete_task_attachment.go
  9. 55
      backend/internal/handler/task/delete_task_comment.go
  10. 37
      backend/internal/handler/task/get_task.go
  11. 38
      backend/internal/handler/task/get_task_attachments.go
  12. 39
      backend/internal/handler/task/get_task_comments.go
  13. 121
      backend/internal/handler/task/get_tasks.go
  14. 95
      backend/internal/handler/task/update_task.go
  15. 77
      backend/internal/handler/task/update_task_status.go
  16. 64
      backend/internal/handler/task/upload_file.go
  17. 10
      backend/model/model.go
  18. 15
      backend/pkg/database/database.go
  19. 351
      frontend/src/api/index.ts
  20. 469
      frontend/src/views/Task/index.vue

193
backend/internal/handler/statistics/get_overview.go

@ -1,7 +1,196 @@
package statistics
import "github.com/gin-gonic/gin"
import (
"net/http"
"strconv"
"time"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
// OverviewResponse 总览统计响应
type OverviewResponse struct {
// 基本统计
TotalTasks int64 `json:"total_tasks"`
TotalUsers int64 `json:"total_users"`
TotalOrganizations int64 `json:"total_organizations"`
// 任务状态统计
TasksByStatus map[string]int64 `json:"tasks_by_status"`
// 任务优先级统计
TasksByPriority map[string]int64 `json:"tasks_by_priority"`
// 最近7天任务创建数
RecentTasksCount int64 `json:"recent_tasks_count"`
// 最近7天任务完成数
RecentCompletedTasks int64 `json:"recent_completed_tasks"`
// 今日任务统计
TodayTasks struct {
Created int64 `json:"created"`
Completed int64 `json:"completed"`
} `json:"today_tasks"`
// 本周任务统计
WeeklyTasks struct {
Created int64 `json:"created"`
Completed int64 `json:"completed"`
} `json:"weekly_tasks"`
// 本月任务统计
MonthlyTasks struct {
Created int64 `json:"created"`
Completed int64 `json:"completed"`
} `json:"monthly_tasks"`
}
func (h *StatisticsHandler) GetOverview(c *gin.Context) {
// TODO: 实现统计概览接口
// 获取组织ID参数(可选)
organizationIDStr := c.Query("organization_id")
var organizationID uint
if organizationIDStr != "" {
if id, err := strconv.ParseUint(organizationIDStr, 10, 32); err == nil {
organizationID = uint(id)
}
}
db := database.GetDB()
var response OverviewResponse
// 基础查询构建器
baseTaskQuery := db.Model(&model.Task{})
if organizationID > 0 {
baseTaskQuery = baseTaskQuery.Where("organization_id = ?", organizationID)
}
// 1. 基本统计
// 总任务数
if err := baseTaskQuery.Count(&response.TotalTasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务统计失败"})
return
}
// 总用户数
userQuery := db.Model(&model.User{})
if err := userQuery.Count(&response.TotalUsers).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户统计失败"})
return
}
// 总组织数
if err := db.Model(&model.Organization{}).Count(&response.TotalOrganizations).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取组织统计失败"})
return
}
// 2. 任务状态统计
var statusStats []struct {
Status string `json:"status"`
Count int64 `json:"count"`
}
statusQuery := baseTaskQuery.Select("status, count(*) as count").Group("status")
if err := statusQuery.Find(&statusStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务状态统计失败"})
return
}
response.TasksByStatus = make(map[string]int64)
for _, stat := range statusStats {
response.TasksByStatus[stat.Status] = stat.Count
}
// 3. 任务优先级统计
var priorityStats []struct {
Priority string `json:"priority"`
Count int64 `json:"count"`
}
priorityQuery := baseTaskQuery.Select("priority, count(*) as count").Group("priority")
if err := priorityQuery.Find(&priorityStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务优先级统计失败"})
return
}
response.TasksByPriority = make(map[string]int64)
for _, stat := range priorityStats {
response.TasksByPriority[stat.Priority] = stat.Count
}
// 4. 时间范围统计
now := time.Now()
// 今日开始时间
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
// 本周开始时间(周一)
weekStart := todayStart.AddDate(0, 0, -int(now.Weekday())+1)
if now.Weekday() == time.Sunday {
weekStart = weekStart.AddDate(0, 0, -7)
}
// 本月开始时间
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 最近7天开始时间
sevenDaysAgo := now.AddDate(0, 0, -7)
// 今日任务统计
todayTaskQuery := baseTaskQuery.Where("created_at >= ?", todayStart)
if err := todayTaskQuery.Count(&response.TodayTasks.Created).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取今日任务统计失败"})
return
}
todayCompletedQuery := baseTaskQuery.Where("status = ? AND updated_at >= ?", "completed", todayStart)
if err := todayCompletedQuery.Count(&response.TodayTasks.Completed).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取今日完成任务统计失败"})
return
}
// 本周任务统计
weekTaskQuery := baseTaskQuery.Where("created_at >= ?", weekStart)
if err := weekTaskQuery.Count(&response.WeeklyTasks.Created).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本周任务统计失败"})
return
}
weekCompletedQuery := baseTaskQuery.Where("status = ? AND updated_at >= ?", "completed", weekStart)
if err := weekCompletedQuery.Count(&response.WeeklyTasks.Completed).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本周完成任务统计失败"})
return
}
// 本月任务统计
monthTaskQuery := baseTaskQuery.Where("created_at >= ?", monthStart)
if err := monthTaskQuery.Count(&response.MonthlyTasks.Created).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本月任务统计失败"})
return
}
monthCompletedQuery := baseTaskQuery.Where("status = ? AND updated_at >= ?", "completed", monthStart)
if err := monthCompletedQuery.Count(&response.MonthlyTasks.Completed).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取本月完成任务统计失败"})
return
}
// 最近7天任务统计
recentTaskQuery := baseTaskQuery.Where("created_at >= ?", sevenDaysAgo)
if err := recentTaskQuery.Count(&response.RecentTasksCount).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取最近任务统计失败"})
return
}
recentCompletedQuery := baseTaskQuery.Where("status = ? AND updated_at >= ?", "completed", sevenDaysAgo)
if err := recentCompletedQuery.Count(&response.RecentCompletedTasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取最近完成任务统计失败"})
return
}
c.JSON(http.StatusOK, response)
}

336
backend/internal/handler/statistics/get_task_statistics.go

@ -1,7 +1,339 @@
package statistics
import "github.com/gin-gonic/gin"
import (
"net/http"
"strconv"
"time"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
// TaskStatisticsResponse 任务统计响应
type TaskStatisticsResponse struct {
// 基本统计
TotalTasks int64 `json:"total_tasks"`
PendingTasks int64 `json:"pending_tasks"`
InProgressTasks int64 `json:"in_progress_tasks"`
CompletedTasks int64 `json:"completed_tasks"`
CancelledTasks int64 `json:"cancelled_tasks"`
// 按状态统计
TasksByStatus []TaskStatusStat `json:"tasks_by_status"`
// 按优先级统计
TasksByPriority []TaskPriorityStat `json:"tasks_by_priority"`
// 按类型统计
TasksByType []TaskTypeStat `json:"tasks_by_type"`
// 按创建者统计
TasksByCreator []TaskCreatorStat `json:"tasks_by_creator"`
// 按分配者统计
TasksByAssignee []TaskAssigneeStat `json:"tasks_by_assignee"`
// 时间趋势统计(最近30天)
DailyTrend []DailyTaskStat `json:"daily_trend"`
// 完成率统计
CompletionRate float64 `json:"completion_rate"`
// 平均完成时间(小时)
AverageCompletionTime float64 `json:"average_completion_time"`
// 逾期任务统计
OverdueTasks int64 `json:"overdue_tasks"`
// 即将到期任务统计(7天内)
UpcomingTasks int64 `json:"upcoming_tasks"`
}
// TaskStatusStat 任务状态统计
type TaskStatusStat struct {
Status string `json:"status"`
Count int64 `json:"count"`
Percentage float64 `json:"percentage"`
}
// TaskPriorityStat 任务优先级统计
type TaskPriorityStat struct {
Priority string `json:"priority"`
Count int64 `json:"count"`
Percentage float64 `json:"percentage"`
}
// TaskTypeStat 任务类型统计
type TaskTypeStat struct {
Type string `json:"type"`
Count int64 `json:"count"`
Percentage float64 `json:"percentage"`
}
// TaskCreatorStat 任务创建者统计
type TaskCreatorStat struct {
CreatorID uint `json:"creator_id"`
CreatorName string `json:"creator_name"`
Count int64 `json:"count"`
}
// TaskAssigneeStat 任务分配者统计
type TaskAssigneeStat struct {
AssigneeID uint `json:"assignee_id"`
AssigneeName string `json:"assignee_name"`
Count int64 `json:"count"`
}
// DailyTaskStat 每日任务统计
type DailyTaskStat struct {
Date string `json:"date"`
Created int64 `json:"created"`
Completed int64 `json:"completed"`
}
func (h *StatisticsHandler) GetTaskStatistics(c *gin.Context) {
// TODO: 实现任务统计接口
// 获取组织ID参数(可选)
organizationIDStr := c.Query("organization_id")
var organizationID uint
if organizationIDStr != "" {
if id, err := strconv.ParseUint(organizationIDStr, 10, 32); err == nil {
organizationID = uint(id)
}
}
// 获取时间范围参数
daysStr := c.DefaultQuery("days", "30")
days, err := strconv.Atoi(daysStr)
if err != nil {
days = 30
}
db := database.GetDB()
var response TaskStatisticsResponse
// 基础查询构建器
baseTaskQuery := db.Model(&model.Task{})
if organizationID > 0 {
baseTaskQuery = baseTaskQuery.Where("organization_id = ?", organizationID)
}
// 1. 基本统计
if err := baseTaskQuery.Count(&response.TotalTasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务总数失败"})
return
}
// 各状态任务数
if err := baseTaskQuery.Where("status = ?", "pending").Count(&response.PendingTasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取待处理任务数失败"})
return
}
if err := baseTaskQuery.Where("status = ?", "in_progress").Count(&response.InProgressTasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取进行中任务数失败"})
return
}
if err := baseTaskQuery.Where("status = ?", "completed").Count(&response.CompletedTasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取已完成任务数失败"})
return
}
if err := baseTaskQuery.Where("status = ?", "cancelled").Count(&response.CancelledTasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取已取消任务数失败"})
return
}
// 2. 按状态统计
var statusStats []struct {
Status string `json:"status"`
Count int64 `json:"count"`
}
statusQuery := baseTaskQuery.Select("status, count(*) as count").Group("status")
if err := statusQuery.Find(&statusStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务状态统计失败"})
return
}
response.TasksByStatus = make([]TaskStatusStat, len(statusStats))
for i, stat := range statusStats {
percentage := float64(0)
if response.TotalTasks > 0 {
percentage = float64(stat.Count) / float64(response.TotalTasks) * 100
}
response.TasksByStatus[i] = TaskStatusStat{
Status: stat.Status,
Count: stat.Count,
Percentage: percentage,
}
}
// 3. 按优先级统计
var priorityStats []struct {
Priority string `json:"priority"`
Count int64 `json:"count"`
}
priorityQuery := baseTaskQuery.Select("priority, count(*) as count").Group("priority")
if err := priorityQuery.Find(&priorityStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务优先级统计失败"})
return
}
response.TasksByPriority = make([]TaskPriorityStat, len(priorityStats))
for i, stat := range priorityStats {
percentage := float64(0)
if response.TotalTasks > 0 {
percentage = float64(stat.Count) / float64(response.TotalTasks) * 100
}
response.TasksByPriority[i] = TaskPriorityStat{
Priority: stat.Priority,
Count: stat.Count,
Percentage: percentage,
}
}
// 4. 按类型统计
var typeStats []struct {
Type string `json:"type"`
Count int64 `json:"count"`
}
typeQuery := baseTaskQuery.Select("type, count(*) as count").Group("type")
if err := typeQuery.Find(&typeStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务类型统计失败"})
return
}
response.TasksByType = make([]TaskTypeStat, len(typeStats))
for i, stat := range typeStats {
percentage := float64(0)
if response.TotalTasks > 0 {
percentage = float64(stat.Count) / float64(response.TotalTasks) * 100
}
response.TasksByType[i] = TaskTypeStat{
Type: stat.Type,
Count: stat.Count,
Percentage: percentage,
}
}
// 5. 按创建者统计
var creatorStats []struct {
CreatorID uint `json:"creator_id"`
CreatorName string `json:"creator_name"`
Count int64 `json:"count"`
}
creatorQuery := baseTaskQuery.Select("creator_id, users.real_name as creator_name, count(*) as count").
Joins("LEFT JOIN users ON tasks.creator_id = users.id").
Group("creator_id, users.real_name").
Order("count desc").
Limit(10)
if err := creatorQuery.Find(&creatorStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取创建者统计失败"})
return
}
response.TasksByCreator = make([]TaskCreatorStat, len(creatorStats))
for i, stat := range creatorStats {
response.TasksByCreator[i] = TaskCreatorStat{
CreatorID: stat.CreatorID,
CreatorName: stat.CreatorName,
Count: stat.Count,
}
}
// 6. 按分配者统计
var assigneeStats []struct {
AssigneeID uint `json:"assignee_id"`
AssigneeName string `json:"assignee_name"`
Count int64 `json:"count"`
}
assigneeQuery := baseTaskQuery.Select("assignee_id, users.real_name as assignee_name, count(*) as count").
Joins("LEFT JOIN users ON tasks.assignee_id = users.id").
Where("assignee_id IS NOT NULL").
Group("assignee_id, users.real_name").
Order("count desc").
Limit(10)
if err := assigneeQuery.Find(&assigneeStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取分配者统计失败"})
return
}
response.TasksByAssignee = make([]TaskAssigneeStat, len(assigneeStats))
for i, stat := range assigneeStats {
response.TasksByAssignee[i] = TaskAssigneeStat{
AssigneeID: stat.AssigneeID,
AssigneeName: stat.AssigneeName,
Count: stat.Count,
}
}
// 7. 时间趋势统计
now := time.Now()
startDate := now.AddDate(0, 0, -days)
response.DailyTrend = make([]DailyTaskStat, 0)
for i := 0; i < days; i++ {
date := startDate.AddDate(0, 0, i)
dateStr := date.Format("2006-01-02")
// 当天创建的任务数
var createdCount int64
createdQuery := baseTaskQuery.Where("DATE(created_at) = ?", dateStr)
if err := createdQuery.Count(&createdCount).Error; err != nil {
createdCount = 0
}
// 当天完成的任务数
var completedCount int64
completedQuery := baseTaskQuery.Where("status = ? AND DATE(updated_at) = ?", "completed", dateStr)
if err := completedQuery.Count(&completedCount).Error; err != nil {
completedCount = 0
}
response.DailyTrend = append(response.DailyTrend, DailyTaskStat{
Date: dateStr,
Created: createdCount,
Completed: completedCount,
})
}
// 8. 完成率
if response.TotalTasks > 0 {
response.CompletionRate = float64(response.CompletedTasks) / float64(response.TotalTasks) * 100
}
// 9. 平均完成时间
var avgCompletionTime struct {
AverageHours float64 `json:"average_hours"`
}
avgQuery := baseTaskQuery.Select("AVG(TIMESTAMPDIFF(HOUR, created_at, updated_at)) as average_hours").
Where("status = ?", "completed")
if err := avgQuery.Scan(&avgCompletionTime).Error; err == nil {
response.AverageCompletionTime = avgCompletionTime.AverageHours
}
// 10. 逾期任务统计
now = time.Now()
overdueQuery := baseTaskQuery.Where("end_time < ? AND status != ?", now, "completed")
if err := overdueQuery.Count(&response.OverdueTasks).Error; err != nil {
response.OverdueTasks = 0
}
// 11. 即将到期任务统计
sevenDaysLater := now.AddDate(0, 0, 7)
upcomingQuery := baseTaskQuery.Where("end_time BETWEEN ? AND ? AND status != ?", now, sevenDaysLater, "completed")
if err := upcomingQuery.Count(&response.UpcomingTasks).Error; err != nil {
response.UpcomingTasks = 0
}
c.JSON(http.StatusOK, response)
}

447
backend/internal/handler/statistics/get_user_statistics.go

@ -1,7 +1,450 @@
package statistics
import "github.com/gin-gonic/gin"
import (
"fmt"
"net/http"
"strconv"
"time"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
// UserStatisticsResponse 用户统计响应
type UserStatisticsResponse struct {
// 基本统计
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"` // 最近30天有活动的用户
// 用户任务统计
UserTaskStats []UserTaskStat `json:"user_task_stats"`
// 用户效率统计
UserEfficiencyStats []UserEfficiencyStat `json:"user_efficiency_stats"`
// 用户活跃度统计
UserActivityStats []UserActivityStat `json:"user_activity_stats"`
// 用户完成率排行
UserCompletionRank []UserCompletionRank `json:"user_completion_rank"`
// 用户任务分配统计
UserAssignmentStats []UserAssignmentStat `json:"user_assignment_stats"`
// 总体用户效率指标
OverallEfficiency struct {
AverageCompletionRate float64 `json:"average_completion_rate"`
AverageTasksPerUser float64 `json:"average_tasks_per_user"`
AverageCompletionTime float64 `json:"average_completion_time"`
} `json:"overall_efficiency"`
}
// UserTaskStat 用户任务统计
type UserTaskStat struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
RealName string `json:"real_name"`
CreatedTasks int64 `json:"created_tasks"`
AssignedTasks int64 `json:"assigned_tasks"`
CompletedTasks int64 `json:"completed_tasks"`
PendingTasks int64 `json:"pending_tasks"`
InProgressTasks int64 `json:"in_progress_tasks"`
}
// UserEfficiencyStat 用户效率统计
type UserEfficiencyStat struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
RealName string `json:"real_name"`
CompletionRate float64 `json:"completion_rate"`
AverageCompletionTime float64 `json:"average_completion_time"`
TasksCompletedThisWeek int64 `json:"tasks_completed_this_week"`
TasksCompletedThisMonth int64 `json:"tasks_completed_this_month"`
}
// UserActivityStat 用户活跃度统计
type UserActivityStat struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
RealName string `json:"real_name"`
LastActivity time.Time `json:"last_activity"`
CommentsCount int64 `json:"comments_count"`
TasksCreatedThisWeek int64 `json:"tasks_created_this_week"`
TasksCreatedThisMonth int64 `json:"tasks_created_this_month"`
}
// UserCompletionRank 用户完成率排行
type UserCompletionRank struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
RealName string `json:"real_name"`
CompletionRate float64 `json:"completion_rate"`
CompletedTasks int64 `json:"completed_tasks"`
TotalTasks int64 `json:"total_tasks"`
Rank int `json:"rank"`
}
// UserAssignmentStat 用户任务分配统计
type UserAssignmentStat struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
RealName string `json:"real_name"`
UrgentTasks int64 `json:"urgent_tasks"`
HighPriorityTasks int64 `json:"high_priority_tasks"`
MediumPriorityTasks int64 `json:"medium_priority_tasks"`
LowPriorityTasks int64 `json:"low_priority_tasks"`
OverdueTasks int64 `json:"overdue_tasks"`
}
func (h *StatisticsHandler) GetUserStatistics(c *gin.Context) {
// TODO: 实现用户统计接口
// 获取组织ID参数(可选)
organizationIDStr := c.Query("organization_id")
var organizationID uint
if organizationIDStr != "" {
if id, err := strconv.ParseUint(organizationIDStr, 10, 32); err == nil {
organizationID = uint(id)
}
}
// 获取用户ID参数(可选,如果提供则只查询该用户)
userIDStr := c.Query("user_id")
var userID uint
if userIDStr != "" {
if id, err := strconv.ParseUint(userIDStr, 10, 32); err == nil {
userID = uint(id)
}
}
db := database.GetDB()
var response UserStatisticsResponse
// 基础用户查询构建器
baseUserQuery := db.Model(&model.User{})
if organizationID > 0 {
baseUserQuery = baseUserQuery.Joins("JOIN user_organizations ON users.id = user_organizations.user_id").
Where("user_organizations.organization_id = ?", organizationID)
}
if userID > 0 {
baseUserQuery = baseUserQuery.Where("users.id = ?", userID)
}
// 1. 基本统计
if err := baseUserQuery.Count(&response.TotalUsers).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户总数失败"})
return
}
// 活跃用户数(最近30天有任务活动)
thirtyDaysAgo := time.Now().AddDate(0, 0, -30)
activeUserQuery := baseUserQuery.Joins("JOIN tasks ON users.id = tasks.creator_id OR users.id = tasks.assignee_id").
Where("tasks.created_at >= ? OR tasks.updated_at >= ?", thirtyDaysAgo, thirtyDaysAgo).
Distinct("users.id")
if err := activeUserQuery.Count(&response.ActiveUsers).Error; err != nil {
response.ActiveUsers = 0
}
// 2. 用户任务统计
var userTaskStats []UserTaskStat
// 构建用户任务统计查询
userTaskQuery := `
SELECT
u.id as user_id,
u.username,
u.real_name,
COALESCE(created_tasks.count, 0) as created_tasks,
COALESCE(assigned_tasks.count, 0) as assigned_tasks,
COALESCE(completed_tasks.count, 0) as completed_tasks,
COALESCE(pending_tasks.count, 0) as pending_tasks,
COALESCE(in_progress_tasks.count, 0) as in_progress_tasks
FROM users u
LEFT JOIN (
SELECT creator_id, COUNT(*) as count
FROM tasks
WHERE 1=1 %s
GROUP BY creator_id
) created_tasks ON u.id = created_tasks.creator_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL %s
GROUP BY assignee_id
) assigned_tasks ON u.id = assigned_tasks.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND status = 'completed' %s
GROUP BY assignee_id
) completed_tasks ON u.id = completed_tasks.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND status = 'pending' %s
GROUP BY assignee_id
) pending_tasks ON u.id = pending_tasks.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND status = 'in_progress' %s
GROUP BY assignee_id
) in_progress_tasks ON u.id = in_progress_tasks.assignee_id
WHERE 1=1 %s
ORDER BY (COALESCE(created_tasks.count, 0) + COALESCE(assigned_tasks.count, 0)) DESC
LIMIT 20
`
// 构建WHERE条件
orgCondition := ""
userCondition := ""
if organizationID > 0 {
orgCondition = "AND organization_id = " + strconv.FormatUint(uint64(organizationID), 10)
}
if userID > 0 {
userCondition = "AND u.id = " + strconv.FormatUint(uint64(userID), 10)
}
finalQuery := fmt.Sprintf(userTaskQuery, orgCondition, orgCondition, orgCondition, orgCondition, orgCondition, userCondition)
if err := db.Raw(finalQuery).Scan(&userTaskStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户任务统计失败"})
return
}
response.UserTaskStats = userTaskStats
// 3. 用户效率统计
var userEfficiencyStats []UserEfficiencyStat
now := time.Now()
weekStart := now.AddDate(0, 0, -int(now.Weekday())+1)
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
efficiencyQuery := `
SELECT
u.id as user_id,
u.username,
u.real_name,
CASE
WHEN total_assigned.count > 0 THEN
(completed_assigned.count * 100.0 / total_assigned.count)
ELSE 0
END as completion_rate,
COALESCE(avg_completion.avg_hours, 0) as average_completion_time,
COALESCE(week_completed.count, 0) as tasks_completed_this_week,
COALESCE(month_completed.count, 0) as tasks_completed_this_month
FROM users u
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL %s
GROUP BY assignee_id
) total_assigned ON u.id = total_assigned.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND status = 'completed' %s
GROUP BY assignee_id
) completed_assigned ON u.id = completed_assigned.assignee_id
LEFT JOIN (
SELECT assignee_id, AVG(TIMESTAMPDIFF(HOUR, created_at, updated_at)) as avg_hours
FROM tasks
WHERE assignee_id IS NOT NULL AND status = 'completed' %s
GROUP BY assignee_id
) avg_completion ON u.id = avg_completion.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND status = 'completed' AND updated_at >= '%s' %s
GROUP BY assignee_id
) week_completed ON u.id = week_completed.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND status = 'completed' AND updated_at >= '%s' %s
GROUP BY assignee_id
) month_completed ON u.id = month_completed.assignee_id
WHERE 1=1 %s
HAVING total_assigned.count > 0
ORDER BY completion_rate DESC
LIMIT 20
`
finalEfficiencyQuery := fmt.Sprintf(efficiencyQuery,
orgCondition, orgCondition, orgCondition,
weekStart.Format("2006-01-02"), orgCondition,
monthStart.Format("2006-01-02"), orgCondition,
userCondition)
if err := db.Raw(finalEfficiencyQuery).Scan(&userEfficiencyStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户效率统计失败"})
return
}
response.UserEfficiencyStats = userEfficiencyStats
// 4. 用户活跃度统计
var userActivityStats []UserActivityStat
activityQuery := `
SELECT
u.id as user_id,
u.username,
u.real_name,
COALESCE(MAX(GREATEST(t.created_at, t.updated_at)), u.created_at) as last_activity,
COALESCE(comments.count, 0) as comments_count,
COALESCE(week_created.count, 0) as tasks_created_this_week,
COALESCE(month_created.count, 0) as tasks_created_this_month
FROM users u
LEFT JOIN tasks t ON u.id = t.creator_id OR u.id = t.assignee_id
LEFT JOIN (
SELECT user_id, COUNT(*) as count
FROM task_comments
GROUP BY user_id
) comments ON u.id = comments.user_id
LEFT JOIN (
SELECT creator_id, COUNT(*) as count
FROM tasks
WHERE created_at >= '%s' %s
GROUP BY creator_id
) week_created ON u.id = week_created.creator_id
LEFT JOIN (
SELECT creator_id, COUNT(*) as count
FROM tasks
WHERE created_at >= '%s' %s
GROUP BY creator_id
) month_created ON u.id = month_created.creator_id
WHERE 1=1 %s
GROUP BY u.id, u.username, u.real_name, u.created_at, comments.count, week_created.count, month_created.count
ORDER BY last_activity DESC
LIMIT 20
`
finalActivityQuery := fmt.Sprintf(activityQuery,
weekStart.Format("2006-01-02"), orgCondition,
monthStart.Format("2006-01-02"), orgCondition,
userCondition)
if err := db.Raw(finalActivityQuery).Scan(&userActivityStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户活跃度统计失败"})
return
}
response.UserActivityStats = userActivityStats
// 5. 用户完成率排行
var userCompletionRank []UserCompletionRank
for i, stat := range userEfficiencyStats {
rank := UserCompletionRank{
UserID: stat.UserID,
Username: stat.Username,
RealName: stat.RealName,
CompletionRate: stat.CompletionRate,
Rank: i + 1,
}
// 获取用户的总任务数和完成任务数
for _, taskStat := range userTaskStats {
if taskStat.UserID == stat.UserID {
rank.TotalTasks = taskStat.AssignedTasks
rank.CompletedTasks = taskStat.CompletedTasks
break
}
}
userCompletionRank = append(userCompletionRank, rank)
}
response.UserCompletionRank = userCompletionRank
// 6. 用户任务分配统计
var userAssignmentStats []UserAssignmentStat
assignmentQuery := `
SELECT
u.id as user_id,
u.username,
u.real_name,
COALESCE(urgent.count, 0) as urgent_tasks,
COALESCE(high_priority.count, 0) as high_priority_tasks,
COALESCE(medium_priority.count, 0) as medium_priority_tasks,
COALESCE(low_priority.count, 0) as low_priority_tasks,
COALESCE(overdue.count, 0) as overdue_tasks
FROM users u
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND priority = 'urgent' %s
GROUP BY assignee_id
) urgent ON u.id = urgent.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND priority = 'high' %s
GROUP BY assignee_id
) high_priority ON u.id = high_priority.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND priority = 'medium' %s
GROUP BY assignee_id
) medium_priority ON u.id = medium_priority.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND priority = 'low' %s
GROUP BY assignee_id
) low_priority ON u.id = low_priority.assignee_id
LEFT JOIN (
SELECT assignee_id, COUNT(*) as count
FROM tasks
WHERE assignee_id IS NOT NULL AND end_time < NOW() AND status != 'completed' %s
GROUP BY assignee_id
) overdue ON u.id = overdue.assignee_id
WHERE 1=1 %s
ORDER BY (COALESCE(urgent.count, 0) + COALESCE(high_priority.count, 0)) DESC
LIMIT 20
`
finalAssignmentQuery := fmt.Sprintf(assignmentQuery,
orgCondition, orgCondition, orgCondition, orgCondition, orgCondition, userCondition)
if err := db.Raw(finalAssignmentQuery).Scan(&userAssignmentStats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取用户任务分配统计失败"})
return
}
response.UserAssignmentStats = userAssignmentStats
// 7. 总体效率指标
if len(userEfficiencyStats) > 0 {
var totalCompletionRate float64
var totalTasks int64
var totalCompletionTime float64
var validTimeCount int64
for _, stat := range userEfficiencyStats {
totalCompletionRate += stat.CompletionRate
if stat.AverageCompletionTime > 0 {
totalCompletionTime += stat.AverageCompletionTime
validTimeCount++
}
}
for _, stat := range userTaskStats {
totalTasks += stat.AssignedTasks
}
response.OverallEfficiency.AverageCompletionRate = totalCompletionRate / float64(len(userEfficiencyStats))
if response.TotalUsers > 0 {
response.OverallEfficiency.AverageTasksPerUser = float64(totalTasks) / float64(response.TotalUsers)
}
if validTimeCount > 0 {
response.OverallEfficiency.AverageCompletionTime = totalCompletionTime / float64(validTimeCount)
}
}
c.JSON(http.StatusOK, response)
}

79
backend/internal/handler/task/add_task_attachment.go

@ -1,9 +1,86 @@
package task
import (
"fmt"
"net/http"
"path/filepath"
"strconv"
"time"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) AddTaskAttachment(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
// 获取上传的用户ID
userIDStr := c.PostForm("user_id")
if userIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户ID不能为空"})
return
}
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
return
}
// 处理文件上传
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件上传失败"})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 生成文件名和路径
timestamp := time.Now().UnixNano()
fileName := file.Filename
filePath := fmt.Sprintf("uploads/%d_%s", timestamp, fileName)
// 保存文件
if err := c.SaveUploadedFile(file, filePath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
// 创建附件记录
attachment := model.TaskAttachment{
TaskID: uint(taskID),
FileName: fileName,
FilePath: filePath,
FileSize: file.Size,
FileType: filepath.Ext(fileName),
UploadedBy: uint(userID),
CreatedAt: time.Now(),
}
if err := db.Create(&attachment).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建附件记录失败"})
return
}
c.JSON(http.StatusCreated, attachment)
}

72
backend/internal/handler/task/add_task_comment.go

@ -1,9 +1,79 @@
package task
import (
"net/http"
"strconv"
"time"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
// AddTaskCommentRequest 添加任务评论请求结构
type AddTaskCommentRequest struct {
Content string `json:"content" binding:"required"`
UserID uint `json:"user_id" binding:"required"`
}
func (h *TaskHandler) AddTaskComment(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
var req AddTaskCommentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 检查用户是否存在
var user model.User
if err := db.First(&user, req.UserID).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询用户失败"})
return
}
// 创建评论
comment := model.TaskComment{
TaskID: uint(taskID),
UserID: req.UserID,
Content: req.Content,
CreatedAt: time.Now(),
}
if err := db.Create(&comment).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建评论失败"})
return
}
// 预加载用户信息
if err := db.Preload("User").First(&comment, comment.ID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论失败"})
return
}
c.JSON(http.StatusCreated, comment)
}

100
backend/internal/handler/task/create_task.go

@ -1,9 +1,107 @@
package task
import (
"net/http"
"time"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
// CreateTaskRequest 创建任务请求结构
type CreateTaskRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Type string `json:"type"`
Priority string `json:"priority"`
AssigneeID *uint `json:"assignee_id"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
OrganizationID uint `json:"organization_id" binding:"required"`
CreatorID uint `json:"creator_id" binding:"required"`
}
func (h *TaskHandler) CreateTask(c *gin.Context) {
// ... 复制原有实现 ...
var req CreateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "请求参数错误: " + err.Error(),
})
return
}
// 验证优先级
if req.Priority != "" && req.Priority != "urgent" && req.Priority != "high" && req.Priority != "medium" && req.Priority != "low" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "优先级值无效,必须是 urgent、high、medium 或 low",
})
return
}
// 如果没有指定优先级,默认为 medium
if req.Priority == "" {
req.Priority = "medium"
}
// 验证时间逻辑
if req.StartTime != nil && req.EndTime != nil && req.StartTime.After(*req.EndTime) {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "开始时间不能晚于结束时间",
})
return
}
// 创建任务对象
task := model.Task{
Title: req.Title,
Description: req.Description,
Type: req.Type,
Priority: req.Priority,
Status: "pending", // 默认状态
CreatorID: req.CreatorID,
OrganizationID: req.OrganizationID,
StartTime: req.StartTime,
EndTime: req.EndTime,
}
// 如果指定了执行者
if req.AssigneeID != nil {
task.AssigneeID = *req.AssigneeID
}
// 保存到数据库
db := database.GetDB()
if err := db.Create(&task).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "创建任务失败: " + err.Error(),
})
return
}
// 返回成功响应
c.JSON(http.StatusCreated, gin.H{
"code": 201,
"message": "任务创建成功",
"data": gin.H{
"id": task.ID,
"title": task.Title,
"description": task.Description,
"type": task.Type,
"priority": task.Priority,
"status": task.Status,
"creator_id": task.CreatorID,
"assignee_id": task.AssigneeID,
"organization_id": task.OrganizationID,
"start_time": task.StartTime,
"end_time": task.EndTime,
"created_at": task.CreatedAt,
"updated_at": task.UpdatedAt,
},
})
}

59
backend/internal/handler/task/delete_task.go

@ -1,9 +1,66 @@
package task
import (
"net/http"
"strconv"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) DeleteTask(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 开始事务
tx := db.Begin()
// 删除相关的评论
if err := tx.Where("task_id = ?", taskID).Delete(&model.TaskComment{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除任务评论失败"})
return
}
// 删除相关的附件
if err := tx.Where("task_id = ?", taskID).Delete(&model.TaskAttachment{}).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除任务附件失败"})
return
}
// 删除任务
if err := tx.Delete(&existingTask).Error; err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除任务失败"})
return
}
// 提交事务
if err := tx.Commit().Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "提交事务失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "任务删除成功"})
}

62
backend/internal/handler/task/delete_task_attachment.go

@ -1,9 +1,69 @@
package task
import (
"net/http"
"os"
"strconv"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) DeleteTaskAttachment(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
// 获取附件ID
attachmentIDStr := c.Param("attachment_id")
attachmentID, err := strconv.ParseUint(attachmentIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的附件ID"})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 检查附件是否存在且属于该任务
var existingAttachment model.TaskAttachment
if err := db.Where("id = ? AND task_id = ?", uint(attachmentID), uint(taskID)).
First(&existingAttachment).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "附件不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询附件失败"})
return
}
// 删除物理文件
if err := os.Remove(existingAttachment.FilePath); err != nil {
// 即使文件删除失败,也继续删除数据库记录
// 记录日志但不返回错误
}
// 删除数据库记录
if err := db.Delete(&existingAttachment).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除附件失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "附件删除成功"})
}

55
backend/internal/handler/task/delete_task_comment.go

@ -1,9 +1,62 @@
package task
import (
"net/http"
"strconv"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) DeleteTaskComment(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
// 获取评论ID
commentIDStr := c.Param("comment_id")
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的评论ID"})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 检查评论是否存在且属于该任务
var existingComment model.TaskComment
if err := db.Where("id = ? AND task_id = ?", uint(commentID), uint(taskID)).
First(&existingComment).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "评论不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询评论失败"})
return
}
// 删除评论
if err := db.Delete(&existingComment).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除评论失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "评论删除成功"})
}

37
backend/internal/handler/task/get_task.go

@ -1,9 +1,44 @@
package task
import (
"net/http"
"strconv"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) GetTask(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
db := database.GetDB()
var task model.Task
// 预加载相关数据:创建者、分配者、组织、评论、附件
err = db.Preload("Creator").
Preload("Assignee").
Preload("Organization").
Preload("Comments").
Preload("Comments.User").
Preload("Attachments").
First(&task, uint(taskID)).Error
if err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务失败"})
return
}
c.JSON(http.StatusOK, task)
}

38
backend/internal/handler/task/get_task_attachments.go

@ -1,9 +1,45 @@
package task
import (
"net/http"
"strconv"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) GetTaskAttachments(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 获取附件列表
var attachments []model.TaskAttachment
if err := db.Where("task_id = ?", uint(taskID)).
Order("created_at DESC").
Find(&attachments).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取附件列表失败"})
return
}
c.JSON(http.StatusOK, attachments)
}

39
backend/internal/handler/task/get_task_comments.go

@ -1,9 +1,46 @@
package task
import (
"net/http"
"strconv"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) GetTaskComments(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 获取评论列表
var comments []model.TaskComment
if err := db.Where("task_id = ?", uint(taskID)).
Preload("User").
Order("created_at DESC").
Find(&comments).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取评论列表失败"})
return
}
c.JSON(http.StatusOK, comments)
}

121
backend/internal/handler/task/get_tasks.go

@ -1,9 +1,128 @@
package task
import (
"net/http"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
// GetTasksQuery 获取任务列表查询参数
type GetTasksQuery struct {
Page int `form:"page" binding:"min=1"`
PageSize int `form:"page_size" binding:"min=1,max=100"`
Status string `form:"status"`
Priority string `form:"priority"`
Type string `form:"type"`
AssigneeID uint `form:"assignee_id"`
CreatorID uint `form:"creator_id"`
OrganizationID uint `form:"organization_id"`
Search string `form:"search"`
SortBy string `form:"sort_by"`
SortOrder string `form:"sort_order"`
}
// GetTasksResponse 获取任务列表响应
type GetTasksResponse struct {
Tasks []model.Task `json:"tasks"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
func (h *TaskHandler) GetTasks(c *gin.Context) {
// ... 复制原有实现 ...
var query GetTasksQuery
// 设置默认值
query.Page = 1
query.PageSize = 10
query.SortBy = "created_at"
query.SortOrder = "desc"
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db := database.GetDB()
// 构建查询
taskQuery := db.Model(&model.Task{})
// 预加载相关数据
taskQuery = taskQuery.Preload("Creator").Preload("Assignee").Preload("Organization")
// 应用过滤条件
if query.Status != "" {
taskQuery = taskQuery.Where("status = ?", query.Status)
}
if query.Priority != "" {
taskQuery = taskQuery.Where("priority = ?", query.Priority)
}
if query.Type != "" {
taskQuery = taskQuery.Where("type = ?", query.Type)
}
if query.AssigneeID > 0 {
taskQuery = taskQuery.Where("assignee_id = ?", query.AssigneeID)
}
if query.CreatorID > 0 {
taskQuery = taskQuery.Where("creator_id = ?", query.CreatorID)
}
if query.OrganizationID > 0 {
taskQuery = taskQuery.Where("organization_id = ?", query.OrganizationID)
}
// 搜索功能
if query.Search != "" {
searchTerm := "%" + query.Search + "%"
taskQuery = taskQuery.Where("title LIKE ? OR description LIKE ?", searchTerm, searchTerm)
}
// 计算总数
var total int64
if err := taskQuery.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "计算任务总数失败"})
return
}
// 计算总页数
totalPages := int((total + int64(query.PageSize) - 1) / int64(query.PageSize))
// 应用排序
orderBy := query.SortBy
if query.SortOrder == "asc" {
orderBy += " ASC"
} else {
orderBy += " DESC"
}
taskQuery = taskQuery.Order(orderBy)
// 应用分页
offset := (query.Page - 1) * query.PageSize
taskQuery = taskQuery.Offset(offset).Limit(query.PageSize)
// 执行查询
var tasks []model.Task
if err := taskQuery.Find(&tasks).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取任务列表失败"})
return
}
response := GetTasksResponse{
Tasks: tasks,
Total: total,
Page: query.Page,
PageSize: query.PageSize,
TotalPages: totalPages,
}
c.JSON(http.StatusOK, response)
}

95
backend/internal/handler/task/update_task.go

@ -1,9 +1,102 @@
package task
import (
"net/http"
"strconv"
"time"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
// UpdateTaskRequest 更新任务请求结构
type UpdateTaskRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
Type *string `json:"type"`
Priority *string `json:"priority"`
Status *string `json:"status"`
AssigneeID *uint `json:"assignee_id"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
}
func (h *TaskHandler) UpdateTask(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
var req UpdateTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 构建更新数据
updateData := make(map[string]interface{})
if req.Title != nil {
updateData["title"] = *req.Title
}
if req.Description != nil {
updateData["description"] = *req.Description
}
if req.Type != nil {
updateData["type"] = *req.Type
}
if req.Priority != nil {
updateData["priority"] = *req.Priority
}
if req.Status != nil {
updateData["status"] = *req.Status
}
if req.AssigneeID != nil {
updateData["assignee_id"] = *req.AssigneeID
}
if req.StartTime != nil {
updateData["start_time"] = *req.StartTime
}
if req.EndTime != nil {
updateData["end_time"] = *req.EndTime
}
// 设置更新时间
updateData["updated_at"] = time.Now()
// 执行更新
if err := db.Model(&existingTask).Updates(updateData).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新任务失败"})
return
}
// 重新查询更新后的任务,包含关联数据
var updatedTask model.Task
if err := db.Preload("Creator").
Preload("Assignee").
Preload("Organization").
First(&updatedTask, uint(taskID)).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取更新后的任务失败"})
return
}
c.JSON(http.StatusOK, updatedTask)
}

77
backend/internal/handler/task/update_task_status.go

@ -1,9 +1,84 @@
package task
import (
"net/http"
"strconv"
"time"
"task-track-backend/model"
"task-track-backend/pkg/database"
"github.com/gin-gonic/gin"
)
// UpdateTaskStatusRequest 更新任务状态请求结构
type UpdateTaskStatusRequest struct {
Status string `json:"status" binding:"required"`
}
func (h *TaskHandler) UpdateTaskStatus(c *gin.Context) {
// ... 复制原有实现 ...
// 获取任务ID
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务ID"})
return
}
var req UpdateTaskStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 验证状态值
validStatuses := []string{"pending", "in_progress", "completed", "cancelled"}
isValidStatus := false
for _, status := range validStatuses {
if req.Status == status {
isValidStatus = true
break
}
}
if !isValidStatus {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的任务状态"})
return
}
db := database.GetDB()
// 检查任务是否存在
var existingTask model.Task
if err := db.First(&existingTask, uint(taskID)).Error; err != nil {
if err.Error() == "record not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询任务失败"})
return
}
// 更新任务状态
updateData := map[string]interface{}{
"status": req.Status,
"updated_at": time.Now(),
}
if err := db.Model(&existingTask).Updates(updateData).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新任务状态失败"})
return
}
// 重新查询更新后的任务
var updatedTask model.Task
if err := db.Preload("Creator").
Preload("Assignee").
Preload("Organization").
First(&updatedTask, uint(taskID)).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取更新后的任务失败"})
return
}
c.JSON(http.StatusOK, updatedTask)
}

64
backend/internal/handler/task/upload_file.go

@ -1,9 +1,71 @@
package task
import (
"fmt"
"net/http"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
)
// UploadFileResponse 上传文件响应结构
type UploadFileResponse struct {
FileName string `json:"file_name"`
FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
FileType string `json:"file_type"`
}
func (h *TaskHandler) UploadFile(c *gin.Context) {
// ... 复制原有实现 ...
// 处理文件上传
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件上传失败"})
return
}
// 验证文件大小 (限制为 10MB)
maxSize := int64(10 * 1024 * 1024)
if file.Size > maxSize {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小超过限制"})
return
}
// 验证文件类型
allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt"}
fileExt := filepath.Ext(file.Filename)
isAllowed := false
for _, allowedType := range allowedTypes {
if fileExt == allowedType {
isAllowed = true
break
}
}
if !isAllowed {
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"})
return
}
// 生成文件名和路径
timestamp := time.Now().UnixNano()
fileName := file.Filename
filePath := fmt.Sprintf("uploads/%d_%s", timestamp, fileName)
// 保存文件
if err := c.SaveUploadedFile(file, filePath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
// 返回文件信息
response := UploadFileResponse{
FileName: fileName,
FilePath: filePath,
FileSize: file.Size,
FileType: fileExt,
}
c.JSON(http.StatusOK, response)
}

10
backend/model/model.go

@ -65,6 +65,13 @@ type Task struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// 关联关系
Creator User `json:"creator" gorm:"foreignKey:CreatorID"`
Assignee User `json:"assignee" gorm:"foreignKey:AssigneeID"`
Organization Organization `json:"organization" gorm:"foreignKey:OrganizationID"`
Comments []TaskComment `json:"comments" gorm:"foreignKey:TaskID"`
Attachments []TaskAttachment `json:"attachments" gorm:"foreignKey:TaskID"`
}
// TaskTag 任务标签表
@ -92,6 +99,9 @@ type TaskComment struct {
Content string `json:"content" gorm:"type:text;not null"`
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// 关联关系
User User `json:"user" gorm:"foreignKey:UserID"`
}
// TaskAttachment 任务附件表

15
backend/pkg/database/database.go

@ -13,6 +13,8 @@ import (
"gorm.io/gorm/logger"
)
var db *gorm.DB
func Init(cfg config.DatabaseConfig) (*gorm.DB, error) {
// 首先尝试连接到数据库
db, err := connectToDatabase(cfg)
@ -41,9 +43,22 @@ func Init(cfg config.DatabaseConfig) (*gorm.DB, error) {
return nil, fmt.Errorf("failed to auto migrate models: %w", err)
}
// 设置全局数据库实例
setDB(db)
return db, nil
}
// GetDB 获取全局数据库实例
func GetDB() *gorm.DB {
return db
}
// setDB 设置全局数据库实例
func setDB(database *gorm.DB) {
db = database
}
func connectToDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
cfg.Username,

351
frontend/src/api/index.ts

@ -33,19 +33,72 @@ api.interceptors.response.use(
}
)
// 认证相关API
export const authApi = {
// 登录
login: (data: { username: string; password: string }) => api.post('/auth/login', data),
// 注册
register: (data: { username: string; email: string; password: string; real_name?: string }) =>
api.post('/auth/register', data),
// 测试登录
testLogin: () => api.post('/auth/test-login'),
// 登出
logout: () => api.post('/auth/logout'),
// 刷新token
refresh: () => api.post('/auth/refresh'),
// 获取用户信息
getMe: () => api.get('/auth/me')
}
// 任务相关API
export const taskApi = {
// 获取任务列表
getTasks: (params?: any) => api.get('/tasks', { params }),
getTasks: (params?: {
page?: number;
page_size?: number;
status?: string;
priority?: string;
type?: string;
assignee_id?: number;
creator_id?: number;
organization_id?: number;
search?: string;
sort_by?: string;
sort_order?: string;
}) => api.get('/tasks', { params }),
// 获取单个任务详情
getTask: (id: number) => api.get(`/tasks/${id}`),
// 创建任务
createTask: (data: any) => api.post('/tasks', data),
createTask: (data: {
title: string;
description?: string;
type?: string;
priority?: string;
assignee_id?: number;
start_time?: string;
end_time?: string;
organization_id: number;
creator_id: number;
}) => api.post('/tasks', data),
// 更新任务
updateTask: (id: number, data: any) => api.post(`/tasks/update/${id}`, data),
updateTask: (id: number, data: {
title?: string;
description?: string;
type?: string;
priority?: string;
status?: string;
assignee_id?: number;
start_time?: string;
end_time?: string;
}) => api.post(`/tasks/update/${id}`, data),
// 删除任务
deleteTask: (id: number) => api.post(`/tasks/delete/${id}`),
@ -55,12 +108,17 @@ export const taskApi = {
// 任务评论相关
getTaskComments: (taskId: number) => api.get(`/tasks/${taskId}/comments`),
addTaskComment: (taskId: number, data: any) => api.post(`/tasks/${taskId}/comments`, data),
deleteTaskComment: (taskId: number, commentId: number) => api.delete(`/tasks/${taskId}/comments/${commentId}`),
addTaskComment: (taskId: number, data: { content: string; user_id: number }) =>
api.post(`/tasks/${taskId}/comments`, data),
deleteTaskComment: (taskId: number, commentId: number) =>
api.delete(`/tasks/${taskId}/comments/${commentId}`),
// 任务附件相关
getTaskAttachments: (taskId: number) => api.get(`/tasks/${taskId}/attachments`),
addTaskAttachment: (taskId: number, data: any) => api.post(`/tasks/${taskId}/attachments`, data),
addTaskAttachment: (taskId: number, data: FormData) =>
api.post(`/tasks/${taskId}/attachments`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
}),
deleteTaskAttachment: (attachmentId: number) => api.delete(`/attachments/${attachmentId}`),
// 上传文件
@ -68,16 +126,289 @@ export const taskApi = {
const formData = new FormData()
formData.append('file', file)
return api.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
headers: { 'Content-Type': 'multipart/form-data' }
})
}
}
// 用户相关API
export const userApi = {
getUsers: () => api.get('/users')
// 获取用户列表
getUsers: () => api.get('/users'),
// 获取单个用户详情
getUser: (id: number) => api.get(`/users/${id}`),
// 更新用户
updateUser: (id: number, data: {
username?: string;
email?: string;
real_name?: string;
phone?: string;
avatar?: string;
status?: number;
}) => api.post(`/users/update/${id}`, data),
// 删除用户
deleteUser: (id: number) => api.post(`/users/delete/${id}`)
}
// 组织相关API
export const organizationApi = {
// 获取组织列表
getOrganizations: () => api.get('/organizations'),
// 创建组织
createOrganization: (data: {
name: string;
code: string;
parent_id?: number;
level?: number;
sort?: number;
}) => api.post('/organizations', data),
// 获取单个组织详情
getOrganization: (id: number) => api.get(`/organizations/${id}`),
// 更新组织
updateOrganization: (id: number, data: {
name?: string;
code?: string;
parent_id?: number;
level?: number;
sort?: number;
status?: number;
}) => api.post(`/organizations/update/${id}`, data),
// 删除组织
deleteOrganization: (id: number) => api.post(`/organizations/delete/${id}`),
// 获取组织用户
getOrganizationUsers: (id: number) => api.get(`/organizations/${id}/users`)
}
// 统计相关API
export const statisticsApi = {
// 获取总览统计
getOverview: (params?: { organization_id?: number }) =>
api.get('/statistics/overview', { params }),
// 获取任务统计
getTaskStatistics: (params?: {
organization_id?: number;
days?: number
}) => api.get('/statistics/tasks', { params }),
// 获取用户统计
getUserStatistics: (params?: {
organization_id?: number;
user_id?: number
}) => api.get('/statistics/users', { params })
}
// 通用API
export const commonApi = {
// 测试接口
test: () => api.get('/test')
}
// 导出所有API
export const apiClient = {
auth: authApi,
task: taskApi,
user: userApi,
organization: organizationApi,
statistics: statisticsApi,
common: commonApi
}
// 类型定义
export interface Task {
id: number;
title: string;
description?: string;
type?: string;
priority: string;
status: string;
creator_id: number;
assignee_id?: number;
organization_id: number;
start_time?: string;
end_time?: string;
completed_at?: string;
created_at: string;
updated_at: string;
creator?: User;
assignee?: User;
organization?: Organization;
comments?: TaskComment[];
attachments?: TaskAttachment[];
}
export interface User {
id: number;
username: string;
email: string;
real_name?: string;
phone?: string;
avatar?: string;
status: number;
created_at: string;
updated_at: string;
}
export interface Organization {
id: number;
name: string;
code: string;
parent_id: number;
level: number;
sort: number;
status: number;
created_at: string;
updated_at: string;
}
export interface TaskComment {
id: number;
task_id: number;
user_id: number;
content: string;
created_at: string;
user?: User;
}
export interface TaskAttachment {
id: number;
task_id: number;
file_name: string;
file_path: string;
file_size: number;
file_type: string;
uploaded_by: number;
created_at: string;
}
// 统计响应类型
export interface OverviewResponse {
total_tasks: number;
total_users: number;
total_organizations: number;
tasks_by_status: Record<string, number>;
tasks_by_priority: Record<string, number>;
recent_tasks_count: number;
recent_completed_tasks: number;
today_tasks: {
created: number;
completed: number;
};
weekly_tasks: {
created: number;
completed: number;
};
monthly_tasks: {
created: number;
completed: number;
};
}
export interface TaskStatisticsResponse {
total_tasks: number;
pending_tasks: number;
in_progress_tasks: number;
completed_tasks: number;
cancelled_tasks: number;
tasks_by_status: Array<{
status: string;
count: number;
percentage: number;
}>;
tasks_by_priority: Array<{
priority: string;
count: number;
percentage: number;
}>;
tasks_by_type: Array<{
type: string;
count: number;
percentage: number;
}>;
tasks_by_creator: Array<{
creator_id: number;
creator_name: string;
count: number;
}>;
tasks_by_assignee: Array<{
assignee_id: number;
assignee_name: string;
count: number;
}>;
daily_trend: Array<{
date: string;
created: number;
completed: number;
}>;
completion_rate: number;
average_completion_time: number;
overdue_tasks: number;
upcoming_tasks: number;
}
export interface UserStatisticsResponse {
total_users: number;
active_users: number;
user_task_stats: Array<{
user_id: number;
username: string;
real_name: string;
created_tasks: number;
assigned_tasks: number;
completed_tasks: number;
pending_tasks: number;
in_progress_tasks: number;
}>;
user_efficiency_stats: Array<{
user_id: number;
username: string;
real_name: string;
completion_rate: number;
average_completion_time: number;
tasks_completed_this_week: number;
tasks_completed_this_month: number;
}>;
user_activity_stats: Array<{
user_id: number;
username: string;
real_name: string;
last_activity: string;
comments_count: number;
tasks_created_this_week: number;
tasks_created_this_month: number;
}>;
user_completion_rank: Array<{
user_id: number;
username: string;
real_name: string;
completion_rate: number;
completed_tasks: number;
total_tasks: number;
rank: number;
}>;
user_assignment_stats: Array<{
user_id: number;
username: string;
real_name: string;
urgent_tasks: number;
high_priority_tasks: number;
medium_priority_tasks: number;
low_priority_tasks: number;
overdue_tasks: number;
}>;
overall_efficiency: {
average_completion_rate: number;
average_tasks_per_user: number;
average_completion_time: number;
};
}
export default api

469
frontend/src/views/Task/index.vue

@ -4,11 +4,15 @@
<h1>任务管理</h1>
<div class="header-actions">
<el-button @click="$router.push('/tasks/board')">
<el-icon><Grid /></el-icon>
<el-icon>
<Grid />
</el-icon>
看板视图
</el-button>
<el-button type="primary" @click="showCreateDialog = true">
<el-icon><Plus /></el-icon>
<el-icon>
<Plus />
</el-icon>
创建任务
</el-button>
</div>
@ -20,7 +24,7 @@
<el-form-item label="任务标题">
<el-input v-model="searchForm.title" placeholder="请输入任务标题" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="待处理" value="pending" />
@ -29,7 +33,7 @@
<el-option label="已取消" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="优先级">
<el-select v-model="searchForm.priority" placeholder="请选择优先级" clearable>
<el-option label="紧急" value="urgent" />
@ -38,7 +42,7 @@
<el-option label="低" value="low" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchTasks">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
@ -50,7 +54,7 @@
<el-card>
<el-table :data="tasks" style="width: 100%" v-loading="loading">
<el-table-column prop="title" label="任务标题" min-width="200" />
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
@ -58,7 +62,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="priority" label="优先级" width="100">
<template #default="{ row }">
<el-tag :type="getPriorityType(row.priority)">
@ -66,9 +70,9 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assignee_name" label="执行者" width="120" />
<el-table-column prop="end_time" label="截止时间" width="180">
<template #default="{ row }">
<span :class="{ 'text-danger': isOverdue(row.end_time) }">
@ -76,28 +80,23 @@
</span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="附件" width="100">
<template #default="{ row }">
<el-button
v-if="row.attachments && row.attachments.length > 0"
size="small"
type="primary"
link
@click="viewAttachments(row)"
>
<el-button v-if="row.attachments && row.attachments.length > 0" size="small" type="primary" link
@click="viewAttachments(row)">
{{ row.attachments.length }}个附件
</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="text" size="small" @click="viewTask(row)">查看</el-button>
@ -107,45 +106,30 @@
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.size"
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @current-change="handleCurrentChange" />
</div>
</el-card>
<!-- 创建/编辑任务对话框 -->
<el-dialog
v-model="showCreateDialog"
:title="editingTask ? '编辑任务' : '创建任务'"
width="600px"
>
<el-dialog v-model="showCreateDialog" :title="editingTask ? '编辑任务' : '创建任务'" width="600px">
<el-form :model="taskForm" :rules="taskRules" ref="taskFormRef" label-width="100px">
<el-form-item label="任务标题" prop="title">
<el-input v-model="taskForm.title" placeholder="请输入任务标题" />
</el-form-item>
<el-form-item label="任务描述" prop="description">
<el-input
v-model="taskForm.description"
type="textarea"
:rows="4"
placeholder="请输入任务描述"
/>
<el-input v-model="taskForm.description" type="textarea" :rows="4" placeholder="请输入任务描述" />
</el-form-item>
<el-form-item label="任务类型" prop="type">
<el-input v-model="taskForm.type" placeholder="请输入任务类型" />
</el-form-item>
<el-form-item label="优先级" prop="priority">
<el-select v-model="taskForm.priority" placeholder="请选择优先级">
<el-option label="紧急" value="urgent" />
@ -154,52 +138,27 @@
<el-option label="低" value="low" />
</el-select>
</el-form-item>
<el-form-item label="执行者" prop="assignee_id">
<el-select v-model="taskForm.assignee_id" placeholder="请选择执行者" clearable>
<el-option
v-for="user in users"
:key="user.id"
:label="user.real_name"
:value="user.id"
/>
<el-option v-for="user in users" :key="user.id" :label="user.real_name" :value="user.id" />
</el-select>
</el-form-item>
<el-form-item label="开始时间" prop="start_time">
<el-date-picker
v-model="taskForm.start_time"
type="datetime"
placeholder="请选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<el-date-picker v-model="taskForm.start_time" type="datetime" placeholder="请选择开始时间"
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="截止时间" prop="end_time">
<el-date-picker
v-model="taskForm.end_time"
type="datetime"
placeholder="请选择截止时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
<el-date-picker v-model="taskForm.end_time" type="datetime" placeholder="请选择截止时间" format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss" />
</el-form-item>
<el-form-item label="附件">
<el-upload
ref="uploadRef"
:action="uploadUrl"
:headers="uploadHeaders"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-remove="handleFileRemove"
:file-list="fileList"
:auto-upload="false"
multiple
drag
accept=".doc,.docx,.pdf,.png,.jpg,.jpeg,.gif,.txt,.xls,.xlsx,.ppt,.pptx"
>
<el-upload ref="uploadRef" :action="uploadUrl" :headers="uploadHeaders" :on-success="handleUploadSuccess"
:on-error="handleUploadError" :on-remove="handleFileRemove" :file-list="fileList" :auto-upload="false"
multiple drag accept=".doc,.docx,.pdf,.png,.jpg,.jpeg,.gif,.txt,.xls,.xlsx,.ppt,.pptx">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
@ -212,20 +171,15 @@
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">取消</el-button>
<el-button type="primary" @click="saveTask">确定</el-button>
</template>
</el-dialog>
<!-- 任务详情对话框 -->
<el-dialog
v-model="showTaskDetailDialog"
title="任务详情"
width="80%"
destroy-on-close
>
<el-dialog v-model="showTaskDetailDialog" title="任务详情" width="80%" destroy-on-close>
<div v-if="taskDetail" class="task-detail">
<!-- 任务基本信息 -->
<el-card class="detail-card">
@ -235,7 +189,7 @@
<el-button type="primary" size="small" @click="editTask(taskDetail)">编辑任务</el-button>
</div>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="任务标题">
<span class="task-title">{{ taskDetail.title }}</span>
@ -284,12 +238,7 @@
<span>标签</span>
</template>
<div class="tags-container">
<el-tag
v-for="tag in taskDetail.tags"
:key="tag.id"
:color="tag.color"
class="tag-item"
>
<el-tag v-for="tag in taskDetail.tags" :key="tag.id" :color="tag.color" class="tag-item">
{{ tag.name }}
</el-tag>
</div>
@ -300,54 +249,42 @@
<template #header>
<div class="card-header">
<span>附件 ({{ taskDetail.attachments?.length || 0 }})</span>
<el-upload
ref="detailUploadRef"
:action="uploadUrl"
:headers="uploadHeaders"
:on-success="handleDetailUploadSuccess"
:show-file-list="false"
:before-upload="beforeUpload"
>
<el-upload ref="detailUploadRef" :action="uploadUrl" :headers="uploadHeaders"
:on-success="handleDetailUploadSuccess" :show-file-list="false" :before-upload="beforeUpload">
<el-button type="primary" size="small">
<el-icon><UploadFilled /></el-icon>
<el-icon>
<UploadFilled />
</el-icon>
上传附件
</el-button>
</el-upload>
</div>
</template>
<el-list v-if="taskDetail.attachments && taskDetail.attachments.length > 0">
<el-list-item v-for="attachment in taskDetail.attachments" :key="attachment.id">
<div class="attachment-item">
<div class="attachment-info">
<el-icon><Document /></el-icon>
<el-icon>
<Document />
</el-icon>
<div>
<div class="filename">{{ attachment.file_name }}</div>
<div class="filesize">{{ formatFileSize(attachment.file_size) }}</div>
</div>
</div>
<div class="attachment-actions">
<el-button
type="primary"
size="small"
link
@click="downloadAttachment(attachment)"
>
<el-button type="primary" size="small" link @click="downloadAttachment(attachment)">
下载
</el-button>
<el-button
type="danger"
size="small"
link
@click="removeDetailAttachment(attachment.id)"
>
<el-button type="danger" size="small" link @click="removeDetailAttachment(attachment.id)">
删除
</el-button>
</div>
</div>
</el-list-item>
</el-list>
<div v-else class="no-attachments">
暂无附件
</div>
@ -358,24 +295,13 @@
<template #header>
<span>评论 ({{ taskDetail.comments?.length || 0 }})</span>
</template>
<!-- 添加评论 -->
<div class="add-comment">
<el-input
v-model="newComment"
type="textarea"
:rows="3"
placeholder="添加评论..."
maxlength="500"
show-word-limit
/>
<el-input v-model="newComment" type="textarea" :rows="3" placeholder="添加评论..." maxlength="500"
show-word-limit />
<div class="comment-actions">
<el-button
type="primary"
size="small"
@click="addComment"
:disabled="!newComment.trim()"
>
<el-button type="primary" size="small" @click="addComment" :disabled="!newComment.trim()">
发表评论
</el-button>
</div>
@ -383,34 +309,24 @@
<!-- 评论列表 -->
<div class="comments-list" v-if="taskDetail.comments && taskDetail.comments.length > 0">
<div
v-for="comment in taskDetail.comments"
:key="comment.id"
class="comment-item"
>
<div v-for="comment in taskDetail.comments" :key="comment.id" class="comment-item">
<div class="comment-header">
<span class="comment-author">{{ comment.user_name }}</span>
<span class="comment-time">{{ formatDate(comment.created_at) }}</span>
<el-button
type="danger"
size="small"
link
@click="deleteComment(comment.id)"
v-if="canDeleteComment()"
>
<el-button type="danger" size="small" link @click="deleteComment(comment.id)" v-if="canDeleteComment()">
删除
</el-button>
</div>
<div class="comment-content">{{ comment.content }}</div>
</div>
</div>
<div v-else class="no-comments">
暂无评论
</div>
</el-card>
</div>
<template #footer>
<el-button @click="showTaskDetailDialog = false">关闭</el-button>
<el-button type="primary" @click="updateStatus(taskDetail)">更新状态</el-button>
@ -418,46 +334,33 @@
</el-dialog>
<!-- 附件查看对话框 -->
<el-dialog
v-model="showAttachmentsDialog"
title="任务附件"
width="500px"
>
<el-dialog v-model="showAttachmentsDialog" title="任务附件" width="500px">
<el-list>
<el-list-item
v-for="attachment in currentAttachments"
:key="attachment.id"
>
<el-list-item v-for="attachment in currentAttachments" :key="attachment.id">
<div class="attachment-item">
<div class="attachment-info">
<el-icon><Document /></el-icon>
<el-icon>
<Document />
</el-icon>
<span class="filename">{{ attachment.file_name }}</span>
<span class="filesize">({{ formatFileSize(attachment.file_size) }})</span>
</div>
<div class="attachment-actions">
<el-button
size="small"
type="primary"
@click="downloadAttachment(attachment.id)"
>
<el-button size="small" type="primary" @click="downloadAttachment(attachment.id)">
下载
</el-button>
<el-button
size="small"
type="danger"
@click="removeAttachment(attachment.id)"
>
<el-button size="small" type="danger" @click="removeAttachment(attachment.id)">
删除
</el-button>
</div>
</div>
</el-list-item>
</el-list>
<div v-if="currentAttachments.length === 0" class="no-attachments">
暂无附件
</div>
<template #footer>
<el-button @click="showAttachmentsDialog = false">关闭</el-button>
</template>
@ -466,16 +369,25 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { UploadFilled, Document } from '@element-plus/icons-vue'
import { store } from '@/store'
import { taskApi, userApi } from '@/api'
import type { Task, User } from '@/api'
const currentUser = computed(() => store.getters.currentUser)
// ID使
const getCurrentUserId = () => {
return currentUser.value.id || 1
}
const showCreateDialog = ref(false)
const editingTask = ref<any>(null)
const loading = ref(false)
const tasks = ref([])
const users = ref([])
const users = ref<User[]>([])
const uploadRef = ref()
const detailUploadRef = ref()
const fileList = ref([])
@ -504,7 +416,7 @@ const taskForm = reactive({
description: '',
type: '',
priority: 'medium',
assignee_id: null,
assignee_id: undefined as number | undefined,
start_time: '',
end_time: '',
attachments: [] as any[] //
@ -580,13 +492,15 @@ const loadTasks = async () => {
try {
const response: any = await taskApi.getTasks({
page: pagination.page,
size: pagination.size,
...searchForm
page_size: pagination.size,
search: searchForm.title,
status: searchForm.status,
priority: searchForm.priority
})
if (response.code === 200) {
tasks.value = response.data.list || []
pagination.total = response.data.total || 0
if (response && response.tasks) {
tasks.value = response.tasks || []
pagination.total = response.total || 0
}
} catch (error) {
console.error('Load tasks error:', error)
@ -635,16 +549,16 @@ const resetSearch = () => {
const handleUploadSuccess = (response: any, file: any) => {
console.log('Upload success - raw response:', response)
console.log('Upload success - file:', file)
//
if (!response || !response.data) {
ElMessage.error('文件上传响应格式错误')
console.error('Invalid upload response:', response)
return
}
ElMessage.success('文件上传成功')
// taskForm.attachments
// id
const attachmentData = {
@ -654,7 +568,7 @@ const handleUploadSuccess = (response: any, file: any) => {
file_type: response.data.mime_type || file.type || '',
// id
}
console.log('Adding new attachment data:', attachmentData)
taskForm.attachments.push(attachmentData)
}
@ -666,7 +580,7 @@ const handleUploadError = (error: any, file: any) => {
const handleFileRemove = async (file: any) => {
console.log('Removing file:', file)
//
const index = taskForm.attachments.findIndex(
(attachment: any) => {
@ -679,15 +593,15 @@ const handleFileRemove = async (file: any) => {
return attachment.id === file.uid
}
)
if (index > -1) {
const attachment = taskForm.attachments[index]
// idAPI
if (attachment.id) {
try {
console.log('Deleting existing attachment:', attachment.id)
await taskApi.deleteAttachment(attachment.id)
await taskApi.deleteTaskAttachment(attachment.id)
ElMessage.success(`附件 "${attachment.file_name}" 删除成功`)
} catch (error) {
console.error('Delete attachment error:', error)
@ -695,7 +609,7 @@ const handleFileRemove = async (file: any) => {
return //
}
}
console.log('Removing attachment at index:', index)
taskForm.attachments.splice(index, 1)
} else {
@ -718,7 +632,7 @@ const viewTask = async (task: any) => {
try {
loading.value = true
const response: any = await taskApi.getTask(task.id)
if (response.code === 200) {
taskDetail.value = response.data
showTaskDetailDialog.value = true
@ -746,18 +660,17 @@ const beforeUpload = (file: File) => {
const handleDetailUploadSuccess = async (response: any) => {
if (response.code === 200) {
//
const attachmentData = {
file_name: response.data.file_name,
file_path: response.data.file_path,
file_size: response.data.file_size,
file_type: response.data.file_type || 'application/octet-stream',
uploaded_by: 1 // TODO: ID
}
const formData = new FormData()
formData.append('file_name', response.data.file_name)
formData.append('file_path', response.data.file_path)
formData.append('file_size', response.data.file_size.toString())
formData.append('file_type', response.data.file_type || 'application/octet-stream')
formData.append('uploaded_by', getCurrentUserId().toString())
try {
await taskApi.addTaskAttachment(taskDetail.value.id, attachmentData)
await taskApi.addTaskAttachment(taskDetail.value.id, formData)
ElMessage.success('附件上传成功')
//
await viewTask(taskDetail.value)
} catch (error) {
@ -780,7 +693,7 @@ const removeDetailAttachment = async (attachmentId: number) => {
await taskApi.deleteTaskAttachment(attachmentId)
ElMessage.success('附件删除成功')
//
await viewTask(taskDetail.value)
} catch (error: any) {
@ -811,12 +724,12 @@ const addComment = async () => {
try {
await taskApi.addTaskComment(taskDetail.value.id, {
content: newComment.value.trim(),
user_id: 1 // TODO: ID
user_id: getCurrentUserId()
})
newComment.value = ''
ElMessage.success('评论添加成功')
//
await viewTask(taskDetail.value)
} catch (error) {
@ -836,7 +749,7 @@ const deleteComment = async (commentId: number) => {
await taskApi.deleteTaskComment(taskDetail.value.id, commentId)
ElMessage.success('评论删除成功')
//
await viewTask(taskDetail.value)
} catch (error: any) {
@ -866,20 +779,20 @@ const formatFileSize = (bytes: number) => {
const editTask = async (task: any) => {
editingTask.value = task
Object.assign(taskForm, task)
//
taskForm.attachments = []
fileList.value = []
//
if (task.id) {
try {
console.log('Loading attachments for task:', task.id)
const response: any = await taskApi.getTaskAttachments(task.id)
if (response.code === 200 && response.data && response.data.length > 0) {
console.log('Found attachments:', response.data)
// taskForm.attachments
taskForm.attachments = response.data.map((attachment: any) => ({
id: attachment.id, // ID
@ -888,7 +801,7 @@ const editTask = async (task: any) => {
file_size: attachment.file_size,
file_type: attachment.file_type
}))
// fileList
fileList.value = response.data.map((attachment: any) => ({
name: attachment.file_name,
@ -905,7 +818,7 @@ const editTask = async (task: any) => {
}
}
}))
console.log('Converted attachments to fileList:', fileList.value)
} else {
console.log('No attachments found for task')
@ -915,7 +828,7 @@ const editTask = async (task: any) => {
ElMessage.warning('加载任务附件失败')
}
}
showCreateDialog.value = true
}
@ -926,14 +839,15 @@ const saveTask = async () => {
ElMessage.error('任务标题不能为空')
return
}
console.log('Starting task save process...')
console.log('Current user info:', currentUser.value)
console.log('Current fileList:', fileList.value)
console.log('Current attachments:', taskForm.attachments)
//
const hasUnuploadedFiles = fileList.value.some((file: any) => !file.response)
if (hasUnuploadedFiles) {
console.log('Found unuploaded files, uploading...')
//
@ -943,68 +857,86 @@ const saveTask = async () => {
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
// 使
let userId = currentUser.value.id
let organizationId = currentUser.value.organizationId
if (!userId || !organizationId) {
console.warn('用户信息不完整,使用默认值', currentUser.value)
// 使fallback
userId = 1
organizationId = 1
// tokentoken
if (!localStorage.getItem('token')) {
ElMessage.error('用户未登录,请重新登录')
return
}
}
// -
const taskData = {
title: taskForm.title.trim(),
description: taskForm.description || '',
type: taskForm.type || '',
priority: taskForm.priority || 'medium',
assignee_id: taskForm.assignee_id || null,
start_time: taskForm.start_time || null,
end_time: taskForm.end_time || null,
organization_id: 1, //
creator_id: 1 //
assignee_id: taskForm.assignee_id || undefined,
start_time: taskForm.start_time || undefined,
end_time: taskForm.end_time || undefined,
organization_id: organizationId,
creator_id: userId
}
//
console.log('Sending task data:', taskData)
let response: any
if (editingTask.value) {
response = await taskApi.updateTask(editingTask.value.id, taskData)
} else {
response = await taskApi.createTask(taskData)
}
//
if (response && (response.code === 200 || response.code === 201)) {
const taskId = response.data.id
console.log('Task created/updated successfully, ID:', taskId)
//
if (editingTask.value) {
//
const newAttachments = taskForm.attachments.filter((attachment: any) => !attachment.id)
if (newAttachments.length > 0) {
console.log('Processing new attachments for existing task:', newAttachments)
for (let i = 0; i < newAttachments.length; i++) {
const attachment = newAttachments[i]
try {
console.log(`Adding new attachment ${i + 1}/${newAttachments.length}:`, attachment)
const attachmentResponse = await taskApi.addTaskAttachment(taskId, {
file_name: attachment.file_name,
file_path: attachment.file_path,
file_size: attachment.file_size,
file_type: attachment.file_type,
uploaded_by: 1
})
const formData = new FormData()
formData.append('file_name', attachment.file_name)
formData.append('file_path', attachment.file_path)
formData.append('file_size', attachment.file_size.toString())
formData.append('file_type', attachment.file_type)
formData.append('uploaded_by', getCurrentUserId().toString())
const attachmentResponse = await taskApi.addTaskAttachment(taskId, formData)
console.log(`New attachment ${i + 1} added successfully:`, attachmentResponse)
} catch (attachmentError: any) {
console.error(`Add new attachment ${i + 1} error:`, attachmentError)
let errorMsg = `附件 "${attachment.file_name}" 添加失败`
if (attachmentError.response?.data?.message) {
errorMsg += `: ${attachmentError.response.data.message}`
} else if (attachmentError.message) {
errorMsg += `: ${attachmentError.message}`
}
ElMessage.warning(errorMsg)
}
}
@ -1013,38 +945,39 @@ const saveTask = async () => {
//
if (taskForm.attachments && taskForm.attachments.length > 0) {
console.log('Processing attachments for new task:', taskForm.attachments)
for (let i = 0; i < taskForm.attachments.length; i++) {
const attachment = taskForm.attachments[i]
try {
console.log(`Adding attachment ${i + 1}/${taskForm.attachments.length}:`, attachment)
const attachmentResponse = await taskApi.addTaskAttachment(taskId, {
file_name: attachment.file_name,
file_path: attachment.file_path,
file_size: attachment.file_size,
file_type: attachment.file_type,
uploaded_by: 1
})
const formData = new FormData()
formData.append('file_name', attachment.file_name)
formData.append('file_path', attachment.file_path)
formData.append('file_size', attachment.file_size.toString())
formData.append('file_type', attachment.file_type)
formData.append('uploaded_by', getCurrentUserId().toString())
const attachmentResponse = await taskApi.addTaskAttachment(taskId, formData)
console.log(`Attachment ${i + 1} added successfully:`, attachmentResponse)
} catch (attachmentError: any) {
console.error(`Add attachment ${i + 1} error:`, attachmentError)
let errorMsg = `附件 "${attachment.file_name}" 添加失败`
if (attachmentError.response?.data?.message) {
errorMsg += `: ${attachmentError.response.data.message}`
} else if (attachmentError.message) {
errorMsg += `: ${attachmentError.message}`
}
ElMessage.warning(errorMsg)
}
}
}
}
ElMessage.success(editingTask.value ? '任务更新成功' : '任务创建成功')
} else {
// API
@ -1055,7 +988,7 @@ const saveTask = async () => {
}
} catch (error: any) {
console.error('Save task error:', error)
//
let errorMessage = '保存失败'
if (error.response && error.response.data) {
@ -1063,9 +996,9 @@ const saveTask = async () => {
} else if (error.message) {
errorMessage = error.message
}
ElMessage.error(errorMessage)
//
console.log('Full error details:', {
error,
@ -1076,7 +1009,7 @@ const saveTask = async () => {
} finally {
showCreateDialog.value = false
editingTask.value = null
//
Object.assign(taskForm, {
title: '',
@ -1088,10 +1021,10 @@ const saveTask = async () => {
end_time: '',
attachments: []
})
//
fileList.value = []
loadTasks()
}
}
@ -1110,12 +1043,12 @@ const deleteTask = async (task: any) => {
cancelButtonText: '取消',
type: 'warning'
})
console.log('Deleting task:', task)
// API
const response: any = await taskApi.deleteTask(task.id)
if (response && response.code === 200) {
ElMessage.success('任务删除成功')
loadTasks() //
@ -1129,9 +1062,9 @@ const deleteTask = async (task: any) => {
if (error === 'cancel' || error.action === 'cancel') {
return //
}
console.error('Delete task error:', error)
//
let errorMessage = '删除失败'
if (error.response && error.response.data) {
@ -1139,7 +1072,7 @@ const deleteTask = async (task: any) => {
} else if (error.message) {
errorMessage = error.message
}
ElMessage.error(errorMessage)
}
}
@ -1165,12 +1098,12 @@ const removeAttachment = async (attachmentId: number) => {
cancelButtonText: '取消',
type: 'warning'
})
const response: any = await taskApi.deleteTaskAttachment(attachmentId)
if (response.code === 200) {
ElMessage.success('附件删除成功')
//
const currentTask = tasks.value.find((task: any) =>
const currentTask = tasks.value.find((task: any) =>
task.attachments && task.attachments.some((att: any) => att.id === attachmentId)
)
if (currentTask) {
@ -1186,6 +1119,12 @@ const removeAttachment = async (attachmentId: number) => {
}
onMounted(() => {
// token
if (!localStorage.getItem('token')) {
ElMessage.error('用户未登录,请先登录')
return
}
loadTasks()
loadUsers()
})

Loading…
Cancel
Save