add: 任务列表

This commit is contained in:
ctwj
2025-08-09 23:47:30 +08:00
parent 76eb9c689b
commit 14f22f9128
18 changed files with 2256 additions and 334 deletions

View File

@@ -5,7 +5,6 @@ import (
"time"
"github.com/ctwj/urldb/db/entity"
"gorm.io/gorm"
)
@@ -32,7 +31,9 @@ type ResourceRepository interface {
FindExists(url string, excludeID ...uint) (bool, error)
BatchFindByURLs(urls []string) ([]entity.Resource, error)
GetResourcesForTransfer(panID uint, sinceTime time.Time, limit int) ([]*entity.Resource, error)
CreateResourceTag(resourceID, tagID uint) error
GetByURL(url string) (*entity.Resource, error)
UpdateSaveURL(id uint, saveURL string) error
CreateResourceTag(resourceTag *entity.ResourceTag) error
}
// ResourceRepositoryImpl Resource的Repository实现
@@ -432,11 +433,22 @@ func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime t
return resources, nil
}
// CreateResourceTag 创建资源与标签的关联
func (r *ResourceRepositoryImpl) CreateResourceTag(resourceID, tagID uint) error {
resourceTag := &entity.ResourceTag{
ResourceID: resourceID,
TagID: tagID,
// GetByURL 根据URL获取资源
func (r *ResourceRepositoryImpl) GetByURL(url string) (*entity.Resource, error) {
var resource entity.Resource
err := r.GetDB().Where("url = ?", url).First(&resource).Error
if err != nil {
return nil, err
}
return &resource, nil
}
// UpdateSaveURL 更新资源的转存链接
func (r *ResourceRepositoryImpl) UpdateSaveURL(id uint, saveURL string) error {
return r.GetDB().Model(&entity.Resource{}).Where("id = ?", id).Update("save_url", saveURL).Error
}
// CreateResourceTag 创建资源与标签的关联
func (r *ResourceRepositoryImpl) CreateResourceTag(resourceTag *entity.ResourceTag) error {
return r.GetDB().Create(resourceTag).Error
}

View File

@@ -7,52 +7,82 @@ import (
// TaskItemRepository 任务项仓库接口
type TaskItemRepository interface {
BaseRepository[entity.TaskItem]
FindByTaskID(taskID uint) ([]entity.TaskItem, error)
FindByTaskIDWithPagination(taskID uint, page, pageSize int) ([]entity.TaskItem, int64, error)
UpdateStatus(id uint, status entity.TaskItemStatus, errorMsg string) error
UpdateSuccess(id uint, outputData string) error
GetPendingItemsByTaskID(taskID uint) ([]entity.TaskItem, error)
BatchCreate(items []entity.TaskItem) error
GetByID(id uint) (*entity.TaskItem, error)
Create(item *entity.TaskItem) error
Delete(id uint) error
DeleteByTaskID(taskID uint) error
GetByTaskIDAndStatus(taskID uint, status string) ([]*entity.TaskItem, error)
GetListByTaskID(taskID uint, page, pageSize int, status string) ([]*entity.TaskItem, int64, error)
UpdateStatus(id uint, status string) error
UpdateStatusAndOutput(id uint, status, outputData string) error
GetStatsByTaskID(taskID uint) (map[string]int, error)
}
// TaskItemRepositoryImpl 任务项仓库实现
type TaskItemRepositoryImpl struct {
BaseRepositoryImpl[entity.TaskItem]
db *gorm.DB
}
// NewTaskItemRepository 创建任务项仓库
func NewTaskItemRepository(db *gorm.DB) TaskItemRepository {
return &TaskItemRepositoryImpl{
BaseRepositoryImpl: BaseRepositoryImpl[entity.TaskItem]{db: db},
db: db,
}
}
// FindByTaskID 根据任务ID查找所有任务项
func (r *TaskItemRepositoryImpl) FindByTaskID(taskID uint) ([]entity.TaskItem, error) {
var items []entity.TaskItem
err := r.db.Where("task_id = ?", taskID).Order("created_at ASC").Find(&items).Error
// GetByID 根据ID获取任务项
func (r *TaskItemRepositoryImpl) GetByID(id uint) (*entity.TaskItem, error) {
var item entity.TaskItem
err := r.db.First(&item, id).Error
if err != nil {
return nil, err
}
return &item, nil
}
// Create 创建任务项
func (r *TaskItemRepositoryImpl) Create(item *entity.TaskItem) error {
return r.db.Create(item).Error
}
// Delete 删除任务项
func (r *TaskItemRepositoryImpl) Delete(id uint) error {
return r.db.Delete(&entity.TaskItem{}, id).Error
}
// DeleteByTaskID 根据任务ID删除所有任务项
func (r *TaskItemRepositoryImpl) DeleteByTaskID(taskID uint) error {
return r.db.Where("task_id = ?", taskID).Delete(&entity.TaskItem{}).Error
}
// GetByTaskIDAndStatus 根据任务ID和状态获取任务项
func (r *TaskItemRepositoryImpl) GetByTaskIDAndStatus(taskID uint, status string) ([]*entity.TaskItem, error) {
var items []*entity.TaskItem
err := r.db.Where("task_id = ? AND status = ?", taskID, status).Order("id ASC").Find(&items).Error
return items, err
}
// FindByTaskIDWithPagination 根据任务ID分页查找任务项
func (r *TaskItemRepositoryImpl) FindByTaskIDWithPagination(taskID uint, page, pageSize int) ([]entity.TaskItem, int64, error) {
var items []entity.TaskItem
// GetListByTaskID 根据任务ID分页获取任务项
func (r *TaskItemRepositoryImpl) GetListByTaskID(taskID uint, page, pageSize int, status string) ([]*entity.TaskItem, int64, error) {
var items []*entity.TaskItem
var total int64
query := r.db.Model(&entity.TaskItem{}).Where("task_id = ?", taskID)
// 添加状态过滤
if status != "" {
query = query.Where("status = ?", status)
}
// 获取总数
err := r.db.Model(&entity.TaskItem{}).Where("task_id = ?", taskID).Count(&total).Error
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * pageSize
err = r.db.Where("task_id = ?", taskID).
Order("created_at ASC").
Offset(offset).
Limit(pageSize).
Find(&items).Error
err = query.Offset(offset).Limit(pageSize).Order("item_index ASC").Find(&items).Error
if err != nil {
return nil, 0, err
}
@@ -61,43 +91,47 @@ func (r *TaskItemRepositoryImpl) FindByTaskIDWithPagination(taskID uint, page, p
}
// UpdateStatus 更新任务项状态
func (r *TaskItemRepositoryImpl) UpdateStatus(id uint, status entity.TaskItemStatus, errorMsg string) error {
updates := map[string]interface{}{
"status": status,
}
if errorMsg != "" {
updates["error_message"] = errorMsg
}
if status != entity.TaskItemStatusPending {
updates["processed_at"] = gorm.Expr("CURRENT_TIMESTAMP")
}
return r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Updates(updates).Error
func (r *TaskItemRepositoryImpl) UpdateStatus(id uint, status string) error {
return r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Update("status", status).Error
}
// UpdateSuccess 更新任务项为成功状态
func (r *TaskItemRepositoryImpl) UpdateSuccess(id uint, outputData string) error {
updates := map[string]interface{}{
"status": entity.TaskItemStatusSuccess,
"output_data": outputData,
"processed_at": gorm.Expr("CURRENT_TIMESTAMP"),
// UpdateStatusAndOutput 更新任务项状态和输出数据
func (r *TaskItemRepositoryImpl) UpdateStatusAndOutput(id uint, status, outputData string) error {
return r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Updates(map[string]interface{}{
"status": status,
"output_data": outputData,
}).Error
}
// GetStatsByTaskID 获取任务项统计信息
func (r *TaskItemRepositoryImpl) GetStatsByTaskID(taskID uint) (map[string]int, error) {
var results []struct {
Status string
Count int
}
return r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Updates(updates).Error
}
err := r.db.Model(&entity.TaskItem{}).
Select("status, count(*) as count").
Where("task_id = ?", taskID).
Group("status").
Find(&results).Error
// GetPendingItemsByTaskID 获取任务的待处理项目
func (r *TaskItemRepositoryImpl) GetPendingItemsByTaskID(taskID uint) ([]entity.TaskItem, error) {
var items []entity.TaskItem
err := r.db.Where("task_id = ? AND status = ?", taskID, entity.TaskItemStatusPending).
Order("created_at ASC").
Find(&items).Error
return items, err
}
if err != nil {
return nil, err
}
// BatchCreate 批量创建任务项
func (r *TaskItemRepositoryImpl) BatchCreate(items []entity.TaskItem) error {
return r.db.CreateInBatches(items, 100).Error
stats := map[string]int{
"total": 0,
"pending": 0,
"processing": 0,
"completed": 0,
"failed": 0,
}
for _, result := range results {
stats[result.Status] = result.Count
stats["total"] += result.Count
}
return stats, nil
}

View File

@@ -7,50 +7,71 @@ import (
// TaskRepository 任务仓库接口
type TaskRepository interface {
BaseRepository[entity.Task]
FindWithItems(id uint) (*entity.Task, error)
FindWithPagination(page, pageSize int) ([]entity.Task, int64, error)
UpdateProgress(id uint, processed, success, failed int) error
UpdateStatus(id uint, status entity.TaskStatus) error
GetRunningTasks() ([]entity.Task, error)
GetByID(id uint) (*entity.Task, error)
Create(task *entity.Task) error
Delete(id uint) error
GetList(page, pageSize int, taskType, status string) ([]*entity.Task, int64, error)
UpdateStatus(id uint, status string) error
UpdateProgress(id uint, progress float64, progressData string) error
UpdateStatusAndMessage(id uint, status, message string) error
}
// TaskRepositoryImpl 任务仓库实现
type TaskRepositoryImpl struct {
BaseRepositoryImpl[entity.Task]
db *gorm.DB
}
// NewTaskRepository 创建任务仓库
func NewTaskRepository(db *gorm.DB) TaskRepository {
return &TaskRepositoryImpl{
BaseRepositoryImpl: BaseRepositoryImpl[entity.Task]{db: db},
db: db,
}
}
// FindWithItems 查找任务及其所有项目
func (r *TaskRepositoryImpl) FindWithItems(id uint) (*entity.Task, error) {
// GetByID 根据ID获取任务
func (r *TaskRepositoryImpl) GetByID(id uint) (*entity.Task, error) {
var task entity.Task
err := r.db.Preload("TaskItems").First(&task, id).Error
err := r.db.First(&task, id).Error
if err != nil {
return nil, err
}
return &task, nil
}
// FindWithPagination 分页查询任务
func (r *TaskRepositoryImpl) FindWithPagination(page, pageSize int) ([]entity.Task, int64, error) {
var tasks []entity.Task
// Create 创建任务
func (r *TaskRepositoryImpl) Create(task *entity.Task) error {
return r.db.Create(task).Error
}
// Delete 删除任务
func (r *TaskRepositoryImpl) Delete(id uint) error {
return r.db.Delete(&entity.Task{}, id).Error
}
// GetList 获取任务列表
func (r *TaskRepositoryImpl) GetList(page, pageSize int, taskType, status string) ([]*entity.Task, int64, error) {
var tasks []*entity.Task
var total int64
query := r.db.Model(&entity.Task{})
// 添加过滤条件
if taskType != "" {
query = query.Where("task_type = ?", taskType)
}
if status != "" {
query = query.Where("status = ?", status)
}
// 获取总数
err := r.db.Model(&entity.Task{}).Count(&total).Error
err := query.Count(&total).Error
if err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * pageSize
err = r.db.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tasks).Error
err = query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&tasks).Error
if err != nil {
return nil, 0, err
}
@@ -58,40 +79,23 @@ func (r *TaskRepositoryImpl) FindWithPagination(page, pageSize int) ([]entity.Ta
return tasks, total, nil
}
// UpdateStatus 更新任务状态
func (r *TaskRepositoryImpl) UpdateStatus(id uint, status string) error {
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
}
// UpdateProgress 更新任务进度
func (r *TaskRepositoryImpl) UpdateProgress(id uint, processed, success, failed int) error {
func (r *TaskRepositoryImpl) UpdateProgress(id uint, progress float64, progressData string) error {
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
"processed_items": processed,
"success_items": success,
"failed_items": failed,
"progress": progress,
"progress_data": progressData,
}).Error
}
// UpdateStatus 更新任务状态
func (r *TaskRepositoryImpl) UpdateStatus(id uint, status entity.TaskStatus) error {
updates := map[string]interface{}{
"status": status,
}
// 如果状态为运行中,设置开始时间
if status == entity.TaskStatusRunning {
updates["started_at"] = gorm.Expr("CURRENT_TIMESTAMP")
}
// 如果状态为完成或失败,设置完成时间
if status == entity.TaskStatusCompleted || status == entity.TaskStatusFailed {
updates["completed_at"] = gorm.Expr("CURRENT_TIMESTAMP")
}
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(updates).Error
}
// GetRunningTasks 获取正在运行的任务
func (r *TaskRepositoryImpl) GetRunningTasks() ([]entity.Task, error) {
var tasks []entity.Task
err := r.db.Where("status IN ?", []entity.TaskStatus{
entity.TaskStatusRunning,
entity.TaskStatusPending,
}).Find(&tasks).Error
return tasks, err
// UpdateStatusAndMessage 更新任务状态和消息
func (r *TaskRepositoryImpl) UpdateStatusAndMessage(id uint, status, message string) error {
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
"status": status,
"message": message,
}).Error
}

340
handlers/task_handler.go Normal file
View File

@@ -0,0 +1,340 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/task"
"github.com/ctwj/urldb/utils"
"github.com/gin-gonic/gin"
)
// TaskHandler 任务处理器
type TaskHandler struct {
repoMgr *repo.RepositoryManager
taskManager *task.TaskManager
}
// NewTaskHandler 创建任务处理器
func NewTaskHandler(repoMgr *repo.RepositoryManager, taskManager *task.TaskManager) *TaskHandler {
return &TaskHandler{
repoMgr: repoMgr,
taskManager: taskManager,
}
}
// 批量转存任务资源项
type BatchTransferResource struct {
Title string `json:"title" binding:"required"`
URL string `json:"url" binding:"required"`
CategoryID uint `json:"category_id,omitempty"`
Tags []uint `json:"tags,omitempty"`
}
// CreateBatchTransferTask 创建批量转存任务
func (h *TaskHandler) CreateBatchTransferTask(c *gin.Context) {
var req struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Resources []BatchTransferResource `json:"resources" binding:"required,min=1"`
}
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, "参数错误: "+err.Error(), http.StatusBadRequest)
return
}
utils.Info("创建批量转存任务: %s资源数量: %d", req.Title, len(req.Resources))
// 创建任务
newTask := &entity.Task{
Title: req.Title,
Description: req.Description,
Type: "transfer",
Status: "pending",
TotalItems: len(req.Resources),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := h.repoMgr.TaskRepository.Create(newTask)
if err != nil {
utils.Error("创建任务失败: %v", err)
ErrorResponse(c, "创建任务失败: "+err.Error(), http.StatusInternalServerError)
return
}
// 创建任务项
for _, resource := range req.Resources {
// 构建转存输入数据
transferInput := task.TransferInput{
Title: resource.Title,
URL: resource.URL,
CategoryID: resource.CategoryID,
Tags: resource.Tags,
}
inputJSON, _ := json.Marshal(transferInput)
taskItem := &entity.TaskItem{
TaskID: newTask.ID,
Status: "pending",
InputData: string(inputJSON),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = h.repoMgr.TaskItemRepository.Create(taskItem)
if err != nil {
utils.Error("创建任务项失败: %v", err)
// 继续创建其他任务项
}
}
utils.Info("批量转存任务创建完成: %d, 共 %d 项", newTask.ID, len(req.Resources))
SuccessResponse(c, gin.H{
"task_id": newTask.ID,
"total_items": len(req.Resources),
"message": "任务创建成功",
})
}
// StartTask 启动任务
func (h *TaskHandler) StartTask(c *gin.Context) {
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的任务ID: "+err.Error(), http.StatusBadRequest)
return
}
utils.Info("启动任务: %d", taskID)
err = h.taskManager.StartTask(uint(taskID))
if err != nil {
utils.Error("启动任务失败: %v", err)
ErrorResponse(c, "启动任务失败: "+err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"message": "任务启动成功",
})
}
// StopTask 停止任务
func (h *TaskHandler) StopTask(c *gin.Context) {
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的任务ID: "+err.Error(), http.StatusBadRequest)
return
}
utils.Info("停止任务: %d", taskID)
err = h.taskManager.StopTask(uint(taskID))
if err != nil {
utils.Error("停止任务失败: %v", err)
ErrorResponse(c, "停止任务失败: "+err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"message": "任务停止成功",
})
}
// GetTaskStatus 获取任务状态
func (h *TaskHandler) GetTaskStatus(c *gin.Context) {
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的任务ID: "+err.Error(), http.StatusBadRequest)
return
}
// 获取任务详情
task, err := h.repoMgr.TaskRepository.GetByID(uint(taskID))
if err != nil {
ErrorResponse(c, "任务不存在: "+err.Error(), http.StatusNotFound)
return
}
// 获取任务项统计
stats, err := h.repoMgr.TaskItemRepository.GetStatsByTaskID(uint(taskID))
if err != nil {
utils.Error("获取任务项统计失败: %v", err)
stats = map[string]int{
"total": 0,
"pending": 0,
"processing": 0,
"completed": 0,
"failed": 0,
}
}
// 检查任务是否在运行
isRunning := h.taskManager.IsTaskRunning(uint(taskID))
SuccessResponse(c, gin.H{
"id": task.ID,
"title": task.Title,
"description": task.Description,
"task_type": task.Type,
"status": task.Status,
"total_items": task.TotalItems,
"is_running": isRunning,
"stats": stats,
"created_at": task.CreatedAt,
"updated_at": task.UpdatedAt,
})
}
// GetTasks 获取任务列表
func (h *TaskHandler) GetTasks(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
taskType := c.Query("task_type")
status := c.Query("status")
utils.Info("GetTasks: 获取任务列表 page=%d, pageSize=%d, taskType=%s, status=%s", page, pageSize, taskType, status)
tasks, total, err := h.repoMgr.TaskRepository.GetList(page, pageSize, taskType, status)
if err != nil {
utils.Error("获取任务列表失败: %v", err)
ErrorResponse(c, "获取任务列表失败: "+err.Error(), http.StatusInternalServerError)
return
}
utils.Info("GetTasks: 从数据库获取到 %d 个任务", len(tasks))
// 为每个任务添加运行状态
var result []gin.H
for _, task := range tasks {
isRunning := h.taskManager.IsTaskRunning(task.ID)
utils.Info("GetTasks: 任务 %d (%s) 数据库状态: %s, TaskManager运行状态: %v", task.ID, task.Title, task.Status, isRunning)
result = append(result, gin.H{
"id": task.ID,
"title": task.Title,
"description": task.Description,
"task_type": task.Type,
"status": task.Status,
"total_items": task.TotalItems,
"processed_items": task.ProcessedItems,
"success_items": task.SuccessItems,
"failed_items": task.FailedItems,
"is_running": isRunning,
"created_at": task.CreatedAt,
"updated_at": task.UpdatedAt,
})
}
SuccessResponse(c, gin.H{
"items": result,
"total": total,
"page": page,
"size": pageSize,
})
}
// GetTaskItems 获取任务项列表
func (h *TaskHandler) GetTaskItems(c *gin.Context) {
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的任务ID: "+err.Error(), http.StatusBadRequest)
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
status := c.Query("status")
items, total, err := h.repoMgr.TaskItemRepository.GetListByTaskID(uint(taskID), page, pageSize, status)
if err != nil {
utils.Error("获取任务项列表失败: %v", err)
ErrorResponse(c, "获取任务项列表失败: "+err.Error(), http.StatusInternalServerError)
return
}
// 解析输入和输出数据
var result []gin.H
for _, item := range items {
itemData := gin.H{
"id": item.ID,
"status": item.Status,
"created_at": item.CreatedAt,
"updated_at": item.UpdatedAt,
}
// 解析输入数据
if item.InputData != "" {
var inputData map[string]interface{}
if err := json.Unmarshal([]byte(item.InputData), &inputData); err == nil {
itemData["input"] = inputData
}
}
// 解析输出数据
if item.OutputData != "" {
var outputData map[string]interface{}
if err := json.Unmarshal([]byte(item.OutputData), &outputData); err == nil {
itemData["output"] = outputData
}
}
result = append(result, itemData)
}
SuccessResponse(c, gin.H{
"items": result,
"total": total,
"page": page,
"size": pageSize,
})
}
// DeleteTask 删除任务
func (h *TaskHandler) DeleteTask(c *gin.Context) {
taskIDStr := c.Param("id")
taskID, err := strconv.ParseUint(taskIDStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的任务ID: "+err.Error(), http.StatusBadRequest)
return
}
// 检查任务是否在运行
if h.taskManager.IsTaskRunning(uint(taskID)) {
ErrorResponse(c, "任务正在运行,请先停止任务", http.StatusBadRequest)
return
}
// 删除任务项
err = h.repoMgr.TaskItemRepository.DeleteByTaskID(uint(taskID))
if err != nil {
utils.Error("删除任务项失败: %v", err)
ErrorResponse(c, "删除任务项失败: "+err.Error(), http.StatusInternalServerError)
return
}
// 删除任务
err = h.repoMgr.TaskRepository.Delete(uint(taskID))
if err != nil {
utils.Error("删除任务失败: %v", err)
ErrorResponse(c, "删除任务失败: "+err.Error(), http.StatusInternalServerError)
return
}
utils.Info("任务删除成功: %d", taskID)
SuccessResponse(c, gin.H{
"message": "任务删除成功",
})
}

87
main.go
View File

@@ -4,14 +4,12 @@ import (
"log"
"os"
"github.com/ctwj/urldb/scheduler"
"github.com/ctwj/urldb/utils"
"github.com/ctwj/urldb/db"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/handlers"
"github.com/ctwj/urldb/middleware"
"github.com/ctwj/urldb/task"
"github.com/ctwj/urldb/utils"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
@@ -70,56 +68,14 @@ func main() {
// 创建Repository管理器
repoManager := repo.NewRepositoryManager(db.DB)
// 创建全局调度
scheduler := scheduler.GetGlobalScheduler(
repoManager.HotDramaRepository,
repoManager.ReadyResourceRepository,
repoManager.ResourceRepository,
repoManager.SystemConfigRepository,
repoManager.PanRepository,
repoManager.CksRepository,
repoManager.TagRepository,
repoManager.CategoryRepository,
)
// 创建任务管理
taskManager := task.NewTaskManager(repoManager)
// 确保默认配置存在
// _, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
// if err != nil {
// utils.Error("初始化默认配置失败: %v", err)
// } else {
// utils.Info("默认配置初始化完成")
// }
// 注册转存任务处理器
transferProcessor := task.NewTransferProcessor(repoManager)
taskManager.RegisterProcessor(transferProcessor)
// 检查系统配置,决定是否启动各种自动任务
autoProcessReadyResources, err := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoProcessReadyResources)
if err != nil {
utils.Error("获取自动处理待处理资源配置失败: %v", err)
} else if autoProcessReadyResources {
scheduler.StartReadyResourceScheduler()
utils.Info("已启动待处理资源自动处理任务")
} else {
utils.Info("系统配置中自动处理待处理资源功能已禁用,跳过启动定时任务")
}
autoFetchHotDramaEnabled, err := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoFetchHotDramaEnabled)
if err != nil {
utils.Error("获取自动拉取热播剧配置失败: %v", err)
} else if autoFetchHotDramaEnabled {
scheduler.StartHotDramaScheduler()
utils.Info("已启动热播剧自动拉取任务")
} else {
utils.Info("系统配置中自动拉取热播剧功能已禁用,跳过启动定时任务")
}
// autoTransferEnabled, err := repoManager.SystemConfigRepository.GetConfigBool(entity.ConfigKeyAutoTransferEnabled)
// if err != nil {
// utils.Error("获取自动转存配置失败: %v", err)
// } else if autoTransferEnabled {
// scheduler.StartAutoTransferScheduler()
// utils.Info("已启动自动转存任务")
// } else {
// utils.Info("系统配置中自动转存功能已禁用,跳过启动定时任务")
// }
utils.Info("任务管理器初始化完成")
// 创建Gin实例
r := gin.Default()
@@ -140,6 +96,9 @@ func main() {
// 创建公开API处理器
publicAPIHandler := handlers.NewPublicAPIHandler()
// 创建任务处理器
taskHandler := handlers.NewTaskHandler(repoManager, taskManager)
// API路由
api := r.Group("/api")
{
@@ -255,22 +214,14 @@ func main() {
api.PUT("/hot-dramas/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateHotDrama)
api.DELETE("/hot-dramas/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteHotDrama)
// 调度器管理路由(查询接口无需认证)
api.GET("/scheduler/status", handlers.GetSchedulerStatus)
api.GET("/scheduler/hot-drama/names", handlers.FetchHotDramaNames)
api.POST("/scheduler/hot-drama/start", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.StartHotDramaScheduler)
api.POST("/scheduler/hot-drama/stop", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.StopHotDramaScheduler)
api.POST("/scheduler/hot-drama/trigger", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.TriggerHotDramaScheduler)
// 待处理资源自动处理管理路由
api.POST("/scheduler/ready-resource/start", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.StartReadyResourceScheduler)
api.POST("/scheduler/ready-resource/stop", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.StopReadyResourceScheduler)
api.POST("/scheduler/ready-resource/trigger", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.TriggerReadyResourceScheduler)
// 自动转存管理路由
api.POST("/scheduler/auto-transfer/start", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.StartAutoTransferScheduler)
api.POST("/scheduler/auto-transfer/stop", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.StopAutoTransferScheduler)
api.POST("/scheduler/auto-transfer/trigger", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.TriggerAutoTransferScheduler)
// 任务管理路由
api.POST("/tasks/transfer", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.CreateBatchTransferTask)
api.GET("/tasks", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.GetTasks)
api.GET("/tasks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.GetTaskStatus)
api.POST("/tasks/:id/start", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.StartTask)
api.POST("/tasks/:id/stop", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.StopTask)
api.DELETE("/tasks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.DeleteTask)
api.GET("/tasks/:id/items", middleware.AuthMiddleware(), middleware.AdminMiddleware(), taskHandler.GetTaskItems)
// 版本管理路由
api.GET("/version", handlers.GetVersion)

View File

@@ -324,7 +324,11 @@ func (r *ReadyResourceScheduler) convertReadyResourceToResource(readyResource en
// 创建资源标签关联
for _, tagID := range tagIDs {
err = r.resourceRepo.CreateResourceTag(resource.ID, tagID)
resourceTag := &entity.ResourceTag{
ResourceID: resource.ID,
TagID: tagID,
}
err = r.resourceRepo.CreateResourceTag(resourceTag)
if err != nil {
utils.Error(fmt.Sprintf("创建资源标签关联失败: %v", err))
}

278
task/task_processor.go Normal file
View File

@@ -0,0 +1,278 @@
package task
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/utils"
)
// TaskProcessor 任务处理器接口
type TaskProcessor interface {
Process(ctx context.Context, taskID uint, item *entity.TaskItem) error
GetTaskType() string
}
// TaskManager 任务管理器
type TaskManager struct {
processors map[string]TaskProcessor
repoMgr *repo.RepositoryManager
mu sync.RWMutex
running map[uint]context.CancelFunc // 正在运行的任务
}
// NewTaskManager 创建任务管理器
func NewTaskManager(repoMgr *repo.RepositoryManager) *TaskManager {
return &TaskManager{
processors: make(map[string]TaskProcessor),
repoMgr: repoMgr,
running: make(map[uint]context.CancelFunc),
}
}
// RegisterProcessor 注册任务处理器
func (tm *TaskManager) RegisterProcessor(processor TaskProcessor) {
tm.mu.Lock()
defer tm.mu.Unlock()
tm.processors[processor.GetTaskType()] = processor
utils.Info("注册任务处理器: %s", processor.GetTaskType())
}
// getRegisteredProcessors 获取已注册的处理器列表(用于调试)
func (tm *TaskManager) getRegisteredProcessors() []string {
var types []string
for taskType := range tm.processors {
types = append(types, taskType)
}
return types
}
// StartTask 启动任务
func (tm *TaskManager) StartTask(taskID uint) error {
tm.mu.Lock()
defer tm.mu.Unlock()
utils.Info("StartTask: 尝试启动任务 %d", taskID)
// 检查任务是否已在运行
if _, exists := tm.running[taskID]; exists {
utils.Info("任务 %d 已在运行中", taskID)
return fmt.Errorf("任务 %d 已在运行中", taskID)
}
// 获取任务信息
task, err := tm.repoMgr.TaskRepository.GetByID(taskID)
if err != nil {
utils.Error("获取任务失败: %v", err)
return fmt.Errorf("获取任务失败: %v", err)
}
utils.Info("StartTask: 获取到任务 %d, 类型: %s, 状态: %s", task.ID, task.Type, task.Status)
// 获取处理器
processor, exists := tm.processors[string(task.Type)]
if !exists {
utils.Error("未找到任务类型 %s 的处理器, 已注册的处理器: %v", task.Type, tm.getRegisteredProcessors())
return fmt.Errorf("未找到任务类型 %s 的处理器", task.Type)
}
utils.Info("StartTask: 找到处理器 %s", task.Type)
// 创建上下文
ctx, cancel := context.WithCancel(context.Background())
tm.running[taskID] = cancel
utils.Info("StartTask: 启动后台任务协程")
// 启动后台任务
go tm.processTask(ctx, task, processor)
utils.Info("StartTask: 任务 %d 启动成功", taskID)
return nil
}
// StopTask 停止任务
func (tm *TaskManager) StopTask(taskID uint) error {
tm.mu.Lock()
defer tm.mu.Unlock()
cancel, exists := tm.running[taskID]
if !exists {
return fmt.Errorf("任务 %d 未在运行", taskID)
}
cancel()
delete(tm.running, taskID)
// 更新任务状态为暂停
err := tm.repoMgr.TaskRepository.UpdateStatus(taskID, "paused")
if err != nil {
utils.Error("更新任务状态失败: %v", err)
}
return nil
}
// processTask 处理任务
func (tm *TaskManager) processTask(ctx context.Context, task *entity.Task, processor TaskProcessor) {
defer func() {
tm.mu.Lock()
delete(tm.running, task.ID)
tm.mu.Unlock()
utils.Info("processTask: 任务 %d 处理完成,清理资源", task.ID)
}()
utils.Info("processTask: 开始处理任务: %d, 类型: %s", task.ID, task.Type)
// 更新任务状态为运行中
err := tm.repoMgr.TaskRepository.UpdateStatus(task.ID, "running")
if err != nil {
utils.Error("更新任务状态失败: %v", err)
return
}
// 获取待处理的任务项
items, err := tm.repoMgr.TaskItemRepository.GetByTaskIDAndStatus(task.ID, "pending")
if err != nil {
utils.Error("获取任务项失败: %v", err)
tm.markTaskFailed(task.ID, fmt.Sprintf("获取任务项失败: %v", err))
return
}
totalItems := len(items)
processedItems := 0
successItems := 0
failedItems := 0
utils.Info("任务 %d 共有 %d 个待处理项", task.ID, totalItems)
for _, item := range items {
select {
case <-ctx.Done():
utils.Info("任务 %d 被取消", task.ID)
return
default:
// 处理单个任务项
err := tm.processTaskItem(ctx, task.ID, item, processor)
processedItems++
if err != nil {
failedItems++
utils.Error("处理任务项 %d 失败: %v", item.ID, err)
} else {
successItems++
}
// 更新任务进度
progress := float64(processedItems) / float64(totalItems) * 100
tm.updateTaskProgress(task.ID, progress, processedItems, successItems, failedItems)
}
}
// 任务完成
status := "completed"
message := fmt.Sprintf("任务完成,共处理 %d 项,成功 %d 项,失败 %d 项", processedItems, successItems, failedItems)
if failedItems > 0 && successItems == 0 {
status = "failed"
message = fmt.Sprintf("任务失败,共处理 %d 项,全部失败", processedItems)
} else if failedItems > 0 {
status = "partial_success"
message = fmt.Sprintf("任务部分成功,共处理 %d 项,成功 %d 项,失败 %d 项", processedItems, successItems, failedItems)
}
err = tm.repoMgr.TaskRepository.UpdateStatusAndMessage(task.ID, status, message)
if err != nil {
utils.Error("更新任务状态失败: %v", err)
}
utils.Info("任务 %d 处理完成: %s", task.ID, message)
}
// processTaskItem 处理单个任务项
func (tm *TaskManager) processTaskItem(ctx context.Context, taskID uint, item *entity.TaskItem, processor TaskProcessor) error {
// 更新任务项状态为处理中
err := tm.repoMgr.TaskItemRepository.UpdateStatus(item.ID, "processing")
if err != nil {
return fmt.Errorf("更新任务项状态失败: %v", err)
}
// 处理任务项
err = processor.Process(ctx, taskID, item)
if err != nil {
// 处理失败
outputData := map[string]interface{}{
"error": err.Error(),
"time": time.Now(),
}
outputJSON, _ := json.Marshal(outputData)
updateErr := tm.repoMgr.TaskItemRepository.UpdateStatusAndOutput(item.ID, "failed", string(outputJSON))
if updateErr != nil {
utils.Error("更新失败任务项状态失败: %v", updateErr)
}
return err
}
// 处理成功
outputData := map[string]interface{}{
"success": true,
"time": time.Now(),
}
outputJSON, _ := json.Marshal(outputData)
err = tm.repoMgr.TaskItemRepository.UpdateStatusAndOutput(item.ID, "completed", string(outputJSON))
if err != nil {
utils.Error("更新成功任务项状态失败: %v", err)
}
return nil
}
// updateTaskProgress 更新任务进度
func (tm *TaskManager) updateTaskProgress(taskID uint, progress float64, processed, success, failed int) {
progressData := map[string]interface{}{
"progress": progress,
"processed": processed,
"success": success,
"failed": failed,
"time": time.Now(),
}
progressJSON, _ := json.Marshal(progressData)
err := tm.repoMgr.TaskRepository.UpdateProgress(taskID, progress, string(progressJSON))
if err != nil {
utils.Error("更新任务进度失败: %v", err)
}
}
// markTaskFailed 标记任务失败
func (tm *TaskManager) markTaskFailed(taskID uint, message string) {
err := tm.repoMgr.TaskRepository.UpdateStatusAndMessage(taskID, "failed", message)
if err != nil {
utils.Error("标记任务失败状态失败: %v", err)
}
}
// GetTaskStatus 获取任务状态
func (tm *TaskManager) GetTaskStatus(taskID uint) (string, error) {
task, err := tm.repoMgr.TaskRepository.GetByID(taskID)
if err != nil {
return "", err
}
return string(task.Status), nil
}
// IsTaskRunning 检查任务是否在运行
func (tm *TaskManager) IsTaskRunning(taskID uint) bool {
tm.mu.RLock()
defer tm.mu.RUnlock()
_, exists := tm.running[taskID]
return exists
}

258
task/transfer_processor.go Normal file
View File

@@ -0,0 +1,258 @@
package task
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/utils"
)
// TransferProcessor 转存任务处理器
type TransferProcessor struct {
repoMgr *repo.RepositoryManager
}
// NewTransferProcessor 创建转存任务处理器
func NewTransferProcessor(repoMgr *repo.RepositoryManager) *TransferProcessor {
return &TransferProcessor{
repoMgr: repoMgr,
}
}
// GetTaskType 获取任务类型
func (tp *TransferProcessor) GetTaskType() string {
return "transfer"
}
// TransferInput 转存任务输入数据结构
type TransferInput struct {
Title string `json:"title"`
URL string `json:"url"`
CategoryID uint `json:"category_id"`
Tags []uint `json:"tags"`
}
// TransferOutput 转存任务输出数据结构
type TransferOutput struct {
ResourceID uint `json:"resource_id,omitempty"`
SaveURL string `json:"save_url,omitempty"`
Error string `json:"error,omitempty"`
Success bool `json:"success"`
Time string `json:"time"`
}
// Process 处理转存任务项
func (tp *TransferProcessor) Process(ctx context.Context, taskID uint, item *entity.TaskItem) error {
utils.Info("开始处理转存任务项: %d", item.ID)
// 解析输入数据
var input TransferInput
if err := json.Unmarshal([]byte(item.InputData), &input); err != nil {
return fmt.Errorf("解析输入数据失败: %v", err)
}
// 验证输入数据
if err := tp.validateInput(&input); err != nil {
return fmt.Errorf("输入数据验证失败: %v", err)
}
// 检查资源是否已存在
exists, existingResource, err := tp.checkResourceExists(input.URL)
if err != nil {
utils.Error("检查资源是否存在失败: %v", err)
}
if exists {
// 资源已存在,更新输出数据
output := TransferOutput{
ResourceID: existingResource.ID,
SaveURL: existingResource.SaveURL,
Success: true,
Time: time.Now().Format("2006-01-02 15:04:05"),
}
outputJSON, _ := json.Marshal(output)
item.OutputData = string(outputJSON)
utils.Info("资源已存在,跳过转存: %s", input.Title)
return nil
}
// 执行转存操作
resourceID, saveURL, err := tp.performTransfer(ctx, &input)
if err != nil {
return fmt.Errorf("转存失败: %v", err)
}
// 更新输出数据
output := TransferOutput{
ResourceID: resourceID,
SaveURL: saveURL,
Success: true,
Time: time.Now().Format("2006-01-02 15:04:05"),
}
outputJSON, _ := json.Marshal(output)
item.OutputData = string(outputJSON)
utils.Info("转存任务项处理完成: %d, 资源ID: %d", item.ID, resourceID)
return nil
}
// validateInput 验证输入数据
func (tp *TransferProcessor) validateInput(input *TransferInput) error {
if strings.TrimSpace(input.Title) == "" {
return fmt.Errorf("标题不能为空")
}
if strings.TrimSpace(input.URL) == "" {
return fmt.Errorf("链接不能为空")
}
// 验证URL格式
if !tp.isValidURL(input.URL) {
return fmt.Errorf("链接格式不正确")
}
return nil
}
// isValidURL 验证URL格式
func (tp *TransferProcessor) isValidURL(url string) bool {
// 简单的URL验证可以根据需要扩展
quarkPattern := `https://pan\.quark\.cn/s/[a-zA-Z0-9]+`
matched, _ := regexp.MatchString(quarkPattern, url)
return matched
}
// checkResourceExists 检查资源是否已存在
func (tp *TransferProcessor) checkResourceExists(url string) (bool, *entity.Resource, error) {
// 根据URL查找资源
resource, err := tp.repoMgr.ResourceRepository.GetByURL(url)
if err != nil {
// 如果是未找到记录的错误,则表示资源不存在
if strings.Contains(err.Error(), "record not found") {
return false, nil, nil
}
return false, nil, err
}
return true, resource, nil
}
// performTransfer 执行转存操作
func (tp *TransferProcessor) performTransfer(ctx context.Context, input *TransferInput) (uint, string, error) {
// 解析URL获取分享信息
shareInfo, err := tp.parseShareURL(input.URL)
if err != nil {
return 0, "", fmt.Errorf("解析分享链接失败: %v", err)
}
// 创建资源记录
var categoryID *uint
if input.CategoryID != 0 {
categoryID = &input.CategoryID
}
resource := &entity.Resource{
Title: input.Title,
URL: input.URL,
CategoryID: categoryID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// 保存资源到数据库
err = tp.repoMgr.ResourceRepository.Create(resource)
if err != nil {
return 0, "", fmt.Errorf("保存资源失败: %v", err)
}
// 添加标签关联
if len(input.Tags) > 0 {
err = tp.addResourceTags(resource.ID, input.Tags)
if err != nil {
utils.Error("添加资源标签失败: %v", err)
}
}
// 执行实际转存操作
saveURL, err := tp.transferToCloud(ctx, shareInfo)
if err != nil {
utils.Error("云端转存失败: %v", err)
// 转存失败但资源已创建返回原始URL
return resource.ID, input.URL, nil
}
// 更新资源的转存链接
if saveURL != "" {
err = tp.repoMgr.ResourceRepository.UpdateSaveURL(resource.ID, saveURL)
if err != nil {
utils.Error("更新转存链接失败: %v", err)
}
}
return resource.ID, saveURL, nil
}
// ShareInfo 分享信息结构
type ShareInfo struct {
PanType string
ShareID string
URL string
}
// parseShareURL 解析分享链接
func (tp *TransferProcessor) parseShareURL(url string) (*ShareInfo, error) {
// 解析夸克网盘链接
quarkPattern := `https://pan\.quark\.cn/s/([a-zA-Z0-9]+)`
re := regexp.MustCompile(quarkPattern)
matches := re.FindStringSubmatch(url)
if len(matches) >= 2 {
return &ShareInfo{
PanType: "quark",
ShareID: matches[1],
URL: url,
}, nil
}
return nil, fmt.Errorf("不支持的分享链接格式: %s", url)
}
// addResourceTags 添加资源标签
func (tp *TransferProcessor) addResourceTags(resourceID uint, tagIDs []uint) error {
for _, tagID := range tagIDs {
// 创建资源标签关联
resourceTag := &entity.ResourceTag{
ResourceID: resourceID,
TagID: tagID,
}
err := tp.repoMgr.ResourceRepository.CreateResourceTag(resourceTag)
if err != nil {
return fmt.Errorf("创建资源标签关联失败: %v", err)
}
}
return nil
}
// transferToCloud 执行云端转存
func (tp *TransferProcessor) transferToCloud(ctx context.Context, shareInfo *ShareInfo) (string, error) {
// 检查是否启用自动转存
autoTransferEnabled, err := tp.repoMgr.SystemConfigRepository.GetConfigBool("auto_transfer")
if err != nil || !autoTransferEnabled {
utils.Info("自动转存未启用,跳过云端转存")
return "", nil
}
// TODO: 实现云端转存逻辑
utils.Info("云端转存功能暂未实现,跳过转存: %s", shareInfo.ShareID)
return "", nil
}

View File

@@ -1,15 +1,8 @@
<template>
<div class="space-y-6">
<!-- 说明信息 -->
<n-alert type="info" show-icon>
<template #icon>
<i class="fas fa-info-circle"></i>
</template>
批量转存功能支持批量输入资源URL进行转存操作每行一个链接系统将自动处理转存任务
</n-alert>
<!-- 输入区域 -->
<n-card title="批量转存配置">
<n-card title="批量转存资源列表">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 左侧资源输入 -->
<div class="space-y-4">
@@ -37,10 +30,9 @@
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
默认分类
</label>
<n-select
v-model:value="selectedCategory"
<CategorySelector
v-model="selectedCategory"
placeholder="选择分类"
:options="categoryOptions"
clearable
/>
</div>
@@ -49,10 +41,9 @@
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
标签
</label>
<n-select
v-model:value="selectedTags"
<TagSelector
v-model="selectedTags"
placeholder="选择标签"
:options="tagOptions"
multiple
clearable
/>
@@ -126,13 +117,26 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, h } from 'vue'
import { useCategoryApi, useTagApi, usePanApi } from '~/composables/useApi'
import { ref, computed, onMounted, onBeforeUnmount, h } from 'vue'
import { usePanApi, useTaskApi } from '~/composables/useApi'
import { useMessage } from 'naive-ui'
// 数据状态
const resourceText = ref('')
const processing = ref(false)
const results = ref([])
const results = ref<any[]>([])
// 任务状态
const currentTaskId = ref<number | null>(null)
const taskStatus = ref<any>(null)
const taskStats = ref({
total: 0,
pending: 0,
processing: 0,
completed: 0,
failed: 0
})
const statusCheckInterval = ref<NodeJS.Timeout | null>(null)
// 配置选项
const selectedCategory = ref(null)
@@ -143,14 +147,12 @@ const skipExisting = ref(true)
const autoTransfer = ref(false)
// 选项数据
const categoryOptions = ref([])
const tagOptions = ref([])
const platformOptions = ref([])
const platformOptions = ref<any[]>([])
// API实例
const categoryApi = useCategoryApi()
const tagApi = useTagApi()
const panApi = usePanApi()
const taskApi = useTaskApi()
const message = useMessage()
// 计算属性
const totalLines = computed(() => {
@@ -181,10 +183,18 @@ const processingCount = computed(() => {
// 结果表格列
const resultColumns = [
{
title: '标题',
key: 'title',
width: 200,
ellipsis: {
tooltip: true
}
},
{
title: '链接',
key: 'url',
width: 300,
width: 250,
ellipsis: {
tooltip: true
}
@@ -197,9 +207,10 @@ const resultColumns = [
const statusMap = {
success: { color: 'success', text: '成功', icon: 'fas fa-check' },
failed: { color: 'error', text: '失败', icon: 'fas fa-times' },
processing: { color: 'info', text: '处理中', icon: 'fas fa-spinner fa-spin' }
processing: { color: 'info', text: '处理中', icon: 'fas fa-spinner fa-spin' },
pending: { color: 'warning', text: '等待中', icon: 'fas fa-clock' }
}
const status = statusMap[row.status] || statusMap.failed
const status = statusMap[row.status as keyof typeof statusMap] || statusMap.failed
return h('n-tag', { type: status.color }, {
icon: () => h('i', { class: status.icon }),
default: () => status.text
@@ -245,36 +256,6 @@ const isValidUrl = (url: string) => {
}
}
// 获取分类选项
const fetchCategories = async () => {
try {
const result = await categoryApi.getCategories() as any
if (result && result.items) {
categoryOptions.value = result.items.map((item: any) => ({
label: item.name,
value: item.id
}))
}
} catch (error) {
console.error('获取分类失败:', error)
}
}
// 获取标签选项
const fetchTags = async () => {
try {
const result = await tagApi.getTags() as any
if (result && result.items) {
tagOptions.value = result.items.map((item: any) => ({
label: item.name,
value: item.id
}))
}
} catch (error) {
console.error('获取标签失败:', error)
}
}
// 获取平台选项
const fetchPlatforms = async () => {
try {
@@ -293,7 +274,7 @@ const fetchPlatforms = async () => {
// 处理批量转存
const handleBatchTransfer = async () => {
if (!resourceText.value.trim()) {
$message.warning('请输入资源链接')
message.warning('请输入资源内容')
return
}
@@ -301,56 +282,172 @@ const handleBatchTransfer = async () => {
results.value = []
try {
const lines = resourceText.value.split('\n').filter(line => line.trim())
const validLines = lines.filter(line => isValidUrl(line.trim()))
if (validLines.length === 0) {
$message.warning('没有找到有效的资源链接')
// 第一步:拆解资源信息,按照一行标题,一行链接的形式
const resourceList = parseResourceText(resourceText.value)
if (resourceList.length === 0) {
message.warning('没有找到有效的资源信息,请按照"标题"和"链接"分行输入')
return
}
// 初始化结果
results.value = validLines.map(url => ({
url: url.trim(),
status: 'processing',
message: '准备处理...',
saveUrl: null
}))
// 这里应该调用实际的批量转存API
// 由于只是UI展示这里模拟处理过程
for (let i = 0; i < results.value.length; i++) {
const result = results.value[i]
// 模拟处理延迟
await new Promise(resolve => setTimeout(resolve, 1000))
// 模拟随机成功/失败
const isSuccess = Math.random() > 0.3
if (isSuccess) {
result.status = 'success'
result.message = '转存成功'
result.saveUrl = `https://pan.quark.cn/s/mock${Date.now()}`
} else {
result.status = 'failed'
result.message = '转存失败:网络错误'
}
// 触发响应式更新
results.value = [...results.value]
// 第二步:生成任务标题和数据
const taskTitle = `批量转存任务_${new Date().toLocaleString('zh-CN')}`
const taskData = {
title: taskTitle,
description: `批量转存 ${resourceList.length} 个资源`,
resources: resourceList.map(item => {
const resource: any = {
title: item.title,
url: item.url
}
if (selectedCategory.value) {
resource.category_id = selectedCategory.value
}
if (selectedTags.value && selectedTags.value.length > 0) {
resource.tags = selectedTags.value
}
return resource
})
}
$message.success(`批量转存完成,成功 ${successCount.value} 个,失败 ${failedCount.value}`)
console.log('创建任务数据:', taskData)
} catch (error) {
console.error('批量转存失败:', error)
$message.error('批量转存失败')
} finally {
// 第三步:创建任务
const taskResponse = await taskApi.createBatchTransferTask(taskData) as any
console.log('任务创建响应:', taskResponse)
currentTaskId.value = taskResponse.task_id
// 第四步:启动任务
await taskApi.startTask(currentTaskId.value!)
// 第五步:开始实时监控任务状态
startTaskMonitoring()
message.success('任务已创建并启动,开始处理...')
} catch (error: any) {
console.error('创建任务失败:', error)
message.error('创建任务失败: ' + (error.message || '未知错误'))
processing.value = false
}
}
// 解析资源文本,按照 标题\n链接 的格式
const parseResourceText = (text: string) => {
const lines = text.split('\n').filter((line: string) => line.trim())
const resourceList = []
for (let i = 0; i < lines.length; i += 2) {
const title = lines[i]?.trim()
const url = lines[i + 1]?.trim()
if (title && url && isValidUrl(url)) {
resourceList.push({
title,
url,
category_id: selectedCategory.value || 0,
tags: selectedTags.value || []
})
}
}
return resourceList
}
// 开始任务监控
const startTaskMonitoring = () => {
if (statusCheckInterval.value) {
clearInterval(statusCheckInterval.value)
}
statusCheckInterval.value = setInterval(async () => {
try {
const status = await taskApi.getTaskStatus(currentTaskId.value!) as any
console.log('任务状态更新:', status)
taskStatus.value = status
taskStats.value = status.stats || {
total: 0,
pending: 0,
processing: 0,
completed: 0,
failed: 0
}
// 更新结果显示
updateResultsDisplay()
// 如果任务完成,停止监控
if (status.status === 'completed' || status.status === 'failed' || status.status === 'partial_success') {
stopTaskMonitoring()
processing.value = false
const { completed, failed } = taskStats.value
message.success(`批量转存完成!成功: ${completed}, 失败: ${failed}`)
}
} catch (error) {
console.error('获取任务状态失败:', error)
// 如果连续失败,停止监控
stopTaskMonitoring()
processing.value = false
}
}, 2000) // 每2秒检查一次
}
// 停止任务监控
const stopTaskMonitoring = () => {
if (statusCheckInterval.value) {
clearInterval(statusCheckInterval.value)
statusCheckInterval.value = null
}
}
// 更新结果显示
const updateResultsDisplay = () => {
if (!taskStatus.value) return
// 如果还没有结果,初始化
if (results.value.length === 0) {
const resourceList = parseResourceText(resourceText.value)
results.value = resourceList.map(item => ({
title: item.title,
url: item.url,
status: 'pending',
message: '等待处理...',
saveUrl: null
}))
}
// 更新整体进度显示
const { pending, processing, completed, failed } = taskStats.value
const processed = completed + failed
// 简单的状态更新逻辑 - 这里可以根据需要获取详细的任务项状态
for (let i = 0; i < results.value.length; i++) {
const result = results.value[i]
if (i < completed) {
// 已完成的项目
result.status = 'success'
result.message = '转存成功'
} else if (i < completed + failed) {
// 失败的项目
result.status = 'failed'
result.message = '转存失败'
} else if (i < processed + processing) {
// 正在处理的项目
result.status = 'processing'
result.message = '正在处理...'
} else {
// 等待处理的项目
result.status = 'pending'
result.message = '等待处理...'
}
}
}
// 清空输入
const clearInput = () => {
resourceText.value = ''
@@ -359,8 +456,11 @@ const clearInput = () => {
// 初始化
onMounted(() => {
fetchCategories()
fetchTags()
fetchPlatforms()
})
// 组件销毁时清理定时器
onBeforeUnmount(() => {
stopTaskMonitoring()
})
</script>

View File

@@ -15,11 +15,41 @@
</div>
<!-- 中间状态信息 -->
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-6">
<!-- 系统状态 -->
<div class="flex items-center space-x-2">
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
<span class="text-sm text-gray-600 dark:text-gray-300">系统正常</span>
</div>
<!-- 自动处理状态 -->
<div class="flex items-center space-x-2">
<div :class="autoProcessEnabled ? 'w-2 h-2 bg-green-500 rounded-full' : 'w-2 h-2 bg-gray-400 rounded-full'"></div>
<span class="text-sm text-gray-600 dark:text-gray-300">
自动处理{{ autoProcessEnabled ? '已开启' : '已关闭' }}
</span>
</div>
<!-- 自动转存状态 -->
<div class="flex items-center space-x-2">
<div :class="autoTransferEnabled ? 'w-2 h-2 bg-blue-500 rounded-full' : 'w-2 h-2 bg-gray-400 rounded-full'"></div>
<span class="text-sm text-gray-600 dark:text-gray-300">
自动转存{{ autoTransferEnabled ? '已开启' : '已关闭' }}
</span>
</div>
<!-- 任务状态 -->
<div v-if="taskStore.hasActiveTasks" class="flex items-center space-x-2">
<div class="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
<span class="text-sm text-gray-600 dark:text-gray-300">
<template v-if="taskStore.runningTaskCount > 0">
{{ taskStore.runningTaskCount }}个任务运行中
</template>
<template v-else>
{{ taskStore.activeTaskCount }}个任务待处理
</template>
</span>
</div>
</div>
<!-- 右侧用户菜单 -->
@@ -45,7 +75,67 @@
</template>
<script setup lang="ts">
// 简化的头部组件
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useTaskStore } from '~/stores/task'
import { useSystemConfigStore } from '~/stores/systemConfig'
// 任务状态管理
const taskStore = useTaskStore()
// 系统配置状态管理
const systemConfigStore = useSystemConfigStore()
// 自动处理和自动转存状态
const autoProcessEnabled = ref(false)
const autoTransferEnabled = ref(false)
// 获取系统配置状态
const fetchSystemStatus = async () => {
try {
await systemConfigStore.initConfig()
// 从系统配置中获取自动处理和自动转存状态
const config = systemConfigStore.config
if (config) {
// 检查自动处理状态
autoProcessEnabled.value = config.auto_process_enabled === '1' || config.auto_process_enabled === true
// 检查自动转存状态
autoTransferEnabled.value = config.auto_transfer_enabled === '1' || config.auto_transfer_enabled === true
}
} catch (error) {
console.error('获取系统状态失败:', error)
}
}
// 组件挂载时启动
onMounted(() => {
// 启动任务状态自动更新
taskStore.startAutoUpdate()
// 获取系统配置状态
fetchSystemStatus()
// 定期更新系统配置状态每30秒
const configInterval = setInterval(fetchSystemStatus, 30000)
// 保存定时器引用用于清理
;(globalThis as any).__configInterval = configInterval
})
// 组件销毁时清理
onBeforeUnmount(() => {
// 停止任务状态自动更新
taskStore.stopAutoUpdate()
// 清理配置更新定时器
if ((globalThis as any).__configInterval) {
clearInterval((globalThis as any).__configInterval)
delete (globalThis as any).__configInterval
}
})
</script>
<style scoped>

View File

@@ -13,17 +13,15 @@
</template>
</n-input>
<n-select
v-model:value="selectedCategory"
<CategorySelector
v-model="selectedCategory"
placeholder="选择分类"
:options="categoryOptions"
clearable
/>
<n-select
v-model:value="selectedTag"
<TagSelector
v-model="selectedTag"
placeholder="选择标签"
:options="tagOptions"
clearable
/>
@@ -52,7 +50,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, h } from 'vue'
import { useResourceApi, useCategoryApi, useTagApi } from '~/composables/useApi'
import { useResourceApi } from '~/composables/useApi'
// 数据状态
const loading = ref(false)
@@ -66,14 +64,8 @@ const searchQuery = ref('')
const selectedCategory = ref(null)
const selectedTag = ref(null)
// 选项数据
const categoryOptions = ref([])
const tagOptions = ref([])
// API实例
const resourceApi = useResourceApi()
const categoryApi = useCategoryApi()
const tagApi = useTagApi()
// 分页配置
const pagination = reactive({
@@ -203,40 +195,6 @@ const fetchTransferredResources = async () => {
}
}
// 获取分类选项
const fetchCategories = async () => {
try {
const result = await categoryApi.getCategories() as any
console.log('分类结果:', result)
if (result && result.items) {
categoryOptions.value = result.items.map((item: any) => ({
label: item.name,
value: item.id
}))
}
} catch (error) {
console.error('获取分类失败:', error)
}
}
// 获取标签选项
const fetchTags = async () => {
try {
const result = await tagApi.getTags() as any
console.log('标签结果:', result)
if (result && result.items) {
tagOptions.value = result.items.map((item: any) => ({
label: item.name,
value: item.id
}))
}
} catch (error) {
console.error('获取标签失败:', error)
}
}
// 搜索处理
const handleSearch = () => {
currentPage.value = 1
@@ -278,8 +236,6 @@ const copyLink = async (url: string) => {
// 初始化
onMounted(() => {
fetchCategories()
fetchTags()
fetchTransferredResources()
})
</script>

View File

@@ -13,17 +13,15 @@
</template>
</n-input>
<n-select
v-model:value="selectedCategory"
<CategorySelector
v-model="selectedCategory"
placeholder="选择分类"
:options="categoryOptions"
clearable
/>
<n-select
v-model:value="selectedTag"
<TagSelector
v-model="selectedTag"
placeholder="选择标签"
:options="tagOptions"
clearable
/>

View File

@@ -0,0 +1,105 @@
<template>
<n-select
v-model:value="selectedValue"
:placeholder="placeholder"
:options="categoryOptions"
:loading="loading"
:clearable="clearable"
:filterable="true"
:disabled="disabled"
@update:value="handleUpdate"
/>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { useCategoryApi } from '~/composables/useApi'
// Props定义
interface Props {
modelValue?: number | null
placeholder?: string
clearable?: boolean
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '选择分类',
clearable: true,
disabled: false
})
// Emits定义
const emit = defineEmits<{
'update:modelValue': [value: number | null]
}>()
// 定义选项类型
interface CategoryOption {
label: string
value: number
disabled: boolean
}
// 内部状态
const selectedValue = ref(props.modelValue)
const categoryOptions = ref<CategoryOption[]>([])
const loading = ref(false)
// API实例
const categoryApi = useCategoryApi()
// 监听外部值变化
watch(
() => props.modelValue,
(newValue) => {
selectedValue.value = newValue
}
)
// 监听内部值变化并向外发射
const handleUpdate = (value: number | null) => {
selectedValue.value = value
emit('update:modelValue', value)
}
// 加载分类数据
const loadCategories = async () => {
// 如果已经加载过,直接返回
if (categoryOptions.value.length > 0) {
return
}
loading.value = true
try {
const result = await categoryApi.getCategories() as any
const options: CategoryOption[] = []
if (result && result.items) {
options.push(...result.items.map((item: any) => ({
label: item.name,
value: item.id,
disabled: false
})))
} else if (Array.isArray(result)) {
options.push(...result.map((item: any) => ({
label: item.name,
value: item.id,
disabled: false
})))
}
categoryOptions.value = options
} catch (error) {
console.error('获取分类失败:', error)
categoryOptions.value = []
} finally {
loading.value = false
}
}
// 组件挂载时立即加载分类
onMounted(() => {
loadCategories()
})
</script>

View File

@@ -0,0 +1,129 @@
<template>
<n-select
v-model:value="selectedValue"
:placeholder="placeholder"
:options="tagOptions"
:loading="loading"
:multiple="multiple"
:clearable="clearable"
:filterable="true"
:remote="true"
:clear-filter-after-select="false"
@search="handleSearch"
@focus="loadInitialTags"
@update:value="handleUpdate"
/>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { useTagApi } from '~/composables/useApi'
// Props定义
interface Props {
modelValue?: number | number[] | null
placeholder?: string
multiple?: boolean
clearable?: boolean
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '选择标签',
multiple: false,
clearable: true,
disabled: false
})
// Emits定义
const emit = defineEmits<{
'update:modelValue': [value: number | number[] | null]
}>()
// 定义选项类型
interface TagOption {
label: string
value: number
disabled: boolean
}
// 内部状态
const selectedValue = ref(props.modelValue)
const tagOptions = ref<TagOption[]>([])
const loading = ref(false)
const searchCache = ref(new Map<string, TagOption[]>()) // 搜索缓存
// API实例
const tagApi = useTagApi()
// 监听外部值变化
watch(
() => props.modelValue,
(newValue) => {
selectedValue.value = newValue
}
)
// 监听内部值变化并向外发射
const handleUpdate = (value: number | number[] | null) => {
selectedValue.value = value
emit('update:modelValue', value)
}
// 加载标签数据
const loadTags = async (query: string = '') => {
// 检查缓存
if (searchCache.value.has(query)) {
const cachedOptions = searchCache.value.get(query)
if (cachedOptions) {
tagOptions.value = cachedOptions
return
}
}
loading.value = true
try {
const result = await tagApi.getTags({
search: query,
page: 1,
page_size: 50 // 限制返回数量,避免数据过多
}) as any
const options: TagOption[] = []
if (result && result.items) {
options.push(...result.items.map((item: any) => ({
label: item.name,
value: item.id,
disabled: false
})))
}
// 缓存结果
searchCache.value.set(query, options)
tagOptions.value = options
} catch (error) {
console.error('获取标签失败:', error)
tagOptions.value = []
} finally {
loading.value = false
}
}
// 初始加载标签
const loadInitialTags = async () => {
if (tagOptions.value.length === 0) {
await loadTags('')
}
}
// 搜索处理
const handleSearch = async (query: string) => {
await loadTags(query)
}
// 组件挂载时预加载一些标签(可选)
onMounted(() => {
// 可以选择在挂载时就加载标签,或者等用户聚焦时再加载
// loadInitialTags()
})
</script>

View File

@@ -200,6 +200,18 @@ export const usePublicSystemConfigApi = () => {
return { getPublicSystemConfig }
}
// 任务管理API
export const useTaskApi = () => {
const createBatchTransferTask = (data: any) => useApiFetch('/tasks/transfer', { method: 'POST', body: data }).then(parseApiResponse)
const getTasks = (params?: any) => useApiFetch('/tasks', { params }).then(parseApiResponse)
const getTaskStatus = (id: number) => useApiFetch(`/tasks/${id}`).then(parseApiResponse)
const startTask = (id: number) => useApiFetch(`/tasks/${id}/start`, { method: 'POST' }).then(parseApiResponse)
const stopTask = (id: number) => useApiFetch(`/tasks/${id}/stop`, { method: 'POST' }).then(parseApiResponse)
const deleteTask = (id: number) => useApiFetch(`/tasks/${id}`, { method: 'DELETE' }).then(parseApiResponse)
const getTaskItems = (id: number, params?: any) => useApiFetch(`/tasks/${id}/items`, { params }).then(parseApiResponse)
return { createBatchTransferTask, getTasks, getTaskStatus, startTask, stopTask, deleteTask, getTaskItems }
}
// 日志函数:只在开发环境打印
function log(...args: any[]) {
if (process.env.NODE_ENV !== 'production') {

View File

@@ -48,6 +48,23 @@
</span>
</div>
<!-- 任务状态 -->
<div
v-if="taskStore.hasActiveTasks"
@click="navigateToTasks"
class="flex items-center gap-2 bg-orange-50 dark:bg-orange-900/20 rounded-lg px-3 py-2 cursor-pointer hover:bg-orange-100 dark:hover:bg-orange-900/30 transition-colors"
>
<div class="w-2 h-2 bg-orange-500 rounded-full animate-pulse"></div>
<span class="text-xs text-orange-700 dark:text-orange-300 font-medium">
<template v-if="taskStore.runningTaskCount > 0">
{{ taskStore.runningTaskCount }}个任务运行中
</template>
<template v-else>
{{ taskStore.activeTaskCount }}个任务待处理
</template>
</span>
</div>
<NuxtLink to="/" class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<i class="fas fa-home text-lg"></i>
</NuxtLink>
@@ -271,6 +288,7 @@
<script setup lang="ts">
import { useUserStore } from '~/stores/user'
import { useSystemConfigStore } from '~/stores/systemConfig'
import { useTaskStore } from '~/stores/task'
// 用户状态管理
const userStore = useUserStore()
@@ -279,6 +297,9 @@ const router = useRouter()
// 系统配置store
const systemConfigStore = useSystemConfigStore()
// 任务状态管理
const taskStore = useTaskStore()
// 初始化系统配置
await systemConfigStore.initConfig()
@@ -299,9 +320,20 @@ const fetchVersionInfo = async () => {
}
}
// 初始化版本信息
// 初始化版本信息和任务状态管理
onMounted(() => {
fetchVersionInfo()
// 启动任务状态自动更新
taskStore.startAutoUpdate()
console.log('Admin layout: 任务状态自动更新已启动')
})
// 组件销毁时清理任务状态管理
onBeforeUnmount(() => {
// 停止任务状态自动更新
taskStore.stopAutoUpdate()
console.log('Admin layout: 任务状态自动更新已停止')
})
// 系统配置
@@ -528,6 +560,11 @@ onMounted(() => {
}
})
})
// 导航到任务列表页面
const navigateToTasks = () => {
router.push('/admin/tasks')
}
</script>
<style scoped>

376
web/pages/admin/tasks.vue Normal file
View File

@@ -0,0 +1,376 @@
<template>
<div class="p-6 space-y-6">
<!-- 页面标题 -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">任务管理</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">查看和管理系统中的所有任务</p>
</div>
</div>
<!-- 任务统计卡片 -->
<div class="grid grid-cols-6 gap-4">
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">总任务数</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ taskStore.taskStats.total }}</p>
</div>
<div class="w-8 h-8 bg-blue-100 dark:bg-blue-900/20 rounded-lg flex items-center justify-center">
<i class="fas fa-tasks text-blue-600 dark:text-blue-400"></i>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">运行中</p>
<p class="text-2xl font-bold text-orange-600">{{ taskStore.taskStats.running }}</p>
</div>
<div class="w-8 h-8 bg-orange-100 dark:bg-orange-900/20 rounded-lg flex items-center justify-center">
<i class="fas fa-play text-orange-600 dark:text-orange-400"></i>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">待处理</p>
<p class="text-2xl font-bold text-yellow-600">{{ taskStore.taskStats.pending }}</p>
</div>
<div class="w-8 h-8 bg-yellow-100 dark:bg-yellow-900/20 rounded-lg flex items-center justify-center">
<i class="fas fa-clock text-yellow-600 dark:text-yellow-400"></i>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">已完成</p>
<p class="text-2xl font-bold text-green-600">{{ taskStore.taskStats.completed }}</p>
</div>
<div class="w-8 h-8 bg-green-100 dark:bg-green-900/20 rounded-lg flex items-center justify-center">
<i class="fas fa-check text-green-600 dark:text-green-400"></i>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">失败</p>
<p class="text-2xl font-bold text-red-600">{{ taskStore.taskStats.failed }}</p>
</div>
<div class="w-8 h-8 bg-red-100 dark:bg-red-900/20 rounded-lg flex items-center justify-center">
<i class="fas fa-times text-red-600 dark:text-red-400"></i>
</div>
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">暂停</p>
<p class="text-2xl font-bold text-gray-600">{{ taskStore.taskStats.paused }}</p>
</div>
<div class="w-8 h-8 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center">
<i class="fas fa-pause text-gray-600 dark:text-gray-400"></i>
</div>
</div>
</div>
</div>
<!-- 任务列表 -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">任务列表</h2>
</div>
<div class="p-6">
<n-data-table
:columns="taskColumns"
:data="tasks"
:loading="loading"
:pagination="paginationConfig"
:row-class-name="getRowClassName"
size="small"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, h } from 'vue'
import { useTaskStore } from '~/stores/task'
import { useMessage, useDialog } from 'naive-ui'
// 任务状态管理
const taskStore = useTaskStore()
const message = useMessage()
const dialog = useDialog()
// 数据状态
const tasks = ref([])
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(20)
const total = ref(0)
// 分页配置
const paginationConfig = computed(() => ({
page: currentPage.value,
pageSize: pageSize.value,
itemCount: total.value,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
onChange: (page: number) => {
currentPage.value = page
fetchTasks()
},
onUpdatePageSize: (size: number) => {
pageSize.value = size
currentPage.value = 1
fetchTasks()
}
}))
// 表格列定义
const taskColumns = [
{
title: 'ID',
key: 'id',
width: 80,
sorter: true
},
{
title: '任务标题',
key: 'title',
minWidth: 200,
ellipsis: {
tooltip: true
}
},
{
title: '类型',
key: 'task_type',
width: 100,
render: (row: any) => {
const typeMap: Record<string, { text: string; color: string }> = {
transfer: { text: '转存', color: 'blue' }
}
const type = typeMap[row.task_type] || { text: row.task_type, color: 'gray' }
return h('n-tag', { type: type.color, size: 'small' }, { default: () => type.text })
}
},
{
title: '状态',
key: 'status',
width: 120,
render: (row: any) => {
const statusMap: Record<string, { text: string; color: string }> = {
pending: { text: '待处理', color: 'warning' },
running: { text: '运行中', color: 'info' },
completed: { text: '已完成', color: 'success' },
failed: { text: '失败', color: 'error' },
paused: { text: '暂停', color: 'default' }
}
// 优先使用 is_running 状态
let currentStatus = row.status
if (row.is_running) {
currentStatus = 'running'
}
const status = statusMap[currentStatus] || { text: currentStatus, color: 'default' }
return h('n-tag', { type: status.color, size: 'small' }, { default: () => status.text })
}
},
{
title: '进度',
key: 'progress',
width: 120,
render: (row: any) => {
const total = row.total_items || 0
const processed = (row.processed_items || 0)
const percentage = total > 0 ? Math.round((processed / total) * 100) : 0
return h('div', { class: 'flex items-center space-x-2' }, [
h('span', { class: 'text-sm' }, `${processed}/${total}`),
h('n-progress', {
type: 'line',
percentage,
height: 4,
showIndicator: false,
style: { width: '60px' }
})
])
}
},
{
title: '创建时间',
key: 'created_at',
width: 180,
render: (row: any) => {
return new Date(row.created_at).toLocaleString('zh-CN')
}
},
{
title: '操作',
key: 'actions',
width: 160,
render: (row: any) => {
const buttons = []
if (row.status === 'pending' || (row.status !== 'running' && !row.is_running)) {
buttons.push(
h('n-button', {
size: 'small',
type: 'primary',
onClick: () => startTask(row.id)
}, { default: () => '启动' })
)
}
if (row.is_running) {
buttons.push(
h('n-button', {
size: 'small',
type: 'warning',
onClick: () => stopTask(row.id)
}, { default: () => '停止' })
)
}
if (row.status === 'completed' || row.status === 'failed') {
buttons.push(
h('n-button', {
size: 'small',
type: 'error',
onClick: () => deleteTask(row.id)
}, { default: () => '删除' })
)
}
return h('div', { class: 'flex space-x-2' }, buttons)
}
}
]
// 行样式
const getRowClassName = (row: any) => {
if (row.is_running) {
return 'bg-blue-50 dark:bg-blue-900/10'
}
return ''
}
// 获取任务列表
const fetchTasks = async () => {
loading.value = true
try {
const { useTaskApi } = await import('~/composables/useApi')
const taskApi = useTaskApi()
const response = await taskApi.getTasks({
page: currentPage.value,
page_size: pageSize.value
}) as any
if (response && response.items) {
tasks.value = response.items
total.value = response.total || 0
}
} catch (error) {
console.error('获取任务列表失败:', error)
message.error('获取任务列表失败')
} finally {
loading.value = false
}
}
// 启动任务
const startTask = async (taskId: number) => {
try {
const success = await taskStore.startTask(taskId)
if (success) {
message.success('任务启动成功')
await fetchTasks()
} else {
message.error('任务启动失败')
}
} catch (error) {
console.error('启动任务失败:', error)
message.error('启动任务失败')
}
}
// 停止任务
const stopTask = async (taskId: number) => {
try {
const success = await taskStore.stopTask(taskId)
if (success) {
message.success('任务停止成功')
await fetchTasks()
} else {
message.error('任务停止失败')
}
} catch (error) {
console.error('停止任务失败:', error)
message.error('停止任务失败')
}
}
// 删除任务
const deleteTask = async (taskId: number) => {
dialog.warning({
title: '确认删除',
content: '确定要删除这个任务吗?此操作不可逆。',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
const success = await taskStore.deleteTask(taskId)
if (success) {
message.success('任务删除成功')
await fetchTasks()
} else {
message.error('任务删除失败')
}
} catch (error) {
console.error('删除任务失败:', error)
message.error('删除任务失败')
}
}
})
}
// 初始化
onMounted(() => {
fetchTasks()
// 确保任务状态管理已启动因为页面可能直接访问而不是通过layout
taskStore.startAutoUpdate()
})
// 设置页面meta
definePageMeta({
layout: 'admin'
})
</script>
<style scoped>
:deep(.n-data-table-th) {
background-color: var(--n-th-color);
}
:deep(.bg-blue-50) {
background-color: rgb(239 246 255);
}
:deep(.dark .bg-blue-50) {
background-color: rgb(30 58 138 / 0.1);
}
</style>

238
web/stores/task.ts Normal file
View File

@@ -0,0 +1,238 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useTaskApi } from '~/composables/useApi'
export interface TaskStats {
total: number
running: number
pending: number
completed: number
failed: number
paused: number
}
export interface TaskInfo {
id: number
title: string
type: string
status: string
total_items: number
processed_items?: number
success_items?: number
failed_items?: number
created_at: string
updated_at: string
is_running?: boolean // 任务是否在TaskManager中运行
}
export const useTaskStore = defineStore('task', () => {
const taskApi = useTaskApi()
// 任务统计信息
const taskStats = ref<TaskStats>({
total: 0,
running: 0,
pending: 0,
completed: 0,
failed: 0,
paused: 0
})
// 正在运行的任务列表
const runningTasks = ref<TaskInfo[]>([])
// 未完成的任务列表pending + running + paused
const incompleteTasks = ref<TaskInfo[]>([])
// 更新定时器
let updateInterval: NodeJS.Timeout | null = null
// 计算属性:是否有活跃任务
const hasActiveTasks = computed(() => {
return taskStats.value.running > 0 || taskStats.value.pending > 0 || taskStats.value.paused > 0
})
// 计算属性:活跃任务总数
const activeTaskCount = computed(() => {
return taskStats.value.running + taskStats.value.pending + taskStats.value.paused
})
// 计算属性:正在运行的任务数
const runningTaskCount = computed(() => {
return taskStats.value.running
})
// 获取任务统计信息
const fetchTaskStats = async () => {
try {
const response = await taskApi.getTasks() as any
console.log('原始任务API响应:', response)
// 处理API响应格式
let tasks: TaskInfo[] = []
if (response && response.items && Array.isArray(response.items)) {
tasks = response.items
} else if (Array.isArray(response)) {
tasks = response
}
console.log('解析后的任务列表:', tasks)
if (tasks && tasks.length >= 0) {
// 重置统计
const stats: TaskStats = {
total: tasks.length,
running: 0,
pending: 0,
completed: 0,
failed: 0,
paused: 0
}
const running: TaskInfo[] = []
const incomplete: TaskInfo[] = []
// 统计各种状态的任务
tasks.forEach((task: TaskInfo) => {
console.log('处理任务:', task.id, '状态:', task.status, '是否运行中:', task.is_running)
// 如果任务标记为运行中优先使用running状态
let currentStatus = task.status
if (task.is_running) {
currentStatus = 'running'
}
switch (currentStatus) {
case 'running':
stats.running++
running.push(task)
incomplete.push(task)
break
case 'pending':
stats.pending++
incomplete.push(task)
break
case 'completed':
stats.completed++
break
case 'failed':
stats.failed++
break
case 'paused':
stats.paused++
incomplete.push(task)
break
}
})
// 更新状态
taskStats.value = stats
runningTasks.value = running
incompleteTasks.value = incomplete
console.log('任务统计更新:', stats)
console.log('运行中的任务:', running)
console.log('未完成的任务:', incomplete)
}
} catch (error) {
console.error('获取任务统计失败:', error)
}
}
// 开始定时更新
const startAutoUpdate = () => {
if (updateInterval) {
clearInterval(updateInterval)
}
// 立即执行一次
fetchTaskStats()
// 每5秒更新一次
updateInterval = setInterval(() => {
fetchTaskStats()
}, 5000)
console.log('任务状态自动更新已启动')
}
// 停止定时更新
const stopAutoUpdate = () => {
if (updateInterval) {
clearInterval(updateInterval)
updateInterval = null
console.log('任务状态自动更新已停止')
}
}
// 获取特定任务的详细状态
const getTaskStatus = async (taskId: number) => {
try {
const status = await taskApi.getTaskStatus(taskId)
return status
} catch (error) {
console.error('获取任务状态失败:', error)
return null
}
}
// 启动任务
const startTask = async (taskId: number) => {
try {
await taskApi.startTask(taskId)
// 立即更新状态
await fetchTaskStats()
return true
} catch (error) {
console.error('启动任务失败:', error)
return false
}
}
// 停止任务
const stopTask = async (taskId: number) => {
try {
await taskApi.stopTask(taskId)
// 立即更新状态
await fetchTaskStats()
return true
} catch (error) {
console.error('停止任务失败:', error)
return false
}
}
// 删除任务
const deleteTask = async (taskId: number) => {
try {
await taskApi.deleteTask(taskId)
// 立即更新状态
await fetchTaskStats()
return true
} catch (error) {
console.error('删除任务失败:', error)
return false
}
}
return {
// 状态
taskStats,
runningTasks,
incompleteTasks,
// 计算属性
hasActiveTasks,
activeTaskCount,
runningTaskCount,
// 方法
fetchTaskStats,
startAutoUpdate,
stopAutoUpdate,
getTaskStatus,
startTask,
stopTask,
deleteTask
}
})