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