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;