feat: 更新任务列表接口,增加限制参数并返回任务摘要信息

This commit is contained in:
Wenjie Zhang 2025-10-25 14:55:06 +08:00
parent 12bfc58709
commit 66abbc986b
5 changed files with 74 additions and 19 deletions

View File

@ -10,11 +10,11 @@ tasks = APIRouter(prefix="/tasks", tags=["tasks"])
@tasks.get("")
async def list_tasks(
status: str | None = Query(default=None),
limit: int = Query(default=100, ge=1, le=100),
current_user: User = Depends(get_admin_user),
):
"""List tasks, optionally filtered by status."""
task_list = await tasker.list_tasks(status=status)
return {"tasks": task_list}
return await tasker.list_tasks(status=status, limit=limit)
@tasks.get("/{task_id}")

View File

@ -6,6 +6,7 @@ from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any
from collections.abc import Awaitable, Callable
from collections import Counter
from src.config import config
from src.utils.logging_config import logger
@ -137,13 +138,31 @@ class Tasker:
logger.info("Enqueued task {} ({})", task_id, name)
return task
async def list_tasks(self, status: str | None = None) -> list[dict[str, Any]]:
async def list_tasks(self, status: str | None = None, limit: int = 100) -> dict[str, Any]:
async with self._lock:
tasks = list(self._tasks.values())
all_tasks = list(self._tasks.values())
status_counter = Counter(task.status for task in all_tasks)
type_counter = Counter(task.type for task in all_tasks)
all_tasks.sort(key=lambda item: item.created_at or utc_isoformat(), reverse=True)
tasks = all_tasks
if status:
tasks = [task for task in tasks if task.status == status]
tasks.sort(key=lambda item: item.created_at or utc_isoformat(), reverse=True)
return [task.to_dict() for task in tasks]
limited_tasks = tasks[: max(limit, 0)]
summary: dict[str, Any] = {
"total": len(all_tasks),
"filtered_total": len(tasks),
"status_counts": dict(status_counter),
"type_counts": dict(type_counter),
}
return {
"tasks": [task.to_dict() for task in limited_tasks],
"summary": summary,
}
async def get_task(self, task_id: str) -> dict[str, Any] | None:
async with self._lock:

View File

@ -33,6 +33,8 @@ async def test_admin_can_list_tasks(test_client, admin_headers):
payload = response.json()
assert "tasks" in payload
assert isinstance(payload["tasks"], list)
assert "summary" in payload
assert isinstance(payload["summary"], dict)
async def test_cancel_unknown_task_returns_client_error(test_client, admin_headers):

View File

@ -98,7 +98,7 @@
<div v-else class="task-empty">
<div class="task-empty-icon">🗂</div>
<div class="task-empty-title">暂无任务</div>
<div class="task-empty-subtitle">当你提交知识库导入或其他后台任务时会在这里展示实时进度</div>
<div class="task-empty-subtitle">当你提交知识库导入或其他后台任务时会在这里展示实时进度仅展示最近的 100 个任务</div>
</div>
</div>
</a-drawer>
@ -112,27 +112,32 @@ import { storeToRefs } from 'pinia'
import { formatFullDateTime, formatRelative, parseToShanghai } from '@/utils/time'
const taskerStore = useTaskerStore()
const { isDrawerOpen, sortedTasks, loading, lastError } = storeToRefs(taskerStore)
const {
isDrawerOpen,
sortedTasks,
loading,
lastError,
activeCount,
totalCount,
successCount,
failedCount
} = storeToRefs(taskerStore)
const isOpen = isDrawerOpen
const tasks = computed(() => sortedTasks.value)
const loadingState = computed(() => Boolean(loading.value))
const lastErrorState = computed(() => lastError.value)
const statusFilter = ref('all')
const inProgressCount = computed(
() => tasks.value.filter((task) => ACTIVE_CLASS_STATUSES.has(task.status)).length
)
const completedCount = computed(() => tasks.value.filter((task) => task.status === 'success').length)
const failedCount = computed(
() => tasks.value.filter((task) => FAILED_STATUSES.has(task.status)).length
)
const totalCount = computed(() => tasks.value.length)
const inProgressCount = computed(() => activeCount.value || 0)
const completedCount = computed(() => successCount.value || 0)
const failedTaskCount = computed(() => failedCount.value || 0)
const totalTaskCount = computed(() => totalCount.value || 0)
const taskFilterOptions = computed(() => [
{
label: () =>
h('span', { class: 'task-filter-option' }, [
'全部',
h('span', { class: 'filter-count' }, totalCount.value)
h('span', { class: 'filter-count' }, totalTaskCount.value)
]),
value: 'all'
},
@ -156,7 +161,7 @@ const taskFilterOptions = computed(() => [
label: () =>
h('span', { class: 'task-filter-option' }, [
'失败',
h('span', { class: 'filter-count' }, failedCount.value)
h('span', { class: 'filter-count' }, failedTaskCount.value)
]),
value: 'failed'
}

View File

@ -5,6 +5,14 @@ import { taskerApi } from '@/apis/tasker'
import { parseToShanghai } from '@/utils/time'
const ACTIVE_STATUSES = new Set(['pending', 'running', 'queued'])
const FAILED_STATUSES = new Set(['failed', 'cancelled'])
const createDefaultSummary = () => ({
total: 0,
filtered_total: 0,
status_counts: {},
type_counts: {}
})
const toTask = (raw = {}) => ({
id: raw.id,
@ -29,6 +37,7 @@ export const useTaskerStore = defineStore('tasker', () => {
const lastError = ref(null)
const isPolling = ref(false)
const isDrawerOpen = ref(false)
const summary = ref(createDefaultSummary())
let pollingTimer = null
const sortedTasks = computed(() => {
@ -42,7 +51,16 @@ export const useTaskerStore = defineStore('tasker', () => {
})
})
const activeCount = computed(() => sortedTasks.value.filter(task => ACTIVE_STATUSES.has(task.status)).length)
const statusCounts = computed(() => summary.value?.status_counts || {})
const activeCount = computed(() =>
Array.from(ACTIVE_STATUSES).reduce((count, status) => count + (statusCounts.value?.[status] || 0), 0)
)
const failedCount = computed(() =>
Array.from(FAILED_STATUSES).reduce((count, status) => count + (statusCounts.value?.[status] || 0), 0)
)
const successCount = computed(() => statusCounts.value?.success || 0)
const totalCount = computed(() => summary.value?.total || 0)
function upsertTask(rawTask) {
if (!rawTask || !rawTask.id) return
@ -61,10 +79,15 @@ export const useTaskerStore = defineStore('tasker', () => {
try {
const response = await taskerApi.fetchTasks(params)
const taskList = response?.tasks || []
summary.value = {
...createDefaultSummary(),
...(response?.summary || {})
}
tasks.value = taskList.map(toTask)
} catch (error) {
console.error('加载任务列表失败', error)
lastError.value = error
summary.value = createDefaultSummary()
} finally {
loading.value = false
}
@ -144,12 +167,18 @@ export const useTaskerStore = defineStore('tasker', () => {
tasks.value = []
lastError.value = null
isDrawerOpen.value = false
summary.value = createDefaultSummary()
}
return {
isDrawerOpen,
tasks,
sortedTasks,
summary,
statusCounts,
totalCount,
successCount,
failedCount,
loading,
lastError,
activeCount,