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("") @tasks.get("")
async def list_tasks( async def list_tasks(
status: str | None = Query(default=None), status: str | None = Query(default=None),
limit: int = Query(default=100, ge=1, le=100),
current_user: User = Depends(get_admin_user), current_user: User = Depends(get_admin_user),
): ):
"""List tasks, optionally filtered by status.""" """List tasks, optionally filtered by status."""
task_list = await tasker.list_tasks(status=status) return await tasker.list_tasks(status=status, limit=limit)
return {"tasks": task_list}
@tasks.get("/{task_id}") @tasks.get("/{task_id}")

View File

@ -6,6 +6,7 @@ from dataclasses import asdict, dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from collections import Counter
from src.config import config from src.config import config
from src.utils.logging_config import logger from src.utils.logging_config import logger
@ -137,13 +138,31 @@ class Tasker:
logger.info("Enqueued task {} ({})", task_id, name) logger.info("Enqueued task {} ({})", task_id, name)
return task 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: 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: if status:
tasks = [task for task in tasks if task.status == 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 def get_task(self, task_id: str) -> dict[str, Any] | None:
async with self._lock: async with self._lock:

View File

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

View File

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