[fix]:[FL-156][任务监控移动端无限滚动对齐]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-20 15:10:03 +08:00
parent 7022d7f266
commit 57fbdbf25a
2 changed files with 133 additions and 23 deletions
+22
View File
@@ -447,3 +447,25 @@
- 风险与关注点:
- 改动仅影响角色管理页前端表格分页回调和 hook 组织顺序,不改变接口、schema、权限或 CRUD 语义。
## Follow-up - 任务监控页移动端无限滚动对齐(FL-156)
- 背景:
- 复核指出任务监控页移动端卡片一次性渲染全部筛选任务,缺少与用户管理页一致的卡片分页累积、滚动加载和加载更多状态。
- 本次处理:
- 任务监控页补齐 `cardViewPage``allLoadedTasks``isLoadingMore``pageCardRef`,与用户管理页移动卡片基础设施对齐。
- 移动端卡片视图改为按当前 pageSize 分页累积渲染,滚动到卡片容器底部时自动追加下一页。
- 卡片视图补齐“加载更多...”指示器,并仅在已加载全部筛选任务时显示“已加载全部 X 条数据”。
- 筛选条件变更和重置筛选时同步重置表格分页、卡片页码、累积任务和加载更多状态。
- 移动端筛选 `Form.Item``marginBottom` 统一调整为 0,对齐用户管理页表单样式口径。
- 验证:
- 基线:`npm --workspace web exec eslint src/app/admin/task-monitor/page.tsx --max-warnings=0` 通过。
- 基线:`npm --workspace web exec tsc --noEmit` 通过。
- 修改后:`npm --workspace web exec eslint src/app/admin/task-monitor/page.tsx --max-warnings=0` 通过。
- 修改后:`npm --workspace web exec tsc --noEmit` 通过。
- 修改后:`node --experimental-strip-types web/src/lib/task-monitor-display.test.js` 通过,4 passed,仍有既有 MODULE_TYPELESS_PACKAGE_JSON warning。
- 风险与关注点:
- 任务监控数据仍来自既有 Flower 批量接口,本次只改变移动端前端渲染批次和滚动加载交互,不改变接口字段、权限或任务筛选规则。
+111 -23
View File
@@ -164,6 +164,18 @@ export default function AdminTaskMonitorPage() {
const [tableScrollY, setTableScrollY] = useState(TASK_MONITOR_TABLE_MIN_SCROLL_Y);
const tableScrollAnchorRef = useRef<HTMLDivElement | null>(null);
const viewMode: "table" | "card" = isMobile ? "card" : "table";
const [cardViewPage, setCardViewPage] = useState(1);
const [allLoadedTasks, setAllLoadedTasks] = useState<TaskTableRow[]>([]);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const pageCardRef = useRef<HTMLDivElement | null>(null);
const { current: paginationCurrent, pageSize: paginationPageSize } = pagination;
const resetTaskListPagination = useCallback(() => {
setPagination((prev) => ({ ...prev, current: 1 }));
setCardViewPage(1);
setAllLoadedTasks([]);
setIsLoadingMore(false);
}, []);
const workersOverviewQuery = useQuery({
queryKey: ["flower-workers-overview"],
@@ -381,7 +393,7 @@ export default function AdminTaskMonitorPage() {
setQueueKeyword("");
setTaskKeyword("");
setStatusFilter("all");
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
};
const workersOverviewErrorMessage = workersOverviewQuery.error instanceof Error
@@ -396,6 +408,74 @@ export default function AdminTaskMonitorPage() {
errorMessage: anyError,
});
useEffect(() => {
if (viewMode !== "card" || allTasksQuery.isLoading) {
return;
}
const frameId = window.requestAnimationFrame(() => {
const nextTasks = filteredTaskRows.slice(0, cardViewPage * paginationPageSize);
if (cardViewPage === 1) {
setAllLoadedTasks(() => nextTasks);
} else {
setAllLoadedTasks((prev) => {
if (nextTasks.length === 0) {
return prev;
}
const existingKeys = new Set(prev.map((task) => task.key));
const newTasks = nextTasks.filter((task) => !existingKeys.has(task.key));
return [...prev, ...newTasks];
});
}
setIsLoadingMore(false);
});
return () => {
window.cancelAnimationFrame(frameId);
};
}, [allTasksQuery.isLoading, cardViewPage, filteredTaskRows, paginationPageSize, viewMode]);
useEffect(() => {
if (viewMode !== "card") {
return;
}
const pageCard = pageCardRef.current;
if (!pageCard) {
return;
}
const cardBody = pageCard.querySelector<HTMLElement>(".ant-card-body");
if (!cardBody) {
return;
}
const handleScroll = () => {
if (isLoadingMore || allTasksQuery.isLoading) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = cardBody;
if (scrollTop + clientHeight >= scrollHeight - 100) {
const total = filteredTaskRows.length;
const loadedCount = allLoadedTasks.length;
if (loadedCount < total) {
setIsLoadingMore(true);
setCardViewPage((prev) => prev + 1);
setPagination((prev) => ({ ...prev, current: prev.current + 1 }));
}
}
};
cardBody.addEventListener("scroll", handleScroll);
return () => {
cardBody.removeEventListener("scroll", handleScroll);
};
}, [allLoadedTasks.length, allTasksQuery.isLoading, filteredTaskRows.length, isLoadingMore, viewMode]);
const updateTableScrollY = useCallback(() => {
if (typeof window === "undefined") {
return;
@@ -578,6 +658,7 @@ export default function AdminTaskMonitorPage() {
return (
<div className="flex min-h-0 flex-1 flex-col">
<AntCard
ref={pageCardRef}
className="admin-task-monitor-page-card"
title="任务监控"
extra={(
@@ -597,48 +678,48 @@ export default function AdminTaskMonitorPage() {
>
{viewMode === "card" ? (
<Form layout="vertical" style={{ marginBottom: 16 }}>
<Form.Item label="执行节点" style={{ marginBottom: 12 }}>
<Form.Item label="执行节点" style={{ marginBottom: 0 }}>
<Input
allowClear
placeholder="按执行节点名称筛选"
value={workerKeyword}
onChange={(event) => {
setWorkerKeyword(event.target.value);
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
}}
/>
</Form.Item>
<Form.Item label="队列" style={{ marginBottom: 12 }}>
<Form.Item label="队列" style={{ marginBottom: 0 }}>
<Input
allowClear
placeholder="按队列名称筛选"
value={queueKeyword}
onChange={(event) => {
setQueueKeyword(event.target.value);
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
}}
/>
</Form.Item>
<Form.Item label="任务" style={{ marginBottom: 12 }}>
<Form.Item label="任务" style={{ marginBottom: 0 }}>
<Input
allowClear
placeholder="按任务 ID / 任务名称筛选"
value={taskKeyword}
onChange={(event) => {
setTaskKeyword(event.target.value);
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
}}
/>
</Form.Item>
<Form.Item label="状态" style={{ marginBottom: 12 }}>
<Form.Item label="状态" style={{ marginBottom: 0 }}>
<Select
value={statusFilter}
onChange={(value) => {
setStatusFilter(parseStatusFilter(value));
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
}}
options={[
{ label: "全部状态", value: "all" },
@@ -667,7 +748,7 @@ export default function AdminTaskMonitorPage() {
value={workerKeyword}
onChange={(event) => {
setWorkerKeyword(event.target.value);
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
}}
/>
</Form.Item>
@@ -679,7 +760,7 @@ export default function AdminTaskMonitorPage() {
value={queueKeyword}
onChange={(event) => {
setQueueKeyword(event.target.value);
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
}}
/>
</Form.Item>
@@ -691,7 +772,7 @@ export default function AdminTaskMonitorPage() {
value={taskKeyword}
onChange={(event) => {
setTaskKeyword(event.target.value);
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
}}
/>
</Form.Item>
@@ -701,7 +782,7 @@ export default function AdminTaskMonitorPage() {
value={statusFilter}
onChange={(value) => {
setStatusFilter(parseStatusFilter(value));
setPagination((prev) => ({ ...prev, current: 1 }));
resetTaskListPagination();
}}
options={[
{ label: "全部状态", value: "all" },
@@ -754,8 +835,8 @@ export default function AdminTaskMonitorPage() {
loading={workersOverviewQuery.isLoading || allTasksQuery.isLoading}
tableLayout="fixed"
pagination={{
current: pagination.current,
pageSize: pagination.pageSize,
current: paginationCurrent,
pageSize: paginationPageSize,
total: Math.max(filteredTaskRows.length, 1),
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
@@ -779,11 +860,11 @@ export default function AdminTaskMonitorPage() {
</div>
) : workersOverview ? (
<div className="admin-task-monitor-card-view">
{allTasksQuery.isLoading ? (
{allTasksQuery.isLoading && allLoadedTasks.length === 0 ? (
<div className="admin-task-monitor-card-view-state">
<Spin tip="加载中..." />
</div>
) : filteredTaskRows.length === 0 ? (
) : allLoadedTasks.length === 0 ? (
<div className="admin-task-monitor-card-view-state">
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
@@ -793,17 +874,24 @@ export default function AdminTaskMonitorPage() {
) : (
<div className="admin-task-monitor-card-view-content">
<Row gutter={[12, 12]}>
{filteredTaskRows.map((task) => (
{allLoadedTasks.map((task) => (
<Col key={task.key} xs={24} sm={24} md={12} lg={8} xl={6}>
{renderTaskCard(task)}
</Col>
))}
</Row>
<div style={{ textAlign: "center", padding: "20px 0" }}>
<Typography.Text type="secondary">
{filteredTaskRows.length}
</Typography.Text>
</div>
{isLoadingMore && (
<div style={{ textAlign: "center", padding: "20px 0" }}>
<Spin tip="加载更多..." />
</div>
)}
{allLoadedTasks.length >= filteredTaskRows.length && allLoadedTasks.length > 0 && (
<div style={{ textAlign: "center", padding: "20px 0" }}>
<Typography.Text type="secondary">
{allLoadedTasks.length}
</Typography.Text>
</div>
)}
</div>
)}
</div>