From 7e4a1ff5e4948c5a1b952e95d8f05d7b5e8f3478 Mon Sep 17 00:00:00 2001 From: chengkai3 Date: Sat, 20 Jun 2026 07:53:19 +0800 Subject: [PATCH] =?UTF-8?q?[feat]:[FL-157][Worker=E7=9B=91=E6=8E=A7?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=B8=80=E8=87=B4=E6=80=A7=E4=BC=98=E5=8C=96?= =?UTF-8?q?]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- memory/2026-06-20.md | 21 ++ web/src/app/admin/workers/page.tsx | 359 ++++++++++++++++++++--------- web/src/app/globals.css | 76 +++++- 3 files changed, 349 insertions(+), 107 deletions(-) diff --git a/memory/2026-06-20.md b/memory/2026-06-20.md index 965a9c9..68882e4 100644 --- a/memory/2026-06-20.md +++ b/memory/2026-06-20.md @@ -146,3 +146,24 @@ - 风险与关注点: - 改动仅影响菜单管理页前端数据 mutation 组织和移动端展示条件,不改变接口路径、请求/响应字段或权限语义。 + +# Work Log - Worker监控页面一致性优化(FL-157) + +- 背景: + - Worker监控页需对齐用户管理页的后台列表页外层卡片、筛选表单、表格空态、移动端卡片和反馈规范。 + +- 本次处理: + - Worker监控页外层容器改为与用户管理页一致的 `page-card` flex 布局,筛选项改为固定宽度表单项,自动刷新与刷新操作收口到标题右侧。 + - 桌面表格对齐用户页:启用 `tableLayout="fixed"`、统一 loading / pagination / Empty 写法,仅保留纵向动态滚动。 + - 新增移动端卡片视图,按用户页卡片规范展示执行节点、状态、队列、并发/预取、任务统计、累计处理、注册任务和最近心跳。 + - 错误反馈改为 `useToastFeedback`,并复用任务监控展示工具统一任务状态、来源、队列名、运行时长和 Flower 错误文案。 + - Worker 任务明细抽屉保留原有分组和数据来源,仅统一表头文案、任务标签和空态。 + +- 验证: + - 基线:`npm --workspace web exec eslint src/app/admin/users/page.tsx src/app/admin/workers/page.tsx` 通过,仅用户页存在 1 条既有 unused eslint-disable warning。 + - 基线:`npm --workspace web exec tsc --noEmit` 通过。 + - 修改后:`npm --workspace web exec eslint src/app/admin/workers/page.tsx --max-warnings=0` 通过。 + - 修改后:`npm --workspace web exec tsc --noEmit` 通过。 + +- 风险与关注点: + - 改动仅影响 `/admin/workers` 前端展示与交互排布,不改变 Flower 代理接口路径、请求/响应字段或 `celery.read/celery.manage` 权限语义。 diff --git a/web/src/app/admin/workers/page.tsx b/web/src/app/admin/workers/page.tsx index 4944e70..72706d7 100644 --- a/web/src/app/admin/workers/page.tsx +++ b/web/src/app/admin/workers/page.tsx @@ -6,13 +6,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties, ComponentType } from "react"; import { useQuery } from "@tanstack/react-query"; import { - Alert, Button, Card, + Col, Drawer, Empty, Form, Input, + Row, Select, Space, Spin, @@ -23,11 +24,21 @@ import { type CardProps, type TableColumnsType, } from "antd"; +import { EyeOutlined } from "@ant-design/icons"; import { useAuth } from "@/components/auth-provider"; import { AdminPageLoading } from "@/components/admin-page-loading"; +import { useMobileDetection } from "@/hooks/use-mobile-detection"; +import { useToastFeedback } from "@/hooks/use-toast-feedback"; import { readApiError } from "@/lib/api"; import { getTaskDisplayName } from "@/lib/celery-task-display"; +import { + formatTaskMonitorDuration, + formatTaskMonitorErrorMessage, + getQueueDisplayName, + getTaskSourceDisplay, + getTaskStateDisplay, +} from "@/lib/task-monitor-display"; const { Text } = Typography; const AntCard = Card as unknown as ComponentType; @@ -106,30 +117,33 @@ function formatDateTime(value: string | null | undefined): string { } function renderTaskStateTag(state: string) { - const normalized = (state || "").toUpperCase(); - const color = - normalized === "STARTED" - ? "processing" - : normalized === "RECEIVED" - ? "blue" - : normalized === "SCHEDULED" - ? "purple" - : normalized === "RETRY" - ? "orange" - : normalized === "SUCCESS" - ? "green" - : normalized === "FAILURE" - ? "red" - : normalized === "REVOKED" - ? "default" - : "geekblue"; - return {normalized || "UNKNOWN"}; + const display = getTaskStateDisplay(state); + return {display.label}; +} + +function renderTaskSourceTag(source: string) { + const display = getTaskSourceDisplay(source); + return {display.label}; } function renderWorkerStatusTag(status: string) { return (status || "").toUpperCase() === "ONLINE" ? 在线 : 离线; } +function renderQueueTags(queueNames: string[]) { + return queueNames.length > 0 ? ( + + {queueNames.map((queueName) => ( + + {getQueueDisplayName(queueName)} + + ))} + + ) : ( + - + ); +} + function containsText(source: string | null | undefined, keyword: string): boolean { if (!keyword) { return true; @@ -146,6 +160,7 @@ function parseStatusFilter(value: string | undefined): "all" | "online" | "offli export default function AdminWorkersPage() { const { user, initializing, fetchWithAuth, hasPermission } = useAuth(); + const isMobile = useMobileDetection(); const canRead = hasPermission("celery.read") || hasPermission("celery.manage"); const [autoRefresh, setAutoRefresh] = useState(true); @@ -155,6 +170,7 @@ export default function AdminWorkersPage() { const [selectedWorker, setSelectedWorker] = useState(null); const [tableScrollY, setTableScrollY] = useState(WORKERS_TABLE_MIN_SCROLL_Y); const tableScrollAnchorRef = useRef(null); + const viewMode: "table" | "card" = isMobile ? "card" : "table"; const overviewQuery = useQuery({ queryKey: ["worker-monitor-overview"], @@ -201,47 +217,62 @@ export default function AdminWorkersPage() { const workerColumns = useMemo>( () => [ { - title: "Worker", + title: "执行节点", dataIndex: "worker", key: "worker", - width: 280, + width: 220, + render: (value: string) => ( + + {value || "-"} + + ), }, { title: "状态", dataIndex: "status", key: "status", width: 90, + align: "center", render: (value: string) => renderWorkerStatusTag(value), }, { title: "队列", dataIndex: "queue_names", key: "queue_names", - render: (value: string[]) => (value.length > 0 ? value.join(", ") : "-"), + width: 180, + render: (value: string[]) => renderQueueTags(value), }, { title: "并发", dataIndex: "concurrency", key: "concurrency", - width: 80, + width: 70, + align: "center", }, { - title: "Prefetch", + title: "预取", dataIndex: "prefetch_count", key: "prefetch_count", - width: 90, + width: 70, + align: "center", }, { - title: "已注册任务", - dataIndex: "registered_count", - key: "registered_count", - width: 110, + title: "任务统计", + key: "runtime_counts", + width: 130, + render: (_: unknown, record) => `${record.active_count}/${record.reserved_count}/${record.scheduled_count}`, }, { title: "累计处理", dataIndex: "processed_count", key: "processed_count", - width: 110, + width: 100, + }, + { + title: "注册任务", + dataIndex: "registered_count", + key: "registered_count", + width: 100, }, { title: "最近心跳", @@ -250,20 +281,13 @@ export default function AdminWorkersPage() { width: 170, render: (value: string | null) => formatDateTime(value), }, - { - title: "Active/Reserved/Scheduled", - key: "runtime_counts", - width: 190, - render: (_: unknown, record) => `${record.active_count}/${record.reserved_count}/${record.scheduled_count}`, - }, { title: "操作", key: "action", - width: 120, - fixed: "right", + width: 110, render: (_: unknown, record) => ( - ), }, @@ -274,14 +298,14 @@ export default function AdminWorkersPage() { const taskColumns = useMemo>( () => [ { - title: "Task ID", + title: "任务 ID", dataIndex: "task_id", key: "task_id", width: 260, render: (value: string) => {value}, }, { - title: "任务名", + title: "任务名称", dataIndex: "name", key: "name", width: 220, @@ -295,21 +319,21 @@ export default function AdminWorkersPage() { render: (value: string) => renderTaskStateTag(value), }, { - title: "来源", + title: "监控分组", dataIndex: "source", key: "source", - width: 90, - render: (value: string) => value || "-", + width: 120, + render: (value: string) => renderTaskSourceTag(value), }, { title: "队列", dataIndex: "queue_name", key: "queue_name", width: 120, - render: (value: string | null) => value || "-", + render: (value: string | null) => getQueueDisplayName(value), }, { - title: "Worker", + title: "执行节点", dataIndex: "worker", key: "worker", width: 220, @@ -323,21 +347,21 @@ export default function AdminWorkersPage() { render: (value: string | null) => formatDateTime(value), }, { - title: "接收", + title: "接收时间", dataIndex: "received_at", key: "received_at", width: 170, render: (value: string | null) => formatDateTime(value), }, { - title: "开始", + title: "开始时间", dataIndex: "started_at", key: "started_at", width: 170, render: (value: string | null) => formatDateTime(value), }, { - title: "完成", + title: "完成时间", dataIndex: "finished_at", key: "finished_at", width: 170, @@ -348,24 +372,24 @@ export default function AdminWorkersPage() { dataIndex: "runtime_seconds", key: "runtime_seconds", width: 110, - render: (value: number | null) => (value === null ? "-" : `${value.toFixed(3)}s`), + render: (value: number | null) => formatTaskMonitorDuration(value), }, { - title: "Args", + title: "位置参数", dataIndex: "args_text", key: "args_text", width: 220, render: (value: string | null) => (value ? {value} : "-"), }, { - title: "Kwargs", + title: "关键字参数", dataIndex: "kwargs_text", key: "kwargs_text", width: 220, render: (value: string | null) => (value ? {value} : "-"), }, { - title: "Result", + title: "执行结果", dataIndex: "result_text", key: "result_text", width: 220, @@ -379,7 +403,7 @@ export default function AdminWorkersPage() { ), }, { - title: "Exception", + title: "异常信息", dataIndex: "exception_text", key: "exception_text", width: 220, @@ -419,6 +443,17 @@ export default function AdminWorkersPage() { }); }, [overviewQuery.data?.workers, queueKeyword, statusFilter, workerKeyword]); + const overviewErrorMessage = overviewQuery.error instanceof Error + ? formatTaskMonitorErrorMessage(overviewQuery.error.message, "Worker监控数据加载失败,请稍后重试。") + : ""; + const taskErrorMessage = workerTasksQuery.error instanceof Error + ? formatTaskMonitorErrorMessage(workerTasksQuery.error.message, "Worker任务数据加载失败,请稍后重试。") + : ""; + + useToastFeedback({ + errorMessage: overviewErrorMessage || taskErrorMessage, + }); + const updateTableScrollY = useCallback(() => { if (typeof window === "undefined") { return; @@ -450,7 +485,7 @@ export default function AdminWorkersPage() { return; } window.requestAnimationFrame(updateTableScrollY); - }, [filteredWorkers.length, overviewQuery.isFetching, updateTableScrollY]); + }, [filteredWorkers.length, overviewErrorMessage, overviewQuery.isFetching, updateTableScrollY, viewMode]); useEffect(() => { if (typeof window === "undefined") { @@ -522,47 +557,87 @@ export default function AdminWorkersPage() { const overview = overviewQuery.data; const taskOverview = workerTasksQuery.data; - return ( -
- {overviewQuery.error && ( - ( + + + {workerItem.worker} + + {renderWorkerStatusTag(workerItem.status)} + + } + extra={ + + return ( +
+ + {overviewQuery.isFetching && } + + 自动刷新 + - } - > -
- + + + )} + > + {viewMode === "card" ? ( + + setWorkerKeyword(event.target.value)} /> - + - + setWorkerKeyword(event.target.value)} + /> + - - + + setQueueKeyword(event.target.value)} + /> + + + +