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:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user