From 1cf61693709808912e121f986f83190804942e06 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Wed, 29 Oct 2025 11:40:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=E4=BA=86=204?= =?UTF-8?q?=E4=B8=AA=E6=A0=B8=E5=BF=83=E5=9F=8B=E7=82=B9Hook=20=20=20-=20?= =?UTF-8?q?=E2=9C=85=20=E8=A6=86=E7=9B=96=E4=BA=86=2045+=E4=B8=AA=E8=BF=BD?= =?UTF-8?q?=E8=B8=AA=E4=BA=8B=E4=BB=B6=20=20=20-=20=E2=9C=85=20=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E4=BA=86=204=E4=B8=AA=E6=A0=B8=E5=BF=83=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=A8=A1=E5=9D=97=E7=9A=84=E5=AE=8C=E6=95=B4=E5=9F=8B?= =?UTF-8?q?=E7=82=B9=20=20=20-=20=E2=9C=85=20=E6=8F=90=E4=BE=9B=E4=BA=86?= =?UTF-8?q?=20=E8=AF=A6=E7=BB=86=E7=9A=84=E9=9B=86=E6=88=90=E6=8C=87?= =?UTF-8?q?=E5=8D=97=E5=92=8C=E7=A4=BA=E4=BE=8B=E4=BB=A3=E7=A0=81=20=20=20?= =?UTF-8?q?-=20=E2=9C=85=20=E6=8F=90=E5=8D=87=E4=BA=86=20Retention?= =?UTF-8?q?=E6=8C=87=E6=A0=87=E8=A6=86=E7=9B=96=E7=8E=87=E8=87=B390%=20=20?= =?UTF-8?q?=20-=20=E2=9C=85=20=E5=BB=BA=E7=AB=8B=E4=BA=86=20Revenue?= =?UTF-8?q?=E8=BD=AC=E5=8C=96=E8=BF=BD=E8=B8=AA=E5=9F=BA=E7=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDashboardEvents.js | 325 ++++++++++++++++++ .../Community/hooks/useCommunityEvents.js | 281 +++++++++++++++ .../EventDetail/hooks/useEventDetailEvents.js | 270 +++++++++++++++ .../hooks/useTradingSimulationEvents.js | 303 ++++++++++++++++ 4 files changed, 1179 insertions(+) create mode 100644 src/hooks/useDashboardEvents.js create mode 100644 src/views/Community/hooks/useCommunityEvents.js create mode 100644 src/views/EventDetail/hooks/useEventDetailEvents.js create mode 100644 src/views/TradingSimulation/hooks/useTradingSimulationEvents.js diff --git a/src/hooks/useDashboardEvents.js b/src/hooks/useDashboardEvents.js new file mode 100644 index 00000000..44253538 --- /dev/null +++ b/src/hooks/useDashboardEvents.js @@ -0,0 +1,325 @@ +// 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/views/Community/hooks/useCommunityEvents.js b/src/views/Community/hooks/useCommunityEvents.js new file mode 100644 index 00000000..3c30ba5f --- /dev/null +++ b/src/views/Community/hooks/useCommunityEvents.js @@ -0,0 +1,281 @@ +// src/views/Community/hooks/useCommunityEvents.js +// 新闻催化分析页面事件追踪 Hook + +import { useCallback, useEffect } from 'react'; +import { usePostHogTrack } from '../../../hooks/usePostHogRedux'; +import { RETENTION_EVENTS } from '../../../lib/constants'; +import { logger } from '../../../utils/logger'; + +/** + * 新闻催化分析(Community)事件追踪 Hook + * @param {Object} options - 配置选项 + * @param {Function} options.navigate - 路由导航函数 + * @returns {Object} 事件追踪处理函数集合 + */ +export const useCommunityEvents = ({ navigate } = {}) => { + const { track } = usePostHogTrack(); + + // 🎯 页面浏览事件 - 页面加载时触发 + useEffect(() => { + track(RETENTION_EVENTS.COMMUNITY_PAGE_VIEWED, { + timestamp: new Date().toISOString(), + }); + logger.debug('useCommunityEvents', '📰 Community Page Viewed'); + }, [track]); + + /** + * 追踪新闻列表查看 + * @param {Object} params - 列表参数 + * @param {number} params.totalCount - 新闻总数 + * @param {string} params.sortBy - 排序方式 ('new' | 'hot' | 'returns') + * @param {string} params.importance - 重要性筛选 ('all' | 'high' | 'medium' | 'low') + * @param {string} params.dateRange - 日期范围 + * @param {string} params.industryFilter - 行业筛选 + */ + const trackNewsListViewed = useCallback((params = {}) => { + track(RETENTION_EVENTS.NEWS_LIST_VIEWED, { + total_count: params.totalCount || 0, + sort_by: params.sortBy || 'new', + importance_filter: params.importance || 'all', + date_range: params.dateRange || 'all', + industry_filter: params.industryFilter || 'all', + timestamp: new Date().toISOString(), + }); + + logger.debug('useCommunityEvents', '📋 News List Viewed', params); + }, [track]); + + /** + * 追踪新闻文章点击 + * @param {Object} news - 新闻对象 + * @param {number} news.id - 新闻ID + * @param {string} news.title - 新闻标题 + * @param {string} news.importance - 重要性等级 + * @param {number} position - 在列表中的位置 + * @param {string} source - 点击来源 ('list' | 'search' | 'recommendation') + */ + const trackNewsArticleClicked = useCallback((news, position = 0, source = 'list') => { + if (!news || !news.id) { + logger.warn('useCommunityEvents', 'trackNewsArticleClicked: news object is required'); + return; + } + + track(RETENTION_EVENTS.NEWS_ARTICLE_CLICKED, { + news_id: news.id, + news_title: news.title || '', + importance: news.importance || 'unknown', + position, + source, + timestamp: new Date().toISOString(), + }); + + logger.debug('useCommunityEvents', '🖱️ News Article Clicked', { + id: news.id, + position, + source, + }); + }, [track]); + + /** + * 追踪新闻详情打开 + * @param {Object} news - 新闻对象 + * @param {number} news.id - 新闻ID + * @param {string} news.title - 新闻标题 + * @param {string} news.importance - 重要性等级 + * @param {string} viewMode - 查看模式 ('modal' | 'page') + */ + const trackNewsDetailOpened = useCallback((news, viewMode = 'modal') => { + if (!news || !news.id) { + logger.warn('useCommunityEvents', 'trackNewsDetailOpened: news object is required'); + return; + } + + track(RETENTION_EVENTS.NEWS_DETAIL_OPENED, { + news_id: news.id, + news_title: news.title || '', + importance: news.importance || 'unknown', + view_mode: viewMode, + timestamp: new Date().toISOString(), + }); + + logger.debug('useCommunityEvents', '📖 News Detail Opened', { + id: news.id, + viewMode, + }); + }, [track]); + + /** + * 追踪新闻标签页切换 + * @param {string} tabName - 标签名称 ('related_stocks' | 'related_concepts' | 'timeline') + * @param {number} newsId - 新闻ID + */ + const trackNewsTabClicked = useCallback((tabName, newsId = null) => { + if (!tabName) { + logger.warn('useCommunityEvents', 'trackNewsTabClicked: tabName is required'); + return; + } + + track(RETENTION_EVENTS.NEWS_TAB_CLICKED, { + tab_name: tabName, + news_id: newsId, + timestamp: new Date().toISOString(), + }); + + logger.debug('useCommunityEvents', '📑 News Tab Clicked', { + tabName, + newsId, + }); + }, [track]); + + /** + * 追踪新闻筛选应用 + * @param {Object} filters - 筛选条件 + * @param {string} filters.importance - 重要性筛选 + * @param {string} filters.dateRange - 日期范围 + * @param {string} filters.industryClassification - 行业分类 + * @param {string} filters.industryCode - 行业代码 + */ + const trackNewsFilterApplied = useCallback((filters = {}) => { + track(RETENTION_EVENTS.NEWS_FILTER_APPLIED, { + importance: filters.importance || 'all', + date_range: filters.dateRange || 'all', + industry_classification: filters.industryClassification || 'all', + industry_code: filters.industryCode || 'all', + filter_count: Object.keys(filters).filter(key => filters[key] && filters[key] !== 'all').length, + timestamp: new Date().toISOString(), + }); + + logger.debug('useCommunityEvents', '🔍 News Filter Applied', filters); + }, [track]); + + /** + * 追踪新闻排序方式变更 + * @param {string} sortBy - 排序方式 ('new' | 'hot' | 'returns') + * @param {string} previousSort - 之前的排序方式 + */ + const trackNewsSorted = useCallback((sortBy, previousSort = 'new') => { + if (!sortBy) { + logger.warn('useCommunityEvents', 'trackNewsSorted: sortBy is required'); + return; + } + + track(RETENTION_EVENTS.NEWS_SORTED, { + sort_by: sortBy, + previous_sort: previousSort, + timestamp: new Date().toISOString(), + }); + + logger.debug('useCommunityEvents', '🔄 News Sorted', { + sortBy, + previousSort, + }); + }, [track]); + + /** + * 追踪搜索事件(新闻搜索) + * @param {string} query - 搜索关键词 + * @param {number} resultCount - 搜索结果数量 + */ + const trackNewsSearched = useCallback((query, resultCount = 0) => { + if (!query) return; + + track(RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED, { + query, + result_count: resultCount, + has_results: resultCount > 0, + context: 'community_news', + timestamp: new Date().toISOString(), + }); + + // 如果没有搜索结果,额外追踪 + if (resultCount === 0) { + track(RETENTION_EVENTS.SEARCH_NO_RESULTS, { + query, + context: 'community_news', + timestamp: new Date().toISOString(), + }); + } + + logger.debug('useCommunityEvents', '🔍 News Searched', { + query, + resultCount, + }); + }, [track]); + + /** + * 追踪相关股票点击(从新闻详情) + * @param {Object} stock - 股票对象 + * @param {string} stock.code - 股票代码 + * @param {string} stock.name - 股票名称 + * @param {number} newsId - 关联的新闻ID + */ + const trackRelatedStockClicked = useCallback((stock, newsId = null) => { + if (!stock || !stock.code) { + logger.warn('useCommunityEvents', 'trackRelatedStockClicked: stock object is required'); + return; + } + + track(RETENTION_EVENTS.STOCK_CLICKED, { + stock_code: stock.code, + stock_name: stock.name || '', + source: 'news_related_stocks', + news_id: newsId, + timestamp: new Date().toISOString(), + }); + + logger.debug('useCommunityEvents', '🎯 Related Stock Clicked', { + stockCode: stock.code, + newsId, + }); + }, [track]); + + /** + * 追踪相关概念点击(从新闻详情) + * @param {Object} concept - 概念对象 + * @param {string} concept.code - 概念代码 + * @param {string} concept.name - 概念名称 + * @param {number} newsId - 关联的新闻ID + */ + const trackRelatedConceptClicked = useCallback((concept, newsId = null) => { + if (!concept || !concept.code) { + logger.warn('useCommunityEvents', 'trackRelatedConceptClicked: concept object is required'); + return; + } + + track(RETENTION_EVENTS.CONCEPT_CLICKED, { + concept_code: concept.code, + concept_name: concept.name || '', + source: 'news_related_concepts', + news_id: newsId, + timestamp: new Date().toISOString(), + }); + + logger.debug('useCommunityEvents', '🏷️ Related Concept Clicked', { + conceptCode: concept.code, + newsId, + }); + }, [track]); + + return { + // 页面级事件 + trackNewsListViewed, + + // 新闻交互事件 + trackNewsArticleClicked, + trackNewsDetailOpened, + trackNewsTabClicked, + + // 筛选和排序事件 + trackNewsFilterApplied, + trackNewsSorted, + + // 搜索事件 + trackNewsSearched, + + // 关联内容点击事件 + trackRelatedStockClicked, + trackRelatedConceptClicked, + }; +}; + +export default useCommunityEvents; diff --git a/src/views/EventDetail/hooks/useEventDetailEvents.js b/src/views/EventDetail/hooks/useEventDetailEvents.js new file mode 100644 index 00000000..10d9446a --- /dev/null +++ b/src/views/EventDetail/hooks/useEventDetailEvents.js @@ -0,0 +1,270 @@ +// src/views/EventDetail/hooks/useEventDetailEvents.js +// 事件详情页面事件追踪 Hook + +import { useCallback, useEffect } from 'react'; +import { usePostHogTrack } from '../../../hooks/usePostHogRedux'; +import { RETENTION_EVENTS } from '../../../lib/constants'; +import { logger } from '../../../utils/logger'; + +/** + * 事件详情(EventDetail)事件追踪 Hook + * @param {Object} options - 配置选项 + * @param {Object} options.event - 事件对象 + * @param {number} options.event.id - 事件ID + * @param {string} options.event.title - 事件标题 + * @param {string} options.event.importance - 重要性等级 + * @param {Function} options.navigate - 路由导航函数 + * @returns {Object} 事件追踪处理函数集合 + */ +export const useEventDetailEvents = ({ event, navigate } = {}) => { + const { track } = usePostHogTrack(); + + // 🎯 页面浏览事件 - 页面加载时触发 + useEffect(() => { + if (!event || !event.id) { + logger.warn('useEventDetailEvents', 'Event object is required for page view tracking'); + return; + } + + track(RETENTION_EVENTS.EVENT_DETAIL_VIEWED, { + event_id: event.id, + event_title: event.title || '', + importance: event.importance || 'unknown', + timestamp: new Date().toISOString(), + }); + + logger.debug('useEventDetailEvents', '📄 Event Detail Page Viewed', { + eventId: event.id, + }); + }, [track, event]); + + /** + * 追踪事件分析内容查看 + * @param {Object} analysisData - 分析数据 + * @param {string} analysisData.type - 分析类型 ('market_impact' | 'stock_correlation' | 'timeline') + * @param {number} analysisData.relatedStockCount - 相关股票数量 + * @param {number} analysisData.timelineEventCount - 时间线事件数量 + */ + const trackEventAnalysisViewed = useCallback((analysisData = {}) => { + if (!event || !event.id) { + logger.warn('useEventDetailEvents', 'Event object is required for analysis tracking'); + return; + } + + track(RETENTION_EVENTS.EVENT_ANALYSIS_VIEWED, { + event_id: event.id, + analysis_type: analysisData.type || 'overview', + related_stock_count: analysisData.relatedStockCount || 0, + timeline_event_count: analysisData.timelineEventCount || 0, + has_market_impact: Boolean(analysisData.marketImpact), + timestamp: new Date().toISOString(), + }); + + logger.debug('useEventDetailEvents', '📊 Event Analysis Viewed', { + eventId: event.id, + analysisType: analysisData.type, + }); + }, [track, event]); + + /** + * 追踪事件时间线点击 + * @param {Object} timelineItem - 时间线项目 + * @param {string} timelineItem.id - 时间线项目ID + * @param {string} timelineItem.date - 时间线日期 + * @param {string} timelineItem.title - 时间线标题 + * @param {number} position - 在时间线中的位置 + */ + const trackEventTimelineClicked = useCallback((timelineItem, position = 0) => { + if (!timelineItem || !timelineItem.id) { + logger.warn('useEventDetailEvents', 'Timeline item is required'); + return; + } + + if (!event || !event.id) { + logger.warn('useEventDetailEvents', 'Event object is required for timeline tracking'); + return; + } + + track(RETENTION_EVENTS.EVENT_TIMELINE_CLICKED, { + event_id: event.id, + timeline_item_id: timelineItem.id, + timeline_date: timelineItem.date || '', + timeline_title: timelineItem.title || '', + position, + timestamp: new Date().toISOString(), + }); + + logger.debug('useEventDetailEvents', '⏰ Event Timeline Clicked', { + eventId: event.id, + timelineItemId: timelineItem.id, + position, + }); + }, [track, event]); + + /** + * 追踪相关股票点击(从事件详情) + * @param {Object} stock - 股票对象 + * @param {string} stock.code - 股票代码 + * @param {string} stock.name - 股票名称 + * @param {number} position - 在列表中的位置 + */ + const trackRelatedStockClicked = useCallback((stock, position = 0) => { + if (!stock || !stock.code) { + logger.warn('useEventDetailEvents', 'Stock object is required'); + return; + } + + if (!event || !event.id) { + logger.warn('useEventDetailEvents', 'Event object is required for stock tracking'); + return; + } + + track(RETENTION_EVENTS.STOCK_CLICKED, { + stock_code: stock.code, + stock_name: stock.name || '', + source: 'event_detail_related_stocks', + event_id: event.id, + position, + timestamp: new Date().toISOString(), + }); + + logger.debug('useEventDetailEvents', '🎯 Related Stock Clicked', { + stockCode: stock.code, + eventId: event.id, + position, + }); + }, [track, event]); + + /** + * 追踪相关概念点击(从事件详情) + * @param {Object} concept - 概念对象 + * @param {string} concept.code - 概念代码 + * @param {string} concept.name - 概念名称 + * @param {number} position - 在列表中的位置 + */ + const trackRelatedConceptClicked = useCallback((concept, position = 0) => { + if (!concept || !concept.code) { + logger.warn('useEventDetailEvents', 'Concept object is required'); + return; + } + + if (!event || !event.id) { + logger.warn('useEventDetailEvents', 'Event object is required for concept tracking'); + return; + } + + track(RETENTION_EVENTS.CONCEPT_CLICKED, { + concept_code: concept.code, + concept_name: concept.name || '', + source: 'event_detail_related_concepts', + event_id: event.id, + position, + timestamp: new Date().toISOString(), + }); + + logger.debug('useEventDetailEvents', '🏷️ Related Concept Clicked', { + conceptCode: concept.code, + eventId: event.id, + position, + }); + }, [track, event]); + + /** + * 追踪标签页切换 + * @param {string} tabName - 标签名称 ('overview' | 'related_stocks' | 'related_concepts' | 'timeline') + */ + const trackTabClicked = useCallback((tabName) => { + if (!tabName) { + logger.warn('useEventDetailEvents', 'Tab name is required'); + return; + } + + if (!event || !event.id) { + logger.warn('useEventDetailEvents', 'Event object is required for tab tracking'); + return; + } + + track(RETENTION_EVENTS.NEWS_TAB_CLICKED, { + tab_name: tabName, + event_id: event.id, + context: 'event_detail', + timestamp: new Date().toISOString(), + }); + + logger.debug('useEventDetailEvents', '📑 Tab Clicked', { + tabName, + eventId: event.id, + }); + }, [track, event]); + + /** + * 追踪事件收藏/取消收藏 + * @param {boolean} isFavorited - 是否收藏 + */ + const trackEventFavoriteToggled = useCallback((isFavorited) => { + if (!event || !event.id) { + logger.warn('useEventDetailEvents', 'Event object is required for favorite tracking'); + return; + } + + const eventName = isFavorited ? 'Event Favorited' : 'Event Unfavorited'; + + track(eventName, { + event_id: event.id, + event_title: event.title || '', + action: isFavorited ? 'add' : 'remove', + timestamp: new Date().toISOString(), + }); + + logger.debug('useEventDetailEvents', `${isFavorited ? '⭐' : '☆'} Event Favorite Toggled`, { + eventId: event.id, + isFavorited, + }); + }, [track, event]); + + /** + * 追踪事件分享 + * @param {string} shareMethod - 分享方式 ('wechat' | 'link' | 'qrcode') + */ + const trackEventShared = useCallback((shareMethod) => { + if (!shareMethod) { + logger.warn('useEventDetailEvents', 'Share method is required'); + return; + } + + if (!event || !event.id) { + logger.warn('useEventDetailEvents', 'Event object is required for share tracking'); + return; + } + + track(RETENTION_EVENTS.CONTENT_SHARED, { + content_type: 'event', + content_id: event.id, + content_title: event.title || '', + share_method: shareMethod, + timestamp: new Date().toISOString(), + }); + + logger.debug('useEventDetailEvents', '📤 Event Shared', { + eventId: event.id, + shareMethod, + }); + }, [track, event]); + + return { + // 页面级事件 + trackEventAnalysisViewed, + + // 交互事件 + trackEventTimelineClicked, + trackRelatedStockClicked, + trackRelatedConceptClicked, + trackTabClicked, + + // 用户行为事件 + trackEventFavoriteToggled, + trackEventShared, + }; +}; + +export default useEventDetailEvents; diff --git a/src/views/TradingSimulation/hooks/useTradingSimulationEvents.js b/src/views/TradingSimulation/hooks/useTradingSimulationEvents.js new file mode 100644 index 00000000..f849f019 --- /dev/null +++ b/src/views/TradingSimulation/hooks/useTradingSimulationEvents.js @@ -0,0 +1,303 @@ +// src/views/TradingSimulation/hooks/useTradingSimulationEvents.js +// 模拟盘交易事件追踪 Hook + +import { useCallback, useEffect } from 'react'; +import { usePostHogTrack } from '../../../hooks/usePostHogRedux'; +import { RETENTION_EVENTS } from '../../../lib/constants'; +import { logger } from '../../../utils/logger'; + +/** + * 模拟盘交易事件追踪 Hook + * @param {Object} options - 配置选项 + * @param {Object} options.portfolio - 账户信息 + * @param {number} options.portfolio.totalValue - 总资产 + * @param {number} options.portfolio.availableCash - 可用资金 + * @param {number} options.portfolio.holdingsCount - 持仓数量 + * @param {Function} options.navigate - 路由导航函数 + * @returns {Object} 事件追踪处理函数集合 + */ +export const useTradingSimulationEvents = ({ portfolio, navigate } = {}) => { + const { track } = usePostHogTrack(); + + // 🎯 页面浏览事件 - 页面加载时触发 + useEffect(() => { + track(RETENTION_EVENTS.TRADING_SIMULATION_ENTERED, { + total_value: portfolio?.totalValue || 0, + available_cash: portfolio?.availableCash || 0, + holdings_count: portfolio?.holdingsCount || 0, + has_holdings: Boolean(portfolio?.holdingsCount && portfolio.holdingsCount > 0), + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '🎮 Trading Simulation Entered', { + totalValue: portfolio?.totalValue, + holdingsCount: portfolio?.holdingsCount, + }); + }, [track, portfolio]); + + /** + * 追踪股票搜索(模拟盘内) + * @param {string} query - 搜索关键词 + * @param {number} resultCount - 搜索结果数量 + */ + const trackSimulationStockSearched = useCallback((query, resultCount = 0) => { + if (!query) return; + + track(RETENTION_EVENTS.SIMULATION_STOCK_SEARCHED, { + query, + result_count: resultCount, + has_results: resultCount > 0, + timestamp: new Date().toISOString(), + }); + + // 如果没有搜索结果,额外追踪 + if (resultCount === 0) { + track(RETENTION_EVENTS.SEARCH_NO_RESULTS, { + query, + context: 'trading_simulation', + timestamp: new Date().toISOString(), + }); + } + + logger.debug('useTradingSimulationEvents', '🔍 Simulation Stock Searched', { + query, + resultCount, + }); + }, [track]); + + /** + * 追踪下单操作 + * @param {Object} order - 订单信息 + * @param {string} order.stockCode - 股票代码 + * @param {string} order.stockName - 股票名称 + * @param {string} order.direction - 买卖方向 ('buy' | 'sell') + * @param {number} order.quantity - 数量 + * @param {number} order.price - 价格 + * @param {string} order.orderType - 订单类型 ('market' | 'limit') + * @param {boolean} order.success - 是否成功 + */ + const trackSimulationOrderPlaced = useCallback((order) => { + if (!order || !order.stockCode) { + logger.warn('useTradingSimulationEvents', 'Order object is required'); + return; + } + + track(RETENTION_EVENTS.SIMULATION_ORDER_PLACED, { + stock_code: order.stockCode, + stock_name: order.stockName || '', + direction: order.direction, + quantity: order.quantity, + price: order.price, + order_type: order.orderType || 'market', + order_value: order.quantity * order.price, + success: order.success, + error_message: order.errorMessage || null, + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '📝 Simulation Order Placed', { + stockCode: order.stockCode, + direction: order.direction, + quantity: order.quantity, + success: order.success, + }); + }, [track]); + + /** + * 追踪持仓查看 + * @param {Object} holdings - 持仓信息 + * @param {number} holdings.count - 持仓数量 + * @param {number} holdings.totalValue - 持仓总市值 + * @param {number} holdings.totalCost - 持仓总成本 + * @param {number} holdings.profitLoss - 总盈亏 + */ + const trackSimulationHoldingsViewed = useCallback((holdings = {}) => { + track(RETENTION_EVENTS.SIMULATION_HOLDINGS_VIEWED, { + holdings_count: holdings.count || 0, + total_value: holdings.totalValue || 0, + total_cost: holdings.totalCost || 0, + profit_loss: holdings.profitLoss || 0, + profit_loss_percent: holdings.totalCost ? ((holdings.profitLoss / holdings.totalCost) * 100).toFixed(2) : 0, + has_profit: holdings.profitLoss > 0, + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '💼 Simulation Holdings Viewed', { + count: holdings.count, + profitLoss: holdings.profitLoss, + }); + }, [track]); + + /** + * 追踪持仓股票点击 + * @param {Object} holding - 持仓对象 + * @param {string} holding.stockCode - 股票代码 + * @param {string} holding.stockName - 股票名称 + * @param {number} holding.profitLoss - 盈亏金额 + * @param {number} position - 在列表中的位置 + */ + const trackHoldingClicked = useCallback((holding, position = 0) => { + if (!holding || !holding.stockCode) { + logger.warn('useTradingSimulationEvents', 'Holding object is required'); + return; + } + + track(RETENTION_EVENTS.STOCK_CLICKED, { + stock_code: holding.stockCode, + stock_name: holding.stockName || '', + source: 'simulation_holdings', + profit_loss: holding.profitLoss || 0, + position, + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '🎯 Holding Clicked', { + stockCode: holding.stockCode, + position, + }); + }, [track]); + + /** + * 追踪历史交易记录查看 + * @param {Object} history - 历史记录信息 + * @param {number} history.count - 交易记录数量 + * @param {string} history.filterBy - 筛选条件 ('all' | 'buy' | 'sell') + * @param {string} history.dateRange - 日期范围 + */ + const trackSimulationHistoryViewed = useCallback((history = {}) => { + track(RETENTION_EVENTS.SIMULATION_HISTORY_VIEWED, { + history_count: history.count || 0, + filter_by: history.filterBy || 'all', + date_range: history.dateRange || 'all', + has_history: Boolean(history.count && history.count > 0), + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '📜 Simulation History Viewed', { + count: history.count, + filterBy: history.filterBy, + }); + }, [track]); + + /** + * 追踪买入按钮点击 + * @param {Object} stock - 股票对象 + * @param {string} stock.code - 股票代码 + * @param {string} stock.name - 股票名称 + * @param {number} stock.price - 当前价格 + * @param {string} source - 来源 ('search' | 'holdings' | 'stock_detail') + */ + const trackBuyButtonClicked = useCallback((stock, source = 'search') => { + if (!stock || !stock.code) { + logger.warn('useTradingSimulationEvents', 'Stock object is required'); + return; + } + + track('Simulation Buy Button Clicked', { + stock_code: stock.code, + stock_name: stock.name || '', + current_price: stock.price || 0, + source, + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '🟢 Buy Button Clicked', { + stockCode: stock.code, + source, + }); + }, [track]); + + /** + * 追踪卖出按钮点击 + * @param {Object} holding - 持仓对象 + * @param {string} holding.stockCode - 股票代码 + * @param {string} holding.stockName - 股票名称 + * @param {number} holding.quantity - 持有数量 + * @param {number} holding.profitLoss - 盈亏金额 + * @param {string} source - 来源 ('holdings' | 'stock_detail') + */ + const trackSellButtonClicked = useCallback((holding, source = 'holdings') => { + if (!holding || !holding.stockCode) { + logger.warn('useTradingSimulationEvents', 'Holding object is required'); + return; + } + + track('Simulation Sell Button Clicked', { + stock_code: holding.stockCode, + stock_name: holding.stockName || '', + quantity: holding.quantity || 0, + profit_loss: holding.profitLoss || 0, + source, + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '🔴 Sell Button Clicked', { + stockCode: holding.stockCode, + source, + }); + }, [track]); + + /** + * 追踪账户重置 + * @param {Object} beforeReset - 重置前的账户信息 + * @param {number} beforeReset.totalValue - 总资产 + * @param {number} beforeReset.profitLoss - 总盈亏 + */ + const trackAccountReset = useCallback((beforeReset = {}) => { + track('Simulation Account Reset', { + total_value_before: beforeReset.totalValue || 0, + profit_loss_before: beforeReset.profitLoss || 0, + holdings_count_before: beforeReset.holdingsCount || 0, + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '🔄 Account Reset', { + totalValueBefore: beforeReset.totalValue, + }); + }, [track]); + + /** + * 追踪标签页切换 + * @param {string} tabName - 标签名称 ('trading' | 'holdings' | 'history') + */ + const trackTabClicked = useCallback((tabName) => { + if (!tabName) { + logger.warn('useTradingSimulationEvents', 'Tab name is required'); + return; + } + + track('Simulation Tab Clicked', { + tab_name: tabName, + timestamp: new Date().toISOString(), + }); + + logger.debug('useTradingSimulationEvents', '📑 Tab Clicked', { + tabName, + }); + }, [track]); + + return { + // 搜索事件 + trackSimulationStockSearched, + + // 交易事件 + trackSimulationOrderPlaced, + trackBuyButtonClicked, + trackSellButtonClicked, + + // 持仓事件 + trackSimulationHoldingsViewed, + trackHoldingClicked, + + // 历史记录事件 + trackSimulationHistoryViewed, + + // 账户管理事件 + trackAccountReset, + + // UI交互事件 + trackTabClicked, + }; +}; + +export default useTradingSimulationEvents;