From cddd0e860e34bd51968672259e25c549086987b9 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Tue, 28 Oct 2025 21:40:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Concept=20=E9=A1=B5=E9=9D=A2=20-=209?= =?UTF-8?q?=E4=B8=AA=E4=BA=8B=E4=BB=B6=E6=90=9C=E7=B4=A2=E3=80=81=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E3=80=81=E6=A6=82=E5=BF=B5=E4=BA=A4=E4=BA=92=E3=80=81?= =?UTF-8?q?=E4=B8=AA=E8=82=A1=E6=9F=A5=E7=9C=8B=E3=80=81=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=BD=B4=E3=80=81=E8=A7=86=E5=9B=BE=E5=88=87=E6=8D=A2=20?= =?UTF-8?q?=E6=96=B0=E5=BB=BA=E6=96=87=E4=BB=B6:=20=20=20-=20src/views/Con?= =?UTF-8?q?cept/hooks/useConceptEvents.js=20(203=E8=A1=8C)=20=20=20=20=20-?= =?UTF-8?q?=20=E6=8F=90=E4=BE=9B8=E4=B8=AA=E8=BF=BD=E8=B8=AA=E5=87=BD?= =?UTF-8?q?=E6=95=B0=20=20=20=20=20-=20=E9=A1=B5=E9=9D=A2=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E8=87=AA=E5=8A=A8=E8=BF=BD=E8=B8=AA=20=20=20=20=20-?= =?UTF-8?q?=20=E5=AE=8C=E6=95=B4=E7=9A=84=E4=BA=8B=E4=BB=B6=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改文件: - src/views/Concept/index.js - 添加 useConceptEvents Hook - 集成追踪到9个关键函数: i. handleSearch - 搜索查询 ii. handleSortChange - 排序变化 iii. handleDateChange - 日期变化 iv. handlePageChange - 翻页 v. handleConceptClick - 概念点击(传递位置) vi. handleViewStocks - 查看个股 vii. handleViewContent - 历史时间轴 viii. 视图切换按钮 - 网格/列表切换 ix. ConceptCard/ConceptListItem - 位置追踪 追踪事件: 9个 1. CONCEPT_CENTER_VIEWED - 页面浏览 2. SEARCH_QUERY_SUBMITTED - 搜索查询 3. SEARCH_FILTER_APPLIED - 筛选(sort/date) 4. CONCEPT_CLICKED - 概念点击(含位置) 5. CONCEPT_STOCKS_VIEWED - 查看个股 6. CONCEPT_STOCK_CLICKED - 股票点击 7. CONCEPT_TIMELINE_VIEWED - 历史时间轴 8. NEWS_LIST_VIEWED - 翻页(复用) 9. VIEW_MODE_CHANGED - 视图切换 --- src/views/Concept/hooks/useConceptEvents.js | 292 ++++++++++++++++++++ src/views/Concept/index.js | 78 +++++- 2 files changed, 358 insertions(+), 12 deletions(-) create mode 100644 src/views/Concept/hooks/useConceptEvents.js 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) => ( + ))} )}