Compare commits

...

2 Commits

4 changed files with 423 additions and 2 deletions

View File

@@ -0,0 +1,103 @@
// src/views/Company/hooks/useCompanyEvents.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 {string} options.stockCode - 当前股票代码
* @returns {Object} 事件追踪处理函数集合
*/
export const useCompanyEvents = ({ stockCode } = {}) => {
const { track } = usePostHogTrack();
// 🎯 页面浏览事件 - 页面加载时触发
useEffect(() => {
track(RETENTION_EVENTS.COMPANY_PAGE_VIEWED, {
timestamp: new Date().toISOString(),
stock_code: stockCode || null,
});
logger.debug('useCompanyEvents', '📊 Company Page Viewed', { stockCode });
}, [track, stockCode]);
/**
* 追踪股票搜索/切换
* @param {string} newStockCode - 新的股票代码
* @param {string} previousStockCode - 之前的股票代码
*/
const trackStockSearched = useCallback((newStockCode, previousStockCode = null) => {
if (!newStockCode) return;
track(RETENTION_EVENTS.STOCK_SEARCHED, {
query: newStockCode,
stock_code: newStockCode,
previous_stock_code: previousStockCode,
context: 'company_page',
});
logger.debug('useCompanyEvents', '🔍 Stock Searched', {
newStockCode,
previousStockCode,
});
}, [track]);
/**
* 追踪 Tab 切换
* @param {number} tabIndex - Tab 索引 (0: 公司概览, 1: 股票行情, 2: 财务全景, 3: 盈利预测)
* @param {string} tabName - Tab 名称
* @param {number} previousTabIndex - 之前的 Tab 索引
*/
const trackTabChanged = useCallback((tabIndex, tabName, previousTabIndex = null) => {
track(RETENTION_EVENTS.TAB_CHANGED, {
tab_index: tabIndex,
tab_name: tabName,
previous_tab_index: previousTabIndex,
stock_code: stockCode,
context: 'company_page',
});
logger.debug('useCompanyEvents', '🔄 Tab Changed', {
tabIndex,
tabName,
previousTabIndex,
stockCode,
});
}, [track, stockCode]);
/**
* 追踪加入自选股
* @param {string} stock_code - 股票代码
*/
const trackWatchlistAdded = useCallback((stock_code) => {
track(RETENTION_EVENTS.WATCHLIST_ADDED, {
stock_code,
source: 'company_page',
});
logger.debug('useCompanyEvents', '⭐ Watchlist Added', { stock_code });
}, [track]);
/**
* 追踪移除自选股
* @param {string} stock_code - 股票代码
*/
const trackWatchlistRemoved = useCallback((stock_code) => {
track(RETENTION_EVENTS.WATCHLIST_REMOVED, {
stock_code,
source: 'company_page',
});
logger.debug('useCompanyEvents', '❌ Watchlist Removed', { stock_code });
}, [track]);
return {
trackStockSearched,
trackTabChanged,
trackWatchlistAdded,
trackWatchlistRemoved,
};
};

View File

