[fix]:[FL-72][任务监控页面样式布局优化]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-09 13:49:25 +08:00
parent f7013ff32c
commit 098780b14e
3 changed files with 247 additions and 80 deletions
+28
View File
@@ -145,3 +145,31 @@
- 风险与关注点:
- 当前仓库内仍没有 `tpbig.exe``rjtzl.exe`、旧 ATP 模型目录和 EGM 子目录等真实生产运行资产;本次交付的是代码层接入、状态检测和可测的 worker 执行协议。
- 上线前仍需在容器/部署环境提供实际 legacy 资产挂载,并用真实资产做 smoke test。
## Work Log - 任务监控页面样式布局优化(2026-06-09)
- 背景:
- Issue `FL-72` 要求“参考用户管理页面的样式和布局优化任务监控页面,包括 margin、padding、border 以及自适应高度”等。
- 现状是 `web/src/app/admin/task-monitor/page.tsx` 采用“筛选卡 + 表格卡”的分离布局,和用户管理页的单卡结构不一致;任务表格也缺少在少量数据下的最小高度占位与视口自适应滚动高度。
- 本次改动:
- `web/src/app/admin/task-monitor/page.tsx`
- 页面主体收敛为与用户管理页一致的单卡布局,标题统一为“任务监控”。
- 将 Worker/队列/任务/状态筛选与自动刷新开关合并进卡片顶部 `Form inline` 区域,补充“重置筛选”按钮。
- 卡片顶部 `extra` 区域增加统一的刷新按钮和加载中的小型 `Spin`,保持和管理页操作区的交互节奏一致。
- 增加生成时间、Worker 总数、在线/离线数、筛选后任务数的二级信息行,替代原本散落在筛选区尾部的时间文案。
- 任务表格新增与用户管理页一致的动态高度计算(`scroll.y` + `resize` 监听 + `ResizeObserver`),避免筛选结果较少时表格区域坍塌。
- 加载态统一为 `min-h-[240px]` 居中 `Spin`;空态文案改为“暂无符合筛选条件的任务数据。”。
- 保持任务监控的数据请求、轮询、权限判断和表格列定义不变,仅调整布局与展示容器。
- `web/src/app/globals.css`
- 新增 `.admin-task-monitor-table-anchor .ant-table-body` 作用域样式,通过页面注入变量设置最小高度,避免影响其他 Ant Design 表格。
- 验证:
- 基线:
- `npm --workspace web exec eslint src/app/admin/task-monitor/page.tsx --max-warnings=0` -> 通过。
- 修改后:
- `npm --workspace web exec eslint src/app/admin/task-monitor/page.tsx --max-warnings=0` -> 通过。
- `npm run build:web` -> 通过。
- 风险与关注点:
- 影响范围仅 `任务监控` 页面前端展示层与局部表格样式作用域,不涉及后端接口、权限或任务监控数据口径变更。
+215 -80
View File
@@ -2,14 +2,15 @@
import Link from "next/link";
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import type { ComponentType } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
import { useQuery } from "@tanstack/react-query";
import {
Alert,
Button,
Card,
Empty,
Form,
Input,
Select,
Space,
@@ -30,6 +31,9 @@ const { Text } = Typography;
const AntCard = Card as unknown as ComponentType<CardProps>;
const DEFAULT_RECENT_LIMIT = 100;
const TASK_MONITOR_TABLE_MIN_SCROLL_Y = 180;
const TASK_MONITOR_TABLE_VIEWPORT_GAP = 40;
const TASK_MONITOR_TABLE_FALLBACK_RESERVE = 220;
type FlowerWorkerItem = {
worker: string;
@@ -156,6 +160,8 @@ export default function AdminTaskMonitorPage() {
const [queueKeyword, setQueueKeyword] = useState("");
const [taskKeyword, setTaskKeyword] = useState("");
const [statusFilter, setStatusFilter] = useState<"all" | "online" | "offline">("all");
const [tableScrollY, setTableScrollY] = useState(TASK_MONITOR_TABLE_MIN_SCROLL_Y);
const tableScrollAnchorRef = useRef<HTMLDivElement | null>(null);
const workersOverviewQuery = useQuery({
queryKey: ["flower-workers-overview"],
@@ -196,6 +202,8 @@ export default function AdminTaskMonitorPage() {
staleTime: 15_000,
});
const workersOverview = workersOverviewQuery.data;
const taskColumns = useMemo<TableColumnsType<TaskTableRow>>(
() => [
{
@@ -352,9 +360,93 @@ export default function AdminTaskMonitorPage() {
});
}, [allTaskRows, filteredWorkers, taskKeyword]);
const handleResetFilters = () => {
setWorkerKeyword("");
setQueueKeyword("");
setTaskKeyword("");
setStatusFilter("all");
};
const updateTableScrollY = useCallback(() => {
if (typeof window === "undefined") {
return;
}
const anchor = tableScrollAnchorRef.current;
if (!anchor) {
return;
}
const anchorTop = anchor.getBoundingClientRect().top;
const tableWrapper = anchor.querySelector<HTMLElement>(".ant-table-wrapper");
const tableBody = anchor.querySelector<HTMLElement>(".ant-table-body");
let nextHeight = Math.floor(window.innerHeight - anchorTop - TASK_MONITOR_TABLE_FALLBACK_RESERVE);
if (tableWrapper) {
const wrapperRect = tableWrapper.getBoundingClientRect();
const bodyHeight = tableBody?.getBoundingClientRect().height ?? TASK_MONITOR_TABLE_MIN_SCROLL_Y;
const nonBodyHeight = Math.max(0, wrapperRect.height - bodyHeight);
const topGap = Math.max(0, wrapperRect.top - anchorTop);
nextHeight = Math.floor(window.innerHeight - anchorTop - topGap - nonBodyHeight - TASK_MONITOR_TABLE_VIEWPORT_GAP);
}
const clampedHeight = Math.max(TASK_MONITOR_TABLE_MIN_SCROLL_Y, nextHeight);
setTableScrollY((previous) => (Math.abs(previous - clampedHeight) <= 1 ? previous : clampedHeight));
}, []);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
window.requestAnimationFrame(updateTableScrollY);
}, [
allTasksQuery.isFetching,
filteredTaskRows.length,
statusFilter,
updateTableScrollY,
workerKeyword,
workersOverviewQuery.error,
workersOverviewQuery.isFetching,
workersOverview?.summary.total,
]);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const onViewportChange = () => {
window.requestAnimationFrame(updateTableScrollY);
};
window.addEventListener("resize", onViewportChange);
return () => {
window.removeEventListener("resize", onViewportChange);
};
}, [updateTableScrollY]);
useEffect(() => {
if (typeof window === "undefined" || typeof ResizeObserver === "undefined") {
return;
}
const anchor = tableScrollAnchorRef.current;
if (!anchor) {
return;
}
const resizeObserver = new ResizeObserver(() => {
window.requestAnimationFrame(updateTableScrollY);
});
resizeObserver.observe(anchor);
return () => {
resizeObserver.disconnect();
};
}, [updateTableScrollY]);
if (initializing || (workersOverviewQuery.isLoading && !workersOverviewQuery.data && canRead && Boolean(user))) {
return (
<div className="py-10">
<div className="flex min-h-[240px] items-center justify-center">
<Spin tip="任务监控数据加载中..." />
</div>
);
@@ -388,95 +480,138 @@ export default function AdminTaskMonitorPage() {
);
}
const workersOverview = workersOverviewQuery.data;
return (
<Space direction="vertical" size={16} style={{ width: "100%" }}>
<AntCard>
<Space size={16} wrap>
<Input
allowClear
placeholder="按 Worker 名称筛选"
value={workerKeyword}
onChange={(event) => setWorkerKeyword(event.target.value)}
style={{ width: 220 }}
/>
<Input
allowClear
placeholder="按队列名称筛选"
value={queueKeyword}
onChange={(event) => setQueueKeyword(event.target.value)}
style={{ width: 220 }}
/>
<Input
allowClear
placeholder="按 Task ID/任务名筛选"
value={taskKeyword}
onChange={(event) => setTaskKeyword(event.target.value)}
style={{ width: 240 }}
/>
<Select
value={statusFilter}
onChange={(value) => setStatusFilter(parseStatusFilter(value))}
options={[
{ label: "全部状态", value: "all" },
{ label: "在线", value: "online" },
{ label: "离线", value: "offline" },
]}
style={{ width: 150 }}
/>
<Space size={8}>
<Text></Text>
<Switch checked={autoRefresh} onChange={setAutoRefresh} />
<div className="space-y-6">
<AntCard
title="任务监控"
extra={(
<Space>
{(workersOverviewQuery.isFetching || allTasksQuery.isFetching) && <Spin size="small" />}
<Button
onClick={() => {
void workersOverviewQuery.refetch();
void allTasksQuery.refetch();
}}
loading={workersOverviewQuery.isFetching || allTasksQuery.isFetching}
>
</Button>
</Space>
<Button
onClick={() => {
void workersOverviewQuery.refetch();
void allTasksQuery.refetch();
}}
loading={workersOverviewQuery.isFetching || allTasksQuery.isFetching}
>
</Button>
)}
>
<Form layout="inline" style={{ rowGap: 12 }}>
<Form.Item label="Worker" className="min-w-[220px]">
<Input
allowClear
placeholder="按 Worker 名称筛选"
value={workerKeyword}
onChange={(event) => setWorkerKeyword(event.target.value)}
/>
</Form.Item>
<Form.Item label="队列" className="min-w-[220px]">
<Input
allowClear
placeholder="按队列名称筛选"
value={queueKeyword}
onChange={(event) => setQueueKeyword(event.target.value)}
/>
</Form.Item>
<Form.Item label="任务" className="min-w-[240px]">
<Input
allowClear
placeholder="按 Task ID/任务名筛选"
value={taskKeyword}
onChange={(event) => setTaskKeyword(event.target.value)}
/>
</Form.Item>
<Form.Item label="状态" className="min-w-[170px]">
<Select
value={statusFilter}
onChange={(value) => setStatusFilter(parseStatusFilter(value))}
options={[
{ label: "全部状态", value: "all" },
{ label: "在线", value: "online" },
{ label: "离线", value: "offline" },
]}
/>
</Form.Item>
<Form.Item>
<Space size={8}>
<Text></Text>
<Switch checked={autoRefresh} onChange={setAutoRefresh} />
</Space>
</Form.Item>
<Form.Item>
<Button onClick={handleResetFilters}></Button>
</Form.Item>
</Form>
<Space className="mt-4" size={[16, 8]} wrap>
<Text type="secondary">{formatDateTime(workersOverview?.generated_at)}</Text>
<Text type="secondary">Worker{filteredWorkers.length}/{workersOverview?.summary.total ?? 0}</Text>
<Text type="secondary">线{workersOverview?.summary.online ?? 0}</Text>
<Text type="secondary">线{workersOverview?.summary.offline ?? 0}</Text>
<Text type="secondary">{filteredTaskRows.length} </Text>
</Space>
</AntCard>
{workersOverviewQuery.error && (
<Alert
type="error"
showIcon
message={workersOverviewQuery.error instanceof Error ? workersOverviewQuery.error.message : "任务监控数据加载失败"}
/>
)}
{allTasksQuery.error && (
<Alert
type="error"
showIcon
message={allTasksQuery.error instanceof Error ? allTasksQuery.error.message : "任务列表数据加载失败"}
/>
)}
{workersOverviewQuery.error && (
<Alert
className="mt-4"
type="error"
showIcon
message={workersOverviewQuery.error instanceof Error ? workersOverviewQuery.error.message : "任务监控数据加载失败"}
/>
)}
{allTasksQuery.error && (
<Alert
className="mt-4"
type="error"
showIcon
message={allTasksQuery.error instanceof Error ? allTasksQuery.error.message : "任务列表数据加载失败"}
/>
)}
{!workersOverview && !workersOverviewQuery.isFetching && (
<AntCard>
<Empty description="暂无任务监控数据" />
</AntCard>
)}
{!workersOverview && !workersOverviewQuery.isFetching && (
<div className="mt-4">
<Empty description="暂无任务监控数据" />
</div>
)}
{workersOverview && (
<>
<AntCard title="任务明细">
{workersOverview && (
<div
ref={tableScrollAnchorRef}
className="admin-task-monitor-table-anchor mt-4"
style={{ "--admin-task-monitor-table-body-min-height": `${tableScrollY}px` } as CSSProperties}
>
<Table<TaskTableRow>
rowKey={(record) => record.key}
columns={taskColumns}
dataSource={filteredTaskRows}
pagination={{ pageSize: 50, showSizeChanger: true }}
locale={{ emptyText: "暂无任务数据" }}
scroll={{ x: 2600 }}
pagination={{
pageSize: 50,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100, 200],
showTotal: (total) => `${total}`,
style: { marginBottom: 0 },
}}
locale={{
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暂无符合筛选条件的任务数据。"
/>
),
}}
scroll={{ x: 2600, y: tableScrollY }}
/>
</AntCard>
</>
)}
</Space>
</div>
)}
</AntCard>
</div>
);
}
+4
View File
@@ -253,6 +253,10 @@ body {
min-height: var(--admin-workers-table-body-min-height, 180px);
}
.admin-task-monitor-table-anchor .ant-table-body {
min-height: var(--admin-task-monitor-table-body-min-height, 180px);
}
.admin-roles-table-anchor .ant-table-body {
min-height: var(--admin-roles-table-body-min-height, 180px);
}