diff --git a/src/components/Tables/TablesProjectRow.js b/src/components/Tables/TablesProjectRow.js deleted file mode 100755 index 4c272282..00000000 --- a/src/components/Tables/TablesProjectRow.js +++ /dev/null @@ -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 ( - - - - - - {name} - - - - - - {budget} - - - - - {status} - - - - - {`${progression}%`} - - - - - - - - ); -} - -export default DashboardTableRow; diff --git a/src/components/Tables/TablesTableRow.js b/src/components/Tables/TablesTableRow.js deleted file mode 100755 index fdd9af0f..00000000 --- a/src/components/Tables/TablesTableRow.js +++ /dev/null @@ -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 ( - - - - - - - {name} - - - {email} - - - - - - - - - {domain} - - - {subdomain} - - - - - - {status} - - - - - {date} - - - - - - - ); -} - -export default TablesTableRow; diff --git a/src/hooks/useDashboardEvents.js b/src/hooks/useDashboardEvents.js deleted file mode 100644 index 44253538..00000000 --- a/src/hooks/useDashboardEvents.js +++ /dev/null @@ -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} 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; diff --git a/src/hooks/useEventNotifications.js b/src/hooks/useEventNotifications.js deleted file mode 100644 index d1e7d688..00000000 --- a/src/hooks/useEventNotifications.js +++ /dev/null @@ -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
...
; - * } - * ``` - */ - -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; diff --git a/src/hooks/useFirstScreenMetrics.ts b/src/hooks/useFirstScreenMetrics.ts deleted file mode 100644 index 018af60c..00000000 --- a/src/hooks/useFirstScreenMetrics.ts +++ /dev/null @@ -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(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;