feat: 更新任务列表接口,增加限制参数并返回任务摘要信息
This commit is contained in:
parent
12bfc58709
commit
66abbc986b
@ -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}")
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user