From bbeea440f958370fdbf16aa69d70a76146081c24 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Thu, 6 Aug 2026 16:11:13 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20dashboard=20=E5=BE=85=E5=8A=9E?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E7=82=B9=E5=87=BB=E5=AF=BC=E8=88=AA=E5=8E=BB?= =?UTF-8?q?=E9=87=8D=E4=B8=8E=E9=98=B2=E6=8A=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 同一目标路径不再重复 navigate,避免历史记录堆叠 - 400ms 冷却期内忽略重复/幽灵点击,防止来回跳转 - 新增 shouldNavigateTodoCard 守卫单元测试 --- .../DashboardTodoCards.integration.test.ts | 16 ++++++++++ .../pages/Dashboard/DashboardTodoCards.tsx | 31 ++++++++++++++++--- 2 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 apps/admin/src/pages/Dashboard/DashboardTodoCards.integration.test.ts diff --git a/apps/admin/src/pages/Dashboard/DashboardTodoCards.integration.test.ts b/apps/admin/src/pages/Dashboard/DashboardTodoCards.integration.test.ts new file mode 100644 index 0000000..678b0f6 --- /dev/null +++ b/apps/admin/src/pages/Dashboard/DashboardTodoCards.integration.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { shouldNavigateTodoCard } from './DashboardTodoCards'; + +describe('DashboardTodoCards 点击导航守卫', () => { + it('目标路径与当前路径不同且冷却已过时允许导航', () => { + expect(shouldNavigateTodoCard('/dashboard', '/attendance', 1000, 1600)).toBe(true); + }); + + it('目标路径与当前路径相同时不允许导航', () => { + expect(shouldNavigateTodoCard('/attendance', '/attendance', 1000, 1600)).toBe(false); + }); + + it('冷却期内忽略重复点击,防止产生多条历史记录', () => { + expect(shouldNavigateTodoCard('/dashboard', '/attendance', 1000, 1300)).toBe(false); + }); +}); diff --git a/apps/admin/src/pages/Dashboard/DashboardTodoCards.tsx b/apps/admin/src/pages/Dashboard/DashboardTodoCards.tsx index 9b71270..db49ce0 100644 --- a/apps/admin/src/pages/Dashboard/DashboardTodoCards.tsx +++ b/apps/admin/src/pages/Dashboard/DashboardTodoCards.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { Card, Col, Row } from 'antd'; import { ArrowRightOutlined, @@ -6,9 +6,22 @@ import { DollarOutlined, ExclamationCircleOutlined, } from '@ant-design/icons'; -import { useNavigate } from 'react-router'; +import { useLocation, useNavigate } from 'react-router'; import { MARGIN_BOTTOM_16_STYLE, TODO_CARD_DANGER, TODO_CARD_DRAFT, TODO_CARD_OK, TODO_CARD_WARN } from './Dashboard.types'; +/** 冷却期内忽略重复点击,避免快速/幽灵点击压入多条历史记录。 */ +const TODO_CLICK_LOCK_MS = 400; + +export function shouldNavigateTodoCard( + currentPath: string, + targetPath: string, + lastNavAt: number, + now: number, +): boolean { + if (currentPath === targetPath) return false; + return now - lastNavAt >= TODO_CLICK_LOCK_MS; +} + export const DashboardTodoCards: React.FC<{ absentCount: number; draftCount: number; @@ -16,6 +29,14 @@ export const DashboardTodoCards: React.FC<{ pendingDeposits: number; }> = ({ absentCount, draftCount, draftTotal, pendingDeposits }) => { const navigate = useNavigate(); + const location = useLocation(); + const lastNavAtRef = useRef(0); + const go = (target: string) => { + const now = Date.now(); + if (!shouldNavigateTodoCard(location.pathname, target, lastNavAtRef.current, now)) return; + lastNavAtRef.current = now; + navigate(target); + }; return ( @@ -23,7 +44,7 @@ export const DashboardTodoCards: React.FC<{ 0 ? TODO_CARD_WARN : TODO_CARD_OK} styles={{ body: { padding: 16 } }} - onClick={() => navigate('/attendance')} + onClick={() => go('/attendance')} >
0 ? TODO_CARD_DRAFT : TODO_CARD_OK} styles={{ body: { padding: 16 } }} - onClick={() => navigate('/bills')} + onClick={() => go('/bills')} >
0 ? TODO_CARD_DANGER : TODO_CARD_OK} styles={{ body: { padding: 16 } }} - onClick={() => navigate('/deposits')} + onClick={() => go('/deposits')} >