chore: 删除未使用的 Tables 组件和 Hooks (第8批)

删除以下未被引用的文件:
- TablesProjectRow.js / TablesTableRow.js
- useDashboardEvents.js / useEventNotifications.js
- useFirstScreenMetrics.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-29 17:22:05 +08:00
parent 290e9c2dba
commit 8c2260cf44
5 changed files with 0 additions and 1083 deletions

View File

@@ -1,84 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import {
Button,
Flex,
Icon,
Progress,
Td,
Text,
Tr,
useColorModeValue,
} from "@chakra-ui/react";
import React from "react";
import { MoreVertical } from "lucide-react";
function DashboardTableRow(props) {
const { logo, name, status, budget, progression } = props;
const textColor = useColorModeValue("gray.700", "white");
return (
<Tr>
<Td minWidth={{ sm: "250px" }} ps="0px">
<Flex alignItems="center" py=".8rem" minWidth="100%" flexWrap="nowrap">
<Icon as={logo} h={"24px"} w={"24px"} me="18px" />
<Text
fontSize="md"
color={textColor}
fontWeight="bold"
minWidth="100%"
>
{name}
</Text>
</Flex>
</Td>
<Td>
<Text fontSize="md" color={textColor} fontWeight="bold" pb=".5rem">
{budget}
</Text>
</Td>
<Td>
<Text fontSize="md" color={textColor} fontWeight="bold" pb=".5rem">
{status}
</Text>
</Td>
<Td>
<Flex direction="column">
<Text
fontSize="md"
color="teal.300"
fontWeight="bold"
pb=".2rem"
>{`${progression}%`}</Text>
<Progress
colorScheme={progression === 100 ? "teal" : "cyan"}
size="xs"
value={progression}
borderRadius="15px"
/>
</Flex>
</Td>
<Td>
<Button p="0px" bg="transparent">
<Icon as={MoreVertical} color="gray.400" cursor="pointer" />
</Button>
</Td>
</Tr>
);
}
export default DashboardTableRow;

View File

@@ -1,120 +0,0 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
import {
Avatar,
Badge,
Button,
Flex,
Td,
Text,
Tr,
useColorModeValue,
} from "@chakra-ui/react";
import React from "react";
function TablesTableRow(props) {
const {
logo,
name,
email,
subdomain,
domain,
status,
date,
paddingY,
isLast,
} = props;
const textColor = useColorModeValue("gray.500", "white");
const titleColor = useColorModeValue("gray.700", "white");
const bgStatus = useColorModeValue("gray.400", "navy.900");
const borderColor = useColorModeValue("gray.200", "gray.600");
return (
<Tr>
<Td
minWidth={{ sm: "250px" }}
pl="0px"
borderColor={borderColor}
borderBottom={isLast ? "none" : null}
>
<Flex
align="center"
py={paddingY ? paddingY : ".8rem"}
minWidth="100%"
flexWrap="nowrap"
>
<Avatar src={logo} w="50px" borderRadius="12px" me="18px" />
<Flex direction="column">
<Text
fontSize="md"
color={titleColor}
fontWeight="bold"
minWidth="100%"
>
{name}
</Text>
<Text fontSize="sm" color="gray.400" fontWeight="normal">
{email}
</Text>
</Flex>
</Flex>
</Td>
<Td borderColor={borderColor} borderBottom={isLast ? "none" : null}>
<Flex direction="column">
<Text fontSize="md" color={textColor} fontWeight="bold">
{domain}
</Text>
<Text fontSize="sm" color="gray.400" fontWeight="normal">
{subdomain}
</Text>
</Flex>
</Td>
<Td borderColor={borderColor} borderBottom={isLast ? "none" : null}>
<Badge
bg={status === "Online" ? "green.400" : bgStatus}
color={status === "Online" ? "white" : "white"}
fontSize="16px"
p="3px 10px"
borderRadius="8px"
>
{status}
</Badge>
</Td>
<Td borderColor={borderColor} borderBottom={isLast ? "none" : null}>
<Text fontSize="md" color={textColor} fontWeight="bold" pb=".5rem">
{date}
</Text>
</Td>
<Td borderColor={borderColor} borderBottom={isLast ? "none" : null}>
<Button p="0px" bg="transparent" variant="no-effects">
<Text
fontSize="md"
color="gray.400"
fontWeight="bold"
cursor="pointer"
>
Edit
</Text>
</Button>
</Td>
</Tr>
);
}
export default TablesTableRow;

