diff --git a/src/views/Concept/hooks/useConceptEvents.js b/src/views/Concept/hooks/useConceptEvents.js new file mode 100644 index 00000000..6af3a1a8 --- /dev/null +++ b/src/views/Concept/hooks/useConceptEvents.js @@ -0,0 +1,292 @@ +// src/views/Concept/hooks/useConceptEvents.js +// 概念中心页面事件追踪 Hook + +import { useCallback, useEffect } from 'react'; +import { usePostHogTrack } from '../../../hooks/usePostHogRedux'; +import { RETENTION_EVENTS, REVENUE_EVENTS } from '../../../lib/constants'; +import { logger } from '../../../utils/logger'; + +/** + * 概念中心事件追踪 Hook + * @param {Object} options - 配置选项 + * @param {Function} options.navigate - 路由导航函数 + * @returns {Object} 事件追踪处理函数集合 + */ +export const useConceptEvents = ({ navigate } = {}) => { + const { track } = usePostHogTrack(); + + // 🎯 页面浏览事件 - 页面加载时触发 + useEffect(() => { + track(RETENTION_EVENTS.CONCEPT_PAGE_VIEWED, { + timestamp: new Date().toISOString(), + }); + logger.debug('useConceptEvents', '📊 Concept Page Viewed'); + }, [track]); + + /** + * 追踪概念列表数据查看 + * @param {Array} concepts - 概念列表 + * @param {Object} filters - 当前筛选条件 + */ + const trackConceptListViewed = useCallback((concepts, filters = {}) => { + track(RETENTION_EVENTS.CONCEPT_LIST_VIEWED, { + concept_count: concepts.length, + sort_by: filters.sortBy, + view_mode: filters.viewMode, + has_search_query: !!filters.searchQuery, + selected_date: filters.selectedDate, + page: filters.page, + }); + + logger.debug('useConceptEvents', '📋 Concept List Viewed', { + count: concepts.length, + filters, + }); + }, [track]); + + /** + * 追踪搜索开始 + */ + const trackSearchInitiated = useCallback(() => { + track(RETENTION_EVENTS.SEARCH_INITIATED, { + context: 'concept_center', + }); + + logger.debug('useConceptEvents', '🔍 Search Initiated'); + }, [track]); + + /** + * 追踪搜索查询提交 + * @param {string} query - 搜索查询词 + * @param {number} resultCount - 搜索结果数量 + */ + const trackSearchQuerySubmitted = useCallback((query, resultCount = 0) => { + if (!query) return; + + track(RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED, { + query, + category: 'concept', + result_count: resultCount, + has_results: resultCount > 0, + }); + + // 如果没有搜索结果,额外追踪 + if (resultCount === 0) { + track(RETENTION_EVENTS.SEARCH_NO_RESULTS, { + query, + context: 'concept_center', + }); + } + + logger.debug('useConceptEvents', '🔍 Search Query Submitted', { + query, + resultCount, + }); + }, [track]); + + /** + * 追踪排序方式变化 + * @param {string} sortBy - 新的排序方式 + * @param {string} previousSortBy - 之前的排序方式 + */ + const trackSortChanged = useCallback((sortBy, previousSortBy = null) => { + track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, { + filter_type: 'sort', + filter_value: sortBy, + previous_value: previousSortBy, + context: 'concept_center', + }); + + logger.debug('useConceptEvents', '🔄 Sort Changed', { + sortBy, + previousSortBy, + }); + }, [track]); + + /** + * 追踪视图模式切换 + * @param {string} viewMode - 新的视图模式 (grid/list) + * @param {string} previousViewMode - 之前的视图模式 + */ + const trackViewModeChanged = useCallback((viewMode, previousViewMode = null) => { + track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, { + filter_type: 'view_mode', + filter_value: viewMode, + previous_value: previousViewMode, + context: 'concept_center', + }); + + logger.debug('useConceptEvents', '👁️ View Mode Changed', { + viewMode, + previousViewMode, + }); + }, [track]); + + /** + * 追踪日期选择变化 + * @param {string} newDate - 新选择的日期 + * @param {string} previousDate - 之前的日期 + * @param {string} selectionMethod - 选择方式 (today/yesterday/week_ago/month_ago/custom) + */ + const trackDateChanged = useCallback((newDate, previousDate = null, selectionMethod = 'custom') => { + track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, { + filter_type: 'date', + filter_value: newDate, + previous_value: previousDate, + selection_method: selectionMethod, + context: 'concept_center', + }); + + logger.debug('useConceptEvents', '📅 Date Changed', { + newDate, + previousDate, + selectionMethod, + }); + }, [track]); + + /** + * 追踪分页变化 + * @param {number} page - 新的页码 + * @param {Object} filters - 当前筛选条件 + */ + const trackPageChanged = useCallback((page, filters = {}) => { + track(RETENTION_EVENTS.CONCEPT_LIST_VIEWED, { + page, + sort_by: filters.sortBy, + view_mode: filters.viewMode, + has_search_query: !!filters.searchQuery, + }); + + logger.debug('useConceptEvents', '📄 Page Changed', { page, filters }); + }, [track]); + + /** + * 追踪概念卡片点击 + * @param {Object} concept - 概念对象 + * @param {number} position - 在列表中的位置 + * @param {string} source - 来源 (list/stats_panel) + */ + const trackConceptClicked = useCallback((concept, position = 0, source = 'list') => { + track(RETENTION_EVENTS.CONCEPT_CLICKED, { + concept_name: concept.concept_name || concept.name, + concept_code: concept.concept_code || concept.code, + change_percent: concept.change_pct || concept.change_percent, + stock_count: concept.stock_count, + position, + source, + }); + + logger.debug('useConceptEvents', '🎯 Concept Clicked', { + concept: concept.concept_name || concept.name, + position, + source, + }); + }, [track]); + + /** + * 追踪概念下的股票标签点击 + * @param {Object} stock - 股票对象 + * @param {string} conceptName - 所属概念名称 + */ + const trackConceptStockClicked = useCallback((stock, conceptName) => { + track(RETENTION_EVENTS.CONCEPT_STOCK_CLICKED, { + stock_code: stock.code || stock.stock_code, + stock_name: stock.name || stock.stock_name, + concept_name: conceptName, + source: 'concept_center_tag', + }); + + logger.debug('useConceptEvents', '🏷️ Concept Stock Tag Clicked', { + stock: stock.code || stock.stock_code, + concept: conceptName, + }); + }, [track]); + + /** + * 追踪概念详情查看(时间轴Modal) + * @param {string} conceptName - 概念名称 + * @param {string} conceptId - 概念ID + */ + const trackConceptDetailViewed = useCallback((conceptName, conceptId) => { + track(RETENTION_EVENTS.CONCEPT_DETAIL_VIEWED, { + concept_name: conceptName, + concept_id: conceptId, + source: 'concept_center', + }); + + logger.debug('useConceptEvents', '📊 Concept Detail Viewed', { + conceptName, + conceptId, + }); + }, [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: 'concept_center_modal', + }); + + logger.debug('useConceptEvents', '👁️ Stock Detail Modal Opened', { + stockCode, + stockName, + }); + }, [track]); + + /** + * 追踪付费墙展示 + * @param {string} feature - 需要付费的功能 + * @param {string} requiredTier - 需要的订阅等级 + */ + const trackPaywallShown = useCallback((feature, requiredTier = 'pro') => { + track(REVENUE_EVENTS.PAYWALL_SHOWN, { + feature, + required_tier: requiredTier, + page: 'concept_center', + }); + + logger.debug('useConceptEvents', '🔒 Paywall Shown', { + feature, + requiredTier, + }); + }, [track]); + + /** + * 追踪升级按钮点击 + * @param {string} feature - 触发升级的功能 + * @param {string} targetTier - 目标订阅等级 + */ + const trackUpgradeClicked = useCallback((feature, targetTier = 'pro') => { + track(REVENUE_EVENTS.PAYWALL_UPGRADE_CLICKED, { + feature, + target_tier: targetTier, + source_page: 'concept_center', + }); + + logger.debug('useConceptEvents', '⬆️ Upgrade Button Clicked', { + feature, + targetTier, + }); + }, [track]); + + return { + trackConceptListViewed, + trackSearchInitiated, + trackSearchQuerySubmitted, + trackSortChanged, + trackViewModeChanged, + trackDateChanged, + trackPageChanged, + trackConceptClicked, + trackConceptStockClicked, + trackConceptDetailViewed, + trackStockDetailViewed, + trackPaywallShown, + trackUpgradeClicked, + }; +}; diff --git a/src/views/Concept/index.js b/src/views/Concept/index.js index 3d13e2f3..2c0fdde0 100644 --- a/src/views/Concept/index.js +++ b/src/views/Concept/index.js @@ -90,6 +90,8 @@ import { useSubscription } from '../../hooks/useSubscription'; import SubscriptionUpgradeModal from '../../components/SubscriptionUpgradeModal'; // 导入市场服务 import { marketService } from '../../services/marketService'; +// 导入 PostHog 追踪 Hook +import { useConceptEvents } from './hooks/useConceptEvents'; const API_BASE_URL = process.env.NODE_ENV === 'production' ? '/concept-api' @@ -129,6 +131,18 @@ const ConceptCenter = () => { const navigate = useNavigate(); const toast = useToast(); + // 🎯 PostHog 事件追踪 + const { + trackConceptSearched, + trackFilterApplied, + trackConceptClicked, + trackConceptStocksViewed, + trackConceptStockClicked, + trackConceptTimelineViewed, + trackPageChange, + trackViewModeChanged, + } = useConceptEvents({ navigate }); + // 订阅权限管理 const { hasFeatureAccess, getUpgradeRecommendation } = useSubscription(); const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); @@ -192,6 +206,9 @@ const ConceptCenter = () => { return; } + // 🎯 追踪历史时间轴查看 + trackConceptTimelineViewed(conceptName, conceptId); + setSelectedConceptForContent(conceptName); setSelectedConceptId(conceptId); setIsTimelineModalOpen(true); @@ -318,8 +335,14 @@ const ConceptCenter = () => { setSortBy('change_pct'); } + // 🎯 追踪搜索查询(在fetchConcepts后追踪结果数量) updateUrlParams({ q: searchQuery, page: 1, sort: newSortBy }); - fetchConcepts(searchQuery, 1, selectedDate, newSortBy); + fetchConcepts(searchQuery, 1, selectedDate, newSortBy).then(() => { + if (searchQuery && searchQuery.trim() !== '') { + // 使用当前 concepts.length 作为结果数量 + setTimeout(() => trackConceptSearched(searchQuery, concepts.length), 100); + } + }); }; // 处理Enter键搜索 @@ -331,6 +354,11 @@ const ConceptCenter = () => { // 处理排序变化 const handleSortChange = (value) => { + const previousSort = sortBy; + + // 🎯 追踪排序变化 + trackFilterApplied('sort', value, previousSort); + setSortBy(value); setCurrentPage(1); updateUrlParams({ sort: value, page: 1 }); @@ -340,6 +368,11 @@ const ConceptCenter = () => { // 处理日期变化 const handleDateChange = (e) => { const date = new Date(e.target.value); + const previousDate = selectedDate ? selectedDate.toISOString().split('T')[0] : null; + + // 🎯 追踪日期变化 + trackFilterApplied('date', e.target.value, previousDate); + setSelectedDate(date); setCurrentPage(1); updateUrlParams({ date: e.target.value, page: 1 }); @@ -359,6 +392,9 @@ const ConceptCenter = () => { // 处理页码变化 const handlePageChange = (page) => { + // 🎯 追踪翻页 + trackPageChange(page, { sort: sortBy, q: searchQuery, date: selectedDate?.toISOString().split('T')[0] }); + setCurrentPage(page); updateUrlParams({ page }); fetchConcepts(searchQuery, page, selectedDate, sortBy); @@ -366,7 +402,12 @@ const ConceptCenter = () => { }; // 处理概念点击 - const handleConceptClick = (conceptId, conceptName) => { + const handleConceptClick = (conceptId, conceptName, concept = null, position = 0) => { + // 🎯 追踪概念点击 + if (concept) { + trackConceptClicked(concept, position); + } + const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(conceptName)}.html`; window.open(htmlPath, '_blank'); }; @@ -433,6 +474,9 @@ const ConceptCenter = () => { return; } + // 🎯 追踪查看个股 + trackConceptStocksViewed(concept.concept, concept.stocks?.length || 0); + setSelectedConceptStocks(concept.stocks || []); setSelectedConceptName(concept.concept); setStockMarketData({}); // 清空之前的数据 @@ -649,7 +693,7 @@ const ConceptCenter = () => { }, []); // 概念卡片组件 - 优化版 - const ConceptCard = ({ concept }) => { + const ConceptCard = ({ concept, position = 0 }) => { const changePercent = concept.price_info?.avg_change_pct; const changeColor = getChangeColor(changePercent); const hasChange = changePercent !== null && changePercent !== undefined; @@ -657,7 +701,7 @@ const ConceptCenter = () => { return ( handleConceptClick(concept.concept_id, concept.concept)} + onClick={() => handleConceptClick(concept.concept_id, concept.concept, concept, position)} bg="white" borderWidth="1px" borderColor="gray.200" @@ -857,7 +901,7 @@ const ConceptCenter = () => { }; // 概念列表项组件 - 列表视图 - const ConceptListItem = ({ concept }) => { + const ConceptListItem = ({ concept, position = 0 }) => { const changePercent = concept.price_info?.avg_change_pct; const changeColor = getChangeColor(changePercent); const hasChange = changePercent !== null && changePercent !== undefined; @@ -865,7 +909,7 @@ const ConceptCenter = () => { return ( handleConceptClick(concept.concept_id, concept.concept)} + onClick={() => handleConceptClick(concept.concept_id, concept.concept, concept, position)} bg="white" borderWidth="1px" borderColor="gray.200" @@ -1361,7 +1405,12 @@ const ConceptCenter = () => { } - onClick={() => setViewMode('grid')} + onClick={() => { + if (viewMode !== 'grid') { + trackViewModeChanged('grid', viewMode); + setViewMode('grid'); + } + }} bg={viewMode === 'grid' ? 'purple.500' : 'transparent'} color={viewMode === 'grid' ? 'white' : 'purple.500'} borderColor="purple.500" @@ -1370,7 +1419,12 @@ const ConceptCenter = () => { /> } - onClick={() => setViewMode('list')} + onClick={() => { + if (viewMode !== 'list') { + trackViewModeChanged('list', viewMode); + setViewMode('list'); + } + }} bg={viewMode === 'list' ? 'purple.500' : 'transparent'} color={viewMode === 'list' ? 'white' : 'purple.500'} borderColor="purple.500" @@ -1404,16 +1458,16 @@ const ConceptCenter = () => { <> {viewMode === 'grid' ? ( - {concepts.map((concept) => ( + {concepts.map((concept, index) => ( - + ))} ) : ( - {concepts.map((concept) => ( - + {concepts.map((concept, index) => ( + ))} )}