feat: Retention(留存)分析
1. 最受欢迎的功能
- 哪些功能用户使用最频繁?
- 新闻、事件、个股、模拟盘的使用对比
2. 用户行为路径
- 用户从哪里进入?
- 在每个页面停留多久?
- 从哪个环节流失?
3. 内容偏好
- 什么类型的新闻最受欢迎?
- 用户关注哪些行业?
- 哪些事件获得最多关注?
Revenue(收入)转化
1. 付费转化漏斗
个人中心查看 →
自选股/关注事件使用 →
订阅页面查看 →
升级按钮点击 →
(付费转化)
2. 模拟盘转化分析
模拟盘进入 →
搜索股票 →
下单操作 →
持续使用 →
(付费转化)
This commit is contained in:
@@ -17,6 +17,7 @@ import EventModals from './components/EventModals';
|
||||
// 导入自定义 Hooks
|
||||
import { useEventData } from './hooks/useEventData';
|
||||
import { useEventFilters } from './hooks/useEventFilters';
|
||||
import { useCommunityEvents } from './hooks/useCommunityEvents';
|
||||
|
||||
import { logger } from '../../utils/logger';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
@@ -28,7 +29,7 @@ import { RETENTION_EVENTS } from '../../lib/constants';
|
||||
const Community = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { track } = usePostHogTrack(); // PostHog 追踪
|
||||
const { track } = usePostHogTrack(); // PostHog 追踪(保留用于兼容)
|
||||
|
||||
// Redux状态
|
||||
const { popularKeywords, hotEvents } = useSelector(state => state.communityData);
|
||||
@@ -47,6 +48,9 @@ const Community = () => {
|
||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||
const [selectedEventForStock, setSelectedEventForStock] = useState(null);
|
||||
|
||||
// 🎯 初始化Community埋点Hook
|
||||
const communityEvents = useCommunityEvents({ navigate });
|
||||
|
||||
// 自定义 Hooks
|
||||
const { filters, updateFilters, handlePageChange, handleEventClick, handleViewDetail } = useEventFilters({
|
||||
navigate,
|
||||
@@ -63,13 +67,26 @@ const Community = () => {
|
||||
}, [dispatch]);
|
||||
|
||||
// 🎯 PostHog 追踪:页面浏览
|
||||
// useEffect(() => {
|
||||
// track(RETENTION_EVENTS.COMMUNITY_PAGE_VIEWED, {
|
||||
// timestamp: new Date().toISOString(),
|
||||
// has_hot_events: hotEvents && hotEvents.length > 0,
|
||||
// has_keywords: popularKeywords && popularKeywords.length > 0,
|
||||
// });
|
||||
// }, [track]); // 只在组件挂载时执行一次
|
||||
|
||||
// 🎯 追踪新闻列表查看(当事件列表加载完成后)
|
||||
useEffect(() => {
|
||||
track(RETENTION_EVENTS.COMMUNITY_PAGE_VIEWED, {
|
||||
timestamp: new Date().toISOString(),
|
||||
has_hot_events: hotEvents && hotEvents.length > 0,
|
||||
has_keywords: popularKeywords && popularKeywords.length > 0,
|
||||
});
|
||||
}, [track]); // 只在组件挂载时执行一次
|
||||
if (events && events.length > 0 && !loading) {
|
||||
communityEvents.trackNewsListViewed({
|
||||
totalCount: pagination?.total || events.length,
|
||||
sortBy: filters.sort,
|
||||
importance: filters.importance,
|
||||
dateRange: filters.date_range,
|
||||
industryFilter: filters.industry_code,
|
||||
});
|
||||
}
|
||||
}, [events, loading, pagination, filters, communityEvents]);
|
||||
|
||||
// ⚡ 首次访问社区时,延迟显示权限引导
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { getApiBase } from '../../utils/apiConfig';
|
||||
import { useDashboardEvents } from '../../hooks/useDashboardEvents';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@@ -72,6 +73,12 @@ export default function CenterDashboard() {
|
||||
const userId = user?.id;
|
||||
const prevUserIdRef = React.useRef(userId);
|
||||
|
||||
// 🎯 初始化Dashboard埋点Hook
|
||||
const dashboardEvents = useDashboardEvents({
|
||||
pageType: 'center',
|
||||
navigate
|
||||
});
|
||||
|
||||
// 颜色主题
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
@@ -101,14 +108,33 @@ export default function CenterDashboard() {
|
||||
const je = await e.json();
|
||||
const jc = await c.json();
|
||||
if (jw.success) {
|
||||
setWatchlist(Array.isArray(jw.data) ? jw.data : []);
|
||||
const watchlistData = Array.isArray(jw.data) ? jw.data : [];
|
||||
setWatchlist(watchlistData);
|
||||
|
||||
// 🎯 追踪自选股列表查看
|
||||
if (watchlistData.length > 0) {
|
||||
dashboardEvents.trackWatchlistViewed(watchlistData.length, true);
|
||||
}
|
||||
|
||||
// 加载实时行情
|
||||
if (jw.data && jw.data.length > 0) {
|
||||
loadRealtimeQuotes();
|
||||
}
|
||||
}
|
||||
if (je.success) setFollowingEvents(Array.isArray(je.data) ? je.data : []);
|
||||
if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []);
|
||||
if (je.success) {
|
||||
const eventsData = Array.isArray(je.data) ? je.data : [];
|
||||
setFollowingEvents(eventsData);
|
||||
|
||||
// 🎯 追踪关注的事件列表查看
|
||||
dashboardEvents.trackFollowingEventsViewed(eventsData.length);
|
||||
}
|
||||
if (jc.success) {
|
||||
const commentsData = Array.isArray(jc.data) ? jc.data : [];
|
||||
setEventComments(commentsData);
|
||||
|
||||
// 🎯 追踪评论列表查看
|
||||
dashboardEvents.trackCommentsViewed(commentsData.length);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Center', 'loadData', err, {
|
||||
userId,
|
||||
|
||||
@@ -75,6 +75,7 @@ import TransmissionChainAnalysis from './components/TransmissionChainAnalysis';
|
||||
import { eventService } from '../../services/eventService';
|
||||
import { debugEventService } from '../../utils/debugEventService';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { useEventDetailEvents } from './hooks/useEventDetailEvents';
|
||||
|
||||
// 临时调试代码 - 生产环境测试后请删除
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -348,6 +349,15 @@ const EventDetail = () => {
|
||||
const [postsLoading, setPostsLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
// 🎯 初始化事件详情埋点Hook(传入event对象)
|
||||
const eventEvents = useEventDetailEvents({
|
||||
event: eventData ? {
|
||||
id: eventData.id,
|
||||
title: eventData.title,
|
||||
importance: eventData.importance
|
||||
} : null
|
||||
});
|
||||
const [newPostContent, setNewPostContent] = useState('');
|
||||
const [newPostTitle, setNewPostTitle] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
@@ -380,9 +390,11 @@ const EventDetail = () => {
|
||||
setEventData(eventResponse.data);
|
||||
|
||||
// 总是尝试加载相关股票(权限在组件内部检查)
|
||||
let stocksCount = 0;
|
||||
try {
|
||||
const stocksResponse = await eventService.getRelatedStocks(actualEventId);
|
||||
setRelatedStocks(stocksResponse.data || []);
|
||||
stocksCount = stocksResponse.data?.length || 0;
|
||||
} catch (e) {
|
||||
logger.warn('EventDetail', '加载相关股票失败', { eventId: actualEventId, error: e.message });
|
||||
setRelatedStocks([]);
|
||||
@@ -399,13 +411,25 @@ const EventDetail = () => {
|
||||
}
|
||||
|
||||
// 历史事件所有用户都可以访问,但免费用户只看到前2条
|
||||
let timelineCount = 0;
|
||||
try {
|
||||
const eventsResponse = await eventService.getHistoricalEvents(actualEventId);
|
||||
setHistoricalEvents(eventsResponse.data || []);
|
||||
timelineCount = eventsResponse.data?.length || 0;
|
||||
} catch (e) {
|
||||
logger.warn('EventDetail', '历史事件加载失败', { eventId: actualEventId, error: e.message });
|
||||
}
|
||||
|
||||
// 🎯 追踪事件分析内容查看(数据加载完成后)
|
||||
if (eventResponse.data && eventEvents) {
|
||||
eventEvents.trackEventAnalysisViewed({
|
||||
type: 'overview',
|
||||
relatedStockCount: stocksCount,
|
||||
timelineEventCount: timelineCount,
|
||||
marketImpact: eventResponse.data.market_impact
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.error('EventDetail', 'loadEventData', err, { eventId: actualEventId });
|
||||
setError(err.message || '加载事件数据失败');
|
||||
|
||||
@@ -49,6 +49,7 @@ import LineChart from '../../components/Charts/LineChart';
|
||||
|
||||
// 模拟盘账户管理 Hook
|
||||
import { useTradingAccount } from './hooks/useTradingAccount';
|
||||
import { useTradingSimulationEvents } from './hooks/useTradingSimulationEvents';
|
||||
|
||||
export default function TradingSimulation() {
|
||||
// ========== 1. 所有 Hooks 必须放在最顶部,不能有任何条件判断 ==========
|
||||
@@ -76,6 +77,15 @@ export default function TradingSimulation() {
|
||||
getAssetHistory
|
||||
} = useTradingAccount();
|
||||
|
||||
// 🎯 初始化模拟盘埋点Hook(传入账户信息)
|
||||
const tradingEvents = useTradingSimulationEvents({
|
||||
portfolio: account ? {
|
||||
totalValue: account.total_assets,
|
||||
availableCash: account.available_cash,
|
||||
holdingsCount: positions?.length || 0
|
||||
} : null
|
||||
});
|
||||
|
||||
// 所有的 useColorModeValue 也必须在顶部
|
||||
const bgColor = useColorModeValue('gray.50', 'gray.900');
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
|
||||
Reference in New Issue
Block a user