View File

@@ -1,325 +0,0 @@
// src/hooks/useDashboardEvents.js
// 个人中心Dashboard/Center事件追踪 Hook
import { useCallback, useEffect } from 'react';
import { usePostHogTrack } from './usePostHogRedux';
import { RETENTION_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 个人中心事件追踪 Hook
* @param {Object} options - 配置选项
* @param {string} options.pageType - 页面类型 ('center' | 'profile' | 'settings')
* @param {Function} options.navigate - 路由导航函数
* @returns {Object} 事件追踪处理函数集合
*/
export const useDashboardEvents = ({ pageType = 'center', navigate } = {}) => {
const { track } = usePostHogTrack();
// 🎯 页面浏览事件 - 页面加载时触发
useEffect(() => {
const eventMap = {
'center': RETENTION_EVENTS.DASHBOARD_CENTER_VIEWED,
'profile': RETENTION_EVENTS.PROFILE_PAGE_VIEWED,
'settings': RETENTION_EVENTS.SETTINGS_PAGE_VIEWED,
};
const eventName = eventMap[pageType] || RETENTION_EVENTS.DASHBOARD_VIEWED;
track(eventName, {
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', `📊 Dashboard Page Viewed: ${pageType}`);
}, [track, pageType]);
/**
* 追踪功能卡片点击
* @param {string} cardName - 卡片名称 ('watchlist' | 'following_events' | 'comments' | 'subscription')
* @param {Object} cardData - 卡片数据
*/
const trackFunctionCardClicked = useCallback((cardName, cardData = {}) => {
if (!cardName) {
logger.warn('useDashboardEvents', 'Card name is required');
return;
}
track(RETENTION_EVENTS.FUNCTION_CARD_CLICKED, {
card_name: cardName,
data_count: cardData.count || 0,
has_data: Boolean(cardData.count && cardData.count > 0),
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '🎴 Function Card Clicked', {
cardName,
count: cardData.count,
});
}, [track]);
/**
* 追踪自选股列表查看
* @param {number} stockCount - 自选股数量
* @param {boolean} hasRealtime - 是否有实时行情
*/
const trackWatchlistViewed = useCallback((stockCount = 0, hasRealtime = false) => {
track('Watchlist Viewed', {
stock_count: stockCount,
has_realtime: hasRealtime,
is_empty: stockCount === 0,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '⭐ Watchlist Viewed', {
stockCount,
hasRealtime,
});
}, [track]);
/**
* 追踪自选股点击
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
* @param {number} position - 在列表中的位置
*/
const trackWatchlistStockClicked = useCallback((stock, position = 0) => {
if (!stock || !stock.code) {
logger.warn('useDashboardEvents', 'Stock object is required');
return;
}
track(RETENTION_EVENTS.STOCK_CLICKED, {
stock_code: stock.code,
stock_name: stock.name || '',
source: 'watchlist',
position,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '🎯 Watchlist Stock Clicked', {
stockCode: stock.code,
position,
});
}, [track]);
/**
* 追踪自选股添加
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
* @param {string} source - 来源 ('search' | 'stock_detail' | 'manual')
*/
const trackWatchlistStockAdded = useCallback((stock, source = 'manual') => {
if (!stock || !stock.code) {
logger.warn('useDashboardEvents', 'Stock object is required');
return;
}
track('Watchlist Stock Added', {
stock_code: stock.code,
stock_name: stock.name || '',
source,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', ' Watchlist Stock Added', {
stockCode: stock.code,
source,
});
}, [track]);
/**
* 追踪自选股移除
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
*/
const trackWatchlistStockRemoved = useCallback((stock) => {
if (!stock || !stock.code) {
logger.warn('useDashboardEvents', 'Stock object is required');
return;
}
track('Watchlist Stock Removed', {
stock_code: stock.code,
stock_name: stock.name || '',
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', ' Watchlist Stock Removed', {
stockCode: stock.code,
});
}, [track]);
/**
* 追踪关注的事件列表查看
* @param {number} eventCount - 关注的事件数量
*/
const trackFollowingEventsViewed = useCallback((eventCount = 0) => {
track('Following Events Viewed', {
event_count: eventCount,
is_empty: eventCount === 0,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '📌 Following Events Viewed', {
eventCount,
});
}, [track]);
/**
* 追踪关注的事件点击
* @param {Object} event - 事件对象
* @param {number} event.id - 事件ID
* @param {string} event.title - 事件标题
* @param {number} position - 在列表中的位置
*/
const trackFollowingEventClicked = useCallback((event, position = 0) => {
if (!event || !event.id) {
logger.warn('useDashboardEvents', 'Event object is required');
return;
}
track(RETENTION_EVENTS.NEWS_ARTICLE_CLICKED, {
news_id: event.id,
news_title: event.title || '',
source: 'following_events',
position,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '📰 Following Event Clicked', {
eventId: event.id,
position,
});
}, [track]);
/**
* 追踪事件评论列表查看
* @param {number} commentCount - 评论数量
*/
const trackCommentsViewed = useCallback((commentCount = 0) => {
track('Event Comments Viewed', {
comment_count: commentCount,
is_empty: commentCount === 0,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '💬 Comments Viewed', {
commentCount,
});
}, [track]);
/**
* 追踪订阅信息查看
* @param {Object} subscription - 订阅信息
* @param {string} subscription.plan - 订阅计划 ('free' | 'pro' | 'enterprise')
* @param {string} subscription.status - 订阅状态 ('active' | 'expired' | 'cancelled')
*/
const trackSubscriptionViewed = useCallback((subscription = {}) => {
track(RETENTION_EVENTS.SUBSCRIPTION_PAGE_VIEWED, {
subscription_plan: subscription.plan || 'free',
subscription_status: subscription.status || 'unknown',
is_paid_user: subscription.plan !== 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '💳 Subscription Viewed', {
plan: subscription.plan,
status: subscription.status,
});
}, [track]);
/**
* 追踪升级按钮点击
* @param {string} currentPlan - 当前计划
* @param {string} targetPlan - 目标计划
* @param {string} source - 来源位置
*/
const trackUpgradePlanClicked = useCallback((currentPlan = 'free', targetPlan = 'pro', source = 'dashboard') => {
track(RETENTION_EVENTS.UPGRADE_PLAN_CLICKED, {
current_plan: currentPlan,
target_plan: targetPlan,
source,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '⬆️ Upgrade Plan Clicked', {
currentPlan,
targetPlan,
source,
});
}, [track]);
/**
* 追踪个人资料更新
* @param {Array<string>} updatedFields - 更新的字段列表
*/
const trackProfileUpdated = useCallback((updatedFields = []) => {
track(RETENTION_EVENTS.PROFILE_UPDATED, {
updated_fields: updatedFields,
field_count: updatedFields.length,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '✏️ Profile Updated', {
updatedFields,
});
}, [track]);
/**
* 追踪设置更改
* @param {string} settingName - 设置名称
* @param {any} oldValue - 旧值
* @param {any} newValue - 新值
*/
const trackSettingChanged = useCallback((settingName, oldValue, newValue) => {
if (!settingName) {
logger.warn('useDashboardEvents', 'Setting name is required');
return;
}
track(RETENTION_EVENTS.SETTINGS_CHANGED, {
setting_name: settingName,
old_value: String(oldValue),
new_value: String(newValue),
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '⚙️ Setting Changed', {
settingName,
oldValue,
newValue,
});
}, [track]);
return {
// 功能卡片事件
trackFunctionCardClicked,
// 自选股相关事件
trackWatchlistViewed,
trackWatchlistStockClicked,
trackWatchlistStockAdded,
trackWatchlistStockRemoved,
// 关注事件相关
trackFollowingEventsViewed,
trackFollowingEventClicked,
// 评论相关
trackCommentsViewed,
// 订阅相关
trackSubscriptionViewed,
trackUpgradePlanClicked,
// 个人资料和设置
trackProfileUpdated,
trackSettingChanged,
};
};
export default useDashboardEvents;

View File

@@ -1,242 +0,0 @@
// src/hooks/useEventNotifications.js
/**
* React Hook用于在组件中订阅事件推送通知
*
* 使用示例:
* ```jsx
* import { useEventNotifications } from 'hooks/useEventNotifications';
*
* function MyComponent() {
* const { newEvent, isConnected } = useEventNotifications({
* eventType: 'all',
* importance: 'all',
* onNewEvent: (event) => {
* console.log('收到新事件:', event);
* // 显示通知...
* }
* });
*
* return <div>...</div>;
* }
* ```
*/
import { useEffect, useState, useRef } from 'react';
import socket from '../services/socket';
import { logger } from '../utils/logger';
export const useEventNotifications = (options = {}) => {
const {
eventType = 'all',
importance = 'all',
enabled = true,
onNewEvent,
} = options;
const [isConnected, setIsConnected] = useState(false);
const [newEvent, setNewEvent] = useState(null);
const [error, setError] = useState(null);
const unsubscribeRef = useRef(null);
// 使用 ref 存储 onNewEvent 回调,避免因回调函数引用改变导致重新连接
const onNewEventRef = useRef(onNewEvent);
// 每次 onNewEvent 改变时更新 ref
useEffect(() => {
onNewEventRef.current = onNewEvent;
}, [onNewEvent]);
useEffect(() => {
console.log('[useEventNotifications DEBUG] ========== useEffect 执行 ==========');
console.log('[useEventNotifications DEBUG] enabled:', enabled);
console.log('[useEventNotifications DEBUG] eventType:', eventType);
console.log('[useEventNotifications DEBUG] importance:', importance);
// 如果禁用,则不订阅
if (!enabled) {
console.log('[useEventNotifications DEBUG] ⚠️ 订阅已禁用,跳过');
return;
}
// 连接状态监听
const handleConnect = () => {
console.log('[useEventNotifications DEBUG] ✓ WebSocket 已连接');
logger.info('useEventNotifications', 'WebSocket connected');
setIsConnected(true);
setError(null);
};
const handleDisconnect = () => {
console.log('[useEventNotifications DEBUG] ⚠️ WebSocket 已断开');
logger.warn('useEventNotifications', 'WebSocket disconnected');
setIsConnected(false);
};
const handleConnectError = (err) => {
console.error('[useEventNotifications ERROR] WebSocket 连接错误:', err);
logger.error('useEventNotifications', 'WebSocket connect error', err);
setError(err);
setIsConnected(false);
};
// 监听连接事件必须在connect之前设置否则可能错过事件
socket.on('connect', handleConnect);
socket.on('disconnect', handleDisconnect);
socket.on('connect_error', handleConnectError);
// 连接 WebSocket
console.log('[useEventNotifications DEBUG] 准备连接 WebSocket...');
logger.info('useEventNotifications', 'Initializing WebSocket connection');
// 先检查是否已经连接
const alreadyConnected = socket.connected || false;
console.log('[useEventNotifications DEBUG] 当前连接状态:', alreadyConnected);
logger.info('useEventNotifications', 'Pre-connection check', { isConnected: alreadyConnected });
if (alreadyConnected) {
// 如果已经连接,直接更新状态
console.log('[useEventNotifications DEBUG] Socket已连接直接更新状态');
logger.info('useEventNotifications', 'Socket already connected, updating state immediately');
setIsConnected(true);
// 验证状态更新
setTimeout(() => {
console.log('[useEventNotifications DEBUG] 1秒后验证状态更新 - isConnected应该为true');
}, 1000);
} else {
// 否则建立新连接
socket.connect();
}
// 新事件处理函数 - 使用 ref 中的回调
const handleNewEvent = (eventData) => {
console.log('\n[useEventNotifications DEBUG] ========== Hook 收到新事件 ==========');
console.log('[useEventNotifications DEBUG] 事件数据:', eventData);
console.log('[useEventNotifications DEBUG] 事件 ID:', eventData?.id);
console.log('[useEventNotifications DEBUG] 事件标题:', eventData?.title);
console.log('[useEventNotifications DEBUG] 设置 newEvent 状态');
setNewEvent(eventData);
console.log('[useEventNotifications DEBUG] ✓ newEvent 状态已更新');
// 调用外部回调(从 ref 中获取最新的回调)
if (onNewEventRef.current) {
console.log('[useEventNotifications DEBUG] 准备调用外部 onNewEvent 回调');
onNewEventRef.current(eventData);
console.log('[useEventNotifications DEBUG] ✓ 外部 onNewEvent 回调已调用');
} else {
console.log('[useEventNotifications DEBUG] ⚠️ 没有外部 onNewEvent 回调');
}
console.log('[useEventNotifications DEBUG] ========== Hook 事件处理完成 ==========\n');
};
// 订阅事件推送
console.log('\n[useEventNotifications DEBUG] ========== 开始订阅事件 ==========');
console.log('[useEventNotifications DEBUG] eventType:', eventType);
console.log('[useEventNotifications DEBUG] importance:', importance);
console.log('[useEventNotifications DEBUG] enabled:', enabled);
// 检查 socket 是否有 subscribeToEvents 方法mockSocketService 和 socketService 都有)
if (socket.subscribeToEvents) {
socket.subscribeToEvents({
eventType,
importance,
onNewEvent: handleNewEvent,
onSubscribed: (data) => {
console.log('\n[useEventNotifications DEBUG] ========== 订阅成功回调 ==========');
console.log('[useEventNotifications DEBUG] 订阅数据:', data);
console.log('[useEventNotifications DEBUG] ========== 订阅成功处理完成 ==========\n');
},
});
console.log('[useEventNotifications DEBUG] ========== 订阅请求已发送 ==========\n');
} else {
console.warn('[useEventNotifications] socket.subscribeToEvents 方法不存在');
}
// 保存取消订阅函数
unsubscribeRef.current = () => {
if (socket.unsubscribeFromEvents) {
socket.unsubscribeFromEvents({ eventType });
}
};
// 组件卸载时清理
return () => {
console.log('\n[useEventNotifications DEBUG] ========== 清理 WebSocket 订阅 ==========');
// 取消订阅
if (unsubscribeRef.current) {
console.log('[useEventNotifications DEBUG] 取消订阅...');
unsubscribeRef.current();
}
// 移除监听器
console.log('[useEventNotifications DEBUG] 移除事件监听器...');
socket.off('connect', handleConnect);
socket.off('disconnect', handleDisconnect);
socket.off('connect_error', handleConnectError);
// 注意:不断开连接,因为 socket 是全局共享的
// 由 NotificationContext 统一管理连接生命周期
console.log('[useEventNotifications DEBUG] ========== 清理完成 ==========\n');
};
}, [eventType, importance, enabled]); // 移除 onNewEvent 依赖
// 监控 isConnected 状态变化(调试用)
useEffect(() => {
console.log('[useEventNotifications DEBUG] ========== isConnected 状态变化 ==========');
console.log('[useEventNotifications DEBUG] isConnected:', isConnected);
console.log('[useEventNotifications DEBUG] ===========================================');
}, [isConnected]);
console.log('[useEventNotifications DEBUG] Hook返回值 - isConnected:', isConnected);
return {
newEvent, // 最新收到的事件
isConnected, // WebSocket 连接状态
error, // 错误信息
clearNewEvent: () => setNewEvent(null), // 清除新事件状态
};
};
/**
* 简化版 Hook只订阅所有事件
*/
export const useAllEventNotifications = (onNewEvent) => {
return useEventNotifications({
eventType: 'all',
importance: 'all',
onNewEvent,
});
};
/**
* Hook订阅重要事件S 和 A 级)
*/
export const useImportantEventNotifications = (onNewEvent) => {
const [importantEvents, setImportantEvents] = useState([]);
const handleEvent = (event) => {
// 只处理 S 和 A 级事件
if (event.importance === 'S' || event.importance === 'A') {
setImportantEvents(prev => [event, ...prev].slice(0, 10)); // 最多保留 10 个
if (onNewEvent) {
onNewEvent(event);
}
}
};
const result = useEventNotifications({
eventType: 'all',
importance: 'all',
onNewEvent: handleEvent,
});
return {
...result,
importantEvents,
clearImportantEvents: () => setImportantEvents([]),
};
};
export default useEventNotifications;

View File

@@ -1,312 +0,0 @@
/**
* 首屏性能指标收集 Hook
* 整合 Web Vitals、资源加载、API 请求等指标
*
* 使用示例:
* ```tsx
* const { metrics, isLoading, remeasure, exportMetrics } = useFirstScreenMetrics({
* pageType: 'home',
* enableConsoleLog: process.env.NODE_ENV === 'development'
* });
* ```
*
* @module hooks/useFirstScreenMetrics
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { initWebVitalsTracking, getCachedMetrics } from '@utils/performance/webVitals';
import { collectResourceStats, collectApiStats } from '@utils/performance/resourceMonitor';
import { performanceMonitor } from '@utils/performanceMonitor';
import { usePerformanceMark } from '@hooks/usePerformanceTracker';
import posthog from 'posthog-js';
import type {
FirstScreenMetrics,
UseFirstScreenMetricsOptions,
UseFirstScreenMetricsResult,
FirstScreenInteractiveEventProperties,
} from '@/types/metrics';
// ============================================================
// Hook 实现
// ============================================================
/**
* 首屏性能指标收集 Hook
*/
export const useFirstScreenMetrics = (
options: UseFirstScreenMetricsOptions
): UseFirstScreenMetricsResult => {
const {
pageType,
enableConsoleLog = process.env.NODE_ENV === 'development',
trackToPostHog = process.env.NODE_ENV === 'production',
customProperties = {},
} = options;
const [isLoading, setIsLoading] = useState(true);
const [metrics, setMetrics] = useState<FirstScreenMetrics | null>(null);
// 使用 ref 避免重复标记
const hasMarkedRef = useRef(false);
const hasInitializedRef = useRef(false);
// 在组件首次渲染时标记开始时间点
if (!hasMarkedRef.current) {
hasMarkedRef.current = true;
performanceMonitor.mark(`${pageType}-page-load-start`);
performanceMonitor.mark(`${pageType}-skeleton-start`);
}
/**
* 收集所有首屏指标
*/
const collectAllMetrics = useCallback((): FirstScreenMetrics => {
try {
// 1. 初始化 Web Vitals 监控
initWebVitalsTracking({
enableConsoleLog,
trackToPostHog: false, // Web Vitals 自己会上报,这里不重复
pageType,
customProperties,
});
// 2. 获取 Web Vitals 指标(延迟获取,等待 LCP/FCP 等指标完成)
const webVitalsCache = getCachedMetrics();
const webVitals = Object.fromEntries(webVitalsCache.entries());
// 3. 收集资源加载统计
const resourceStats = collectResourceStats({
enableConsoleLog,
trackToPostHog: false, // 避免重复上报
pageType,
customProperties,
});
// 4. 收集 API 请求统计
const apiStats = collectApiStats({
enableConsoleLog,
trackToPostHog: false,
pageType,
customProperties,
});
// 5. 标记可交互时间点,并计算 TTI
performanceMonitor.mark(`${pageType}-interactive`);
const timeToInteractive = performanceMonitor.measure(
`${pageType}-page-load-start`,
`${pageType}-interactive`,
`${pageType} TTI`
) || 0;
// 6. 计算骨架屏展示时长
const skeletonDisplayDuration = performanceMonitor.measure(
`${pageType}-skeleton-start`,
`${pageType}-interactive`,
`${pageType} 骨架屏时长`
) || 0;
const firstScreenMetrics: FirstScreenMetrics = {
webVitals,
resourceStats,
apiStats,
timeToInteractive,
skeletonDisplayDuration,
measuredAt: Date.now(),
};
return firstScreenMetrics;
} catch (error) {
console.error('Failed to collect first screen metrics:', error);
throw error;
}
}, [pageType, enableConsoleLog, trackToPostHog, customProperties]);
/**
* 上报首屏可交互事件到 PostHog
*/
const trackFirstScreenInteractive = useCallback(
(metrics: FirstScreenMetrics) => {
if (!trackToPostHog || process.env.NODE_ENV !== 'production') {
return;
}
try {
const eventProperties: FirstScreenInteractiveEventProperties = {
tti_seconds: metrics.timeToInteractive / 1000,
skeleton_duration_seconds: metrics.skeletonDisplayDuration / 1000,
api_request_count: metrics.apiStats.totalRequests,
api_avg_response_time_ms: metrics.apiStats.avgResponseTime,
page_type: pageType,
measured_at: metrics.measuredAt,
...customProperties,
};
posthog.capture('First Screen Interactive', eventProperties);
if (enableConsoleLog) {
console.log('📊 Tracked First Screen Interactive to PostHog', eventProperties);
}
} catch (error) {
console.error('Failed to track first screen interactive:', error);
}
},
[pageType, trackToPostHog, enableConsoleLog, customProperties]
);
/**
* 手动触发重新测量
*/
const remeasure = useCallback(() => {
setIsLoading(true);
// 重置性能标记
performanceMonitor.mark(`${pageType}-page-load-start`);
performanceMonitor.mark(`${pageType}-skeleton-start`);
// 延迟收集指标(等待 Web Vitals 完成)
setTimeout(() => {
try {
const newMetrics = collectAllMetrics();
setMetrics(newMetrics);
trackFirstScreenInteractive(newMetrics);
if (enableConsoleLog) {
console.group('🎯 First Screen Metrics (Re-measured)');
console.log('TTI:', `${(newMetrics.timeToInteractive / 1000).toFixed(2)}s`);
console.log('Skeleton Duration:', `${(newMetrics.skeletonDisplayDuration / 1000).toFixed(2)}s`);
console.log('API Requests:', newMetrics.apiStats.totalRequests);
console.groupEnd();
}
} catch (error) {
console.error('Failed to remeasure metrics:', error);
} finally {
setIsLoading(false);
}
}, 1000); // 延迟 1 秒收集
}, [pageType, collectAllMetrics, trackFirstScreenInteractive, enableConsoleLog]);
/**
* 导出指标为 JSON
*/
const exportMetrics = useCallback((): string => {
if (!metrics) {
return JSON.stringify({ error: 'No metrics available' }, null, 2);
}
return JSON.stringify(metrics, null, 2);
}, [metrics]);
/**
* 初始化:在组件挂载时自动收集指标
*/
useEffect(() => {
// 防止重复初始化
if (hasInitializedRef.current) {
return;
}
hasInitializedRef.current = true;
if (enableConsoleLog) {
console.log('🚀 useFirstScreenMetrics initialized', { pageType });
}
// 延迟收集指标,等待页面渲染完成和 Web Vitals 指标就绪
const timeoutId = setTimeout(() => {
try {
const firstScreenMetrics = collectAllMetrics();
setMetrics(firstScreenMetrics);
trackFirstScreenInteractive(firstScreenMetrics);
if (enableConsoleLog) {
console.group('🎯 First Screen Metrics');
console.log('━'.repeat(50));
console.log(`✅ TTI: ${(firstScreenMetrics.timeToInteractive / 1000).toFixed(2)}s`);
console.log(`✅ Skeleton Duration: ${(firstScreenMetrics.skeletonDisplayDuration / 1000).toFixed(2)}s`);
console.log(`✅ API Requests: ${firstScreenMetrics.apiStats.totalRequests}`);
console.log(`✅ API Avg Response: ${firstScreenMetrics.apiStats.avgResponseTime.toFixed(0)}ms`);
console.log('━'.repeat(50));
console.groupEnd();
}
} catch (error) {
console.error('Failed to collect initial metrics:', error);
} finally {
setIsLoading(false);
}
}, 2000); // 延迟 2 秒收集(确保 LCP/FCP 等指标已触发)
// Cleanup
return () => {
clearTimeout(timeoutId);
};
}, []); // 空依赖数组,只在挂载时执行一次
// ============================================================
// 返回值
// ============================================================
return {
isLoading,
metrics,
remeasure,
exportMetrics,
};
};
// ============================================================
// 辅助 Hook标记骨架屏结束
// ============================================================
/**
* 标记骨架屏结束的 Hook
* 用于在骨架屏消失时记录时间点
*
* 使用示例:
* ```tsx
* const { markSkeletonEnd } = useSkeletonTiming('home-skeleton');
*
* useEffect(() => {
* if (!loading) {
* markSkeletonEnd();
* }
* }, [loading, markSkeletonEnd]);
* ```
*/
export const useSkeletonTiming = (prefix = 'skeleton') => {
const { mark, getMeasure } = usePerformanceMark(prefix);
const hasMarkedEndRef = useRef(false);
const hasMarkedStartRef = useRef(false);
// 在组件首次渲染时标记开始
if (!hasMarkedStartRef.current) {
hasMarkedStartRef.current = true;
mark('start');
}
const markSkeletonEnd = useCallback(() => {
if (!hasMarkedEndRef.current) {
hasMarkedEndRef.current = true;
mark('end');
const duration = getMeasure('start', 'end');
if (process.env.NODE_ENV === 'development' && duration) {
console.log(`⏱️ Skeleton Display Duration: ${(duration / 1000).toFixed(2)}s`);
}
}
}, [mark, getMeasure]);
const getSkeletonDuration = useCallback((): number | null => {
return getMeasure('start', 'end');
}, [getMeasure]);
return {
markSkeletonEnd,
getSkeletonDuration,
};
};
// ============================================================
// 默认导出
// ============================================================
export default useFirstScreenMetrics;