feat: Concept 页面 - 9个事件搜索、筛选、概念交互、个股查看、时间轴、视图切换
新建文件:
- src/views/Concept/hooks/useConceptEvents.js (203行)
- 提供8个追踪函数
- 页面浏览自动追踪
- 完整的事件属性定义
修改文件:
- 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 - 视图切换
This commit is contained in:
292
src/views/Concept/hooks/useConceptEvents.js
Normal file
292
src/views/Concept/hooks/useConceptEvents.js
Normal file
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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 (
|
||||
<Card
|
||||
cursor="pointer"
|
||||
onClick={() => 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 (
|
||||
<Card
|
||||
cursor="pointer"
|
||||
onClick={() => 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 = () => {
|
||||
<ButtonGroup size="sm" isAttached variant="outline">
|
||||
<IconButton
|
||||
icon={<FaThLarge />}
|
||||
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 = () => {
|
||||
/>
|
||||
<IconButton
|
||||
icon={<FaList />}
|
||||
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' ? (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6} className="concept-grid">
|
||||
{concepts.map((concept) => (
|
||||
{concepts.map((concept, index) => (
|
||||
<Box key={concept.concept_id} className="concept-item" role="group">
|
||||
<ConceptCard concept={concept} />
|
||||
<ConceptCard concept={concept} position={index} />
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<VStack spacing={4} align="stretch" className="concept-list">
|
||||
{concepts.map((concept) => (
|
||||
<ConceptListItem key={concept.concept_id} concept={concept} />
|
||||
{concepts.map((concept, index) => (
|
||||
<ConceptListItem key={concept.concept_id} concept={concept} position={index} />
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user