@@ -34,6 +34,8 @@ import FinancialPanorama from './FinancialPanorama';
import ForecastReport from './ForecastReport';
import MarketDataView from './MarketDataView';
import CompanyOverview from './CompanyOverview';
// 导入 PostHog 追踪 Hook
import { useCompanyEvents } from './hooks/useCompanyEvents';
const CompanyIndex = () => {
const [searchParams, setSearchParams] = useSearchParams();
@@ -42,7 +44,18 @@ const CompanyIndex = () => {
const { colorMode, toggleColorMode } = useColorMode();
const toast = useToast();
const { isAuthenticated } = useAuth();
// 🎯 PostHog 事件追踪
const {
trackStockSearched,
trackTabChanged,
trackWatchlistAdded,
trackWatchlistRemoved,
} = useCompanyEvents({ stockCode });
// Tab 索引状态(用于追踪 Tab 切换)
const [currentTabIndex, setCurrentTabIndex] = useState(0);
const bgColor = useColorModeValue('white', 'gray.800');
const tabBg = useColorModeValue('gray.50', 'gray.700');
const activeBg = useColorModeValue('blue.500', 'blue.400');
@@ -86,6 +99,9 @@ const CompanyIndex = () => {
const handleSearch = () => {
if (inputCode && inputCode !== stockCode) {
// 🎯 追踪股票搜索
trackStockSearched(inputCode, stockCode);
setStockCode(inputCode);
setSearchParams({ scode: inputCode });
}
@@ -123,6 +139,10 @@ const CompanyIndex = () => {
logger.api.response('DELETE', url, resp.status);
if (!resp.ok) throw new Error('删除失败');
// 🎯 追踪移除自选
trackWatchlistRemoved(stockCode);
setIsInWatchlist(false);
toast({ title: '已从自选移除', status: 'info', duration: 1500 });
} else {
@@ -140,6 +160,10 @@ const CompanyIndex = () => {
logger.api.response('POST', url, resp.status);
if (!resp.ok) throw new Error('添加失败');
// 🎯 追踪加入自选
trackWatchlistAdded(stockCode);
setIsInWatchlist(true);
toast({ title: '已加入自选', status: 'success', duration: 1500 });
}
@@ -226,7 +250,18 @@ const CompanyIndex = () => {
{/* 数据展示区域 */}
<Card bg={bgColor} shadow="lg">
<CardBody p={0}>
<Tabs variant="soft-rounded" colorScheme="blue" size="lg">
<Tabs
variant="soft-rounded"
colorScheme="blue"
size="lg"
index={currentTabIndex}
onChange={(index) => {
const tabNames = ['公司概览', '股票行情', '财务全景', '盈利预测'];
// 🎯 追踪 Tab 切换
trackTabChanged(index, tabNames[index], currentTabIndex);
setCurrentTabIndex(index);
}}
>
<TabList p={4} bg={tabBg}>
<Tab
_selected={{

View File

@@ -0,0 +1,252 @@
// src/views/LimitAnalyse/hooks/useLimitAnalyseEvents.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 {Function} options.navigate - 路由导航函数
* @returns {Object} 事件追踪方法集合
*/
export const useLimitAnalyseEvents = ({ navigate } = {}) => {
const { track } = usePostHogTrack();
// 页面浏览追踪 - 组件加载时自动触发
useEffect(() => {
track(RETENTION_EVENTS.LIMIT_ANALYSE_PAGE_VIEWED, {
timestamp: new Date().toISOString(),
});
logger.debug('useLimitAnalyseEvents', '👁️ Limit Analyse Page Viewed');
}, [track]);
/**
* 追踪日期选择
* @param {string} date - 选择的日期YYYYMMDD 格式)
* @param {string} previousDate - 之前的日期
*/
const trackDateSelected = useCallback((date, previousDate = null) => {
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
filter_type: 'date',
filter_value: date,
previous_value: previousDate,
context: 'limit_analyse',
});
logger.debug('useLimitAnalyseEvents', '📅 Date Selected', {
date,
previousDate,
});
}, [track]);
/**
* 追踪每日统计数据查看
* @param {Object} stats - 统计数据
* @param {string} date - 日期
*/
const trackDailyStatsViewed = useCallback((stats, date) => {
if (!stats) return;
track(RETENTION_EVENTS.LIMIT_ANALYSE_PAGE_VIEWED, {
date,
total_stocks: stats.total_stocks,
sector_count: stats.sectors?.length || 0,
hot_sector: stats.hot_sector?.name,
view_type: 'daily_stats',
});
logger.debug('useLimitAnalyseEvents', '📊 Daily Stats Viewed', {
date,
totalStocks: stats.total_stocks,
});
}, [track]);
/**
* 追踪板块展开/收起
* @param {string} sectorName - 板块名称
* @param {boolean} isExpanded - 是否展开
* @param {number} stockCount - 板块内股票数量
*/
const trackSectorToggled = useCallback((sectorName, isExpanded, stockCount = 0) => {
track(RETENTION_EVENTS.LIMIT_SECTOR_EXPANDED, {
sector_name: sectorName,
action: isExpanded ? 'expand' : 'collapse',
stock_count: stockCount,
source: 'limit_analyse',
});
logger.debug('useLimitAnalyseEvents', '🔽 Sector Toggled', {
sectorName,
isExpanded,
stockCount,
});
}, [track]);
/**
* 追踪板块点击
* @param {Object} sector - 板块对象
*/
const trackSectorClicked = useCallback((sector) => {
track(RETENTION_EVENTS.LIMIT_BOARD_CLICKED, {
sector_name: sector.name,
stock_count: sector.count,
source: 'limit_analyse',
});
logger.debug('useLimitAnalyseEvents', '🎯 Sector Clicked', {
sectorName: sector.name,
});
}, [track]);
/**
* 追踪涨停股票点击
* @param {Object} stock - 股票对象
* @param {string} sectorName - 所属板块
*/
const trackLimitStockClicked = useCallback((stock, sectorName = '') => {
track(RETENTION_EVENTS.LIMIT_STOCK_CLICKED, {
stock_code: stock.code || stock.stock_code,
stock_name: stock.name || stock.stock_name,
sector_name: sectorName,
limit_time: stock.limit_time,
source: 'limit_analyse',
});
logger.debug('useLimitAnalyseEvents', '📈 Limit Stock Clicked', {
stockCode: stock.code || stock.stock_code,
sectorName,
});
}, [track]);
/**
* 追踪搜索发起
* @param {string} query - 搜索关键词
* @param {string} searchType - 搜索类型all/sector/stock
* @param {string} searchMode - 搜索模式hybrid/standard
*/
const trackSearchInitiated = useCallback((query, searchType = 'all', searchMode = 'hybrid') => {
track(RETENTION_EVENTS.SEARCH_INITIATED, {
context: 'limit_analyse',
});
track(RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED, {
query,
category: 'limit_analyse',
search_type: searchType,
search_mode: searchMode,
});
logger.debug('useLimitAnalyseEvents', '🔍 Search Initiated', {
query,
searchType,
searchMode,
});
}, [track]);
/**
* 追踪搜索结果点击
* @param {Object} result - 搜索结果对象
* @param {number} position - 在结果列表中的位置
*/
const trackSearchResultClicked = useCallback((result, position = 0) => {
track(RETENTION_EVENTS.SEARCH_RESULT_CLICKED, {
result_type: result.type,
result_id: result.id || result.code,
result_name: result.name,
position,
context: 'limit_analyse',
});
logger.debug('useLimitAnalyseEvents', '🎯 Search Result Clicked', {
type: result.type,
name: result.name,
position,
});
}, [track]);
/**
* 追踪高位股查看
* @param {string} date - 日期
* @param {Object} stats - 高位股统计数据
*/
const trackHighPositionStocksViewed = useCallback((date, stats = {}) => {
track(RETENTION_EVENTS.LIMIT_ANALYSE_PAGE_VIEWED, {
date,
view_type: 'high_position_stocks',
total_count: stats.total_count || 0,
max_consecutive_days: stats.max_consecutive_days || 0,
});
logger.debug('useLimitAnalyseEvents', '📊 High Position Stocks Viewed', {
date,
stats,
});
}, [track]);
/**
* 追踪板块分析查看(分布图/关联图)
* @param {string} date - 日期
* @param {string} analysisType - 分析类型distribution/relation/wordcloud
*/
const trackSectorAnalysisViewed = useCallback((date, analysisType) => {
track(RETENTION_EVENTS.LIMIT_SECTOR_ANALYSIS_VIEWED, {
date,
analysis_type: analysisType,
source: 'limit_analyse',
});
logger.debug('useLimitAnalyseEvents', '📊 Sector Analysis Viewed', {
date,
analysisType,
});
}, [track]);
/**
* 追踪数据刷新
* @param {string} date - 刷新的日期
*/
const trackDataRefreshed = useCallback((date) => {
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
filter_type: 'refresh',
filter_value: date,
context: 'limit_analyse',
});
logger.debug('useLimitAnalyseEvents', '🔄 Data Refreshed', { date });
}, [track]);
/**
* 追踪股票详情Modal打开
* @param {string} stockCode - 股票代码
* @param {string} stockName - 股票名称
*/
const trackStockDetailViewed = useCallback((stockCode, stockName) => {
track(RETENTION_EVENTS.STOCK_DETAIL_VIEWED, {
stock_code: stockCode,
stock_name: stockName,
source: 'limit_analyse_modal',
});
logger.debug('useLimitAnalyseEvents', '👁️ Stock Detail Modal Opened', {
stockCode,
stockName,
});
}, [track]);
return {
trackDateSelected,
trackDailyStatsViewed,
trackSectorToggled,
trackSectorClicked,
trackLimitStockClicked,
trackSearchInitiated,
trackSearchResultClicked,
trackHighPositionStocksViewed,
trackSectorAnalysisViewed,
trackDataRefreshed,
trackStockDetailViewed,
};
};

View File

@@ -48,6 +48,7 @@ import { AdvancedSearch, SearchResultsModal } from './components/SearchComponent
// 导入高位股统计组件
import HighPositionStocks from './components/HighPositionStocks';
import { logger } from '../../utils/logger';
import { useLimitAnalyseEvents } from './hooks/useLimitAnalyseEvents';
// 主组件
export default function LimitAnalyse() {
@@ -62,6 +63,21 @@ export default function LimitAnalyse() {
const toast = useToast();
// 🎯 PostHog 事件追踪
const {
trackDateSelected,
trackDailyStatsViewed,
trackSectorToggled,
trackSectorClicked,
trackLimitStockClicked,
trackSearchInitiated,
trackSearchResultClicked,
trackHighPositionStocksViewed,
trackSectorAnalysisViewed,
trackDataRefreshed,
trackStockDetailViewed,
} = useLimitAnalyseEvents();
const bgColor = useColorModeValue('gray.50', 'gray.900');
const cardBg = useColorModeValue('white', 'gray.800');
const accentColor = useColorModeValue('blue.500', 'blue.300');
@@ -126,6 +142,9 @@ export default function LimitAnalyse() {
if (data.success) {
setDailyData(data.data);
// 🎯 追踪每日统计数据查看
trackDailyStatsViewed(data.data, date);
// 获取词云数据
fetchWordCloudData(date);
@@ -169,14 +188,26 @@ export default function LimitAnalyse() {
// 处理日期选择
const handleDateChange = (date) => {
const previousDateStr = dateStr;
setSelectedDate(date);
const dateString = formatDateStr(date);
setDateStr(dateString);
// 🎯 追踪日期选择
trackDateSelected(dateString, previousDateStr);
fetchDailyAnalysis(dateString);
};
// 处理搜索
const handleSearch = async (searchParams) => {
// 🎯 追踪搜索开始
trackSearchInitiated(
searchParams.query,
searchParams.type || 'all',
searchParams.mode || 'hybrid'
);
setLoading(true);
try {
const response = await fetch(`${API_URL}/api/v1/stocks/search/hybrid`, {