Compare commits

..

4 Commits

Author SHA1 Message Date
zdl
b2380c420c Merge branch '1028_bugfix' into feature_2025/1028_bugfix
* 1028_bugfix:
  手机号格式适配-前端修改
  添加微信扫码的几种其他状态
  整合register端口进入login端口
2025-10-29 16:26:02 +08:00
8417ab17be 手机号格式适配-前端修改 2025-10-29 11:20:41 +08:00
dd59cb6385 添加微信扫码的几种其他状态 2025-10-29 07:33:44 +08:00
512aca16d8 整合register端口进入login端口 2025-10-28 15:47:50 +08:00
56 changed files with 321 additions and 6749 deletions

View File

@@ -11,8 +11,7 @@
"Bash(npm install)",
"Bash(npm run start:mock)",
"Bash(npm install fsevents@latest --save-optional --force)",
"Bash(python -m py_compile:*)",
"Bash(ps -p 20502,53360 -o pid,command)"
"Bash(python -m py_compile:*)"
],
"deny": [],
"ask": []

View File

@@ -1,5 +1,5 @@
# 开发环境配置(连接真实后端)
# 使用方式: npm run start:dev
# 使用方式: npm start
# React 构建优化配置
GENERATE_SOURCEMAP=false
@@ -18,10 +18,3 @@ REACT_APP_ENABLE_MOCK=false
# 开发环境标识
REACT_APP_ENV=development
# PostHog 配置(开发环境)
# 留空 = 仅控制台 debug
# 填入 Key = 控制台 + PostHog Cloud 双模式
REACT_APP_POSTHOG_KEY=
REACT_APP_POSTHOG_HOST=https://app.posthog.com
REACT_APP_ENABLE_SESSION_RECORDING=false

View File

@@ -35,14 +35,3 @@ REACT_APP_ENABLE_MOCK=true
# Mock 环境标识
REACT_APP_ENV=mock
# PostHog 配置Mock 环境)
# 留空 = 仅控制台 debug
# 填入 Key = 控制台 + PostHog Cloud 双模式
REACT_APP_POSTHOG_KEY=phc_xKlRyG69Bx7hgOdFeCeLUvQWvSjw18ZKFgCwCeYezWF
REACT_APP_POSTHOG_HOST=https://app.posthog.com
REACT_APP_ENABLE_SESSION_RECORDING=false
# PostHog Debug 模式Mock 环境永久启用)
# 在浏览器 Console 中打印详细的事件追踪日志
REACT_APP_POSTHOG_DEBUG=true

View File

@@ -1,42 +0,0 @@
# ========================================
# 本地测试环境(前后端都在本地)
# ========================================
# 使用方式: npm run start:test
#
# 工作原理:
# 1. concurrently 同时启动前端和后端
# 2. 前端: localhost:3000
# 3. 后端: localhost:5001 (python app_2.py)
# 4. 数据: 本地数据库
#
# 适用场景:
# - 调试后端代码
# - 性能测试
# - 离线开发
# - 数据库调试
# ========================================
# 环境标识
REACT_APP_ENV=test
NODE_ENV=development
# Mock 配置(关闭 MSW
REACT_APP_ENABLE_MOCK=false
# 后端 API 地址(本地后端)
REACT_APP_API_URL=http://localhost:5001
# PostHog 配置(测试环境)
# 留空 = 仅控制台 debug
# 填入 Key = 控制台 + PostHog Cloud 双模式
REACT_APP_POSTHOG_KEY=
REACT_APP_POSTHOG_HOST=https://app.posthog.com
REACT_APP_ENABLE_SESSION_RECORDING=false
# React 构建优化配置
GENERATE_SOURCEMAP=true # 测试环境保留 sourcemap 便于调试
SKIP_PREFLIGHT_CHECK=true
DISABLE_ESLINT_PLUGIN=false # 测试环境开启 ESLint
TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
NODE_OPTIONS=--max_old_space_size=4096

272
app.py
View File

@@ -1849,6 +1849,15 @@ def send_verification_code():
if not credential or not code_type:
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
# 清理格式字符(空格、横线、括号等)
if code_type == 'phone':
# 移除手机号中的空格、横线、括号、加号等格式字符
credential = re.sub(r'[\s\-\(\)\+]', '', credential)
print(f"📱 清理后的手机号: {credential}")
elif code_type == 'email':
# 邮箱只移除空格
credential = credential.strip()
# 生成验证码
verification_code = generate_verification_code()
@@ -1907,6 +1916,17 @@ def login_with_verification_code():
if not credential or not verification_code or not login_type:
return jsonify({'success': False, 'error': '缺少必要参数'}), 400
# 清理格式字符(空格、横线、括号等)
if login_type == 'phone':
# 移除手机号中的空格、横线、括号、加号等格式字符
original_credential = credential
credential = re.sub(r'[\s\-\(\)\+]', '', credential)
if original_credential != credential:
print(f"📱 登录时清理手机号: {original_credential} -> {credential}")
elif login_type == 'email':
# 邮箱只移除前后空格
credential = credential.strip()
# 检查验证码
session_key = f'verification_code_{login_type}_{credential}_login'
stored_code_info = session.get(session_key)
@@ -1968,12 +1988,51 @@ def login_with_verification_code():
username = f"{base_username}_{counter}"
counter += 1
# 如果用户不存在,自动创建新用户
if not user:
try:
# 生成用户名
if login_type == 'phone':
# 使用手机号生成用户名
base_username = f"用户{credential[-4:]}"
elif login_type == 'email':
# 使用邮箱前缀生成用户名
base_username = credential.split('@')[0]
else:
base_username = "新用户"
# 确保用户名唯一
username = base_username
counter = 1
while User.is_username_taken(username):
username = f"{base_username}_{counter}"
counter += 1
# 创建新用户
user = User(username=username, email=credential)
user.email_confirmed = True
user = User(username=username)
# 设置手机号或邮箱
if login_type == 'phone':
user.phone = credential
elif login_type == 'email':
user.email = credential
# 设置默认密码(使用随机密码,用户后续可以修改)
user.set_password(uuid.uuid4().hex)
user.status = 'active'
user.nickname = username
db.session.add(user)
db.session.commit()
is_new_user = True
print(f"✅ 自动创建新用户: {username}, {login_type}: {credential}")
except Exception as e:
print(f"❌ 创建用户失败: {e}")
db.session.rollback()
return jsonify({'success': False, 'error': '创建用户失败'}), 500
# 清除验证码
session.pop(session_key, None)
@@ -1989,10 +2048,13 @@ def login_with_verification_code():
# 更新最后登录时间
user.update_last_seen()
# 根据是否为新用户返回不同的消息
message = '注册成功,欢迎加入!' if is_new_user else '登录成功'
return jsonify({
'success': True,
'message': '注册成功' if is_new_user else '登录成功',
'isNewUser': is_new_user,
'message': message,
'is_new_user': is_new_user,
'user': {
'id': user.id,
'username': user.username,
@@ -2004,6 +2066,59 @@ def login_with_verification_code():
}
})
except Exception as e:
print(f"验证码登录错误: {e}")
db.session.rollback()
return jsonify({'success': False, 'error': '登录失败'}), 500
@app.route('/api/auth/register', methods=['POST'])
def register():
"""用户注册 - 使用Session"""
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
# 验证输入
if not all([username, email, password]):
return jsonify({'success': False, 'error': '所有字段都是必填的'}), 400
# 检查用户名和邮箱是否已存在
if User.is_username_taken(username):
return jsonify({'success': False, 'error': '用户名已存在'}), 400
if User.is_email_taken(email):
return jsonify({'success': False, 'error': '邮箱已被使用'}), 400
try:
# 创建新用户
user = User(username=username, email=email)
user.set_password(password)
user.email_confirmed = True # 暂时默认已确认
db.session.add(user)
db.session.commit()
# 自动登录
session.permanent = True
session['user_id'] = user.id
session['username'] = user.username
session['logged_in'] = True
# Flask-Login 登录
login_user(user, remember=True)
return jsonify({
'success': True,
'message': '注册成功',
'user': {
'id': user.id,
'username': user.username,
'nickname': user.nickname or user.username,
'email': user.email
}
}), 201
except Exception as e:
db.session.rollback()
print(f"验证码登录/注册错误: {e}")
@@ -2487,13 +2602,9 @@ def get_wechat_qrcode():
# 生成唯一state参数
state = uuid.uuid4().hex
print(f"🆕 [QRCODE] 生成新的微信二维码, state={state[:8]}...")
# URL编码回调地址
redirect_uri = urllib.parse.quote_plus(WECHAT_REDIRECT_URI)
print(f"🔗 [QRCODE] 回调地址: {WECHAT_REDIRECT_URI}")
# 构建微信授权URL
wechat_auth_url = (
f"https://open.weixin.qq.com/connect/qrconnect?"
@@ -2511,8 +2622,6 @@ def get_wechat_qrcode():
'wechat_unionid': None
}
print(f"✅ [QRCODE] session 已存储, 当前总数: {len(wechat_qr_sessions)}")
return jsonify({"code":0,
"data":
{
@@ -2576,8 +2685,6 @@ def check_wechat_scan():
del wechat_qr_sessions[session_id]
return jsonify({'status': 'expired'}), 200
print(f"📡 [CHECK] session_id: {session_id[:8]}..., status: {session['status']}, user_info: {session.get('user_info')}")
return jsonify({
'status': session['status'],
'user_info': session.get('user_info'),
@@ -2619,48 +2726,57 @@ def wechat_callback():
state = request.args.get('state')
error = request.args.get('error')
print(f"🎯 [CALLBACK] 微信回调被调用code={code[:10] if code else None}..., state={state[:8] if state else None}..., error={error}")
# 错误处理:用户拒绝授权
if error:
if state in wechat_qr_sessions:
wechat_qr_sessions[state]['status'] = 'auth_denied'
wechat_qr_sessions[state]['error'] = '用户拒绝授权'
print(f"❌ 用户拒绝授权: state={state}")
return redirect('/auth/signin?error=wechat_auth_denied')
# 错误处理
if error or not code or not state:
print(f"❌ [CALLBACK] 参数错误: error={error}, has_code={bool(code)}, has_state={bool(state)}")
# 参数验证
if not code or not state:
if state in wechat_qr_sessions:
wechat_qr_sessions[state]['status'] = 'auth_failed'
wechat_qr_sessions[state]['error'] = '授权参数缺失'
return redirect('/auth/signin?error=wechat_auth_failed')
# 验证state
if state not in wechat_qr_sessions:
print(f"❌ [CALLBACK] state 不在 wechat_qr_sessions 中: {state[:8]}...")
print(f" 当前 sessions: {list(wechat_qr_sessions.keys())}")
return redirect('/auth/signin?error=session_expired')
session_data = wechat_qr_sessions[state]
print(f"✅ [CALLBACK] 找到 session_data, mode={session_data.get('mode')}")
# 检查过期
if time.time() > session_data['expires']:
print(f"❌ [CALLBACK] session 已过期")
del wechat_qr_sessions[state]
return redirect('/auth/signin?error=session_expired')
try:
# 获取access_token
print(f"🔑 [CALLBACK] 开始获取 access_token...")
# 步骤1: 用户已扫码并授权(微信回调过来说明用户已完成扫码+授权)
session_data['status'] = 'scanned'
print(f"✅ 微信扫码回调: state={state}, code={code[:10]}...")
# 步骤2: 获取access_token
token_data = get_wechat_access_token(code)
if not token_data:
print(f"❌ [CALLBACK] 获取 access_token 失败")
session_data['status'] = 'auth_failed'
session_data['error'] = '获取访问令牌失败'
print(f"❌ 获取微信access_token失败: state={state}")
return redirect('/auth/signin?error=token_failed')
print(f"✅ [CALLBACK] 获取 access_token 成功, openid={token_data.get('openid', '')[:8]}...")
# 步骤3: Token获取成功标记为已授权
session_data['status'] = 'authorized'
print(f"✅ 微信授权成功: openid={token_data['openid']}")
# 获取用户信息
print(f"👤 [CALLBACK] 开始获取用户信息...")
# 步骤4: 获取用户信息
user_info = get_wechat_userinfo(token_data['access_token'], token_data['openid'])
if not user_info:
print(f"❌ [CALLBACK] 获取用户信息失败")
session_data['status'] = 'auth_failed'
session_data['error'] = '获取用户信息失败'
print(f"❌ 获取微信用户信息失败: openid={token_data['openid']}")
return redirect('/auth/signin?error=userinfo_failed')
print(f"✅ [CALLBACK] 获取用户信息成功, nickname={user_info.get('nickname', 'N/A')}")
# 查找或创建用户 / 或处理绑定
openid = token_data['openid']
unionid = user_info.get('unionid') or token_data.get('unionid')
@@ -2711,8 +2827,7 @@ def wechat_callback():
user = User.query.filter_by(wechat_open_id=openid).first()
if not user:
# 创建新用户(自动注册)
is_new_user = True
# 创建新用户
# 先清理微信昵称
raw_nickname = user_info.get('nickname', '微信用户')
# 创建临时用户实例以使用清理方法
@@ -2736,46 +2851,46 @@ def wechat_callback():
db.session.add(user)
db.session.commit()
# 更新 wechat_qr_sessions 状态,供前端轮询检测
print(f"🔍 [DEBUG] state={state}, state in wechat_qr_sessions: {state in wechat_qr_sessions}")
is_new_user = True
print(f"✅ 微信扫码自动创建新用户: {username}, openid: {openid}")
# 更新最后登录时间
user.update_last_seen()
# 设置session
session.permanent = True
session['user_id'] = user.id
session['username'] = user.username
session['logged_in'] = True
session['wechat_login'] = True # 标记是微信登录
# Flask-Login 登录
login_user(user, remember=True)
# 更新微信session状态供前端轮询检测
if state in wechat_qr_sessions:
session_item = wechat_qr_sessions[state]
mode = session_item.get('mode')
print(f"🔍 [DEBUG] session_item mode: {mode}, is_new_user: {is_new_user}")
# 不是绑定模式才更新为登录状态
if not mode:
new_status = 'register_success' if is_new_user else 'login_success'
session_item['status'] = new_status
session_item['user_info'] = {
'user_id': user.id,
'is_new_user': is_new_user
}
print(f"✅ [DEBUG] 更新 wechat_qr_sessions 状态: {new_status}, user_id: {user.id}")
else:
print(f"⚠️ [DEBUG] 跳过状态更新,因为 mode={mode}")
# 仅处理登录/注册流程,不处理绑定流程
if not session_item.get('mode'):
# 更新状态和用户信息
session_item['status'] = 'register_ready' if is_new_user else 'login_ready'
session_item['user_info'] = {'user_id': user.id}
print(f"✅ 微信扫码状态已更新: {session_item['status']}, user_id: {user.id}")
# 返回一个简单的成功页面(前端轮询会检测到状态变化)
return '''
<html>
<head><title>授权成功</title></head>
<body>
<h2>微信授权成功</h2>
<p>请返回原页面继续操作...</p>
<script>
// 尝试关闭窗口(如果是弹窗的话)
setTimeout(function() {
window.close();
}, 1000);
</script>
</body>
</html>
''', 200
# 直接跳转到首页
return redirect('/home')
except Exception as e:
print(f" [CALLBACK] 微信登录失败: {e}")
print(f"❌ 微信登录失败: {e}")
import traceback
print(f"❌ [CALLBACK] 错误堆栈:\n{traceback.format_exc()}")
traceback.print_exc()
db.session.rollback()
# 更新session状态为失败
if state in wechat_qr_sessions:
wechat_qr_sessions[state]['status'] = 'auth_failed'
wechat_qr_sessions[state]['error'] = str(e)
return redirect('/auth/signin?error=login_failed')
@@ -2789,16 +2904,16 @@ def login_with_wechat():
return jsonify({'success': False, 'error': 'session_id不能为空'}), 400
# 验证session
wechat_session = wechat_qr_sessions.get(session_id)
if not wechat_session:
session = wechat_qr_sessions.get(session_id)
if not session:
return jsonify({'success': False, 'error': '会话不存在或已过期'}), 400
# 检查session状态
if wechat_session['status'] not in ['login_success', 'register_success']:
if session['status'] not in ['login_ready', 'register_ready']:
return jsonify({'success': False, 'error': '会话状态无效'}), 400
# 检查是否有用户信息
user_info = wechat_session.get('user_info')
user_info = session.get('user_info')
if not user_info or not user_info.get('user_id'):
return jsonify({'success': False, 'error': '用户信息不完整'}), 400
@@ -2810,33 +2925,18 @@ def login_with_wechat():
# 更新最后登录时间
user.update_last_seen()
# 设置 Flask session
session.permanent = True
session['user_id'] = user.id
session['username'] = user.username
session['logged_in'] = True
session['wechat_login'] = True # 标记是微信登录
# Flask-Login 登录
login_user(user, remember=True)
# 判断是否为新用户
is_new_user = user_info.get('is_new_user', False)
# 清除 wechat_qr_sessions
# 清除session
del wechat_qr_sessions[session_id]
# 生成登录响应
response_data = {
'success': True,
'message': '注册成功' if is_new_user else '登录成功',
'isNewUser': is_new_user,
'message': '登录成功' if session['status'] == 'login_ready' else '注册并登录成功',
'user': {
'id': user.id,
'username': user.username,
'nickname': user.nickname or user.username,
'email': user.email,
'phone': user.phone,
'avatar_url': user.avatar_url,
'has_wechat': True,
'wechat_open_id': user.wechat_open_id,

View File

@@ -43,7 +43,6 @@
"match-sorter": "6.3.0",
"moment": "^2.29.1",
"nouislider": "15.0.0",
"posthog-js": "^1.281.0",
"react": "18.3.1",
"react-apexcharts": "^1.3.9",
"react-big-calendar": "^0.33.2",
@@ -93,14 +92,9 @@
"uuid": "^9.0.1"
},
"scripts": {
"prestart": "kill-port 3000",
"start": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.mock craco start",
"start:real": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.local craco start",
"start": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' craco start",
"start:mock": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.mock craco start",
"start:dev": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.development craco start",
"start:test": "concurrently \"python app_2.py\" \"npm run frontend:test\" --names \"backend,frontend\" --prefix-colors \"blue,green\"",
"frontend:test": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.test craco start",
"dev": "npm start",
"backend": "python app_2.py",
"build": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' craco build && gulp licenses",
"build:analyze": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' ANALYZE=true craco build",
"test": "craco test --env=jsdom",
@@ -110,14 +104,12 @@
"rollback": "bash scripts/rollback-from-local.sh",
"lint:check": "eslint . --ext=js,jsx; exit 0",
"lint:fix": "eslint . --ext=js,jsx --fix; exit 0",
"clean": "rm -rf node_modules/ package-lock.json",
"reinstall": "npm run clean && npm install"
"install:clean": "rm -rf node_modules/ && rm -rf package-lock.json && npm install && npm start"
},
"devDependencies": {
"@craco/craco": "^7.1.0",
"ajv": "^8.17.1",
"autoprefixer": "^10.4.21",
"concurrently": "^8.2.2",
"env-cmd": "^11.0.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.4.0",
@@ -126,7 +118,6 @@
"imagemin": "^9.0.1",
"imagemin-mozjpeg": "^10.0.0",
"imagemin-pngquant": "^10.0.0",
"kill-port": "^2.0.1",
"msw": "^2.11.5",
"postcss": "^8.5.6",
"prettier": "2.2.1",

View File

@@ -61,10 +61,6 @@ import NotificationTestTool from "components/NotificationTestTool";
import ScrollToTop from "components/ScrollToTop";
import { logger } from "utils/logger";
// PostHog Redux 集成
import { useDispatch } from 'react-redux';
import { initializePostHog } from "store/slices/posthogSlice";
/**
* ConnectionStatusBar 包装组件
* 需要在 NotificationProvider 内部使用,所以单独提取
@@ -112,13 +108,6 @@ function ConnectionStatusBarWrapper() {
function AppContent() {
const { colorMode } = useColorMode();
const dispatch = useDispatch();
// 🎯 PostHog Redux 初始化
useEffect(() => {
dispatch(initializePostHog());
logger.info('App', 'PostHog Redux 初始化已触发');
}, [dispatch]);
return (
<Box minH="100vh" bg={colorMode === 'dark' ? 'gray.800' : 'white'}>

View File

@@ -37,7 +37,6 @@ import VerificationCodeInput from './VerificationCodeInput';
import WechatRegister from './WechatRegister';
import { setCurrentUser } from '../../mocks/data/users';
import { logger } from '../../utils/logger';
import { useAuthEvents } from '../../hooks/useAuthEvents';
// 统一配置对象
const AUTH_CONFIG = {
@@ -87,12 +86,6 @@ export default function AuthFormContent() {
// 响应式布局配置
const isMobile = useBreakpointValue({ base: true, md: false });
// 事件追踪
const authEvents = useAuthEvents({
component: 'AuthFormContent',
isMobile: isMobile
});
const stackDirection = useBreakpointValue({ base: "column", md: "row" });
const stackSpacing = useBreakpointValue({ base: 4, md: 2 }); // ✅ 桌面端从32px减至8px更紧凑
@@ -114,16 +107,6 @@ export default function AuthFormContent() {
...prev,
[name]: value
}));
// 追踪用户开始填写手机号 (判断用户选择了手机登录方式)
if (name === 'phone' && value.length === 1 && !formData.phone) {
authEvents.trackPhoneLoginInitiated(value);
}
// 追踪验证码输入变化
if (name === 'verificationCode') {
authEvents.trackVerificationCodeInputChanged(value.length);
}
};
// 倒计时逻辑
@@ -160,11 +143,10 @@ export default function AuthFormContent() {
return;
}
if (!/^1[3-9]\d{9}$/.test(credential)) {
// 追踪手机号验证失败
authEvents.trackPhoneNumberValidated(credential, false, 'invalid_format');
authEvents.trackFormValidationError('phone', 'invalid_format', '请输入有效的手机号');
// 清理手机号格式字符(空格、横线、括号等)
const cleanedCredential = credential.replace(/[\s\-\(\)\+]/g, '');
if (!/^1[3-9]\d{9}$/.test(cleanedCredential)) {
toast({
title: "请输入有效的手机号",
status: "warning",
@@ -173,14 +155,11 @@ export default function AuthFormContent() {
return;
}
// 追踪手机号验证通过
authEvents.trackPhoneNumberValidated(credential, true);
try {
setSendingCode(true);
const requestData = {
credential: credential.trim(), // 添加 trim() 防止空格
credential: cleanedCredential, // 使用清理后的手机号
type: 'phone',
purpose: config.api.purpose
};
@@ -211,23 +190,15 @@ export default function AuthFormContent() {
}
if (response.ok && data.success) {
// 追踪验证码发送成功 (或重发)
const isResend = verificationCodeSent;
if (isResend) {
authEvents.trackVerificationCodeResent(credential, countdown > 0 ? 2 : 1);
} else {
authEvents.trackVerificationCodeSent(credential, config.api.purpose);
}
// ❌ 移除成功 toast静默处理
logger.info('AuthFormContent', '验证码发送成功', {
credential: credential.substring(0, 3) + '****' + credential.substring(7),
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7),
dev_code: data.dev_code
});
// ✅ 开发环境下在控制台显示验证码
if (data.dev_code) {
console.log(`%c✅ [验证码] ${credential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
console.log(`%c✅ [验证码] ${cleanedCredential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
}
setVerificationCodeSent(true);
@@ -236,15 +207,8 @@ export default function AuthFormContent() {
throw new Error(data.error || '发送验证码失败');
}
} catch (error) {
// 追踪验证码发送失败
authEvents.trackVerificationCodeSendFailed(credential, error);
authEvents.trackError('api', error.message || '发送验证码失败', {
endpoint: '/api/auth/send-verification-code',
phone_masked: credential.substring(0, 3) + '****' + credential.substring(7)
});
logger.api.error('POST', '/api/auth/send-verification-code', error, {
credential: credential.substring(0, 3) + '****' + credential.substring(7)
credential: cleanedCredential.substring(0, 3) + '****' + cleanedCredential.substring(7)
});
// ✅ 显示错误提示给用户
@@ -286,7 +250,10 @@ export default function AuthFormContent() {
return;
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
// 清理手机号格式字符(空格、横线、括号等)
const cleanedPhone = phone.replace(/[\s\-\(\)\+]/g, '');
if (!/^1[3-9]\d{9}$/.test(cleanedPhone)) {
toast({
title: "请输入有效的手机号",
status: "warning",
@@ -295,18 +262,15 @@ export default function AuthFormContent() {
return;
}
// 追踪验证码提交
authEvents.trackVerificationCodeSubmitted(phone);
// 构建请求体
const requestBody = {
credential: phone.trim(), // 添加 trim() 防止空格
credential: cleanedPhone, // 使用清理后的手机号
verification_code: verificationCode.trim(), // 添加 trim() 防止空格
login_type: 'phone',
};
logger.api.request('POST', '/api/auth/login-with-code', {
credential: phone.substring(0, 3) + '****' + phone.substring(7),
credential: cleanedPhone.substring(0, 3) + '****' + cleanedPhone.substring(7),
verification_code: verificationCode.substring(0, 2) + '****',
login_type: 'phone'
});
@@ -352,9 +316,6 @@ export default function AuthFormContent() {
// 更新session
await checkSession();
// 追踪登录成功并识别用户
authEvents.trackLoginSuccess(data.user, 'phone', data.isNewUser);
// ✅ 保留登录成功 toast关键操作提示
toast({
title: data.isNewUser ? '注册成功' : '登录成功',
@@ -374,8 +335,6 @@ export default function AuthFormContent() {
setTimeout(() => {
setCurrentPhone(phone);
setShowNicknamePrompt(true);
// 追踪昵称设置引导显示
authEvents.trackNicknamePromptShown(phone);
}, config.features.successDelay);
} else {
// 已有用户,直接登录成功
@@ -396,15 +355,6 @@ export default function AuthFormContent() {
}
} catch (error) {
const { phone, verificationCode } = formData;
// 追踪登录失败
const errorType = error.message.includes('网络') ? 'network' :
error.message.includes('服务器') ? 'api' : 'validation';
authEvents.trackLoginFailed('phone', errorType, error.message, {
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A',
has_verification_code: !!verificationCode
});
logger.error('AuthFormContent', 'handleSubmit', error, {
phone: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : 'N/A',
hasVerificationCode: !!verificationCode
@@ -432,9 +382,6 @@ export default function AuthFormContent() {
// 微信H5登录处理
const handleWechatH5Login = async () => {
// 追踪用户选择微信登录
authEvents.trackWechatLoginInitiated('icon_button');
try {
// 1. 构建回调URL
const redirectUrl = `${window.location.origin}/home/wechat-callback`;
@@ -455,19 +402,11 @@ export default function AuthFormContent() {
throw new Error('获取授权链接失败');
}
// 追踪微信H5跳转
authEvents.trackWechatH5Redirect();
// 4. 延迟跳转,让用户看到提示
setTimeout(() => {
window.location.href = response.auth_url;
}, 500);
} catch (error) {
// 追踪跳转失败
authEvents.trackError('api', error.message || '获取微信授权链接失败', {
context: 'wechat_h5_redirect'
});
logger.error('AuthFormContent', 'handleWechatH5Login', error);
toast({
title: "跳转失败",
@@ -479,17 +418,14 @@ export default function AuthFormContent() {
}
};
// 组件载时追踪页面浏览
// 组件载时清理
useEffect(() => {
isMountedRef.current = true;
// 追踪登录页面浏览
authEvents.trackLoginPageViewed();
return () => {
isMountedRef.current = false;
};
}, [authEvents]);
}, []);
return (
<>
@@ -549,7 +485,6 @@ export default function AuthFormContent() {
color="blue.500"
textDecoration="underline"
_hover={{ color: "blue.600" }}
onClick={authEvents.trackUserAgreementClicked}
>
用户协议
</ChakraLink>
@@ -562,7 +497,6 @@ export default function AuthFormContent() {
color="blue.500"
textDecoration="underline"
_hover={{ color: "blue.600" }}
onClick={authEvents.trackPrivacyPolicyClicked}
>
隐私政策
</ChakraLink>
@@ -590,30 +524,8 @@ export default function AuthFormContent() {
<AlertDialogHeader fontSize="lg" fontWeight="bold">完善个人信息</AlertDialogHeader>
<AlertDialogBody>您已成功注册是否前往个人资料设置昵称和其他信息</AlertDialogBody>
<AlertDialogFooter>
<Button
ref={cancelRef}
onClick={() => {
authEvents.trackNicknamePromptSkipped();
setShowNicknamePrompt(false);
handleLoginSuccess({ phone: currentPhone });
}}
>
稍后再说
</Button>
<Button
colorScheme="green"
onClick={() => {
authEvents.trackNicknamePromptAccepted();
setShowNicknamePrompt(false);
handleLoginSuccess({ phone: currentPhone });
setTimeout(() => {
navigate('/home/profile');
}, 300);
}}
ml={3}
>
去设置
</Button>
<Button ref={cancelRef} onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); }}>稍后再说</Button>
<Button colorScheme="green" onClick={() => { setShowNicknamePrompt(false); handleLoginSuccess({ phone: currentPhone }); setTimeout(() => { navigate('/home/profile'); }, 300); }} ml={3}>去设置</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>

View File

@@ -18,7 +18,6 @@ import { authService, WECHAT_STATUS, STATUS_MESSAGES } from "../../services/auth
import { useAuthModal } from "../../contexts/AuthModalContext";
import { useAuth } from "../../contexts/AuthContext";
import { logger } from "../../utils/logger";
import { useAuthEvents } from "../../hooks/useAuthEvents";
// 配置常量
const POLL_INTERVAL = 2000; // 轮询间隔2秒
@@ -36,6 +35,8 @@ const getStatusColor = (status) => {
case WECHAT_STATUS.EXPIRED: return "orange.600"; // ✅ 橙色文字
case WECHAT_STATUS.LOGIN_SUCCESS: return "green.600"; // ✅ 绿色文字
case WECHAT_STATUS.REGISTER_SUCCESS: return "green.600";
case WECHAT_STATUS.AUTH_DENIED: return "red.600"; // ✅ 红色文字
case WECHAT_STATUS.AUTH_FAILED: return "red.600"; // ✅ 红色文字
default: return "gray.600";
}
};
@@ -52,12 +53,6 @@ export default function WechatRegister() {
const { closeModal } = useAuthModal();
const { refreshSession } = useAuth();
// 事件追踪
const authEvents = useAuthEvents({
component: 'WechatRegister',
isMobile: false // WechatRegister 只在桌面端显示
});
// 状态管理
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
const [wechatSessionId, setWechatSessionId] = useState("");
@@ -126,20 +121,9 @@ export default function WechatRegister() {
*/
const handleLoginSuccess = useCallback(async (sessionId, status) => {
try {
logger.info('WechatRegister', '开始调用登录接口', { sessionId: sessionId.substring(0, 8) + '...', status });
const response = await authService.loginWithWechat(sessionId);
logger.info('WechatRegister', '登录接口返回', { success: response?.success, hasUser: !!response?.user });
if (response?.success) {
// 追踪微信登录成功
authEvents.trackLoginSuccess(
response.user,
'wechat',
response.isNewUser || false
);
// Session cookie 会自动管理,不需要手动存储
// 如果后端返回了 token可以选择性存储兼容旧方式
if (response.token) {
@@ -162,16 +146,10 @@ export default function WechatRegister() {
throw new Error(response?.error || '登录失败');
}
} catch (error) {
// 追踪微信登录失败
authEvents.trackLoginFailed('wechat', 'api', error.message || '登录失败', {
session_id: sessionId?.substring(0, 8) + '...',
status: status
});
logger.error('WechatRegister', 'handleLoginSuccess', error, { sessionId });
showError("登录失败", error.message || "请重试");
}
}, [showSuccess, showError, closeModal, refreshSession, authEvents]);
}, [showSuccess, showError, closeModal, refreshSession]);
/**
* 检查微信扫码状态
@@ -202,30 +180,13 @@ export default function WechatRegister() {
const { status } = response;
logger.debug('WechatRegister', '微信状态', { status });
logger.debug('WechatRegister', '检测到微信状态', {
sessionId: wechatSessionId.substring(0, 8) + '...',
status,
userInfo: response.user_info
});
// 组件卸载后不再更新状态
if (!isMountedRef.current) return;
// 追踪状态变化
if (wechatStatus !== status) {
authEvents.trackWechatStatusChanged(currentSessionId, wechatStatus, status);
// 特别追踪扫码事件
if (status === WECHAT_STATUS.SCANNED) {
authEvents.trackWechatQRScanned(currentSessionId);
}
}
setWechatStatus(status);
// 处理成功状态
if (status === WECHAT_STATUS.LOGIN_SUCCESS || status === WECHAT_STATUS.REGISTER_SUCCESS) {
logger.info('WechatRegister', '检测到登录成功状态,停止轮询', { status });
clearTimers(); // 停止轮询
sessionIdRef.current = null; // 清理 sessionId
@@ -233,9 +194,6 @@ export default function WechatRegister() {
}
// 处理过期状态
else if (status === WECHAT_STATUS.EXPIRED) {
// 追踪二维码过期
authEvents.trackWechatQRExpired(currentSessionId, QR_CODE_TIMEOUT / 1000);
clearTimers();
sessionIdRef.current = null; // 清理 sessionId
if (isMountedRef.current) {
@@ -248,6 +206,33 @@ export default function WechatRegister() {
});
}
}
// 处理用户拒绝授权
else if (status === WECHAT_STATUS.AUTH_DENIED) {
clearTimers();
if (isMountedRef.current) {
toast({
title: "授权已取消",
description: "您已取消微信授权登录",
status: "warning",
duration: 3000,
isClosable: true,
});
}
}
// 处理授权失败
else if (status === WECHAT_STATUS.AUTH_FAILED) {
clearTimers();
if (isMountedRef.current) {
const errorMsg = response.error || "授权过程出现错误";
toast({
title: "授权失败",
description: errorMsg,
status: "error",
duration: 5000,
isClosable: true,
});
}
}
} catch (error) {
logger.error('WechatRegister', 'checkWechatStatus', error, { sessionId: currentSessionId });
// 轮询过程中的错误不显示给用户,避免频繁提示
@@ -301,16 +286,6 @@ export default function WechatRegister() {
try {
setIsLoading(true);
// 追踪用户选择微信登录(首次或刷新)
const isRefresh = Boolean(wechatSessionId);
if (isRefresh) {
const oldSessionId = wechatSessionId;
authEvents.trackWechatLoginInitiated('qr_refresh');
// 稍后会在成功时追踪刷新事件
} else {
authEvents.trackWechatLoginInitiated('qr_area');
}
// 生产环境:调用真实 API
const response = await authService.getWechatQRCode();
@@ -326,13 +301,6 @@ export default function WechatRegister() {
throw new Error(response.message || '获取二维码失败');
}
// 追踪二维码显示 (首次或刷新)
if (isRefresh) {
authEvents.trackWechatQRRefreshed(wechatSessionId, response.data.session_id);
} else {
authEvents.trackWechatQRDisplayed(response.data.session_id, response.data.auth_url);
}
// 同时更新 ref 和 state确保轮询能立即读取到最新值
sessionIdRef.current = response.data.session_id;
setWechatAuthUrl(response.data.auth_url);
@@ -347,11 +315,6 @@ export default function WechatRegister() {
// 启动轮询检查扫码状态
startPolling();
} catch (error) {
// 追踪获取二维码失败
authEvents.trackError('api', error.message || '获取二维码失败', {
context: 'get_wechat_qrcode'
});
logger.error('WechatRegister', 'getWechatQRCode', error);
if (isMountedRef.current) {
showError("获取微信授权失败", error.message || "请稍后重试");
@@ -361,7 +324,7 @@ export default function WechatRegister() {
setIsLoading(false);
}
}
}, [startPolling, showError, wechatSessionId, authEvents]);
}, [startPolling, showError]);
/**
* 安全的按钮点击处理,确保所有错误都被捕获,防止被 ErrorBoundary 捕获

View File

@@ -51,7 +51,6 @@ import SubscriptionButton from '../Subscription/SubscriptionButton';
import SubscriptionModal from '../Subscription/SubscriptionModal';
import { CrownIcon, TooltipContent } from '../Subscription/CrownTooltip';
import InvestmentCalendar from '../../views/Community/components/InvestmentCalendar';
import { useNavigationEvents } from '../../hooks/useNavigationEvents';
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
const SecondaryNav = ({ showCompletenessAlert }) => {
@@ -62,9 +61,6 @@ const SecondaryNav = ({ showCompletenessAlert }) => {
// ⚠️ 必须在组件顶层调用所有Hooks不能在JSX中调用
const borderColorValue = useColorModeValue('gray.200', 'gray.600');
// 🎯 初始化导航埋点Hook
const navEvents = useNavigationEvents({ component: 'secondary_nav' });
// 定义二级导航结构
const secondaryNavConfig = {
'/community': {
@@ -166,11 +162,7 @@ const SecondaryNav = ({ showCompletenessAlert }) => {
) : (
<Button
key={index}
onClick={() => {
// 🎯 追踪侧边栏菜单点击
navEvents.trackSidebarMenuClicked(item.label, item.path, 2, false);
navigate(item.path);
}}
onClick={() => navigate(item.path)}
size="sm"
variant="ghost"
bg={isActive ? 'blue.50' : 'transparent'}
@@ -321,9 +313,6 @@ const NavItems = ({ isAuthenticated, user }) => {
// ⚠️ 必须在组件顶层调用所有Hooks不能在JSX中调用
const contactTextColor = useColorModeValue('gray.500', 'gray.300');
// 🎯 初始化导航埋点Hook
const navEvents = useNavigationEvents({ component: 'top_nav' });
// 辅助函数:判断导航项是否激活
const isActive = useCallback((paths) => {
return paths.some(path => location.pathname.includes(path));
@@ -348,11 +337,7 @@ const NavItems = ({ isAuthenticated, user }) => {
</MenuButton>
<MenuList minW="260px" p={2}>
<MenuItem
onClick={() => {
// 🎯 追踪菜单项点击
navEvents.trackMenuItemClicked('事件中心', 'dropdown', '/community');
navigate('/community');
}}
onClick={() => navigate('/community')}
borderRadius="md"
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
borderLeft={location.pathname.includes('/community') ? '3px solid' : 'none'}
@@ -368,11 +353,7 @@ const NavItems = ({ isAuthenticated, user }) => {
</Flex>
</MenuItem>
<MenuItem
onClick={() => {
// 🎯 追踪菜单项点击
navEvents.trackMenuItemClicked('概念中心', 'dropdown', '/concepts');
navigate('/concepts');
}}
onClick={() => navigate('/concepts')}
borderRadius="md"
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
borderLeft={location.pathname.includes('/concepts') ? '3px solid' : 'none'}
@@ -508,9 +489,6 @@ export default function HomeNavbar() {
const brandHover = useColorModeValue('blue.600', 'blue.300');
const toast = useToast();
// 🎯 初始化导航埋点Hook
const navEvents = useNavigationEvents({ component: 'main_navbar' });
// ⚡ 提取 userId 为独立变量,避免 user 对象引用变化导致无限循环
const userId = user?.id;
const prevUserIdRef = React.useRef(userId);
@@ -904,11 +882,7 @@ export default function HomeNavbar() {
color={brandText}
cursor="pointer"
_hover={{ color: brandHover }}
onClick={() => {
// 🎯 追踪Logo点击
navEvents.trackLogoClicked();
navigate('/home');
}}
onClick={() => navigate('/home')}
style={{ minWidth: isMobile ? '100px' : '140px' }}
noOfLines={1}
>
@@ -938,13 +912,7 @@ export default function HomeNavbar() {
<IconButton
aria-label="切换主题"
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
onClick={() => {
// 🎯 追踪主题切换
const fromTheme = colorMode;
const toTheme = colorMode === 'light' ? 'dark' : 'light';
navEvents.trackThemeChanged(fromTheme, toTheme);
toggleColorMode();
}}
onClick={toggleColorMode}
variant="ghost"
size="sm"
minW={{ base: '36px', md: '40px' }}

View File

@@ -1,83 +0,0 @@
// src/components/PostHogProvider.js
import React, { useEffect, useState } from 'react';
import { initPostHog } from '../lib/posthog';
import { usePageTracking } from '../hooks/usePageTracking';
/**
* PostHog Provider Component
* Initializes PostHog SDK and provides automatic page view tracking
*
* Usage:
* <PostHogProvider>
* <App />
* </PostHogProvider>
*/
export const PostHogProvider = ({ children }) => {
const [isInitialized, setIsInitialized] = useState(false);
// Initialize PostHog once when component mounts
useEffect(() => {
// Only run in browser
if (typeof window === 'undefined') return;
// Initialize PostHog
initPostHog();
setIsInitialized(true);
// Log initialization
if (process.env.NODE_ENV === 'development') {
console.log('✅ PostHogProvider initialized');
}
}, []);
// Automatically track page views
usePageTracking({
enabled: isInitialized,
getProperties: (location) => {
// Add custom properties based on route
const properties = {};
// Identify page type based on path
if (location.pathname === '/home' || location.pathname === '/home/') {
properties.page_type = 'landing';
} else if (location.pathname.startsWith('/home/center')) {
properties.page_type = 'dashboard';
} else if (location.pathname.startsWith('/auth/')) {
properties.page_type = 'auth';
} else if (location.pathname.startsWith('/community')) {
properties.page_type = 'feature';
properties.feature_name = 'community';
} else if (location.pathname.startsWith('/concepts')) {
properties.page_type = 'feature';
properties.feature_name = 'concepts';
} else if (location.pathname.startsWith('/stocks')) {
properties.page_type = 'feature';
properties.feature_name = 'stocks';
} else if (location.pathname.startsWith('/limit-analyse')) {
properties.page_type = 'feature';
properties.feature_name = 'limit_analyse';
} else if (location.pathname.startsWith('/trading-simulation')) {
properties.page_type = 'feature';
properties.feature_name = 'trading_simulation';
} else if (location.pathname.startsWith('/company')) {
properties.page_type = 'detail';
properties.content_type = 'company';
} else if (location.pathname.startsWith('/event-detail')) {
properties.page_type = 'detail';
properties.content_type = 'event';
}
return properties;
},
});
// Don't render children until PostHog is initialized
// This prevents tracking events before SDK is ready
if (!isInitialized) {
return children; // Or return a loading spinner
}
return <>{children}</>;
};
export default PostHogProvider;

View File

@@ -33,7 +33,6 @@ import {
import React, { useState, useEffect } from 'react';
import { logger } from '../../utils/logger';
import { useAuth } from '../../contexts/AuthContext';
import { useSubscriptionEvents } from '../../hooks/useSubscriptionEvents';
// Icons
import {
@@ -55,14 +54,6 @@ export default function SubscriptionContent() {
// Auth context
const { user } = useAuth();
// 🎯 初始化订阅埋点Hook传入当前订阅信息
const subscriptionEvents = useSubscriptionEvents({
currentSubscription: {
plan: user?.subscription_plan || 'free',
status: user?.subscription_status || 'none'
}
});
// Chakra color mode
const textColor = useColorModeValue('gray.700', 'white');
const borderColor = useColorModeValue('gray.200', 'gray.600');
@@ -170,13 +161,6 @@ export default function SubscriptionContent() {
return;
}
// 🎯 追踪定价方案选择
subscriptionEvents.trackPricingPlanSelected(
plan.name,
selectedCycle,
selectedCycle === 'monthly' ? plan.monthly_price : plan.yearly_price
);
setSelectedPlan(plan);
onPaymentModalOpen();
};
@@ -186,17 +170,6 @@ export default function SubscriptionContent() {
setLoading(true);
try {
const price = selectedCycle === 'monthly' ? selectedPlan.monthly_price : selectedPlan.yearly_price;
// 🎯 追踪支付发起
subscriptionEvents.trackPaymentInitiated({
planName: selectedPlan.name,
paymentMethod: 'wechat_pay',
amount: price,
billingCycle: selectedCycle,
orderId: null // Will be set after order creation
});
const response = await fetch('/api/payment/create-order', {
method: 'POST',
headers: {
@@ -231,13 +204,6 @@ export default function SubscriptionContent() {
throw new Error('网络错误');
}
} catch (error) {
// 🎯 追踪支付失败
subscriptionEvents.trackPaymentFailed({
planName: selectedPlan.name,
paymentMethod: 'wechat_pay',
amount: selectedCycle === 'monthly' ? selectedPlan.monthly_price : selectedPlan.yearly_price
}, error.message);
toast({
title: '创建订单失败',
description: error.message,
@@ -285,26 +251,6 @@ export default function SubscriptionContent() {
setAutoCheckInterval(null);
logger.info('SubscriptionContent', '自动检测到支付成功', { orderId });
// 🎯 追踪支付成功
subscriptionEvents.trackPaymentSuccessful({
planName: selectedPlan?.name,
paymentMethod: 'wechat_pay',
amount: paymentOrder?.amount,
billingCycle: selectedCycle,
orderId: orderId,
transactionId: data.transaction_id
});
// 🎯 追踪订阅创建
subscriptionEvents.trackSubscriptionCreated({
plan: selectedPlan?.name,
billingCycle: selectedCycle,
amount: paymentOrder?.amount,
startDate: new Date().toISOString(),
endDate: null // Will be calculated by backend
});
toast({
title: '支付成功!',
description: '订阅已激活,正在跳转...',

View File

@@ -1,463 +0,0 @@
// src/hooks/useAuthEvents.js
// 认证事件追踪 Hook
import { useCallback } from 'react';
import { usePostHogTrack, usePostHogUser } from './usePostHogRedux';
import { ACTIVATION_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 认证事件追踪 Hook
* 提供登录/注册流程中所有关键节点的事件追踪功能
*
* 用法示例:
*
* ```jsx
* import { useAuthEvents } from 'hooks/useAuthEvents';
*
* function AuthComponent() {
* const {
* trackLoginPageViewed,
* trackPhoneLoginInitiated,
* trackVerificationCodeSent,
* trackLoginSuccess
* } = useAuthEvents();
*
* useEffect(() => {
* trackLoginPageViewed();
* }, [trackLoginPageViewed]);
*
* const handlePhoneFocus = () => {
* trackPhoneLoginInitiated(formData.phone);
* };
* }
* ```
*
* @param {Object} options - 配置选项
* @param {string} options.component - 组件名称 ('AuthFormContent' | 'WechatRegister')
* @param {boolean} options.isMobile - 是否为移动设备
* @returns {Object} 事件追踪处理函数集合
*/
export const useAuthEvents = ({ component = 'AuthFormContent', isMobile = false } = {}) => {
const { track } = usePostHogTrack();
const { identify } = usePostHogUser();
// 通用事件属性
const getBaseProperties = useCallback(() => ({
component,
device: isMobile ? 'mobile' : 'desktop',
timestamp: new Date().toISOString(),
}), [component, isMobile]);
// ==================== 页面浏览事件 ====================
/**
* 追踪登录页面浏览
*/
const trackLoginPageViewed = useCallback(() => {
track(ACTIVATION_EVENTS.LOGIN_PAGE_VIEWED, getBaseProperties());
logger.debug('useAuthEvents', '📄 Login Page Viewed', { component });
}, [track, getBaseProperties, component]);
// ==================== 登录方式选择 ====================
/**
* 追踪用户开始手机号登录
* @param {string} phone - 手机号(可选,用于判断是否已填写)
*/
const trackPhoneLoginInitiated = useCallback((phone = '') => {
track(ACTIVATION_EVENTS.PHONE_LOGIN_INITIATED, {
...getBaseProperties(),
has_phone: Boolean(phone),
});
logger.debug('useAuthEvents', '📱 Phone Login Initiated', { hasPhone: Boolean(phone) });
}, [track, getBaseProperties]);
/**
* 追踪用户选择微信登录
* @param {string} source - 触发来源 ('qr_area' | 'icon_button' | 'h5_redirect')
*/
const trackWechatLoginInitiated = useCallback((source = 'qr_area') => {
track(ACTIVATION_EVENTS.WECHAT_LOGIN_INITIATED, {
...getBaseProperties(),
source,
});
logger.debug('useAuthEvents', '💬 WeChat Login Initiated', { source });
}, [track, getBaseProperties]);
// ==================== 手机验证码流程 ====================
/**
* 追踪验证码发送成功
* @param {string} phone - 手机号
* @param {string} purpose - 发送目的 ('login' | 'register')
*/
const trackVerificationCodeSent = useCallback((phone, purpose = 'login') => {
track(ACTIVATION_EVENTS.VERIFICATION_CODE_SENT, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
purpose,
});
logger.debug('useAuthEvents', '✉️ Verification Code Sent', { phone: phone?.substring(0, 3) + '****', purpose });
}, [track, getBaseProperties]);
/**
* 追踪验证码发送失败
* @param {string} phone - 手机号
* @param {Error|string} error - 错误对象或错误消息
*/
const trackVerificationCodeSendFailed = useCallback((phone, error) => {
const errorMessage = typeof error === 'string' ? error : error?.message || 'Unknown error';
track(ACTIVATION_EVENTS.VERIFICATION_CODE_SEND_FAILED, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
error_message: errorMessage,
error_type: 'send_code_failed',
});
logger.debug('useAuthEvents', '❌ Verification Code Send Failed', { error: errorMessage });
}, [track, getBaseProperties]);
/**
* 追踪用户输入验证码
* @param {number} codeLength - 当前输入的验证码长度
*/
const trackVerificationCodeInputChanged = useCallback((codeLength) => {
track(ACTIVATION_EVENTS.VERIFICATION_CODE_INPUT_CHANGED, {
...getBaseProperties(),
code_length: codeLength,
is_complete: codeLength >= 6,
});
logger.debug('useAuthEvents', '⌨️ Verification Code Input Changed', { codeLength });
}, [track, getBaseProperties]);
/**
* 追踪重新发送验证码
* @param {string} phone - 手机号
* @param {number} attemptCount - 第几次重发(可选)
*/
const trackVerificationCodeResent = useCallback((phone, attemptCount = 1) => {
track(ACTIVATION_EVENTS.VERIFICATION_CODE_RESENT, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
attempt_count: attemptCount,
});
logger.debug('useAuthEvents', '🔄 Verification Code Resent', { attempt: attemptCount });
}, [track, getBaseProperties]);
/**
* 追踪手机号验证结果
* @param {string} phone - 手机号
* @param {boolean} isValid - 是否有效
* @param {string} errorType - 错误类型(可选)
*/
const trackPhoneNumberValidated = useCallback((phone, isValid, errorType = '') => {
track(ACTIVATION_EVENTS.PHONE_NUMBER_VALIDATED, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
is_valid: isValid,
error_type: errorType,
});
logger.debug('useAuthEvents', '✓ Phone Number Validated', { isValid, errorType });
}, [track, getBaseProperties]);
/**
* 追踪验证码提交
* @param {string} phone - 手机号
*/
const trackVerificationCodeSubmitted = useCallback((phone) => {
track(ACTIVATION_EVENTS.VERIFICATION_CODE_SUBMITTED, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
});
logger.debug('useAuthEvents', '📤 Verification Code Submitted');
}, [track, getBaseProperties]);
// ==================== 微信登录流程 ====================
/**
* 追踪微信二维码显示
* @param {string} sessionId - 会话ID
* @param {string} authUrl - 授权URL
*/
const trackWechatQRDisplayed = useCallback((sessionId, authUrl = '') => {
track(ACTIVATION_EVENTS.WECHAT_QR_DISPLAYED, {
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
has_auth_url: Boolean(authUrl),
});
logger.debug('useAuthEvents', '🔲 WeChat QR Code Displayed', { sessionId: sessionId?.substring(0, 8) });
}, [track, getBaseProperties]);
/**
* 追踪微信二维码被扫描
* @param {string} sessionId - 会话ID
*/
const trackWechatQRScanned = useCallback((sessionId) => {
track(ACTIVATION_EVENTS.WECHAT_QR_SCANNED, {
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
});
logger.debug('useAuthEvents', '📱 WeChat QR Code Scanned', { sessionId: sessionId?.substring(0, 8) });
}, [track, getBaseProperties]);
/**
* 追踪微信二维码过期
* @param {string} sessionId - 会话ID
* @param {number} timeElapsed - 经过时间(秒)
*/
const trackWechatQRExpired = useCallback((sessionId, timeElapsed = 0) => {
track(ACTIVATION_EVENTS.WECHAT_QR_EXPIRED, {
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
time_elapsed: timeElapsed,
});
logger.debug('useAuthEvents', '⏰ WeChat QR Code Expired', { sessionId: sessionId?.substring(0, 8), timeElapsed });
}, [track, getBaseProperties]);
/**
* 追踪刷新微信二维码
* @param {string} oldSessionId - 旧会话ID
* @param {string} newSessionId - 新会话ID
*/
const trackWechatQRRefreshed = useCallback((oldSessionId, newSessionId) => {
track(ACTIVATION_EVENTS.WECHAT_QR_REFRESHED, {
...getBaseProperties(),
old_session_id: oldSessionId?.substring(0, 8) + '...',
new_session_id: newSessionId?.substring(0, 8) + '...',
});
logger.debug('useAuthEvents', '🔄 WeChat QR Code Refreshed');
}, [track, getBaseProperties]);
/**
* 追踪微信登录状态变化
* @param {string} sessionId - 会话ID
* @param {string} oldStatus - 旧状态
* @param {string} newStatus - 新状态
*/
const trackWechatStatusChanged = useCallback((sessionId, oldStatus, newStatus) => {
track(ACTIVATION_EVENTS.WECHAT_STATUS_CHANGED, {
...getBaseProperties(),
session_id: sessionId?.substring(0, 8) + '...',
old_status: oldStatus,
new_status: newStatus,
});
logger.debug('useAuthEvents', '🔄 WeChat Status Changed', { oldStatus, newStatus });
}, [track, getBaseProperties]);
/**
* 追踪移动端跳转微信H5授权
*/
const trackWechatH5Redirect = useCallback(() => {
track(ACTIVATION_EVENTS.WECHAT_H5_REDIRECT, getBaseProperties());
logger.debug('useAuthEvents', '🔗 WeChat H5 Redirect');
}, [track, getBaseProperties]);
// ==================== 登录/注册结果 ====================
/**
* 追踪登录成功并识别用户
* @param {Object} user - 用户对象
* @param {string} loginMethod - 登录方式 ('wechat' | 'phone')
* @param {boolean} isNewUser - 是否为新注册用户
*/
const trackLoginSuccess = useCallback((user, loginMethod, isNewUser = false) => {
// 追踪登录成功事件
const eventName = isNewUser ? ACTIVATION_EVENTS.USER_SIGNED_UP : ACTIVATION_EVENTS.USER_LOGGED_IN;
track(eventName, {
...getBaseProperties(),
user_id: user.id,
login_method: loginMethod,
is_new_user: isNewUser,
has_nickname: Boolean(user.nickname),
has_email: Boolean(user.email),
has_wechat: Boolean(user.wechat_open_id),
});
// 识别用户(关联 PostHog 用户)
identify(user.id.toString(), {
phone: user.phone,
username: user.username,
nickname: user.nickname,
email: user.email,
login_method: loginMethod,
is_new_user: isNewUser,
registration_date: user.created_at,
last_login: new Date().toISOString(),
has_wechat: Boolean(user.wechat_open_id),
wechat_open_id: user.wechat_open_id,
wechat_union_id: user.wechat_union_id,
});
logger.debug('useAuthEvents', `${isNewUser ? 'User Signed Up' : 'User Logged In'}`, {
userId: user.id,
method: loginMethod,
isNewUser,
});
}, [track, identify, getBaseProperties]);
/**
* 追踪登录失败
* @param {string} loginMethod - 登录方式 ('wechat' | 'phone')
* @param {string} errorType - 错误类型
* @param {string} errorMessage - 错误消息
* @param {Object} context - 额外上下文信息
*/
const trackLoginFailed = useCallback((loginMethod, errorType, errorMessage, context = {}) => {
track(ACTIVATION_EVENTS.LOGIN_FAILED, {
...getBaseProperties(),
login_method: loginMethod,
error_type: errorType,
error_message: errorMessage,
...context,
});
logger.debug('useAuthEvents', '❌ Login Failed', { method: loginMethod, errorType, errorMessage });
}, [track, getBaseProperties]);
// ==================== 用户行为细节 ====================
/**
* 追踪表单字段聚焦
* @param {string} fieldName - 字段名称 ('phone' | 'verificationCode')
*/
const trackFormFocused = useCallback((fieldName) => {
track(ACTIVATION_EVENTS.AUTH_FORM_FOCUSED, {
...getBaseProperties(),
field_name: fieldName,
});
logger.debug('useAuthEvents', '🎯 Form Field Focused', { fieldName });
}, [track, getBaseProperties]);
/**
* 追踪表单验证错误
* @param {string} fieldName - 字段名称
* @param {string} errorType - 错误类型
* @param {string} errorMessage - 错误消息
*/
const trackFormValidationError = useCallback((fieldName, errorType, errorMessage) => {
track(ACTIVATION_EVENTS.AUTH_FORM_VALIDATION_ERROR, {
...getBaseProperties(),
field_name: fieldName,
error_type: errorType,
error_message: errorMessage,
});
logger.debug('useAuthEvents', '⚠️ Form Validation Error', { fieldName, errorType });
}, [track, getBaseProperties]);
/**
* 追踪昵称设置引导弹窗显示
* @param {string} phone - 手机号
*/
const trackNicknamePromptShown = useCallback((phone) => {
track(ACTIVATION_EVENTS.NICKNAME_PROMPT_SHOWN, {
...getBaseProperties(),
phone_masked: phone ? phone.substring(0, 3) + '****' + phone.substring(7) : '',
});
logger.debug('useAuthEvents', '💬 Nickname Prompt Shown');
}, [track, getBaseProperties]);
/**
* 追踪用户接受设置昵称
*/
const trackNicknamePromptAccepted = useCallback(() => {
track(ACTIVATION_EVENTS.NICKNAME_PROMPT_ACCEPTED, getBaseProperties());
logger.debug('useAuthEvents', '✅ Nickname Prompt Accepted');
}, [track, getBaseProperties]);
/**
* 追踪用户跳过设置昵称
*/
const trackNicknamePromptSkipped = useCallback(() => {
track(ACTIVATION_EVENTS.NICKNAME_PROMPT_SKIPPED, getBaseProperties());
logger.debug('useAuthEvents', '⏭️ Nickname Prompt Skipped');
}, [track, getBaseProperties]);
/**
* 追踪用户点击用户协议链接
*/
const trackUserAgreementClicked = useCallback(() => {
track(ACTIVATION_EVENTS.USER_AGREEMENT_LINK_CLICKED, getBaseProperties());
logger.debug('useAuthEvents', '📄 User Agreement Link Clicked');
}, [track, getBaseProperties]);
/**
* 追踪用户点击隐私政策链接
*/
const trackPrivacyPolicyClicked = useCallback(() => {
track(ACTIVATION_EVENTS.PRIVACY_POLICY_LINK_CLICKED, getBaseProperties());
logger.debug('useAuthEvents', '📄 Privacy Policy Link Clicked');
}, [track, getBaseProperties]);
// ==================== 错误追踪 ====================
/**
* 追踪通用错误
* @param {string} errorType - 错误类型 ('network' | 'api' | 'validation' | 'session')
* @param {string} errorMessage - 错误消息
* @param {Object} context - 错误上下文
*/
const trackError = useCallback((errorType, errorMessage, context = {}) => {
const eventMap = {
network: ACTIVATION_EVENTS.NETWORK_ERROR_OCCURRED,
api: ACTIVATION_EVENTS.API_ERROR_OCCURRED,
session: ACTIVATION_EVENTS.SESSION_EXPIRED,
default: ACTIVATION_EVENTS.LOGIN_ERROR_OCCURRED,
};
const eventName = eventMap[errorType] || eventMap.default;
track(eventName, {
...getBaseProperties(),
error_type: errorType,
error_message: errorMessage,
...context,
});
logger.error('useAuthEvents', `${errorType} Error`, { errorMessage, context });
}, [track, getBaseProperties]);
// ==================== 返回接口 ====================
return {
// 页面浏览
trackLoginPageViewed,
// 登录方式选择
trackPhoneLoginInitiated,
trackWechatLoginInitiated,
// 手机验证码流程
trackVerificationCodeSent,
trackVerificationCodeSendFailed,
trackVerificationCodeInputChanged,
trackVerificationCodeResent,
trackPhoneNumberValidated,
trackVerificationCodeSubmitted,
// 微信登录流程
trackWechatQRDisplayed,
trackWechatQRScanned,
trackWechatQRExpired,
trackWechatQRRefreshed,
trackWechatStatusChanged,
trackWechatH5Redirect,
// 登录/注册结果
trackLoginSuccess,
trackLoginFailed,
// 用户行为
trackFormFocused,
trackFormValidationError,
trackNicknamePromptShown,
trackNicknamePromptAccepted,
trackNicknamePromptSkipped,
trackUserAgreementClicked,
trackPrivacyPolicyClicked,
// 错误追踪
trackError,
};
};
export default useAuthEvents;

View File

@@ -1,325 +0,0 @@
// src/hooks/useDashboardEvents.js
// 个人中心Dashboard/Center事件追踪 Hook
import { useCallback, useEffect } from 'react';
import { usePostHogTrack } from './usePostHogRedux';
import { RETENTION_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 个人中心事件追踪 Hook
* @param {Object} options - 配置选项
* @param {string} options.pageType - 页面类型 ('center' | 'profile' | 'settings')
* @param {Function} options.navigate - 路由导航函数
* @returns {Object} 事件追踪处理函数集合
*/
export const useDashboardEvents = ({ pageType = 'center', navigate } = {}) => {
const { track } = usePostHogTrack();
// 🎯 页面浏览事件 - 页面加载时触发
useEffect(() => {
const eventMap = {
'center': RETENTION_EVENTS.DASHBOARD_CENTER_VIEWED,
'profile': RETENTION_EVENTS.PROFILE_PAGE_VIEWED,
'settings': RETENTION_EVENTS.SETTINGS_PAGE_VIEWED,
};
const eventName = eventMap[pageType] || RETENTION_EVENTS.DASHBOARD_VIEWED;
track(eventName, {
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', `📊 Dashboard Page Viewed: ${pageType}`);
}, [track, pageType]);
/**
* 追踪功能卡片点击
* @param {string} cardName - 卡片名称 ('watchlist' | 'following_events' | 'comments' | 'subscription')
* @param {Object} cardData - 卡片数据
*/
const trackFunctionCardClicked = useCallback((cardName, cardData = {}) => {
if (!cardName) {
logger.warn('useDashboardEvents', 'Card name is required');
return;
}
track(RETENTION_EVENTS.FUNCTION_CARD_CLICKED, {
card_name: cardName,
data_count: cardData.count || 0,
has_data: Boolean(cardData.count && cardData.count > 0),
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '🎴 Function Card Clicked', {
cardName,
count: cardData.count,
});
}, [track]);
/**
* 追踪自选股列表查看
* @param {number} stockCount - 自选股数量
* @param {boolean} hasRealtime - 是否有实时行情
*/
const trackWatchlistViewed = useCallback((stockCount = 0, hasRealtime = false) => {
track('Watchlist Viewed', {
stock_count: stockCount,
has_realtime: hasRealtime,
is_empty: stockCount === 0,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '⭐ Watchlist Viewed', {
stockCount,
hasRealtime,
});
}, [track]);
/**
* 追踪自选股点击
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
* @param {number} position - 在列表中的位置
*/
const trackWatchlistStockClicked = useCallback((stock, position = 0) => {
if (!stock || !stock.code) {
logger.warn('useDashboardEvents', 'Stock object is required');
return;
}
track(RETENTION_EVENTS.STOCK_CLICKED, {
stock_code: stock.code,
stock_name: stock.name || '',
source: 'watchlist',
position,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '🎯 Watchlist Stock Clicked', {
stockCode: stock.code,
position,
});
}, [track]);
/**
* 追踪自选股添加
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
* @param {string} source - 来源 ('search' | 'stock_detail' | 'manual')
*/
const trackWatchlistStockAdded = useCallback((stock, source = 'manual') => {
if (!stock || !stock.code) {
logger.warn('useDashboardEvents', 'Stock object is required');
return;
}
track('Watchlist Stock Added', {
stock_code: stock.code,
stock_name: stock.name || '',
source,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', ' Watchlist Stock Added', {
stockCode: stock.code,
source,
});
}, [track]);
/**
* 追踪自选股移除
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
*/
const trackWatchlistStockRemoved = useCallback((stock) => {
if (!stock || !stock.code) {
logger.warn('useDashboardEvents', 'Stock object is required');
return;
}
track('Watchlist Stock Removed', {
stock_code: stock.code,
stock_name: stock.name || '',
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', ' Watchlist Stock Removed', {
stockCode: stock.code,
});
}, [track]);
/**
* 追踪关注的事件列表查看
* @param {number} eventCount - 关注的事件数量
*/
const trackFollowingEventsViewed = useCallback((eventCount = 0) => {
track('Following Events Viewed', {
event_count: eventCount,
is_empty: eventCount === 0,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '📌 Following Events Viewed', {
eventCount,
});
}, [track]);
/**
* 追踪关注的事件点击
* @param {Object} event - 事件对象
* @param {number} event.id - 事件ID
* @param {string} event.title - 事件标题
* @param {number} position - 在列表中的位置
*/
const trackFollowingEventClicked = useCallback((event, position = 0) => {
if (!event || !event.id) {
logger.warn('useDashboardEvents', 'Event object is required');
return;
}
track(RETENTION_EVENTS.NEWS_ARTICLE_CLICKED, {
news_id: event.id,
news_title: event.title || '',
source: 'following_events',
position,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '📰 Following Event Clicked', {
eventId: event.id,
position,
});
}, [track]);
/**
* 追踪事件评论列表查看
* @param {number} commentCount - 评论数量
*/
const trackCommentsViewed = useCallback((commentCount = 0) => {
track('Event Comments Viewed', {
comment_count: commentCount,
is_empty: commentCount === 0,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '💬 Comments Viewed', {
commentCount,
});
}, [track]);
/**
* 追踪订阅信息查看
* @param {Object} subscription - 订阅信息
* @param {string} subscription.plan - 订阅计划 ('free' | 'pro' | 'enterprise')
* @param {string} subscription.status - 订阅状态 ('active' | 'expired' | 'cancelled')
*/
const trackSubscriptionViewed = useCallback((subscription = {}) => {
track(RETENTION_EVENTS.SUBSCRIPTION_PAGE_VIEWED, {
subscription_plan: subscription.plan || 'free',
subscription_status: subscription.status || 'unknown',
is_paid_user: subscription.plan !== 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '💳 Subscription Viewed', {
plan: subscription.plan,
status: subscription.status,
});
}, [track]);
/**
* 追踪升级按钮点击
* @param {string} currentPlan - 当前计划
* @param {string} targetPlan - 目标计划
* @param {string} source - 来源位置
*/
const trackUpgradePlanClicked = useCallback((currentPlan = 'free', targetPlan = 'pro', source = 'dashboard') => {
track(RETENTION_EVENTS.UPGRADE_PLAN_CLICKED, {
current_plan: currentPlan,
target_plan: targetPlan,
source,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '⬆️ Upgrade Plan Clicked', {
currentPlan,
targetPlan,
source,
});
}, [track]);
/**
* 追踪个人资料更新
* @param {Array<string>} updatedFields - 更新的字段列表
*/
const trackProfileUpdated = useCallback((updatedFields = []) => {
track(RETENTION_EVENTS.PROFILE_UPDATED, {
updated_fields: updatedFields,
field_count: updatedFields.length,
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '✏️ Profile Updated', {
updatedFields,
});
}, [track]);
/**
* 追踪设置更改
* @param {string} settingName - 设置名称
* @param {any} oldValue - 旧值
* @param {any} newValue - 新值
*/
const trackSettingChanged = useCallback((settingName, oldValue, newValue) => {
if (!settingName) {
logger.warn('useDashboardEvents', 'Setting name is required');
return;
}
track(RETENTION_EVENTS.SETTINGS_CHANGED, {
setting_name: settingName,
old_value: String(oldValue),
new_value: String(newValue),
timestamp: new Date().toISOString(),
});
logger.debug('useDashboardEvents', '⚙️ Setting Changed', {
settingName,
oldValue,
newValue,
});
}, [track]);
return {
// 功能卡片事件
trackFunctionCardClicked,
// 自选股相关事件
trackWatchlistViewed,
trackWatchlistStockClicked,
trackWatchlistStockAdded,
trackWatchlistStockRemoved,
// 关注事件相关
trackFollowingEventsViewed,
trackFollowingEventClicked,
// 评论相关
trackCommentsViewed,
// 订阅相关
trackSubscriptionViewed,
trackUpgradePlanClicked,
// 个人资料和设置
trackProfileUpdated,
trackSettingChanged,
};
};
export default useDashboardEvents;

View File

@@ -1,293 +0,0 @@
// src/hooks/useNavigationEvents.js
// 导航和菜单事件追踪 Hook
import { useCallback } from 'react';
import { usePostHogTrack } from './usePostHogRedux';
import { RETENTION_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 导航事件追踪 Hook
* @param {Object} options - 配置选项
* @param {string} options.component - 组件名称 ('top_nav' | 'sidebar' | 'breadcrumb' | 'footer')
* @returns {Object} 事件追踪处理函数集合
*/
export const useNavigationEvents = ({ component = 'navigation' } = {}) => {
const { track } = usePostHogTrack();
/**
* 追踪顶部导航点击
* @param {string} itemName - 导航项名称
* @param {string} path - 导航目标路径
* @param {string} category - 导航分类 ('main' | 'user' | 'utility')
*/
const trackTopNavClicked = useCallback((itemName, path = '', category = 'main') => {
if (!itemName) {
logger.warn('useNavigationEvents', 'trackTopNavClicked: itemName is required');
return;
}
track(RETENTION_EVENTS.TOP_NAV_CLICKED, {
item_name: itemName,
path,
category,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '🔝 Top Navigation Clicked', {
itemName,
path,
category,
});
}, [track, component]);
/**
* 追踪侧边栏菜单点击
* @param {string} itemName - 菜单项名称
* @param {string} path - 目标路径
* @param {number} level - 菜单层级 (1=主菜单, 2=子菜单)
* @param {boolean} isExpanded - 是否展开状态
*/
const trackSidebarMenuClicked = useCallback((itemName, path = '', level = 1, isExpanded = false) => {
if (!itemName) {
logger.warn('useNavigationEvents', 'trackSidebarMenuClicked: itemName is required');
return;
}
track(RETENTION_EVENTS.SIDEBAR_MENU_CLICKED, {
item_name: itemName,
path,
level,
is_expanded: isExpanded,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '📂 Sidebar Menu Clicked', {
itemName,
path,
level,
isExpanded,
});
}, [track, component]);
/**
* 追踪通用菜单项点击
* @param {string} itemName - 菜单项名称
* @param {string} menuType - 菜单类型 ('dropdown' | 'context' | 'tab')
* @param {string} path - 目标路径
*/
const trackMenuItemClicked = useCallback((itemName, menuType = 'dropdown', path = '') => {
if (!itemName) {
logger.warn('useNavigationEvents', 'trackMenuItemClicked: itemName is required');
return;
}
track(RETENTION_EVENTS.MENU_ITEM_CLICKED, {
item_name: itemName,
menu_type: menuType,
path,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '📋 Menu Item Clicked', {
itemName,
menuType,
path,
});
}, [track, component]);
/**
* 追踪面包屑导航点击
* @param {string} itemName - 面包屑项名称
* @param {string} path - 目标路径
* @param {number} position - 在面包屑中的位置
* @param {number} totalItems - 面包屑总项数
*/
const trackBreadcrumbClicked = useCallback((itemName, path = '', position = 0, totalItems = 0) => {
if (!itemName) {
logger.warn('useNavigationEvents', 'trackBreadcrumbClicked: itemName is required');
return;
}
track(RETENTION_EVENTS.BREADCRUMB_CLICKED, {
item_name: itemName,
path,
position,
total_items: totalItems,
is_last: position === totalItems - 1,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '🍞 Breadcrumb Clicked', {
itemName,
position,
totalItems,
});
}, [track, component]);
/**
* 追踪Logo点击返回首页
*/
const trackLogoClicked = useCallback(() => {
track('Logo Clicked', {
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '🏠 Logo Clicked');
}, [track, component]);
/**
* 追踪用户菜单展开
* @param {Object} user - 用户对象
* @param {number} menuItemCount - 菜单项数量
*/
const trackUserMenuOpened = useCallback((user = {}, menuItemCount = 0) => {
track('User Menu Opened', {
user_id: user.id || null,
menu_item_count: menuItemCount,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '👤 User Menu Opened', {
userId: user.id,
menuItemCount,
});
}, [track, component]);
/**
* 追踪通知中心打开
* @param {number} unreadCount - 未读通知数量
*/
const trackNotificationCenterOpened = useCallback((unreadCount = 0) => {
track('Notification Center Opened', {
unread_count: unreadCount,
has_unread: unreadCount > 0,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '🔔 Notification Center Opened', {
unreadCount,
});
}, [track, component]);
/**
* 追踪语言切换
* @param {string} fromLanguage - 原语言
* @param {string} toLanguage - 目标语言
*/
const trackLanguageChanged = useCallback((fromLanguage, toLanguage) => {
if (!fromLanguage || !toLanguage) {
logger.warn('useNavigationEvents', 'trackLanguageChanged: both languages are required');
return;
}
track('Language Changed', {
from_language: fromLanguage,
to_language: toLanguage,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '🌐 Language Changed', {
fromLanguage,
toLanguage,
});
}, [track, component]);
/**
* 追踪主题切换(深色/浅色模式)
* @param {string} fromTheme - 原主题
* @param {string} toTheme - 目标主题
*/
const trackThemeChanged = useCallback((fromTheme, toTheme) => {
if (!fromTheme || !toTheme) {
logger.warn('useNavigationEvents', 'trackThemeChanged: both themes are required');
return;
}
track('Theme Changed', {
from_theme: fromTheme,
to_theme: toTheme,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '🎨 Theme Changed', {
fromTheme,
toTheme,
});
}, [track, component]);
/**
* 追踪快捷键使用
* @param {string} shortcut - 快捷键组合 (如 'Ctrl+K', 'Cmd+/')
* @param {string} action - 触发的动作
*/
const trackShortcutUsed = useCallback((shortcut, action = '') => {
if (!shortcut) {
logger.warn('useNavigationEvents', 'trackShortcutUsed: shortcut is required');
return;
}
track('Keyboard Shortcut Used', {
shortcut,
action,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '⌨️ Keyboard Shortcut Used', {
shortcut,
action,
});
}, [track, component]);
/**
* 追踪返回按钮点击
* @param {string} fromPage - 当前页面
* @param {string} toPage - 返回到的页面
*/
const trackBackButtonClicked = useCallback((fromPage = '', toPage = '') => {
track('Back Button Clicked', {
from_page: fromPage,
to_page: toPage,
component,
timestamp: new Date().toISOString(),
});
logger.debug('useNavigationEvents', '◀️ Back Button Clicked', {
fromPage,
toPage,
});
}, [track, component]);
return {
// 导航点击事件
trackTopNavClicked,
trackSidebarMenuClicked,
trackMenuItemClicked,
trackBreadcrumbClicked,
trackLogoClicked,
// 用户交互事件
trackUserMenuOpened,
trackNotificationCenterOpened,
// 设置变更事件
trackLanguageChanged,
trackThemeChanged,
// 其他交互
trackShortcutUsed,
trackBackButtonClicked,
};
};
export default useNavigationEvents;

View File

@@ -1,55 +0,0 @@
// src/hooks/usePageTracking.js
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import posthog from 'posthog-js';
/**
* Custom hook for automatic page view tracking with PostHog
*
* @param {Object} options - Configuration options
* @param {boolean} options.enabled - Whether tracking is enabled
* @param {Function} options.getProperties - Function to get custom properties for each page view
*/
export const usePageTracking = ({ enabled = true, getProperties } = {}) => {
const location = useLocation();
const previousPathRef = useRef('');
useEffect(() => {
if (!enabled) return;
// Get the current path
const currentPath = location.pathname + location.search;
// Skip if it's the same page (prevents duplicate tracking)
if (previousPathRef.current === currentPath) {
return;
}
// Update the previous path
previousPathRef.current = currentPath;
// Get custom properties if function provided
const customProperties = getProperties ? getProperties(location) : {};
// Track page view with PostHog
if (posthog && posthog.__loaded) {
posthog.capture('$pageview', {
$current_url: window.location.href,
path: location.pathname,
search: location.search,
hash: location.hash,
...customProperties,
});
// Log in development
if (process.env.NODE_ENV === 'development') {
console.log('📊 PostHog $pageview:', {
path: location.pathname,
...customProperties,
});
}
}
}, [location, enabled, getProperties]);
};
export default usePageTracking;

View File

@@ -1,101 +0,0 @@
// src/hooks/usePostHog.js
import { useCallback } from 'react';
import {
getPostHog,
trackEvent,
trackPageView,
identifyUser,
setUserProperties,
resetUser,
optIn,
optOut,
hasOptedOut,
getFeatureFlag,
isFeatureEnabled,
} from '../lib/posthog';
/**
* Custom hook to access PostHog functionality
* Provides convenient methods for tracking events and managing user sessions
*
* @returns {object} PostHog methods
*/
export const usePostHog = () => {
// Get PostHog instance
const posthog = getPostHog();
// Track custom event
const track = useCallback((eventName, properties = {}) => {
trackEvent(eventName, properties);
}, []);
// Track page view
const trackPage = useCallback((pagePath, properties = {}) => {
trackPageView(pagePath, properties);
}, []);
// Identify user
const identify = useCallback((userId, userProperties = {}) => {
identifyUser(userId, userProperties);
}, []);
// Set user properties
const setProperties = useCallback((properties) => {
setUserProperties(properties);
}, []);
// Reset user session (logout)
const reset = useCallback(() => {
resetUser();
}, []);
// Opt out of tracking
const optOutTracking = useCallback(() => {
optOut();
}, []);
// Opt in to tracking
const optInTracking = useCallback(() => {
optIn();
}, []);
// Check if user has opted out
const isOptedOut = useCallback(() => {
return hasOptedOut();
}, []);
// Get feature flag value
const getFlag = useCallback((flagKey, defaultValue = false) => {
return getFeatureFlag(flagKey, defaultValue);
}, []);
// Check if feature is enabled
const isEnabled = useCallback((flagKey) => {
return isFeatureEnabled(flagKey);
}, []);
return {
// Core PostHog instance
posthog,
// Tracking methods
track,
trackPage,
// User management
identify,
setProperties,
reset,
// Privacy controls
optOut: optOutTracking,
optIn: optInTracking,
isOptedOut,
// Feature flags
getFlag,
isEnabled,
};
};
export default usePostHog;

View File

@@ -1,272 +0,0 @@
// src/hooks/usePostHogRedux.js
import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
trackEvent,
identifyUser,
resetUser,
optIn,
optOut,
selectPostHog,
selectIsInitialized,
selectUser,
selectFeatureFlags,
selectFeatureFlag,
selectIsOptedOut,
selectStats,
flushCachedEvents,
} from '../store/slices/posthogSlice';
import { trackPageView } from '../lib/posthog';
/**
* PostHog Redux Hook
* 提供便捷的 PostHog 功能访问接口
*
* 用法示例:
*
* ```jsx
* import { usePostHogRedux } from 'hooks/usePostHogRedux';
* import { RETENTION_EVENTS } from 'lib/constants';
*
* function MyComponent() {
* const { track, identify, user, isInitialized } = usePostHogRedux();
*
* const handleClick = () => {
* track(RETENTION_EVENTS.NEWS_ARTICLE_CLICKED, {
* article_id: '123',
* article_title: '标题',
* });
* };
*
* if (!isInitialized) {
* return <div>正在加载...</div>;
* }
*
* return (
* <div>
* <button onClick={handleClick}>点击追踪</button>
* {user && <p>当前用户: {user.userId}</p>}
* </div>
* );
* }
* ```
*/
export const usePostHogRedux = () => {
const dispatch = useDispatch();
// Selectors
const posthog = useSelector(selectPostHog);
const isInitialized = useSelector(selectIsInitialized);
const user = useSelector(selectUser);
const featureFlags = useSelector(selectFeatureFlags);
const stats = useSelector(selectStats);
// ==================== 追踪事件 ====================
/**
* 追踪自定义事件
* @param {string} eventName - 事件名称(建议使用 constants.js 中的常量)
* @param {object} properties - 事件属性
*/
const track = useCallback(
(eventName, properties = {}) => {
dispatch(trackEvent({ eventName, properties }));
},
[dispatch]
);
/**
* 追踪页面浏览
* @param {string} pagePath - 页面路径
* @param {object} properties - 页面属性
*/
const trackPage = useCallback(
(pagePath, properties = {}) => {
trackPageView(pagePath, properties);
},
[]
);
// ==================== 用户管理 ====================
/**
* 识别用户(登录后调用)
* @param {string} userId - 用户 ID
* @param {object} userProperties - 用户属性
*/
const identify = useCallback(
(userId, userProperties = {}) => {
dispatch(identifyUser({ userId, userProperties }));
},
[dispatch]
);
/**
* 重置用户会话(登出时调用)
*/
const reset = useCallback(() => {
dispatch(resetUser());
}, [dispatch]);
// ==================== 隐私控制 ====================
/**
* 用户选择退出追踪
*/
const optOutTracking = useCallback(() => {
dispatch(optOut());
}, [dispatch]);
/**
* 用户选择加入追踪
*/
const optInTracking = useCallback(() => {
dispatch(optIn());
}, [dispatch]);
/**
* 检查用户是否已退出追踪
*/
const isOptedOut = selectIsOptedOut();
// ==================== Feature Flags ====================
/**
* 获取特定 Feature Flag 的值
* @param {string} flagKey - Flag 键名
* @returns {any} Flag 值
*/
const getFlag = useCallback(
(flagKey) => {
return selectFeatureFlag(flagKey)({ posthog });
},
[posthog]
);
/**
* 检查 Feature Flag 是否启用
* @param {string} flagKey - Flag 键名
* @returns {boolean}
*/
const isEnabled = useCallback(
(flagKey) => {
const value = getFlag(flagKey);
return Boolean(value);
},
[getFlag]
);
// ==================== 离线事件管理 ====================
/**
* 刷新缓存的离线事件
*/
const flushEvents = useCallback(() => {
dispatch(flushCachedEvents());
}, [dispatch]);
// ==================== 返回接口 ====================
return {
// 状态
isInitialized,
user,
featureFlags,
stats,
posthog, // 完整的 PostHog 状态
// 追踪方法
track,
trackPage,
// 用户管理
identify,
reset,
// 隐私控制
optOut: optOutTracking,
optIn: optInTracking,
isOptedOut,
// Feature Flags
getFlag,
isEnabled,
// 离线事件
flushEvents,
};
};
// ==================== 便捷 Hooks ====================
/**
* 仅获取追踪功能的 Hook性能优化
*/
export const usePostHogTrack = () => {
const dispatch = useDispatch();
const track = useCallback(
(eventName, properties = {}) => {
dispatch(trackEvent({ eventName, properties }));
},
[dispatch]
);
return { track };
};
/**
* 仅获取 Feature Flags 的 Hook性能优化
*/
export const usePostHogFlags = () => {
const featureFlags = useSelector(selectFeatureFlags);
const posthog = useSelector(selectPostHog);
const getFlag = useCallback(
(flagKey) => {
return selectFeatureFlag(flagKey)({ posthog });
},
[posthog]
);
const isEnabled = useCallback(
(flagKey) => {
const value = getFlag(flagKey);
return Boolean(value);
},
[getFlag]
);
return {
featureFlags,
getFlag,
isEnabled,
};
};
/**
* 获取用户信息的 Hook性能优化
*/
export const usePostHogUser = () => {
const user = useSelector(selectUser);
const dispatch = useDispatch();
const identify = useCallback(
(userId, userProperties = {}) => {
dispatch(identifyUser({ userId, userProperties }));
},
[dispatch]
);
const reset = useCallback(() => {
dispatch(resetUser());
}, [dispatch]);
return {
user,
identify,
reset,
};
};
export default usePostHogRedux;

View File

@@ -1,334 +0,0 @@
// src/hooks/useProfileEvents.js
// 个人资料和设置事件追踪 Hook
import { useCallback } from 'react';
import { usePostHogTrack } from './usePostHogRedux';
import { RETENTION_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 个人资料和设置事件追踪 Hook
* @param {Object} options - 配置选项
* @param {string} options.pageType - 页面类型 ('profile' | 'settings' | 'security')
* @returns {Object} 事件追踪处理函数集合
*/
export const useProfileEvents = ({ pageType = 'profile' } = {}) => {
const { track } = usePostHogTrack();
/**
* 追踪个人资料字段编辑开始
* @param {string} fieldName - 字段名称 ('nickname' | 'email' | 'phone' | 'avatar' | 'bio')
*/
const trackProfileFieldEditStarted = useCallback((fieldName) => {
if (!fieldName) {
logger.warn('useProfileEvents', 'trackProfileFieldEditStarted: fieldName is required');
return;
}
track('Profile Field Edit Started', {
field_name: fieldName,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '✏️ Profile Field Edit Started', {
fieldName,
pageType,
});
}, [track, pageType]);
/**
* 追踪个人资料更新成功
* @param {Array<string>} updatedFields - 更新的字段列表
* @param {Object} changes - 变更详情
*/
const trackProfileUpdated = useCallback((updatedFields = [], changes = {}) => {
if (!updatedFields || updatedFields.length === 0) {
logger.warn('useProfileEvents', 'trackProfileUpdated: updatedFields array is required');
return;
}
track(RETENTION_EVENTS.PROFILE_UPDATED, {
updated_fields: updatedFields,
field_count: updatedFields.length,
changes: changes,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '✅ Profile Updated', {
updatedFields,
fieldCount: updatedFields.length,
pageType,
});
}, [track, pageType]);
/**
* 追踪个人资料更新失败
* @param {Array<string>} attemptedFields - 尝试更新的字段
* @param {string} errorMessage - 错误信息
*/
const trackProfileUpdateFailed = useCallback((attemptedFields = [], errorMessage = '') => {
track('Profile Update Failed', {
attempted_fields: attemptedFields,
error_message: errorMessage,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '❌ Profile Update Failed', {
attemptedFields,
errorMessage,
pageType,
});
}, [track, pageType]);
/**
* 追踪头像上传
* @param {string} uploadMethod - 上传方式 ('file_upload' | 'url' | 'camera' | 'default_avatar')
* @param {number} fileSize - 文件大小bytes
*/
const trackAvatarUploaded = useCallback((uploadMethod = 'file_upload', fileSize = 0) => {
track('Avatar Uploaded', {
upload_method: uploadMethod,
file_size: fileSize,
file_size_mb: (fileSize / (1024 * 1024)).toFixed(2),
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '🖼️ Avatar Uploaded', {
uploadMethod,
fileSize,
pageType,
});
}, [track, pageType]);
/**
* 追踪密码更改
* @param {boolean} success - 是否成功
* @param {string} errorReason - 失败原因
*/
const trackPasswordChanged = useCallback((success = true, errorReason = '') => {
track('Password Changed', {
success,
error_reason: errorReason || null,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', success ? '🔒 Password Changed Successfully' : '❌ Password Change Failed', {
success,
errorReason,
pageType,
});
}, [track, pageType]);
/**
* 追踪邮箱验证发起
* @param {string} email - 邮箱地址
*/
const trackEmailVerificationSent = useCallback((email = '') => {
track('Email Verification Sent', {
email_provided: Boolean(email),
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '📧 Email Verification Sent', {
emailProvided: Boolean(email),
pageType,
});
}, [track, pageType]);
/**
* 追踪手机号验证发起
* @param {string} phone - 手机号
*/
const trackPhoneVerificationSent = useCallback((phone = '') => {
track('Phone Verification Sent', {
phone_provided: Boolean(phone),
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '📱 Phone Verification Sent', {
phoneProvided: Boolean(phone),
pageType,
});
}, [track, pageType]);
/**
* 追踪账号绑定(微信、邮箱、手机等)
* @param {string} accountType - 账号类型 ('wechat' | 'email' | 'phone')
* @param {boolean} success - 是否成功
*/
const trackAccountBound = useCallback((accountType, success = true) => {
if (!accountType) {
logger.warn('useProfileEvents', 'trackAccountBound: accountType is required');
return;
}
track('Account Bound', {
account_type: accountType,
success,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', success ? '🔗 Account Bound' : '❌ Account Bind Failed', {
accountType,
success,
pageType,
});
}, [track, pageType]);
/**
* 追踪账号解绑
* @param {string} accountType - 账号类型
* @param {boolean} success - 是否成功
*/
const trackAccountUnbound = useCallback((accountType, success = true) => {
if (!accountType) {
logger.warn('useProfileEvents', 'trackAccountUnbound: accountType is required');
return;
}
track('Account Unbound', {
account_type: accountType,
success,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', success ? '🔓 Account Unbound' : '❌ Account Unbind Failed', {
accountType,
success,
pageType,
});
}, [track, pageType]);
/**
* 追踪设置项更改
* @param {string} settingName - 设置名称
* @param {any} oldValue - 旧值
* @param {any} newValue - 新值
* @param {string} category - 设置分类 ('notification' | 'privacy' | 'display' | 'advanced')
*/
const trackSettingChanged = useCallback((settingName, oldValue, newValue, category = 'general') => {
if (!settingName) {
logger.warn('useProfileEvents', 'trackSettingChanged: settingName is required');
return;
}
track(RETENTION_EVENTS.SETTINGS_CHANGED, {
setting_name: settingName,
old_value: String(oldValue),
new_value: String(newValue),
category,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '⚙️ Setting Changed', {
settingName,
oldValue,
newValue,
category,
pageType,
});
}, [track, pageType]);
/**
* 追踪通知偏好更改
* @param {Object} preferences - 通知偏好设置
* @param {boolean} preferences.email - 邮件通知
* @param {boolean} preferences.push - 推送通知
* @param {boolean} preferences.sms - 短信通知
*/
const trackNotificationPreferencesChanged = useCallback((preferences = {}) => {
track('Notification Preferences Changed', {
email_enabled: preferences.email || false,
push_enabled: preferences.push || false,
sms_enabled: preferences.sms || false,
total_enabled: Object.values(preferences).filter(Boolean).length,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '🔔 Notification Preferences Changed', {
preferences,
pageType,
});
}, [track, pageType]);
/**
* 追踪隐私设置更改
* @param {string} privacySetting - 隐私设置名称
* @param {boolean} isPublic - 是否公开
*/
const trackPrivacySettingChanged = useCallback((privacySetting, isPublic = false) => {
if (!privacySetting) {
logger.warn('useProfileEvents', 'trackPrivacySettingChanged: privacySetting is required');
return;
}
track('Privacy Setting Changed', {
privacy_setting: privacySetting,
is_public: isPublic,
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '🔐 Privacy Setting Changed', {
privacySetting,
isPublic,
pageType,
});
}, [track, pageType]);
/**
* 追踪账号删除请求
* @param {string} reason - 删除原因
*/
const trackAccountDeletionRequested = useCallback((reason = '') => {
track('Account Deletion Requested', {
reason,
has_reason: Boolean(reason),
page_type: pageType,
timestamp: new Date().toISOString(),
});
logger.debug('useProfileEvents', '🗑️ Account Deletion Requested', {
reason,
pageType,
});
}, [track, pageType]);
return {
// 个人资料编辑
trackProfileFieldEditStarted,
trackProfileUpdated,
trackProfileUpdateFailed,
trackAvatarUploaded,
// 安全和验证
trackPasswordChanged,
trackEmailVerificationSent,
trackPhoneVerificationSent,
// 账号绑定
trackAccountBound,
trackAccountUnbound,
// 设置更改
trackSettingChanged,
trackNotificationPreferencesChanged,
trackPrivacySettingChanged,
// 账号管理
trackAccountDeletionRequested,
};
};
export default useProfileEvents;

View File

@@ -1,244 +0,0 @@
// src/hooks/useSearchEvents.js
// 全局搜索功能事件追踪 Hook
import { useCallback } from 'react';
import { usePostHogTrack } from './usePostHogRedux';
import { RETENTION_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 全局搜索事件追踪 Hook
* @param {Object} options - 配置选项
* @param {string} options.context - 搜索上下文 ('global' | 'stock' | 'news' | 'concept' | 'simulation')
* @returns {Object} 事件追踪处理函数集合
*/
export const useSearchEvents = ({ context = 'global' } = {}) => {
const { track } = usePostHogTrack();
/**
* 追踪搜索开始(聚焦搜索框)
* @param {string} placeholder - 搜索框提示文本
*/
const trackSearchInitiated = useCallback((placeholder = '') => {
track(RETENTION_EVENTS.SEARCH_INITIATED, {
context,
placeholder,
timestamp: new Date().toISOString(),
});
logger.debug('useSearchEvents', '🔍 Search Initiated', {
context,
placeholder,
});
}, [track, context]);
/**
* 追踪搜索查询提交
* @param {string} query - 搜索查询词
* @param {number} resultCount - 搜索结果数量
* @param {Object} filters - 应用的筛选条件
*/
const trackSearchQuerySubmitted = useCallback((query, resultCount = 0, filters = {}) => {
if (!query) {
logger.warn('useSearchEvents', 'trackSearchQuerySubmitted: query is required');
return;
}
track(RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED, {
query,
query_length: query.length,
result_count: resultCount,
has_results: resultCount > 0,
context,
filters: filters,
filter_count: Object.keys(filters).length,
timestamp: new Date().toISOString(),
});
// 如果没有搜索结果,额外追踪
if (resultCount === 0) {
track(RETENTION_EVENTS.SEARCH_NO_RESULTS, {
query,
context,
filters,
timestamp: new Date().toISOString(),
});
logger.debug('useSearchEvents', '❌ Search No Results', {
query,
context,
});
} else {
logger.debug('useSearchEvents', '✅ Search Query Submitted', {
query,
resultCount,
context,
});
}
}, [track, context]);
/**
* 追踪搜索结果点击
* @param {Object} result - 被点击的搜索结果
* @param {string} result.type - 结果类型 ('stock' | 'news' | 'concept' | 'event')
* @param {string} result.id - 结果ID
* @param {string} result.title - 结果标题
* @param {number} position - 在搜索结果中的位置
* @param {string} query - 搜索查询词
*/
const trackSearchResultClicked = useCallback((result, position = 0, query = '') => {
if (!result || !result.type) {
logger.warn('useSearchEvents', 'trackSearchResultClicked: result object with type is required');
return;
}
track(RETENTION_EVENTS.SEARCH_RESULT_CLICKED, {
result_type: result.type,
result_id: result.id || result.code || '',
result_title: result.title || result.name || '',
position,
query,
context,
timestamp: new Date().toISOString(),
});
logger.debug('useSearchEvents', '🎯 Search Result Clicked', {
type: result.type,
id: result.id || result.code,
position,
context,
});
}, [track, context]);
/**
* 追踪搜索筛选应用
* @param {Object} filters - 应用的筛选条件
* @param {string} filterType - 筛选类型 ('sort' | 'category' | 'date_range' | 'price_range')
* @param {any} filterValue - 筛选值
*/
const trackSearchFilterApplied = useCallback((filterType, filterValue, filters = {}) => {
if (!filterType) {
logger.warn('useSearchEvents', 'trackSearchFilterApplied: filterType is required');
return;
}
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
filter_type: filterType,
filter_value: String(filterValue),
all_filters: filters,
context,
timestamp: new Date().toISOString(),
});
logger.debug('useSearchEvents', '🔍 Search Filter Applied', {
filterType,
filterValue,
context,
});
}, [track, context]);
/**
* 追踪搜索建议点击(自动完成)
* @param {string} suggestion - 被点击的搜索建议
* @param {number} position - 在建议列表中的位置
* @param {string} source - 建议来源 ('history' | 'popular' | 'related')
*/
const trackSearchSuggestionClicked = useCallback((suggestion, position = 0, source = 'popular') => {
if (!suggestion) {
logger.warn('useSearchEvents', 'trackSearchSuggestionClicked: suggestion is required');
return;
}
track('Search Suggestion Clicked', {
suggestion,
position,
source,
context,
timestamp: new Date().toISOString(),
});
logger.debug('useSearchEvents', '💡 Search Suggestion Clicked', {
suggestion,
position,
source,
context,
});
}, [track, context]);
/**
* 追踪搜索历史查看
* @param {number} historyCount - 历史记录数量
*/
const trackSearchHistoryViewed = useCallback((historyCount = 0) => {
track('Search History Viewed', {
history_count: historyCount,
has_history: historyCount > 0,
context,
timestamp: new Date().toISOString(),
});
logger.debug('useSearchEvents', '📜 Search History Viewed', {
historyCount,
context,
});
}, [track, context]);
/**
* 追踪搜索历史清除
*/
const trackSearchHistoryCleared = useCallback(() => {
track('Search History Cleared', {
context,
timestamp: new Date().toISOString(),
});
logger.debug('useSearchEvents', '🗑️ Search History Cleared', {
context,
});
}, [track, context]);
/**
* 追踪热门搜索词点击
* @param {string} keyword - 被点击的热门关键词
* @param {number} position - 在列表中的位置
* @param {number} heatScore - 热度分数
*/
const trackPopularKeywordClicked = useCallback((keyword, position = 0, heatScore = 0) => {
if (!keyword) {
logger.warn('useSearchEvents', 'trackPopularKeywordClicked: keyword is required');
return;
}
track('Popular Keyword Clicked', {
keyword,
position,
heat_score: heatScore,
context,
timestamp: new Date().toISOString(),
});
logger.debug('useSearchEvents', '🔥 Popular Keyword Clicked', {
keyword,
position,
context,
});
}, [track, context]);
return {
// 搜索流程事件
trackSearchInitiated,
trackSearchQuerySubmitted,
trackSearchResultClicked,
// 筛选和建议
trackSearchFilterApplied,
trackSearchSuggestionClicked,
// 历史和热门
trackSearchHistoryViewed,
trackSearchHistoryCleared,
trackPopularKeywordClicked,
};
};
export default useSearchEvents;

View File

@@ -1,394 +0,0 @@
// src/hooks/useSubscriptionEvents.js
// 订阅和支付事件追踪 Hook
import { useCallback } from 'react';
import { usePostHogTrack } from './usePostHogRedux';
import { RETENTION_EVENTS, REVENUE_EVENTS } from '../lib/constants';
import { logger } from '../utils/logger';
/**
* 订阅和支付事件追踪 Hook
* @param {Object} options - 配置选项
* @param {Object} options.currentSubscription - 当前订阅信息
* @returns {Object} 事件追踪处理函数集合
*/
export const useSubscriptionEvents = ({ currentSubscription = null } = {}) => {
const { track } = usePostHogTrack();
/**
* 追踪付费墙展示
* @param {string} feature - 被限制的功能名称
* @param {string} requiredPlan - 需要的订阅计划
* @param {string} triggerLocation - 触发位置
*/
const trackPaywallShown = useCallback((feature, requiredPlan = 'pro', triggerLocation = '') => {
if (!feature) {
logger.warn('useSubscriptionEvents', 'trackPaywallShown: feature is required');
return;
}
track(REVENUE_EVENTS.PAYWALL_SHOWN, {
feature,
required_plan: requiredPlan,
current_plan: currentSubscription?.plan || 'free',
trigger_location: triggerLocation,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🚧 Paywall Shown', {
feature,
requiredPlan,
triggerLocation,
});
}, [track, currentSubscription]);
/**
* 追踪付费墙关闭
* @param {string} feature - 功能名称
* @param {string} closeMethod - 关闭方式 ('dismiss' | 'upgrade_clicked' | 'back_button')
*/
const trackPaywallDismissed = useCallback((feature, closeMethod = 'dismiss') => {
if (!feature) {
logger.warn('useSubscriptionEvents', 'trackPaywallDismissed: feature is required');
return;
}
track(REVENUE_EVENTS.PAYWALL_DISMISSED, {
feature,
close_method: closeMethod,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '❌ Paywall Dismissed', {
feature,
closeMethod,
});
}, [track, currentSubscription]);
/**
* 追踪升级按钮点击
* @param {string} targetPlan - 目标订阅计划
* @param {string} source - 来源位置
* @param {string} feature - 关联的功能(如果从付费墙点击)
*/
const trackUpgradePlanClicked = useCallback((targetPlan = 'pro', source = '', feature = '') => {
track(REVENUE_EVENTS.PAYWALL_UPGRADE_CLICKED, {
current_plan: currentSubscription?.plan || 'free',
target_plan: targetPlan,
source,
feature: feature || null,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '⬆️ Upgrade Plan Clicked', {
currentPlan: currentSubscription?.plan,
targetPlan,
source,
feature,
});
}, [track, currentSubscription]);
/**
* 追踪订阅页面查看
* @param {string} source - 来源
*/
const trackSubscriptionPageViewed = useCallback((source = '') => {
track(RETENTION_EVENTS.SUBSCRIPTION_PAGE_VIEWED, {
current_plan: currentSubscription?.plan || 'free',
subscription_status: currentSubscription?.status || 'unknown',
is_paid_user: currentSubscription?.plan && currentSubscription.plan !== 'free',
source,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '💳 Subscription Page Viewed', {
currentPlan: currentSubscription?.plan,
source,
});
}, [track, currentSubscription]);
/**
* 追踪定价计划查看
* @param {string} planName - 计划名称 ('free' | 'pro' | 'enterprise')
* @param {number} price - 价格
*/
const trackPricingPlanViewed = useCallback((planName, price = 0) => {
if (!planName) {
logger.warn('useSubscriptionEvents', 'trackPricingPlanViewed: planName is required');
return;
}
track('Pricing Plan Viewed', {
plan_name: planName,
price,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '👀 Pricing Plan Viewed', {
planName,
price,
});
}, [track, currentSubscription]);
/**
* 追踪定价计划选择
* @param {string} planName - 选择的计划名称
* @param {string} billingCycle - 计费周期 ('monthly' | 'yearly')
* @param {number} price - 价格
*/
const trackPricingPlanSelected = useCallback((planName, billingCycle = 'monthly', price = 0) => {
if (!planName) {
logger.warn('useSubscriptionEvents', 'trackPricingPlanSelected: planName is required');
return;
}
track('Pricing Plan Selected', {
plan_name: planName,
billing_cycle: billingCycle,
price,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '✅ Pricing Plan Selected', {
planName,
billingCycle,
price,
});
}, [track, currentSubscription]);
/**
* 追踪支付页面查看
* @param {string} planName - 购买的计划
* @param {number} amount - 支付金额
*/
const trackPaymentPageViewed = useCallback((planName, amount = 0) => {
track(REVENUE_EVENTS.PAYMENT_PAGE_VIEWED, {
plan_name: planName,
amount,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '💰 Payment Page Viewed', {
planName,
amount,
});
}, [track, currentSubscription]);
/**
* 追踪支付方式选择
* @param {string} paymentMethod - 支付方式 ('wechat_pay' | 'alipay' | 'credit_card')
* @param {number} amount - 支付金额
*/
const trackPaymentMethodSelected = useCallback((paymentMethod, amount = 0) => {
if (!paymentMethod) {
logger.warn('useSubscriptionEvents', 'trackPaymentMethodSelected: paymentMethod is required');
return;
}
track(REVENUE_EVENTS.PAYMENT_METHOD_SELECTED, {
payment_method: paymentMethod,
amount,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '💳 Payment Method Selected', {
paymentMethod,
amount,
});
}, [track, currentSubscription]);
/**
* 追踪支付发起
* @param {Object} paymentInfo - 支付信息
* @param {string} paymentInfo.planName - 计划名称
* @param {string} paymentInfo.paymentMethod - 支付方式
* @param {number} paymentInfo.amount - 金额
* @param {string} paymentInfo.billingCycle - 计费周期
* @param {string} paymentInfo.orderId - 订单ID
*/
const trackPaymentInitiated = useCallback((paymentInfo = {}) => {
track(REVENUE_EVENTS.PAYMENT_INITIATED, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
billing_cycle: paymentInfo.billingCycle,
order_id: paymentInfo.orderId,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🚀 Payment Initiated', {
planName: paymentInfo.planName,
amount: paymentInfo.amount,
paymentMethod: paymentInfo.paymentMethod,
});
}, [track, currentSubscription]);
/**
* 追踪支付成功
* @param {Object} paymentInfo - 支付信息
*/
const trackPaymentSuccessful = useCallback((paymentInfo = {}) => {
track(REVENUE_EVENTS.PAYMENT_SUCCESSFUL, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
billing_cycle: paymentInfo.billingCycle,
order_id: paymentInfo.orderId,
transaction_id: paymentInfo.transactionId,
previous_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '✅ Payment Successful', {
planName: paymentInfo.planName,
amount: paymentInfo.amount,
orderId: paymentInfo.orderId,
});
}, [track, currentSubscription]);
/**
* 追踪支付失败
* @param {Object} paymentInfo - 支付信息
* @param {string} errorReason - 失败原因
*/
const trackPaymentFailed = useCallback((paymentInfo = {}, errorReason = '') => {
track(REVENUE_EVENTS.PAYMENT_FAILED, {
plan_name: paymentInfo.planName,
payment_method: paymentInfo.paymentMethod,
amount: paymentInfo.amount,
error_reason: errorReason,
order_id: paymentInfo.orderId,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '❌ Payment Failed', {
planName: paymentInfo.planName,
errorReason,
orderId: paymentInfo.orderId,
});
}, [track, currentSubscription]);
/**
* 追踪订阅创建成功
* @param {Object} subscription - 订阅信息
*/
const trackSubscriptionCreated = useCallback((subscription = {}) => {
track(REVENUE_EVENTS.SUBSCRIPTION_CREATED, {
plan_name: subscription.plan,
billing_cycle: subscription.billingCycle,
amount: subscription.amount,
start_date: subscription.startDate,
end_date: subscription.endDate,
previous_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🎉 Subscription Created', {
plan: subscription.plan,
billingCycle: subscription.billingCycle,
});
}, [track, currentSubscription]);
/**
* 追踪订阅续费
* @param {Object} subscription - 订阅信息
*/
const trackSubscriptionRenewed = useCallback((subscription = {}) => {
track(REVENUE_EVENTS.SUBSCRIPTION_RENEWED, {
plan_name: subscription.plan,
amount: subscription.amount,
previous_end_date: subscription.previousEndDate,
new_end_date: subscription.newEndDate,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🔄 Subscription Renewed', {
plan: subscription.plan,
amount: subscription.amount,
});
}, [track]);
/**
* 追踪订阅取消
* @param {string} reason - 取消原因
* @param {boolean} cancelImmediately - 是否立即取消
*/
const trackSubscriptionCancelled = useCallback((reason = '', cancelImmediately = false) => {
track(REVENUE_EVENTS.SUBSCRIPTION_CANCELLED, {
plan_name: currentSubscription?.plan,
reason,
has_reason: Boolean(reason),
cancel_immediately: cancelImmediately,
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', '🚫 Subscription Cancelled', {
plan: currentSubscription?.plan,
reason,
cancelImmediately,
});
}, [track, currentSubscription]);
/**
* 追踪优惠券应用
* @param {string} couponCode - 优惠券代码
* @param {number} discountAmount - 折扣金额
* @param {boolean} success - 是否成功
*/
const trackCouponApplied = useCallback((couponCode, discountAmount = 0, success = true) => {
if (!couponCode) {
logger.warn('useSubscriptionEvents', 'trackCouponApplied: couponCode is required');
return;
}
track('Coupon Applied', {
coupon_code: couponCode,
discount_amount: discountAmount,
success,
current_plan: currentSubscription?.plan || 'free',
timestamp: new Date().toISOString(),
});
logger.debug('useSubscriptionEvents', success ? '🎟️ Coupon Applied' : '❌ Coupon Failed', {
couponCode,
discountAmount,
success,
});
}, [track, currentSubscription]);
return {
// 付费墙事件
trackPaywallShown,
trackPaywallDismissed,
trackUpgradePlanClicked,
// 订阅页面事件
trackSubscriptionPageViewed,
trackPricingPlanViewed,
trackPricingPlanSelected,
// 支付流程事件
trackPaymentPageViewed,
trackPaymentMethodSelected,
trackPaymentInitiated,
trackPaymentSuccessful,
trackPaymentFailed,
// 订阅管理事件
trackSubscriptionCreated,
trackSubscriptionRenewed,
trackSubscriptionCancelled,
// 优惠券事件
trackCouponApplied,
};
};
export default useSubscriptionEvents;

View File

@@ -1,381 +0,0 @@
// src/lib/constants.js
// PostHog Event Names and Constants
// Organized by AARRR Framework (Acquisition, Activation, Retention, Referral, Revenue)
// ============================================================================
// ACQUISITION (获客) - Landing page, marketing website events
// ============================================================================
export const ACQUISITION_EVENTS = {
// Landing page
LANDING_PAGE_VIEWED: 'Landing Page Viewed',
CTA_BUTTON_CLICKED: 'CTA Button Clicked',
FEATURE_CARD_VIEWED: 'Feature Card Viewed',
FEATURE_VIDEO_PLAYED: 'Feature Video Played',
// Pricing page
PRICING_PAGE_VIEWED: 'Pricing Page Viewed',
PRICING_PLAN_VIEWED: 'Pricing Plan Viewed',
PRICING_PLAN_SELECTED: 'Pricing Plan Selected',
// How to use page
HOW_TO_USE_PAGE_VIEWED: 'How To Use Page Viewed',
TUTORIAL_STEP_VIEWED: 'Tutorial Step Viewed',
// Roadmap page
ROADMAP_PAGE_VIEWED: 'Roadmap Page Viewed',
ROADMAP_ITEM_CLICKED: 'Roadmap Item Clicked',
};
// ============================================================================
// ACTIVATION (激活) - Sign up, login, onboarding
// ============================================================================
export const ACTIVATION_EVENTS = {
// Auth pages
LOGIN_PAGE_VIEWED: 'Login Page Viewed',
SIGNUP_PAGE_VIEWED: 'Signup Page Viewed',
// Login method selection
PHONE_LOGIN_INITIATED: 'Phone Login Initiated', // 用户开始填写手机号
WECHAT_LOGIN_INITIATED: 'WeChat Login Initiated', // 用户选择微信登录
// Phone verification code flow
VERIFICATION_CODE_SENT: 'Verification Code Sent',
VERIFICATION_CODE_SEND_FAILED: 'Verification Code Send Failed',
VERIFICATION_CODE_INPUT_CHANGED: 'Verification Code Input Changed',
VERIFICATION_CODE_RESENT: 'Verification Code Resent',
VERIFICATION_CODE_SUBMITTED: 'Verification Code Submitted',
PHONE_NUMBER_VALIDATED: 'Phone Number Validated',
// WeChat login flow
WECHAT_QR_DISPLAYED: 'WeChat QR Code Displayed',
WECHAT_QR_SCANNED: 'WeChat QR Code Scanned',
WECHAT_QR_EXPIRED: 'WeChat QR Code Expired',
WECHAT_QR_REFRESHED: 'WeChat QR Code Refreshed',
WECHAT_STATUS_CHANGED: 'WeChat Status Changed',
WECHAT_H5_REDIRECT: 'WeChat H5 Redirect', // 移动端跳转微信H5
// Login/Signup results
USER_LOGGED_IN: 'User Logged In',
USER_SIGNED_UP: 'User Signed Up',
LOGIN_FAILED: 'Login Failed',
SIGNUP_FAILED: 'Signup Failed',
// User behavior details
AUTH_FORM_FOCUSED: 'Auth Form Field Focused',
AUTH_FORM_VALIDATION_ERROR: 'Auth Form Validation Error',
NICKNAME_PROMPT_SHOWN: 'Nickname Prompt Shown',
NICKNAME_PROMPT_ACCEPTED: 'Nickname Prompt Accepted',
NICKNAME_PROMPT_SKIPPED: 'Nickname Prompt Skipped',
USER_AGREEMENT_LINK_CLICKED: 'User Agreement Link Clicked',
PRIVACY_POLICY_LINK_CLICKED: 'Privacy Policy Link Clicked',
// Error tracking
LOGIN_ERROR_OCCURRED: 'Login Error Occurred',
NETWORK_ERROR_OCCURRED: 'Network Error Occurred',
SESSION_EXPIRED: 'Session Expired',
API_ERROR_OCCURRED: 'API Error Occurred',
// Onboarding
ONBOARDING_STARTED: 'Onboarding Started',
ONBOARDING_STEP_COMPLETED: 'Onboarding Step Completed',
ONBOARDING_COMPLETED: 'Onboarding Completed',
ONBOARDING_SKIPPED: 'Onboarding Skipped',
// User agreement (deprecated, use link clicked events instead)
USER_AGREEMENT_VIEWED: 'User Agreement Viewed',
USER_AGREEMENT_ACCEPTED: 'User Agreement Accepted',
PRIVACY_POLICY_VIEWED: 'Privacy Policy Viewed',
PRIVACY_POLICY_ACCEPTED: 'Privacy Policy Accepted',
};
// ============================================================================
// RETENTION (留存) - Core product usage, feature engagement
// ============================================================================
export const RETENTION_EVENTS = {
// Dashboard
DASHBOARD_VIEWED: 'Dashboard Viewed',
DASHBOARD_CENTER_VIEWED: 'Dashboard Center Viewed',
FUNCTION_CARD_CLICKED: 'Function Card Clicked', // Core功能卡片点击
// Navigation
TOP_NAV_CLICKED: 'Top Navigation Clicked',
SIDEBAR_MENU_CLICKED: 'Sidebar Menu Clicked',
MENU_ITEM_CLICKED: 'Menu Item Clicked',
BREADCRUMB_CLICKED: 'Breadcrumb Clicked',
// Search
SEARCH_INITIATED: 'Search Initiated',
SEARCH_QUERY_SUBMITTED: 'Search Query Submitted',
SEARCH_RESULT_CLICKED: 'Search Result Clicked',
SEARCH_NO_RESULTS: 'Search No Results',
SEARCH_FILTER_APPLIED: 'Search Filter Applied',
// News/Community (新闻催化分析)
COMMUNITY_PAGE_VIEWED: 'Community Page Viewed',
NEWS_LIST_VIEWED: 'News List Viewed',
NEWS_ARTICLE_CLICKED: 'News Article Clicked',
NEWS_DETAIL_OPENED: 'News Detail Opened',
NEWS_TAB_CLICKED: 'News Tab Clicked', // 相关标的, 相关概念, etc.
NEWS_FILTER_APPLIED: 'News Filter Applied',
NEWS_SORTED: 'News Sorted',
// Concept Center (概念中心)
CONCEPT_PAGE_VIEWED: 'Concept Page Viewed',
CONCEPT_LIST_VIEWED: 'Concept List Viewed',
CONCEPT_CLICKED: 'Concept Clicked',
CONCEPT_DETAIL_VIEWED: 'Concept Detail Viewed',
CONCEPT_STOCK_CLICKED: 'Concept Stock Clicked',
// Stock Center (个股中心)
STOCK_OVERVIEW_VIEWED: 'Stock Overview Page Viewed',
STOCK_LIST_VIEWED: 'Stock List Viewed',
STOCK_SEARCHED: 'Stock Searched',
STOCK_CLICKED: 'Stock Clicked',
STOCK_DETAIL_VIEWED: 'Stock Detail Viewed',
STOCK_TAB_CLICKED: 'Stock Tab Clicked', // 公司概览, 股票行情, 财务全景, 盈利预测
// Company Details
COMPANY_OVERVIEW_VIEWED: 'Company Overview Viewed',
COMPANY_FINANCIALS_VIEWED: 'Company Financials Viewed',
COMPANY_FORECAST_VIEWED: 'Company Forecast Viewed',
COMPANY_MARKET_DATA_VIEWED: 'Company Market Data Viewed',
// Limit Analysis (涨停分析)
LIMIT_ANALYSE_PAGE_VIEWED: 'Limit Analyse Page Viewed',
LIMIT_BOARD_CLICKED: 'Limit Board Clicked',
LIMIT_SECTOR_EXPANDED: 'Limit Sector Expanded',
LIMIT_SECTOR_ANALYSIS_VIEWED: 'Limit Sector Analysis Viewed',
LIMIT_STOCK_CLICKED: 'Limit Stock Clicked',
// Trading Simulation (模拟盘交易)
TRADING_SIMULATION_ENTERED: 'Trading Simulation Entered',
SIMULATION_ORDER_PLACED: 'Simulation Order Placed',
SIMULATION_HOLDINGS_VIEWED: 'Simulation Holdings Viewed',
SIMULATION_HISTORY_VIEWED: 'Simulation History Viewed',
SIMULATION_STOCK_SEARCHED: 'Simulation Stock Searched',
// Event Details
EVENT_DETAIL_VIEWED: 'Event Detail Viewed',
EVENT_ANALYSIS_VIEWED: 'Event Analysis Viewed',
EVENT_TIMELINE_CLICKED: 'Event Timeline Clicked',
// Profile & Settings
PROFILE_PAGE_VIEWED: 'Profile Page Viewed',
PROFILE_UPDATED: 'Profile Updated',
SETTINGS_PAGE_VIEWED: 'Settings Page Viewed',
SETTINGS_CHANGED: 'Settings Changed',
// Subscription Management
SUBSCRIPTION_PAGE_VIEWED: 'Subscription Page Viewed',
UPGRADE_PLAN_CLICKED: 'Upgrade Plan Clicked',
};
// ============================================================================
// REFERRAL (推荐) - Sharing, inviting
// ============================================================================
export const REFERRAL_EVENTS = {
// Sharing
SHARE_BUTTON_CLICKED: 'Share Button Clicked',
CONTENT_SHARED: 'Content Shared',
SHARE_LINK_GENERATED: 'Share Link Generated',
SHARE_MODAL_OPENED: 'Share Modal Opened',
SHARE_MODAL_CLOSED: 'Share Modal Closed',
// Referral
REFERRAL_PAGE_VIEWED: 'Referral Page Viewed',
REFERRAL_LINK_COPIED: 'Referral Link Copied',
REFERRAL_INVITE_SENT: 'Referral Invite Sent',
};
// ============================================================================
// REVENUE (收入) - Payment, subscription, monetization
// ============================================================================
export const REVENUE_EVENTS = {
// Paywall
PAYWALL_SHOWN: 'Paywall Shown',
PAYWALL_DISMISSED: 'Paywall Dismissed',
PAYWALL_UPGRADE_CLICKED: 'Paywall Upgrade Clicked',
// Payment
PAYMENT_PAGE_VIEWED: 'Payment Page Viewed',
PAYMENT_METHOD_SELECTED: 'Payment Method Selected',
PAYMENT_INITIATED: 'Payment Initiated',
PAYMENT_SUCCESSFUL: 'Payment Successful',
PAYMENT_FAILED: 'Payment Failed',
// Subscription
SUBSCRIPTION_CREATED: 'Subscription Created',
SUBSCRIPTION_RENEWED: 'Subscription Renewed',
SUBSCRIPTION_UPGRADED: 'Subscription Upgraded',
SUBSCRIPTION_DOWNGRADED: 'Subscription Downgraded',
SUBSCRIPTION_CANCELLED: 'Subscription Cancelled',
SUBSCRIPTION_EXPIRED: 'Subscription Expired',
// Refund
REFUND_REQUESTED: 'Refund Requested',
REFUND_PROCESSED: 'Refund Processed',
};
// ============================================================================
// SPECIAL EVENTS (特殊事件) - Errors, performance, chatbot
// ============================================================================
export const SPECIAL_EVENTS = {
// Errors
ERROR_OCCURRED: 'Error Occurred',
API_ERROR: 'API Error',
NOT_FOUND_404: '404 Not Found',
// Performance
PAGE_LOAD_TIME: 'Page Load Time',
API_RESPONSE_TIME: 'API Response Time',
// Chatbot (Dify)
CHATBOT_OPENED: 'Chatbot Opened',
CHATBOT_CLOSED: 'Chatbot Closed',
CHATBOT_MESSAGE_SENT: 'Chatbot Message Sent',
CHATBOT_MESSAGE_RECEIVED: 'Chatbot Message Received',
CHATBOT_FEEDBACK_PROVIDED: 'Chatbot Feedback Provided',
// Scroll depth
SCROLL_DEPTH_25: 'Scroll Depth 25%',
SCROLL_DEPTH_50: 'Scroll Depth 50%',
SCROLL_DEPTH_75: 'Scroll Depth 75%',
SCROLL_DEPTH_100: 'Scroll Depth 100%',
// Session
SESSION_STARTED: 'Session Started',
SESSION_ENDED: 'Session Ended',
USER_IDLE: 'User Idle',
USER_RETURNED: 'User Returned',
// Logout
USER_LOGGED_OUT: 'User Logged Out',
};
// ============================================================================
// USER PROPERTIES (用户属性)
// ============================================================================
export const USER_PROPERTIES = {
// Identity
EMAIL: 'email',
USERNAME: 'username',
USER_ID: 'user_id',
PHONE: 'phone',
// Subscription
SUBSCRIPTION_TIER: 'subscription_tier', // 'free', 'pro', 'enterprise'
SUBSCRIPTION_STATUS: 'subscription_status', // 'active', 'expired', 'cancelled'
SUBSCRIPTION_START_DATE: 'subscription_start_date',
SUBSCRIPTION_END_DATE: 'subscription_end_date',
// Engagement
REGISTRATION_DATE: 'registration_date',
LAST_LOGIN: 'last_login',
LOGIN_COUNT: 'login_count',
DAYS_SINCE_REGISTRATION: 'days_since_registration',
LIFETIME_VALUE: 'lifetime_value',
// Preferences
PREFERRED_LANGUAGE: 'preferred_language',
THEME_PREFERENCE: 'theme_preference', // 'light', 'dark'
NOTIFICATION_ENABLED: 'notification_enabled',
// Attribution
UTM_SOURCE: 'utm_source',
UTM_MEDIUM: 'utm_medium',
UTM_CAMPAIGN: 'utm_campaign',
REFERRER: 'referrer',
// Behavioral
FAVORITE_FEATURES: 'favorite_features',
MOST_VISITED_PAGES: 'most_visited_pages',
TOTAL_SESSIONS: 'total_sessions',
AVERAGE_SESSION_DURATION: 'average_session_duration',
};
// ============================================================================
// SUBSCRIPTION TIERS (订阅等级)
// ============================================================================
export const SUBSCRIPTION_TIERS = {
FREE: 'free',
PRO: 'pro',
ENTERPRISE: 'enterprise',
};
// ============================================================================
// PAGE TYPES (页面类型)
// ============================================================================
export const PAGE_TYPES = {
LANDING: 'landing',
DASHBOARD: 'dashboard',
FEATURE: 'feature',
DETAIL: 'detail',
AUTH: 'auth',
SETTINGS: 'settings',
PAYMENT: 'payment',
ERROR: 'error',
};
// ============================================================================
// CONTENT TYPES (内容类型)
// ============================================================================
export const CONTENT_TYPES = {
NEWS: 'news',
STOCK: 'stock',
CONCEPT: 'concept',
ANALYSIS: 'analysis',
EVENT: 'event',
COMPANY: 'company',
};
// ============================================================================
// SHARE CHANNELS (分享渠道)
// ============================================================================
export const SHARE_CHANNELS = {
WECHAT: 'wechat',
LINK: 'link',
QRCODE: 'qrcode',
EMAIL: 'email',
COPY: 'copy',
};
// ============================================================================
// LOGIN METHODS (登录方式)
// ============================================================================
export const LOGIN_METHODS = {
WECHAT: 'wechat',
EMAIL: 'email',
PHONE: 'phone',
USERNAME: 'username',
};
// ============================================================================
// PAYMENT METHODS (支付方式)
// ============================================================================
export const PAYMENT_METHODS = {
WECHAT_PAY: 'wechat_pay',
ALIPAY: 'alipay',
CREDIT_CARD: 'credit_card',
};
// ============================================================================
// Helper function to get all events
// ============================================================================
export const getAllEvents = () => {
return {
...ACQUISITION_EVENTS,
...ACTIVATION_EVENTS,
...RETENTION_EVENTS,
...REFERRAL_EVENTS,
...REVENUE_EVENTS,
...SPECIAL_EVENTS,
};
};
// ============================================================================
// Helper function to validate event name
// ============================================================================
export const isValidEvent = (eventName) => {
const allEvents = getAllEvents();
return Object.values(allEvents).includes(eventName);
};

View File

@@ -1,271 +0,0 @@
// src/lib/posthog.js
import posthog from 'posthog-js';
/**
* Initialize PostHog SDK
* Should be called once when the app starts
*/
export const initPostHog = () => {
// Only run in browser environment
if (typeof window === 'undefined') return;
const apiKey = process.env.REACT_APP_POSTHOG_KEY;
const apiHost = process.env.REACT_APP_POSTHOG_HOST || 'https://app.posthog.com';
if (!apiKey) {
console.warn('⚠️ PostHog API key not found. Analytics will be disabled.');
return;
}
try {
posthog.init(apiKey, {
api_host: apiHost,
// Pageview tracking - manual control for better accuracy
capture_pageview: false, // We'll manually capture with custom properties
capture_pageleave: true, // Auto-capture when user leaves page
// Session Recording Configuration
session_recording: {
enabled: process.env.REACT_APP_ENABLE_SESSION_RECORDING === 'true',
// Privacy: Mask sensitive input fields
maskInputOptions: {
password: true,
email: true,
phone: true,
'data-sensitive': true, // Custom attribute for sensitive fields
},
// Record canvas for charts/graphs
recordCanvas: true,
// Network payload capture (useful for debugging API issues)
networkPayloadCapture: {
recordHeaders: true,
recordBody: true,
// Don't record sensitive endpoints
urlBlocklist: [
'/api/auth/session',
'/api/auth/login',
'/api/auth/register',
'/api/payment',
],
},
},
// Performance optimization
batch_size: 10, // Send events in batches of 10
batch_interval_ms: 3000, // Or every 3 seconds
// Privacy settings
respect_dnt: true, // Respect Do Not Track browser setting
persistence: 'localStorage+cookie', // Use both for reliability
// Feature flags (for A/B testing)
bootstrap: {
featureFlags: {},
},
// Autocapture settings
autocapture: {
// Automatically capture clicks on buttons, links, etc.
dom_event_allowlist: ['click', 'submit', 'change'],
// Capture additional element properties
capture_copied_text: false, // Don't capture copied text (privacy)
},
// Development debugging
loaded: (posthogInstance) => {
if (process.env.NODE_ENV === 'development') {
console.log('✅ PostHog initialized successfully');
posthogInstance.debug(); // Enable debug mode in development
}
},
});
console.log('📊 PostHog Analytics initialized');
} catch (error) {
console.error('❌ PostHog initialization failed:', error);
}
};
/**
* Get PostHog instance
* @returns {object} PostHog instance
*/
export const getPostHog = () => {
return posthog;
};
/**
* Identify user with PostHog
* Call this after successful login/registration
*
* @param {string} userId - Unique user identifier
* @param {object} userProperties - User properties (email, name, subscription_tier, etc.)
*/
export const identifyUser = (userId, userProperties = {}) => {
if (!userId) {
console.warn('⚠️ Cannot identify user: userId is required');
return;
}
try {
posthog.identify(userId, {
email: userProperties.email,
username: userProperties.username,
subscription_tier: userProperties.subscription_tier || 'free',
role: userProperties.role,
registration_date: userProperties.registration_date,
last_login: new Date().toISOString(),
...userProperties,
});
console.log('👤 User identified:', userId);
} catch (error) {
console.error('❌ User identification failed:', error);
}
};
/**
* Update user properties
* Use this to update user attributes without re-identifying
*
* @param {object} properties - Properties to update
*/
export const setUserProperties = (properties) => {
try {
posthog.people.set(properties);
console.log('📝 User properties updated');
} catch (error) {
console.error('❌ Failed to update user properties:', error);
}
};
/**
* Track custom event
*
* @param {string} eventName - Name of the event
* @param {object} properties - Event properties
*/
export const trackEvent = (eventName, properties = {}) => {
try {
posthog.capture(eventName, {
...properties,
timestamp: new Date().toISOString(),
});
if (process.env.NODE_ENV === 'development') {
console.log('📍 Event tracked:', eventName, properties);
}
} catch (error) {
console.error('❌ Event tracking failed:', error);
}
};
/**
* Track page view
*
* @param {string} pagePath - Current page path
* @param {object} properties - Additional properties
*/
export const trackPageView = (pagePath, properties = {}) => {
try {
posthog.capture('$pageview', {
$current_url: window.location.href,
page_path: pagePath,
page_title: document.title,
referrer: document.referrer,
...properties,
});
if (process.env.NODE_ENV === 'development') {
console.log('📄 Page view tracked:', pagePath);
}
} catch (error) {
console.error('❌ Page view tracking failed:', error);
}
};
/**
* Reset user session
* Call this on logout
*/
export const resetUser = () => {
try {
posthog.reset();
console.log('🔄 User session reset');
} catch (error) {
console.error('❌ Session reset failed:', error);
}
};
/**
* User opt-out from tracking
*/
export const optOut = () => {
try {
posthog.opt_out_capturing();
console.log('🚫 User opted out of tracking');
} catch (error) {
console.error('❌ Opt-out failed:', error);
}
};
/**
* User opt-in to tracking
*/
export const optIn = () => {
try {
posthog.opt_in_capturing();
console.log('✅ User opted in to tracking');
} catch (error) {
console.error('❌ Opt-in failed:', error);
}
};
/**
* Check if user has opted out
* @returns {boolean}
*/
export const hasOptedOut = () => {
try {
return posthog.has_opted_out_capturing();
} catch (error) {
console.error('❌ Failed to check opt-out status:', error);
return false;
}
};
/**
* Get feature flag value
* @param {string} flagKey - Feature flag key
* @param {any} defaultValue - Default value if flag not found
* @returns {any} Feature flag value
*/
export const getFeatureFlag = (flagKey, defaultValue = false) => {
try {
return posthog.getFeatureFlag(flagKey) || defaultValue;
} catch (error) {
console.error('❌ Failed to get feature flag:', error);
return defaultValue;
}
};
/**
* Check if feature flag is enabled
* @param {string} flagKey - Feature flag key
* @returns {boolean}
*/
export const isFeatureEnabled = (flagKey) => {
try {
return posthog.isFeatureEnabled(flagKey);
} catch (error) {
console.error('❌ Failed to check feature flag:', error);
return false;
}
};
export default posthog;

View File

@@ -19,10 +19,7 @@ export async function startMockServiceWorker() {
try {
await worker.start({
// 🎯 智能穿透模式(关键配置
// 'bypass': 未定义 Mock 的请求自动转发到真实后端
// 'warn': 未定义的请求会显示警告(调试用)
// 'error': 未定义的请求会抛出错误(严格模式)
// 不显示未拦截的请求警告(可选
onUnhandledRequest: 'bypass',
// 自定义 Service Worker URL如果需要
@@ -30,7 +27,7 @@ export async function startMockServiceWorker() {
url: '/mockServiceWorker.js',
},
// 是否在控制台显示启动日志和拦截日志 静默模式(不在控制台打印启动消息)
// 静默模式(不在控制台打印启动消息)
quiet: false,
});
@@ -39,11 +36,11 @@ export async function startMockServiceWorker() {
'color: #4CAF50; font-weight: bold; font-size: 14px;'
);
console.log(
'%c智能穿透模式:已定义 Mock → 返回假数据 | 未定义 Mock → 转发到 ' + (process.env.REACT_APP_API_URL || '真实后端'),
'%c提示: 所有 API 请求将使用本地 Mock 数据',
'color: #FF9800; font-size: 12px;'
);
console.log(
'%c查看 src/mocks/handlers/ 目录管理 Mock 接口',
'%c要禁用 Mock请设置 REACT_APP_ENABLE_MOCK=false',
'color: #2196F3; font-size: 12px;'
);
} catch (error) {

View File

@@ -147,6 +147,8 @@ export const WECHAT_STATUS = {
LOGIN_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
REGISTER_SUCCESS: 'authorized', // ✅ 与后端保持一致,统一使用 'authorized'
EXPIRED: 'expired',
AUTH_DENIED: 'auth_denied', // 用户拒绝授权
AUTH_FAILED: 'auth_failed', // 授权失败
};
/**
@@ -157,6 +159,8 @@ export const STATUS_MESSAGES = {
[WECHAT_STATUS.SCANNED]: '扫码成功,请在手机上确认',
[WECHAT_STATUS.AUTHORIZED]: '授权成功,正在登录...',
[WECHAT_STATUS.EXPIRED]: '二维码已过期',
[WECHAT_STATUS.AUTH_DENIED]: '用户取消授权',
[WECHAT_STATUS.AUTH_FAILED]: '授权失败,请重试',
};
export default authService;

View File

@@ -1,25 +1,18 @@
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit';
import communityDataReducer from './slices/communityDataSlice';
import posthogReducer from './slices/posthogSlice';
import posthogMiddleware from './middleware/posthogMiddleware';
export const store = configureStore({
reducer: {
communityData: communityDataReducer,
posthog: posthogReducer, // ✅ PostHog Redux 状态管理
communityData: communityDataReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// 忽略这些 action types 的序列化检查
ignoredActions: [
'communityData/fetchPopularKeywords/fulfilled',
'communityData/fetchHotEvents/fulfilled',
'posthog/trackEvent/fulfilled', // ✅ PostHog 事件追踪
],
ignoredActions: ['communityData/fetchPopularKeywords/fulfilled', 'communityData/fetchHotEvents/fulfilled'],
},
}).concat(posthogMiddleware), // ✅ PostHog 自动追踪中间件
}),
});
export default store;

View File

@@ -1,281 +0,0 @@
// src/store/middleware/posthogMiddleware.js
import { trackPageView } from '../../lib/posthog';
import { trackEvent } from '../slices/posthogSlice';
import { logger } from '../../utils/logger';
import {
ACTIVATION_EVENTS,
RETENTION_EVENTS,
SPECIAL_EVENTS,
REVENUE_EVENTS,
} from '../../lib/constants';
// ==================== 自动追踪规则配置 ====================
/**
* Action 到 PostHog 事件的映射
* 当这些 Redux actions 被 dispatch 时,自动追踪对应的 PostHog 事件
*/
const ACTION_TO_EVENT_MAP = {
// ==================== 登录/登出 ====================
'auth/login/fulfilled': {
event: ACTIVATION_EVENTS.USER_LOGGED_IN,
getProperties: (action) => ({
login_method: action.payload?.login_method || 'unknown',
user_id: action.payload?.user?.id,
}),
},
'auth/logout': {
event: SPECIAL_EVENTS.USER_LOGGED_OUT,
getProperties: () => ({}),
},
'auth/wechatLogin/fulfilled': {
event: ACTIVATION_EVENTS.USER_LOGGED_IN,
getProperties: (action) => ({
login_method: 'wechat',
user_id: action.payload?.user?.id,
}),
},
// ==================== Community/新闻模块 ====================
'communityData/fetchHotEvents/fulfilled': {
event: RETENTION_EVENTS.NEWS_LIST_VIEWED,
getProperties: (action) => ({
event_count: action.payload?.length || 0,
source: 'community_page',
}),
},
'communityData/fetchPopularKeywords/fulfilled': {
event: RETENTION_EVENTS.COMMUNITY_PAGE_VIEWED,
getProperties: () => ({
feature: 'popular_keywords',
}),
},
// ==================== 搜索 ====================
'search/submit': {
event: RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED,
getProperties: (action) => ({
query: action.payload?.query,
category: action.payload?.category,
}),
},
'search/filterApplied': {
event: RETENTION_EVENTS.SEARCH_FILTER_APPLIED,
getProperties: (action) => ({
filter_type: action.payload?.filterType,
filter_value: action.payload?.filterValue,
}),
},
// ==================== 支付/订阅 ====================
'payment/initiated': {
event: REVENUE_EVENTS.PAYMENT_INITIATED,
getProperties: (action) => ({
amount: action.payload?.amount,
payment_method: action.payload?.method,
subscription_tier: action.payload?.tier,
}),
},
'payment/success': {
event: REVENUE_EVENTS.PAYMENT_SUCCESSFUL,
getProperties: (action) => ({
amount: action.payload?.amount,
transaction_id: action.payload?.transactionId,
subscription_tier: action.payload?.tier,
}),
},
'subscription/upgraded': {
event: REVENUE_EVENTS.SUBSCRIPTION_UPGRADED,
getProperties: (action) => ({
from_tier: action.payload?.fromTier,
to_tier: action.payload?.toTier,
}),
},
// ==================== 错误追踪 ====================
'error/occurred': {
event: SPECIAL_EVENTS.ERROR_OCCURRED,
getProperties: (action) => ({
error_type: action.payload?.errorType,
error_message: action.payload?.message,
stack_trace: action.payload?.stack,
}),
},
'api/error': {
event: SPECIAL_EVENTS.API_ERROR,
getProperties: (action) => ({
endpoint: action.payload?.endpoint,
status_code: action.payload?.statusCode,
error_message: action.payload?.message,
}),
},
};
// ==================== 页面路由追踪配置 ====================
/**
* 路由变化的 action type根据不同路由库调整
*/
const LOCATION_CHANGE_ACTIONS = [
'@@router/LOCATION_CHANGE', // Redux-first router
'router/navigate', // 自定义路由 action
];
/**
* 根据路径识别页面类型
*/
const getPageTypeFromPath = (pathname) => {
if (pathname === '/home' || pathname === '/') {
return { page_type: 'landing' };
} else if (pathname.startsWith('/auth/')) {
return { page_type: 'auth' };
} else if (pathname.startsWith('/community')) {
return { page_type: 'feature', feature_name: 'community' };
} else if (pathname.startsWith('/concepts')) {
return { page_type: 'feature', feature_name: 'concepts' };
} else if (pathname.startsWith('/stocks')) {
return { page_type: 'feature', feature_name: 'stocks' };
} else if (pathname.startsWith('/limit-analyse')) {
return { page_type: 'feature', feature_name: 'limit_analyse' };
} else if (pathname.startsWith('/trading-simulation')) {
return { page_type: 'feature', feature_name: 'trading_simulation' };
} else if (pathname.startsWith('/company')) {
return { page_type: 'detail', content_type: 'company' };
} else if (pathname.startsWith('/event-detail')) {
return { page_type: 'detail', content_type: 'event' };
}
return { page_type: 'other' };
};
// ==================== 中间件实现 ====================
/**
* PostHog Middleware
* 自动拦截 Redux actions 并追踪对应的 PostHog 事件
*/
const posthogMiddleware = (store) => (next) => (action) => {
// 先执行 action
const result = next(action);
// 获取当前 PostHog 状态
const state = store.getState();
const posthogState = state.posthog;
// 如果 PostHog 未初始化,不追踪(事件会被缓存到 eventQueue
if (!posthogState?.isInitialized) {
return result;
}
try {
// ==================== 1. 自动追踪特定 actions ====================
if (ACTION_TO_EVENT_MAP[action.type]) {
const { event, getProperties } = ACTION_TO_EVENT_MAP[action.type];
const properties = getProperties(action);
// 通过 dispatch 追踪事件(会走 Redux 状态管理)
store.dispatch(trackEvent({ eventName: event, properties }));
logger.debug('PostHog Middleware', `自动追踪事件: ${event}`, properties);
}
// ==================== 2. 路由变化追踪 ====================
if (LOCATION_CHANGE_ACTIONS.includes(action.type)) {
const location = action.payload?.location || action.payload;
const pathname = location?.pathname || window.location.pathname;
const search = location?.search || window.location.search;
// 识别页面类型
const pageProperties = getPageTypeFromPath(pathname);
// 追踪页面浏览
trackPageView(pathname, {
...pageProperties,
page_path: pathname,
page_search: search,
page_title: document.title,
referrer: document.referrer,
});
logger.debug('PostHog Middleware', `页面浏览追踪: ${pathname}`, pageProperties);
}
// ==================== 3. 离线事件处理 ====================
// 检测网络状态变化
if (action.type === 'network/online') {
// 恢复在线时,刷新缓存的事件
const { eventQueue } = posthogState;
if (eventQueue && eventQueue.length > 0) {
logger.info('PostHog Middleware', `网络恢复,刷新 ${eventQueue.length} 个缓存事件`);
// 这里可以 dispatch flushCachedEvents但为了避免循环依赖直接在 slice 中处理
}
}
// ==================== 4. 性能追踪(可选) ====================
// 追踪耗时较长的 actions
const startTime = action.meta?.startTime;
if (startTime) {
const duration = Date.now() - startTime;
if (duration > 1000) {
// 超过 1 秒的操作
store.dispatch(trackEvent({
eventName: SPECIAL_EVENTS.PAGE_LOAD_TIME,
properties: {
action_type: action.type,
duration_ms: duration,
is_slow: true,
},
}));
}
}
} catch (error) {
logger.error('PostHog Middleware', '追踪失败', error, { actionType: action.type });
}
return result;
};
// ==================== 工具函数 ====================
/**
* 创建带性能追踪的 action creator
* 用法: dispatch(withTiming(someAction(payload)))
*/
export const withTiming = (action) => ({
...action,
meta: {
...action.meta,
startTime: Date.now(),
},
});
/**
* 手动触发页面浏览追踪
* 用于非路由跳转的场景(如 Modal、Tab 切换)
*/
export const trackModalView = (modalName, properties = {}) => (dispatch) => {
dispatch(trackEvent({
eventName: '$pageview',
properties: {
modal_name: modalName,
page_type: 'modal',
...properties,
},
}));
};
/**
* 追踪 Tab 切换
*/
export const trackTabChange = (tabName, properties = {}) => (dispatch) => {
dispatch(trackEvent({
eventName: RETENTION_EVENTS.NEWS_TAB_CLICKED,
properties: {
tab_name: tabName,
...properties,
},
}));
};
// ==================== Export ====================
export default posthogMiddleware;

View File

@@ -1,299 +0,0 @@
// src/store/slices/posthogSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import {
initPostHog,
identifyUser as posthogIdentifyUser,
resetUser as posthogResetUser,
trackEvent as posthogTrackEvent,
getFeatureFlag as posthogGetFeatureFlag,
optIn as posthogOptIn,
optOut as posthogOptOut,
hasOptedOut as posthogHasOptedOut
} from '../../lib/posthog';
import { logger } from '../../utils/logger';
// ==================== Initial State ====================
const initialState = {
// 初始化状态
isInitialized: false,
initError: null,
// 用户信息
user: null,
// 事件队列(用于离线缓存)
eventQueue: [],
// Feature Flags
featureFlags: {},
// 配置
config: {
apiKey: process.env.REACT_APP_POSTHOG_KEY || null,
apiHost: process.env.REACT_APP_POSTHOG_HOST || 'https://app.posthog.com',
sessionRecording: process.env.REACT_APP_ENABLE_SESSION_RECORDING === 'true',
},
// 统计
stats: {
totalEvents: 0,
lastEventTime: null,
},
};
// ==================== Async Thunks ====================
/**
* 初始化 PostHog SDK
*/
export const initializePostHog = createAsyncThunk(
'posthog/initialize',
async (_, { getState, rejectWithValue }) => {
try {
const { config } = getState().posthog;
if (!config.apiKey) {
logger.warn('PostHog', '未配置 API Key分析功能将被禁用');
return { isInitialized: false, warning: 'No API Key' };
}
// 调用 PostHog SDK 初始化
initPostHog();
logger.info('PostHog', 'Redux 初始化成功');
return { isInitialized: true };
} catch (error) {
logger.error('PostHog', '初始化失败', error);
return rejectWithValue(error.message);
}
}
);
/**
* 识别用户
*/
export const identifyUser = createAsyncThunk(
'posthog/identifyUser',
async ({ userId, userProperties }, { rejectWithValue }) => {
try {
posthogIdentifyUser(userId, userProperties);
logger.info('PostHog', '用户已识别', { userId });
return { userId, userProperties };
} catch (error) {
logger.error('PostHog', '用户识别失败', error);
return rejectWithValue(error.message);
}
}
);
/**
* 重置用户会话(登出)
*/
export const resetUser = createAsyncThunk(
'posthog/resetUser',
async (_, { rejectWithValue }) => {
try {
posthogResetUser();
logger.info('PostHog', '用户会话已重置');
return {};
} catch (error) {
logger.error('PostHog', '重置用户会话失败', error);
return rejectWithValue(error.message);
}
}
);
/**
* 追踪事件
*/
export const trackEvent = createAsyncThunk(
'posthog/trackEvent',
async ({ eventName, properties = {} }, { getState, rejectWithValue }) => {
try {
const { isInitialized } = getState().posthog;
if (!isInitialized) {
logger.warn('PostHog', 'PostHog 未初始化,事件将被缓存', { eventName });
return { eventName, properties, cached: true };
}
posthogTrackEvent(eventName, properties);
return {
eventName,
properties,
timestamp: new Date().toISOString(),
cached: false
};
} catch (error) {
logger.error('PostHog', '追踪事件失败', error, { eventName });
return rejectWithValue(error.message);
}
}
);
/**
* 获取所有 Feature Flags
*/
export const fetchFeatureFlags = createAsyncThunk(
'posthog/fetchFeatureFlags',
async (_, { rejectWithValue }) => {
try {
// PostHog SDK 会在初始化时自动获取 feature flags
// 这里只是读取缓存的值
const flags = {};
logger.info('PostHog', 'Feature Flags 已更新');
return flags;
} catch (error) {
logger.error('PostHog', '获取 Feature Flags 失败', error);
return rejectWithValue(error.message);
}
}
);
/**
* 刷新缓存的离线事件
*/
export const flushCachedEvents = createAsyncThunk(
'posthog/flushCachedEvents',
async (_, { getState, dispatch }) => {
try {
const { eventQueue, isInitialized } = getState().posthog;
if (!isInitialized || eventQueue.length === 0) {
return { flushed: 0 };
}
logger.info('PostHog', `刷新 ${eventQueue.length} 个缓存事件`);
// 批量发送缓存的事件
for (const { eventName, properties } of eventQueue) {
dispatch(trackEvent({ eventName, properties }));
}
return { flushed: eventQueue.length };
} catch (error) {
logger.error('PostHog', '刷新缓存事件失败', error);
return { flushed: 0, error: error.message };
}
}
);
// ==================== Slice ====================
const posthogSlice = createSlice({
name: 'posthog',
initialState,
reducers: {
// 设置 Feature Flag
setFeatureFlag: (state, action) => {
const { flagKey, value } = action.payload;
state.featureFlags[flagKey] = value;
},
// 清空事件队列
clearEventQueue: (state) => {
state.eventQueue = [];
},
// 更新配置
updateConfig: (state, action) => {
state.config = { ...state.config, ...action.payload };
},
// 用户 Opt-in
optIn: (state) => {
posthogOptIn();
logger.info('PostHog', '用户已选择加入追踪');
},
// 用户 Opt-out
optOut: (state) => {
posthogOptOut();
logger.info('PostHog', '用户已选择退出追踪');
},
},
extraReducers: (builder) => {
// 初始化
builder.addCase(initializePostHog.fulfilled, (state, action) => {
state.isInitialized = action.payload.isInitialized;
state.initError = null;
});
builder.addCase(initializePostHog.rejected, (state, action) => {
state.isInitialized = false;
state.initError = action.payload;
});
// 识别用户
builder.addCase(identifyUser.fulfilled, (state, action) => {
state.user = {
userId: action.payload.userId,
...action.payload.userProperties,
};
});
// 重置用户
builder.addCase(resetUser.fulfilled, (state) => {
state.user = null;
state.featureFlags = {};
});
// 追踪事件
builder.addCase(trackEvent.fulfilled, (state, action) => {
const { eventName, properties, timestamp, cached } = action.payload;
// 如果事件被缓存,添加到队列
if (cached) {
state.eventQueue.push({ eventName, properties, timestamp });
} else {
// 更新统计
state.stats.totalEvents += 1;
state.stats.lastEventTime = timestamp;
}
});
// 刷新缓存事件
builder.addCase(flushCachedEvents.fulfilled, (state, action) => {
if (action.payload.flushed > 0) {
state.eventQueue = [];
state.stats.totalEvents += action.payload.flushed;
}
});
// 获取 Feature Flags
builder.addCase(fetchFeatureFlags.fulfilled, (state, action) => {
state.featureFlags = action.payload;
});
},
});
// ==================== Actions ====================
export const {
setFeatureFlag,
clearEventQueue,
updateConfig,
optIn,
optOut,
} = posthogSlice.actions;
// ==================== Selectors ====================
export const selectPostHog = (state) => state.posthog;
export const selectIsInitialized = (state) => state.posthog.isInitialized;
export const selectUser = (state) => state.posthog.user;
export const selectFeatureFlags = (state) => state.posthog.featureFlags;
export const selectEventQueue = (state) => state.posthog.eventQueue;
export const selectStats = (state) => state.posthog.stats;
export const selectFeatureFlag = (flagKey) => (state) => {
return state.posthog.featureFlags[flagKey] || posthogGetFeatureFlag(flagKey);
};
export const selectIsOptedOut = () => posthogHasOptedOut();
// ==================== Export ====================
export default posthogSlice.reducer;

View File

@@ -2,21 +2,11 @@
import React from 'react';
import { Card, Input, Radio, Form, Button } from 'antd';
import { SearchOutlined } from '@ant-design/icons';
import { useSearchEvents } from '../../../hooks/useSearchEvents';
const SearchBox = ({ onSearch }) => {
const [form] = Form.useForm();
// 🎯 初始化搜索埋点Hook
const searchEvents = useSearchEvents({ context: 'community' });
const handleSubmit = (values) => {
// 🎯 追踪搜索查询提交在调用onSearch之前
if (values.q) {
searchEvents.trackSearchQuerySubmitted(values.q, 0, {
search_type: values.search_type || 'topic'
});
}
onSearch(values);
};

View File

@@ -1,281 +0,0 @@
// src/views/Community/hooks/useCommunityEvents.js
// 新闻催化分析页面事件追踪 Hook
import { useCallback, useEffect } from 'react';
import { usePostHogTrack } from '../../../hooks/usePostHogRedux';
import { RETENTION_EVENTS } from '../../../lib/constants';
import { logger } from '../../../utils/logger';
/**
* 新闻催化分析Community事件追踪 Hook
* @param {Object} options - 配置选项
* @param {Function} options.navigate - 路由导航函数
* @returns {Object} 事件追踪处理函数集合
*/
export const useCommunityEvents = ({ navigate } = {}) => {
const { track } = usePostHogTrack();
// 🎯 页面浏览事件 - 页面加载时触发
useEffect(() => {
track(RETENTION_EVENTS.COMMUNITY_PAGE_VIEWED, {
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '📰 Community Page Viewed');
}, [track]);
/**
* 追踪新闻列表查看
* @param {Object} params - 列表参数
* @param {number} params.totalCount - 新闻总数
* @param {string} params.sortBy - 排序方式 ('new' | 'hot' | 'returns')
* @param {string} params.importance - 重要性筛选 ('all' | 'high' | 'medium' | 'low')
* @param {string} params.dateRange - 日期范围
* @param {string} params.industryFilter - 行业筛选
*/
const trackNewsListViewed = useCallback((params = {}) => {
track(RETENTION_EVENTS.NEWS_LIST_VIEWED, {
total_count: params.totalCount || 0,
sort_by: params.sortBy || 'new',
importance_filter: params.importance || 'all',
date_range: params.dateRange || 'all',
industry_filter: params.industryFilter || 'all',
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '📋 News List Viewed', params);
}, [track]);
/**
* 追踪新闻文章点击
* @param {Object} news - 新闻对象
* @param {number} news.id - 新闻ID
* @param {string} news.title - 新闻标题
* @param {string} news.importance - 重要性等级
* @param {number} position - 在列表中的位置
* @param {string} source - 点击来源 ('list' | 'search' | 'recommendation')
*/
const trackNewsArticleClicked = useCallback((news, position = 0, source = 'list') => {
if (!news || !news.id) {
logger.warn('useCommunityEvents', 'trackNewsArticleClicked: news object is required');
return;
}
track(RETENTION_EVENTS.NEWS_ARTICLE_CLICKED, {
news_id: news.id,
news_title: news.title || '',
importance: news.importance || 'unknown',
position,
source,
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '🖱️ News Article Clicked', {
id: news.id,
position,
source,
});
}, [track]);
/**
* 追踪新闻详情打开
* @param {Object} news - 新闻对象
* @param {number} news.id - 新闻ID
* @param {string} news.title - 新闻标题
* @param {string} news.importance - 重要性等级
* @param {string} viewMode - 查看模式 ('modal' | 'page')
*/
const trackNewsDetailOpened = useCallback((news, viewMode = 'modal') => {
if (!news || !news.id) {
logger.warn('useCommunityEvents', 'trackNewsDetailOpened: news object is required');
return;
}
track(RETENTION_EVENTS.NEWS_DETAIL_OPENED, {
news_id: news.id,
news_title: news.title || '',
importance: news.importance || 'unknown',
view_mode: viewMode,
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '📖 News Detail Opened', {
id: news.id,
viewMode,
});
}, [track]);
/**
* 追踪新闻标签页切换
* @param {string} tabName - 标签名称 ('related_stocks' | 'related_concepts' | 'timeline')
* @param {number} newsId - 新闻ID
*/
const trackNewsTabClicked = useCallback((tabName, newsId = null) => {
if (!tabName) {
logger.warn('useCommunityEvents', 'trackNewsTabClicked: tabName is required');
return;
}
track(RETENTION_EVENTS.NEWS_TAB_CLICKED, {
tab_name: tabName,
news_id: newsId,
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '📑 News Tab Clicked', {
tabName,
newsId,
});
}, [track]);
/**
* 追踪新闻筛选应用
* @param {Object} filters - 筛选条件
* @param {string} filters.importance - 重要性筛选
* @param {string} filters.dateRange - 日期范围
* @param {string} filters.industryClassification - 行业分类
* @param {string} filters.industryCode - 行业代码
*/
const trackNewsFilterApplied = useCallback((filters = {}) => {
track(RETENTION_EVENTS.NEWS_FILTER_APPLIED, {
importance: filters.importance || 'all',
date_range: filters.dateRange || 'all',
industry_classification: filters.industryClassification || 'all',
industry_code: filters.industryCode || 'all',
filter_count: Object.keys(filters).filter(key => filters[key] && filters[key] !== 'all').length,
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '🔍 News Filter Applied', filters);
}, [track]);
/**
* 追踪新闻排序方式变更
* @param {string} sortBy - 排序方式 ('new' | 'hot' | 'returns')
* @param {string} previousSort - 之前的排序方式
*/
const trackNewsSorted = useCallback((sortBy, previousSort = 'new') => {
if (!sortBy) {
logger.warn('useCommunityEvents', 'trackNewsSorted: sortBy is required');
return;
}
track(RETENTION_EVENTS.NEWS_SORTED, {
sort_by: sortBy,
previous_sort: previousSort,
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '🔄 News Sorted', {
sortBy,
previousSort,
});
}, [track]);
/**
* 追踪搜索事件(新闻搜索)
* @param {string} query - 搜索关键词
* @param {number} resultCount - 搜索结果数量
*/
const trackNewsSearched = useCallback((query, resultCount = 0) => {
if (!query) return;
track(RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED, {
query,
result_count: resultCount,
has_results: resultCount > 0,
context: 'community_news',
timestamp: new Date().toISOString(),
});
// 如果没有搜索结果,额外追踪
if (resultCount === 0) {
track(RETENTION_EVENTS.SEARCH_NO_RESULTS, {
query,
context: 'community_news',
timestamp: new Date().toISOString(),
});
}
logger.debug('useCommunityEvents', '🔍 News Searched', {
query,
resultCount,
});
}, [track]);
/**
* 追踪相关股票点击(从新闻详情)
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
* @param {number} newsId - 关联的新闻ID
*/
const trackRelatedStockClicked = useCallback((stock, newsId = null) => {
if (!stock || !stock.code) {
logger.warn('useCommunityEvents', 'trackRelatedStockClicked: stock object is required');
return;
}
track(RETENTION_EVENTS.STOCK_CLICKED, {
stock_code: stock.code,
stock_name: stock.name || '',
source: 'news_related_stocks',
news_id: newsId,
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '🎯 Related Stock Clicked', {
stockCode: stock.code,
newsId,
});
}, [track]);
/**
* 追踪相关概念点击(从新闻详情)
* @param {Object} concept - 概念对象
* @param {string} concept.code - 概念代码
* @param {string} concept.name - 概念名称
* @param {number} newsId - 关联的新闻ID
*/
const trackRelatedConceptClicked = useCallback((concept, newsId = null) => {
if (!concept || !concept.code) {
logger.warn('useCommunityEvents', 'trackRelatedConceptClicked: concept object is required');
return;
}
track(RETENTION_EVENTS.CONCEPT_CLICKED, {
concept_code: concept.code,
concept_name: concept.name || '',
source: 'news_related_concepts',
news_id: newsId,
timestamp: new Date().toISOString(),
});
logger.debug('useCommunityEvents', '🏷️ Related Concept Clicked', {
conceptCode: concept.code,
newsId,
});
}, [track]);
return {
// 页面级事件
trackNewsListViewed,
// 新闻交互事件
trackNewsArticleClicked,
trackNewsDetailOpened,
trackNewsTabClicked,
// 筛选和排序事件
trackNewsFilterApplied,
trackNewsSorted,
// 搜索事件
trackNewsSearched,
// 关联内容点击事件
trackRelatedStockClicked,
trackRelatedConceptClicked,
};
};
export default useCommunityEvents;

View File

@@ -4,8 +4,6 @@
import { useState, useCallback } from 'react';
import { useSearchParams } from 'react-router-dom';
import { logger } from '../../../utils/logger';
import { usePostHogTrack } from '../../../hooks/usePostHogRedux';
import { RETENTION_EVENTS } from '../../../lib/constants';
/**
* 事件筛选逻辑 Hook
@@ -17,7 +15,6 @@ import { RETENTION_EVENTS } from '../../../lib/constants';
*/
export const useEventFilters = ({ navigate, onEventClick, eventTimelineRef } = {}) => {
const [searchParams] = useSearchParams();
const { track } = usePostHogTrack(); // PostHog 追踪
// 筛选参数状态 - 初始化时从URL读取之后只用本地状态
const [filters, setFilters] = useState(() => {
@@ -38,68 +35,12 @@ export const useEventFilters = ({ navigate, onEventClick, eventTimelineRef } = {
oldFilters: filters,
timestamp: new Date().toISOString()
});
// 🎯 PostHog 追踪:搜索查询
if (newFilters.q !== filters.q && newFilters.q) {
track(RETENTION_EVENTS.SEARCH_QUERY_SUBMITTED, {
query: newFilters.q,
category: 'news',
previous_query: filters.q || null,
});
}
// 🎯 PostHog 追踪:排序变化
if (newFilters.sort !== filters.sort) {
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
filter_type: 'sort',
filter_value: newFilters.sort,
previous_value: filters.sort,
});
}
// 🎯 PostHog 追踪:重要性筛选
if (newFilters.importance !== filters.importance) {
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
filter_type: 'importance',
filter_value: newFilters.importance,
previous_value: filters.importance,
});
}
// 🎯 PostHog 追踪:时间范围筛选
if (newFilters.date_range !== filters.date_range && newFilters.date_range) {
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
filter_type: 'date_range',
filter_value: newFilters.date_range,
previous_value: filters.date_range || null,
});
}
// 🎯 PostHog 追踪:行业筛选
if (newFilters.industry_code !== filters.industry_code && newFilters.industry_code) {
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
filter_type: 'industry',
filter_value: newFilters.industry_code,
previous_value: filters.industry_code || null,
});
}
setFilters(newFilters);
logger.debug('useEventFilters', '✅ setFilters 已调用 (React异步更新中...)');
}, [filters, track]);
}, [filters]);
// 处理分页变化
const handlePageChange = useCallback((page) => {
// 🎯 PostHog 追踪:翻页
track(RETENTION_EVENTS.NEWS_LIST_VIEWED, {
page,
filters: {
sort: filters.sort,
importance: filters.importance,
has_query: !!filters.q,
},
});
// 保持现有筛选条件,只更新页码
updateFilters({ ...filters, page });
@@ -112,37 +53,21 @@ export const useEventFilters = ({ navigate, onEventClick, eventTimelineRef } = {
});
}, 100); // 延迟100ms确保DOM更新
}
}, [filters, updateFilters, eventTimelineRef, track]);
}, [filters, updateFilters, eventTimelineRef]);
// 处理事件点击
const handleEventClick = useCallback((event) => {
// 🎯 PostHog 追踪:新闻事件点击
track(RETENTION_EVENTS.NEWS_ARTICLE_CLICKED, {
event_id: event.id || event.event_id,
event_title: event.title,
importance: event.importance,
source: 'community_page',
has_stocks: !!(event.related_stocks && event.related_stocks.length > 0),
has_concepts: !!(event.related_concepts && event.related_concepts.length > 0),
});
if (onEventClick) {
onEventClick(event);
}
}, [onEventClick, track]);
}, [onEventClick]);
// 处理查看详情
const handleViewDetail = useCallback((eventId) => {
// 🎯 PostHog 追踪:查看详情
track(RETENTION_EVENTS.NEWS_DETAIL_OPENED, {
event_id: eventId,
source: 'community_page',
});
if (navigate) {
navigate(`/event-detail/${eventId}`);
}
}, [navigate, track]);
}, [navigate]);
return {
filters,

View File

@@ -17,19 +17,15 @@ 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';
import { usePostHogTrack } from '../../hooks/usePostHogRedux';
import { RETENTION_EVENTS } from '../../lib/constants';
// 导航栏已由 MainLayout 提供,无需在此导入
const Community = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const { track } = usePostHogTrack(); // PostHog 追踪(保留用于兼容)
// Redux状态
const { popularKeywords, hotEvents } = useSelector(state => state.communityData);
@@ -48,9 +44,6 @@ 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,
@@ -66,28 +59,6 @@ const Community = () => {
dispatch(fetchHotEvents());
}, [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(() => {
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(() => {
if (showCommunityGuide) {

View File

@@ -1,103 +0,0 @@
// 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,8 +34,6 @@ 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();
@@ -44,18 +42,7 @@ 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');
@@ -99,9 +86,6 @@ const CompanyIndex = () => {
const handleSearch = () => {
if (inputCode && inputCode !== stockCode) {
// 🎯 追踪股票搜索
trackStockSearched(inputCode, stockCode);
setStockCode(inputCode);
setSearchParams({ scode: inputCode });
}
@@ -139,10 +123,6 @@ 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 {
@@ -160,10 +140,6 @@ 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 });
}
@@ -250,18 +226,7 @@ const CompanyIndex = () => {
{/* 数据展示区域 */}
<Card bg={bgColor} shadow="lg">
<CardBody p={0}>
<Tabs
variant="soft-rounded"
colorScheme="blue"
size="lg"
index={currentTabIndex}
onChange={(index) => {
const tabNames = ['公司概览', '股票行情', '财务全景', '盈利预测'];
// 🎯 追踪 Tab 切换
trackTabChanged(index, tabNames[index], currentTabIndex);
setCurrentTabIndex(index);
}}
>
<Tabs variant="soft-rounded" colorScheme="blue" size="lg">
<TabList p={4} bg={tabBg}>
<Tab
_selected={{

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import { logger } from '../../utils/logger';
import { useConceptTimelineEvents } from './hooks/useConceptTimelineEvents';
import {
Modal,
ModalOverlay,
@@ -65,17 +64,6 @@ const ConceptTimelineModal = ({
conceptId
}) => {
const toast = useToast();
// 🎯 PostHog 事件追踪
const {
trackDateToggled,
trackNewsClicked,
trackNewsDetailOpened,
trackReportClicked,
trackReportDetailOpened,
trackModalClosed,
} = useConceptTimelineEvents({ conceptName, conceptId, isOpen });
const [timelineData, setTimelineData] = useState([]);
const [loading, setLoading] = useState(true);
const [expandedDates, setExpandedDates] = useState({});
@@ -330,11 +318,6 @@ const ConceptTimelineModal = ({
// 切换日期展开状态
const toggleDateExpand = (date) => {
const willExpand = !expandedDates[date];
// 🎯 追踪日期展开/折叠
trackDateToggled(date, willExpand);
setExpandedDates(prev => ({
...prev,
[date]: !prev[date]
@@ -745,10 +728,6 @@ const ConceptTimelineModal = ({
leftIcon={<ViewIcon />}
onClick={() => {
if (event.type === 'news') {
// 🎯 追踪新闻点击和详情打开
trackNewsClicked(event, date);
trackNewsDetailOpened(event);
setSelectedNews({
title: event.title,
content: event.content,
@@ -758,10 +737,6 @@ const ConceptTimelineModal = ({
});
setIsNewsModalOpen(true);
} else if (event.type === 'report') {
// 🎯 追踪研报点击和详情打开
trackReportClicked(event, date);
trackReportDetailOpened(event);
setSelectedReport({
title: event.title,
content: event.content,

View File

@@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import { logger } from '../../../utils/logger';
import { useConceptStatsEvents } from '../hooks/useConceptStatsEvents';
import {
Box,
SimpleGrid,
@@ -55,15 +54,6 @@ const ConceptStatsPanel = ({ apiBaseUrl, onConceptClick }) => {
? '/concept-api'
: 'http://111.198.58.126:16801';
// 🎯 PostHog 事件追踪
const {
trackTabChanged,
trackTimeRangeChanged,
trackCustomDateRangeSet,
trackRankItemClicked,
trackDataRefreshed,
} = useConceptStatsEvents();
const [statsData, setStatsData] = useState({});
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState(0);
@@ -190,18 +180,10 @@ const ConceptStatsPanel = ({ apiBaseUrl, onConceptClick }) => {
setCustomEndDate(today.toISOString().split('T')[0]);
setCustomStartDate(weekAgo.toISOString().split('T')[0]);
// 🎯 追踪切换到自定义范围
trackTimeRangeChanged(0, true);
} else {
setUseCustomRange(false);
const days = parseInt(newRange);
setTimeRange(days);
// 🎯 追踪时间范围变化
trackTimeRangeChanged(days, false);
fetchStatsData(days);
setTimeRange(parseInt(newRange));
fetchStatsData(parseInt(newRange));
}
};
@@ -217,10 +199,6 @@ const ConceptStatsPanel = ({ apiBaseUrl, onConceptClick }) => {
});
return;
}
// 🎯 追踪自定义日期范围设置
trackCustomDateRangeSet(customStartDate, customEndDate);
fetchStatsData(null, customStartDate, customEndDate);
}
};
@@ -870,17 +848,7 @@ const ConceptStatsPanel = ({ apiBaseUrl, onConceptClick }) => {
{/* 主内容卡片 */}
<Box bg={bg} borderRadius="xl" border="1px" borderColor={borderColor} shadow="sm" overflow="hidden">
<Tabs
index={activeTab}
onChange={(index) => {
const tabNames = ['涨幅榜', '跌幅榜', '活跃榜', '波动榜', '连涨榜'];
// 🎯 追踪Tab切换
trackTabChanged(index, tabNames[index]);
setActiveTab(index);
}}
variant="unstyled"
size="sm"
>
<Tabs index={activeTab} onChange={setActiveTab} variant="unstyled" size="sm">
<TabList
bg="gray.50"
borderBottom="1px"

View File

@@ -1,292 +0,0 @@
// 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,
};
};

View File

@@ -90,8 +90,6 @@ 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'
@@ -131,18 +129,6 @@ 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);
@@ -206,9 +192,6 @@ const ConceptCenter = () => {
return;
}
// 🎯 追踪历史时间轴查看
trackConceptTimelineViewed(conceptName, conceptId);
setSelectedConceptForContent(conceptName);
setSelectedConceptId(conceptId);
setIsTimelineModalOpen(true);
@@ -335,14 +318,8 @@ const ConceptCenter = () => {
setSortBy('change_pct');
}
// 🎯 追踪搜索查询在fetchConcepts后追踪结果数量
updateUrlParams({ q: searchQuery, page: 1, sort: newSortBy });
fetchConcepts(searchQuery, 1, selectedDate, newSortBy).then(() => {
if (searchQuery && searchQuery.trim() !== '') {
// 使用当前 concepts.length 作为结果数量
setTimeout(() => trackConceptSearched(searchQuery, concepts.length), 100);
}
});
fetchConcepts(searchQuery, 1, selectedDate, newSortBy);
};
// 处理Enter键搜索
@@ -354,11 +331,6 @@ const ConceptCenter = () => {
// 处理排序变化
const handleSortChange = (value) => {
const previousSort = sortBy;
// 🎯 追踪排序变化
trackFilterApplied('sort', value, previousSort);
setSortBy(value);
setCurrentPage(1);
updateUrlParams({ sort: value, page: 1 });
@@ -368,11 +340,6 @@ 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 });
@@ -392,9 +359,6 @@ 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);
@@ -402,12 +366,7 @@ const ConceptCenter = () => {
};
// 处理概念点击
const handleConceptClick = (conceptId, conceptName, concept = null, position = 0) => {
// 🎯 追踪概念点击
if (concept) {
trackConceptClicked(concept, position);
}
const handleConceptClick = (conceptId, conceptName) => {
const htmlPath = `https://valuefrontier.cn/htmls/${encodeURIComponent(conceptName)}.html`;
window.open(htmlPath, '_blank');
};
@@ -474,9 +433,6 @@ const ConceptCenter = () => {
return;
}
// 🎯 追踪查看个股
trackConceptStocksViewed(concept.concept, concept.stocks?.length || 0);
setSelectedConceptStocks(concept.stocks || []);
setSelectedConceptName(concept.concept);
setStockMarketData({}); // 清空之前的数据
@@ -693,7 +649,7 @@ const ConceptCenter = () => {
}, []);
// 概念卡片组件 - 优化版
const ConceptCard = ({ concept, position = 0 }) => {
const ConceptCard = ({ concept }) => {
const changePercent = concept.price_info?.avg_change_pct;
const changeColor = getChangeColor(changePercent);
const hasChange = changePercent !== null && changePercent !== undefined;
@@ -701,7 +657,7 @@ const ConceptCenter = () => {
return (
<Card
cursor="pointer"
onClick={() => handleConceptClick(concept.concept_id, concept.concept, concept, position)}
onClick={() => handleConceptClick(concept.concept_id, concept.concept)}
bg="white"
borderWidth="1px"
borderColor="gray.200"
@@ -901,7 +857,7 @@ const ConceptCenter = () => {
};
// 概念列表项组件 - 列表视图
const ConceptListItem = ({ concept, position = 0 }) => {
const ConceptListItem = ({ concept }) => {
const changePercent = concept.price_info?.avg_change_pct;
const changeColor = getChangeColor(changePercent);
const hasChange = changePercent !== null && changePercent !== undefined;
@@ -909,7 +865,7 @@ const ConceptCenter = () => {
return (
<Card
cursor="pointer"
onClick={() => handleConceptClick(concept.concept_id, concept.concept, concept, position)}
onClick={() => handleConceptClick(concept.concept_id, concept.concept)}
bg="white"
borderWidth="1px"
borderColor="gray.200"
@@ -1405,12 +1361,7 @@ const ConceptCenter = () => {
<ButtonGroup size="sm" isAttached variant="outline">
<IconButton
icon={<FaThLarge />}
onClick={() => {
if (viewMode !== 'grid') {
trackViewModeChanged('grid', viewMode);
setViewMode('grid');
}
}}
onClick={() => setViewMode('grid')}
bg={viewMode === 'grid' ? 'purple.500' : 'transparent'}
color={viewMode === 'grid' ? 'white' : 'purple.500'}
borderColor="purple.500"
@@ -1419,12 +1370,7 @@ const ConceptCenter = () => {
/>
<IconButton
icon={<FaList />}
onClick={() => {
if (viewMode !== 'list') {
trackViewModeChanged('list', viewMode);
setViewMode('list');
}
}}
onClick={() => setViewMode('list')}
bg={viewMode === 'list' ? 'purple.500' : 'transparent'}
color={viewMode === 'list' ? 'white' : 'purple.500'}
borderColor="purple.500"
@@ -1458,16 +1404,16 @@ const ConceptCenter = () => {
<>
{viewMode === 'grid' ? (
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6} className="concept-grid">
{concepts.map((concept, index) => (
{concepts.map((concept) => (
<Box key={concept.concept_id} className="concept-item" role="group">
<ConceptCard concept={concept} position={index} />
<ConceptCard concept={concept} />
</Box>
))}
</SimpleGrid>
) : (
<VStack spacing={4} align="stretch" className="concept-list">
{concepts.map((concept, index) => (
<ConceptListItem key={concept.concept_id} concept={concept} position={index} />
{concepts.map((concept) => (
<ConceptListItem key={concept.concept_id} concept={concept} />
))}
</VStack>
)}

View File

@@ -2,7 +2,6 @@
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,
@@ -73,12 +72,6 @@ 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');
@@ -108,33 +101,14 @@ export default function CenterDashboard() {
const je = await e.json();
const jc = await c.json();
if (jw.success) {
const watchlistData = Array.isArray(jw.data) ? jw.data : [];
setWatchlist(watchlistData);
// 🎯 追踪自选股列表查看
if (watchlistData.length > 0) {
dashboardEvents.trackWatchlistViewed(watchlistData.length, true);
}
setWatchlist(Array.isArray(jw.data) ? jw.data : []);
// 加载实时行情
if (jw.data && jw.data.length > 0) {
loadRealtimeQuotes();
}
}
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);
}
if (je.success) setFollowingEvents(Array.isArray(je.data) ? je.data : []);
if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []);
} catch (err) {
logger.error('Center', 'loadData', err, {
userId,

View File

@@ -1,346 +0,0 @@
// src/views/EventDetail/hooks/useEventDetailEvents.js
// 事件详情页面事件追踪 Hook
import { useCallback, useEffect } from 'react';
import { usePostHogTrack } from '../../../hooks/usePostHogRedux';
import { RETENTION_EVENTS } from '../../../lib/constants';
import { logger } from '../../../utils/logger';
/**
* 事件详情EventDetail事件追踪 Hook
* @param {Object} options - 配置选项
* @param {Object} options.event - 事件对象
* @param {number} options.event.id - 事件ID
* @param {string} options.event.title - 事件标题
* @param {string} options.event.importance - 重要性等级
* @param {Function} options.navigate - 路由导航函数
* @returns {Object} 事件追踪处理函数集合
*/
export const useEventDetailEvents = ({ event, navigate } = {}) => {
const { track } = usePostHogTrack();
// 🎯 页面浏览事件 - 页面加载时触发
useEffect(() => {
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for page view tracking');
return;
}
track(RETENTION_EVENTS.EVENT_DETAIL_VIEWED, {
event_id: event.id,
event_title: event.title || '',
importance: event.importance || 'unknown',
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '📄 Event Detail Page Viewed', {
eventId: event.id,
});
}, [track, event]);
/**
* 追踪事件分析内容查看
* @param {Object} analysisData - 分析数据
* @param {string} analysisData.type - 分析类型 ('market_impact' | 'stock_correlation' | 'timeline')
* @param {number} analysisData.relatedStockCount - 相关股票数量
* @param {number} analysisData.timelineEventCount - 时间线事件数量
*/
const trackEventAnalysisViewed = useCallback((analysisData = {}) => {
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for analysis tracking');
return;
}
track(RETENTION_EVENTS.EVENT_ANALYSIS_VIEWED, {
event_id: event.id,
analysis_type: analysisData.type || 'overview',
related_stock_count: analysisData.relatedStockCount || 0,
timeline_event_count: analysisData.timelineEventCount || 0,
has_market_impact: Boolean(analysisData.marketImpact),
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '📊 Event Analysis Viewed', {
eventId: event.id,
analysisType: analysisData.type,
});
}, [track, event]);
/**
* 追踪事件时间线点击
* @param {Object} timelineItem - 时间线项目
* @param {string} timelineItem.id - 时间线项目ID
* @param {string} timelineItem.date - 时间线日期
* @param {string} timelineItem.title - 时间线标题
* @param {number} position - 在时间线中的位置
*/
const trackEventTimelineClicked = useCallback((timelineItem, position = 0) => {
if (!timelineItem || !timelineItem.id) {
logger.warn('useEventDetailEvents', 'Timeline item is required');
return;
}
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for timeline tracking');
return;
}
track(RETENTION_EVENTS.EVENT_TIMELINE_CLICKED, {
event_id: event.id,
timeline_item_id: timelineItem.id,
timeline_date: timelineItem.date || '',
timeline_title: timelineItem.title || '',
position,
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '⏰ Event Timeline Clicked', {
eventId: event.id,
timelineItemId: timelineItem.id,
position,
});
}, [track, event]);
/**
* 追踪相关股票点击(从事件详情)
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
* @param {number} position - 在列表中的位置
*/
const trackRelatedStockClicked = useCallback((stock, position = 0) => {
if (!stock || !stock.code) {
logger.warn('useEventDetailEvents', 'Stock object is required');
return;
}
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for stock tracking');
return;
}
track(RETENTION_EVENTS.STOCK_CLICKED, {
stock_code: stock.code,
stock_name: stock.name || '',
source: 'event_detail_related_stocks',
event_id: event.id,
position,
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '🎯 Related Stock Clicked', {
stockCode: stock.code,
eventId: event.id,
position,
});
}, [track, event]);
/**
* 追踪相关概念点击(从事件详情)
* @param {Object} concept - 概念对象
* @param {string} concept.code - 概念代码
* @param {string} concept.name - 概念名称
* @param {number} position - 在列表中的位置
*/
const trackRelatedConceptClicked = useCallback((concept, position = 0) => {
if (!concept || !concept.code) {
logger.warn('useEventDetailEvents', 'Concept object is required');
return;
}
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for concept tracking');
return;
}
track(RETENTION_EVENTS.CONCEPT_CLICKED, {
concept_code: concept.code,
concept_name: concept.name || '',
source: 'event_detail_related_concepts',
event_id: event.id,
position,
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '🏷️ Related Concept Clicked', {
conceptCode: concept.code,
eventId: event.id,
position,
});
}, [track, event]);
/**
* 追踪标签页切换
* @param {string} tabName - 标签名称 ('overview' | 'related_stocks' | 'related_concepts' | 'timeline')
*/
const trackTabClicked = useCallback((tabName) => {
if (!tabName) {
logger.warn('useEventDetailEvents', 'Tab name is required');
return;
}
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for tab tracking');
return;
}
track(RETENTION_EVENTS.NEWS_TAB_CLICKED, {
tab_name: tabName,
event_id: event.id,
context: 'event_detail',
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '📑 Tab Clicked', {
tabName,
eventId: event.id,
});
}, [track, event]);
/**
* 追踪事件收藏/取消收藏
* @param {boolean} isFavorited - 是否收藏
*/
const trackEventFavoriteToggled = useCallback((isFavorited) => {
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for favorite tracking');
return;
}
const eventName = isFavorited ? 'Event Favorited' : 'Event Unfavorited';
track(eventName, {
event_id: event.id,
event_title: event.title || '',
action: isFavorited ? 'add' : 'remove',
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', `${isFavorited ? '⭐' : '☆'} Event Favorite Toggled`, {
eventId: event.id,
isFavorited,
});
}, [track, event]);
/**
* 追踪事件分享
* @param {string} shareMethod - 分享方式 ('wechat' | 'link' | 'qrcode')
*/
const trackEventShared = useCallback((shareMethod) => {
if (!shareMethod) {
logger.warn('useEventDetailEvents', 'Share method is required');
return;
}
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for share tracking');
return;
}
track(RETENTION_EVENTS.CONTENT_SHARED, {
content_type: 'event',
content_id: event.id,
content_title: event.title || '',
share_method: shareMethod,
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '📤 Event Shared', {
eventId: event.id,
shareMethod,
});
}, [track, event]);
/**
* 追踪评论点赞/取消点赞
* @param {string} commentId - 评论ID
* @param {boolean} isLiked - 是否点赞
*/
const trackCommentLiked = useCallback((commentId, isLiked) => {
if (!commentId) {
logger.warn('useEventDetailEvents', 'Comment ID is required');
return;
}
track(isLiked ? 'Comment Liked' : 'Comment Unliked', {
comment_id: commentId,
event_id: event?.id,
action: isLiked ? 'like' : 'unlike',
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', `${isLiked ? '❤️' : '🤍'} Comment ${isLiked ? 'Liked' : 'Unliked'}`, {
commentId,
eventId: event?.id,
});
}, [track, event]);
/**
* 追踪添加评论
* @param {string} commentId - 评论ID
* @param {number} contentLength - 评论内容长度
*/
const trackCommentAdded = useCallback((commentId, contentLength = 0) => {
if (!event || !event.id) {
logger.warn('useEventDetailEvents', 'Event object is required for comment tracking');
return;
}
track('Comment Added', {
comment_id: commentId,
event_id: event.id,
content_length: contentLength,
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '💬 Comment Added', {
commentId,
eventId: event.id,
contentLength,
});
}, [track, event]);
/**
* 追踪删除评论
* @param {string} commentId - 评论ID
*/
const trackCommentDeleted = useCallback((commentId) => {
if (!commentId) {
logger.warn('useEventDetailEvents', 'Comment ID is required');
return;
}
track('Comment Deleted', {
comment_id: commentId,
event_id: event?.id,
timestamp: new Date().toISOString(),
});
logger.debug('useEventDetailEvents', '🗑️ Comment Deleted', {
commentId,
eventId: event?.id,
});
}, [track, event]);
return {
// 页面级事件
trackEventAnalysisViewed,
// 交互事件
trackEventTimelineClicked,
trackRelatedStockClicked,
trackRelatedConceptClicked,
trackTabClicked,
// 用户行为事件
trackEventFavoriteToggled,
trackEventShared,
// 社交互动事件
trackCommentLiked,
trackCommentAdded,
trackCommentDeleted,
};
};
export default useEventDetailEvents;

View File

@@ -75,7 +75,6 @@ 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') {
@@ -111,7 +110,7 @@ const StatCard = ({ icon, label, value, color }) => {
};
// 帖子组件
const PostItem = ({ post, onRefresh, eventEvents }) => {
const PostItem = ({ post, onRefresh }) => {
const [showComments, setShowComments] = useState(false);
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState('');
@@ -146,14 +145,8 @@ const PostItem = ({ post, onRefresh, eventEvents }) => {
try {
const result = await eventService.likePost(post.id);
if (result.success) {
const newLikedState = result.liked;
setLiked(newLikedState);
setLiked(result.liked);
setLikesCount(result.likes_count);
// 🎯 追踪评论点赞
if (eventEvents && eventEvents.trackCommentLiked) {
eventEvents.trackCommentLiked(post.id, newLikedState);
}
}
} catch (error) {
toast({
@@ -173,14 +166,6 @@ const PostItem = ({ post, onRefresh, eventEvents }) => {
});
if (result.success) {
// 🎯 追踪添加评论
if (eventEvents && eventEvents.trackCommentAdded) {
eventEvents.trackCommentAdded(
result.data?.id || post.id,
newComment.length
);
}
toast({
title: '评论发表成功',
status: 'success',
@@ -207,11 +192,6 @@ const PostItem = ({ post, onRefresh, eventEvents }) => {
try {
const result = await eventService.deletePost(post.id);
if (result.success) {
// 🎯 追踪删除评论
if (eventEvents && eventEvents.trackCommentDeleted) {
eventEvents.trackCommentDeleted(post.id);
}
toast({
title: '删除成功',
status: 'success',
@@ -368,15 +348,6 @@ 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);
@@ -409,11 +380,9 @@ 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([]);
@@ -430,25 +399,13 @@ 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 || '加载事件数据失败');
@@ -843,12 +800,7 @@ const EventDetail = () => {
</VStack>
) : posts.length > 0 ? (
posts.map((post) => (
<PostItem
key={post.id}
post={post}
onRefresh={loadPosts}
eventEvents={eventEvents}
/>
<PostItem key={post.id} post={post} onRefresh={loadPosts} />
))
) : (
<Box

View File

@@ -1,5 +1,5 @@
// src/views/Home/HomePage.js - 专业投资分析平台
import React, { useEffect, useCallback } from 'react';
import React, { useEffect } from 'react';
import {
Box,
Container,
@@ -21,13 +21,10 @@ import heroBg from '../../assets/img/BackgroundCard1.png';
import '../../styles/home-animations.css';
import { logger } from '../../utils/logger';
import MidjourneyHeroSection from '../Community/components/MidjourneyHeroSection';
import { usePostHogTrack } from '../../hooks/usePostHogRedux';
import { ACQUISITION_EVENTS } from '../../lib/constants';
export default function HomePage() {
const { user, isAuthenticated } = useAuth(); // ⚡ 移除 isLoading不再依赖它
const navigate = useNavigate();
const { track } = usePostHogTrack(); // PostHog 追踪
const [imageLoaded, setImageLoaded] = React.useState(false);
// 响应式配置
@@ -49,15 +46,6 @@ export default function HomePage() {
});
}, [user?.id, isAuthenticated]); // 只依赖 user.id,避免无限循环
// 🎯 PostHog 追踪:页面浏览
useEffect(() => {
track(ACQUISITION_EVENTS.LANDING_PAGE_VIEWED, {
timestamp: new Date().toISOString(),
is_authenticated: isAuthenticated,
user_id: user?.id || null,
});
}, [track, isAuthenticated, user?.id]);
// 核心功能配置 - 5个主要功能
const coreFeatures = [
{
@@ -118,25 +106,15 @@ export default function HomePage() {
];
// @TODO 如何区分内部链接和外部链接?
const handleProductClick = useCallback((feature) => {
// 🎯 PostHog 追踪:功能卡片点击
track(ACQUISITION_EVENTS.FEATURE_CARD_CLICKED, {
feature_id: feature.id,
feature_title: feature.title,
feature_url: feature.url,
is_featured: feature.featured || false,
link_type: feature.url.startsWith('http') ? 'external' : 'internal',
});
// 原有导航逻辑
if (feature.url.startsWith('http')) {
const handleProductClick = (url) => {
if (url.startsWith('http')) {
// 外部链接,直接打开
window.open(feature.url, '_blank');
window.open(url, '_blank');
} else {
// 内部路由
navigate(feature.url);
navigate(url);
}
}, [track, navigate]);
};
return (
<Box>
@@ -295,7 +273,7 @@ export default function HomePage() {
borderRadius="full"
fontWeight="bold"
w={{ base: '100%', md: 'auto' }}
onClick={() => handleProductClick(coreFeatures[0])}
onClick={() => handleProductClick(coreFeatures[0].url)}
minH="44px"
flexShrink={0}
>
@@ -327,7 +305,7 @@ export default function HomePage() {
borderColor: `${feature.color}.400`,
transform: 'translateY(-2px)'
}}
onClick={() => handleProductClick(feature)}
onClick={() => handleProductClick(feature.url)}
minH={{ base: 'auto', md: '180px' }}
>
<CardBody p={{ base: 5, md: 6 }}>
@@ -365,7 +343,7 @@ export default function HomePage() {
minH="44px"
onClick={(e) => {
e.stopPropagation();
handleProductClick(feature);
handleProductClick(feature.url);
}}
>
使用

View File

@@ -1,252 +0,0 @@
// 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,7 +48,6 @@ 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() {
@@ -63,21 +62,6 @@ 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');
@@ -142,9 +126,6 @@ export default function LimitAnalyse() {
if (data.success) {
setDailyData(data.data);
// 🎯 追踪每日统计数据查看
trackDailyStatsViewed(data.data, date);
// 获取词云数据
fetchWordCloudData(date);
@@ -188,26 +169,14 @@ 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`, {

View File

@@ -44,15 +44,11 @@ import {
import { EditIcon, CheckIcon, CloseIcon, AddIcon } from '@chakra-ui/icons';
import { useAuth } from '../../contexts/AuthContext';
import { logger } from '../../utils/logger';
import { useProfileEvents } from '../../hooks/useProfileEvents';
export default function ProfilePage() {
const { user, updateUser } = useAuth();
const [isEditing, setIsEditing] = useState(false);
const [isLoading, setIsLoading] = useState(false);
// 🎯 初始化个人资料埋点Hook
const profileEvents = useProfileEvents({ pageType: 'profile' });
const [newTag, setNewTag] = useState('');
const { isOpen, onOpen, onClose } = useDisclosure();
const fileInputRef = useRef();
@@ -99,12 +95,6 @@ export default function ProfilePage() {
updateUser(updatedData);
setIsEditing(false);
// 🎯 追踪个人资料更新成功
const updatedFields = Object.keys(formData).filter(
key => user?.[key] !== formData[key]
);
profileEvents.trackProfileUpdated(updatedFields, updatedData);
// ✅ 保留关键操作提示
toast({
title: "个人资料更新成功",
@@ -115,10 +105,6 @@ export default function ProfilePage() {
} catch (error) {
logger.error('ProfilePage', 'handleSaveProfile', error, { userId: user?.id });
// 🎯 追踪个人资料更新失败
const attemptedFields = Object.keys(formData);
profileEvents.trackProfileUpdateFailed(attemptedFields, error.message);
// ✅ 保留错误提示
toast({
title: "更新失败",
@@ -142,9 +128,6 @@ export default function ProfilePage() {
reader.onload = (e) => {
updateUser({ avatar_url: e.target.result });
// 🎯 追踪头像上传
profileEvents.trackAvatarUploaded('file_upload', file.size);
// ✅ 保留关键操作提示
toast({
title: "头像更新成功",

View File

@@ -59,16 +59,12 @@ import { FaWeixin, FaMobile, FaEnvelope } from 'react-icons/fa';
import { useAuth } from '../../contexts/AuthContext';
import { getApiBase } from '../../utils/apiConfig';
import { logger } from '../../utils/logger';
import { useProfileEvents } from '../../hooks/useProfileEvents';
export default function SettingsPage() {
const { user, updateUser, logout } = useAuth();
const { colorMode, toggleColorMode } = useColorMode();
const toast = useToast();
// 🎯 初始化设置页面埋点Hook
const profileEvents = useProfileEvents({ pageType: 'settings' });
// 模态框状态
const { isOpen: isPasswordOpen, onOpen: onPasswordOpen, onClose: onPasswordClose } = useDisclosure();
const { isOpen: isPhoneOpen, onOpen: onPhoneOpen, onClose: onPhoneClose } = useDisclosure();
@@ -213,12 +209,9 @@ export default function SettingsPage() {
if (response.ok && data.success) {
const isFirstSet = passwordStatus.needsFirstTimeSetup;
// 🎯 追踪密码修改成功
profileEvents.trackPasswordChanged(true);
toast({
title: isFirstSet ? "密码设置成功" : "密码修改成功",
title: isFirstSet ? "密码设置成功" : "密码修改成功",
description: isFirstSet ? "您现在可以使用手机号+密码登录了" : "请重新登录",
status: "success",
duration: 3000,
@@ -227,7 +220,7 @@ export default function SettingsPage() {
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
onPasswordClose();
// 刷新密码状态
fetchPasswordStatus();
@@ -241,9 +234,6 @@ export default function SettingsPage() {
throw new Error(data.error || '密码修改失败');
}
} catch (error) {
// 🎯 追踪密码修改失败
profileEvents.trackPasswordChanged(false, error.message);
toast({
title: "修改失败",
description: error.message,
@@ -374,9 +364,6 @@ export default function SettingsPage() {
email_confirmed: data.user.email_confirmed
});
// 🎯 追踪邮箱绑定成功
profileEvents.trackAccountBound('email', true);
toast({
title: "邮箱绑定成功",
status: "success",
@@ -387,9 +374,6 @@ export default function SettingsPage() {
setEmailForm({ email: '', verificationCode: '' });
onEmailClose();
} catch (error) {
// 🎯 追踪邮箱绑定失败
profileEvents.trackAccountBound('email', false);
toast({
title: "绑定失败",
description: error.message,
@@ -413,13 +397,6 @@ export default function SettingsPage() {
updateUser(notifications);
// 🎯 追踪通知偏好更改
profileEvents.trackNotificationPreferencesChanged({
email: notifications.email_notifications,
push: notifications.system_updates,
sms: notifications.sms_notifications
});
// ❌ 移除设置保存成功toast
logger.info('SettingsPage', '通知设置已保存');
} catch (error) {

View File

@@ -1,236 +0,0 @@
// src/views/StockOverview/hooks/useStockOverviewEvents.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 useStockOverviewEvents = ({ navigate } = {}) => {
const { track } = usePostHogTrack();
// 🎯 页面浏览事件 - 页面加载时触发
useEffect(() => {
track(RETENTION_EVENTS.STOCK_OVERVIEW_VIEWED, {
timestamp: new Date().toISOString(),
});
logger.debug('useStockOverviewEvents', '📊 Stock Overview Page Viewed');
}, [track]);
/**
* 追踪市场统计数据查看
* @param {Object} stats - 市场统计数据
*/
const trackMarketStatsViewed = useCallback((stats) => {
if (!stats) return;
track(RETENTION_EVENTS.STOCK_LIST_VIEWED, {
total_market_cap: stats.total_market_cap,
total_volume: stats.total_volume,
rising_stocks: stats.rising_count,
falling_stocks: stats.falling_count,
data_date: stats.date,
});
logger.debug('useStockOverviewEvents', '📈 Market Statistics Viewed', stats);
}, [track]);
/**
* 追踪股票搜索开始
*/
const trackSearchInitiated = useCallback(() => {
track(RETENTION_EVENTS.SEARCH_INITIATED, {
context: 'stock_overview',
});
logger.debug('useStockOverviewEvents', '🔍 Search Initiated');
}, [track]);
/**
* 追踪股票搜索查询
* @param {string} query - 搜索查询词
* @param {number} resultCount - 搜索结果数量
*/
const trackStockSearched = useCallback((query, resultCount = 0) => {
if (!query) return;
track(RETENTION_EVENTS.STOCK_SEARCHED, {
query,
result_count: resultCount,
has_results: resultCount > 0,
});
// 如果没有搜索结果,额外追踪
if (resultCount === 0) {
track(RETENTION_EVENTS.SEARCH_NO_RESULTS, {
query,
context: 'stock_overview',
});
}
logger.debug('useStockOverviewEvents', '🔍 Stock Searched', {
query,
resultCount,
});
}, [track]);
/**
* 追踪搜索结果点击
* @param {Object} stock - 被点击的股票对象
* @param {number} position - 在搜索结果中的位置
*/
const trackSearchResultClicked = useCallback((stock, position = 0) => {
track(RETENTION_EVENTS.SEARCH_RESULT_CLICKED, {
stock_code: stock.code,
stock_name: stock.name,
exchange: stock.exchange,
position,
context: 'stock_overview',
});
logger.debug('useStockOverviewEvents', '🎯 Search Result Clicked', {
stock: stock.code,
position,
});
}, [track]);
/**
* 追踪概念卡片点击
* @param {Object} concept - 概念对象
* @param {number} rank - 在列表中的排名
*/
const trackConceptClicked = useCallback((concept, rank = 0) => {
track(RETENTION_EVENTS.CONCEPT_CLICKED, {
concept_name: concept.name,
concept_code: concept.code,
change_percent: concept.change_percent,
stock_count: concept.stock_count,
rank,
source: 'daily_hot_concepts',
});
logger.debug('useStockOverviewEvents', '🔥 Concept Clicked', {
concept: concept.name,
rank,
});
}, [track]);
/**
* 追踪概念下的股票标签点击
* @param {Object} stock - 股票对象
* @param {string} conceptName - 所属概念名称
*/
const trackConceptStockClicked = useCallback((stock, conceptName) => {
track(RETENTION_EVENTS.CONCEPT_STOCK_CLICKED, {
stock_code: stock.code,
stock_name: stock.name,
concept_name: conceptName,
source: 'daily_hot_concepts_tag',
});
logger.debug('useStockOverviewEvents', '🏷️ Concept Stock Tag Clicked', {
stock: stock.code,
concept: conceptName,
});
}, [track]);
/**
* 追踪热力图中股票点击
* @param {Object} stock - 被点击的股票对象
* @param {string} marketCapRange - 市值区间
*/
const trackHeatmapStockClicked = useCallback((stock, marketCapRange = '') => {
track(RETENTION_EVENTS.STOCK_CLICKED, {
stock_code: stock.code,
stock_name: stock.name,
change_percent: stock.change_percent,
market_cap_range: marketCapRange,
source: 'market_heatmap',
});
logger.debug('useStockOverviewEvents', '📊 Heatmap Stock Clicked', {
stock: stock.code,
marketCapRange,
});
}, [track]);
/**
* 追踪股票详情查看
* @param {string} stockCode - 股票代码
* @param {string} source - 来源search/concept/heatmap
*/
const trackStockDetailViewed = useCallback((stockCode, source = 'unknown') => {
track(RETENTION_EVENTS.STOCK_DETAIL_VIEWED, {
stock_code: stockCode,
source: `stock_overview_${source}`,
});
logger.debug('useStockOverviewEvents', '👁️ Stock Detail Viewed', {
stockCode,
source,
});
// 导航到公司详情页
if (navigate) {
navigate(`/company/${stockCode}`);
}
}, [track, navigate]);
/**
* 追踪概念详情查看
* @param {string} conceptCode - 概念代码
*/
const trackConceptDetailViewed = useCallback((conceptCode) => {
track(RETENTION_EVENTS.CONCEPT_DETAIL_VIEWED, {
concept_code: conceptCode,
source: 'stock_overview_daily_hot',
});
logger.debug('useStockOverviewEvents', '🎯 Concept Detail Viewed', {
conceptCode,
});
// 导航到概念详情页
if (navigate) {
navigate(`/concept-detail/${conceptCode}`);
}
}, [track, navigate]);
/**
* 追踪日期选择变化
* @param {string} newDate - 新选择的日期
* @param {string} previousDate - 之前的日期
*/
const trackDateChanged = useCallback((newDate, previousDate = null) => {
track(RETENTION_EVENTS.SEARCH_FILTER_APPLIED, {
filter_type: 'date',
filter_value: newDate,
previous_value: previousDate,
context: 'stock_overview',
});
logger.debug('useStockOverviewEvents', '📅 Date Changed', {
newDate,
previousDate,
});
}, [track]);
return {
trackMarketStatsViewed,
trackSearchInitiated,
trackStockSearched,
trackSearchResultClicked,
trackConceptClicked,
trackConceptStockClicked,
trackHeatmapStockClicked,
trackStockDetailViewed,
trackConceptDetailViewed,
trackDateChanged,
};
};

View File

@@ -61,7 +61,6 @@ import { BsGraphUp, BsLightningFill } from 'react-icons/bs';
import { keyframes } from '@emotion/react';
import * as echarts from 'echarts';
import { logger } from '../../utils/logger';
import { useStockOverviewEvents } from './hooks/useStockOverviewEvents';
// Navigation bar now provided by MainLayout
// import HomeNavbar from '../../components/Navbars/HomeNavbar';
@@ -84,20 +83,6 @@ const StockOverview = () => {
const heatmapRef = useRef(null);
const heatmapChart = useRef(null);
// 🎯 事件追踪 Hook
const {
trackMarketStatsViewed,
trackSearchInitiated,
trackStockSearched,
trackSearchResultClicked,
trackConceptClicked,
trackConceptStockClicked,
trackHeatmapStockClicked,
trackStockDetailViewed,
trackConceptDetailViewed,
trackDateChanged,
} = useStockOverviewEvents({ navigate });
// 状态管理
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
@@ -156,18 +141,11 @@ const StockOverview = () => {
});
if (data.success) {
const results = data.data || [];
setSearchResults(results);
setSearchResults(data.data || []);
setShowResults(true);
// 🎯 追踪搜索查询
trackStockSearched(query, results.length);
} else {
logger.warn('StockOverview', '搜索失败', data.error || '请稍后重试', { query });
// ❌ 移除搜索失败 toast非关键操作
// 🎯 追踪搜索无结果
trackStockSearched(query, 0);
}
} catch (error) {
logger.error('StockOverview', 'searchStocks', error, { query });
@@ -241,23 +219,18 @@ const StockOverview = () => {
const data = await response.json();
if (data.success) {
const newStats = {
setMarketStats(prevStats => ({
...data.summary,
// 保留之前从 heatmap 接口获取的上涨/下跌家数
rising_count: prevStats?.rising_count,
falling_count: prevStats?.falling_count,
date: data.trade_date
};
setMarketStats(newStats);
falling_count: prevStats?.falling_count
}));
setAvailableDates(data.available_dates || []);
if (!selectedDate) setSelectedDate(data.trade_date);
logger.debug('StockOverview', '市场统计数据加载成功', {
date: data.trade_date,
availableDatesCount: data.available_dates?.length || 0
});
// 🎯 追踪市场统计数据查看
trackMarketStatsViewed(newStats);
}
} catch (error) {
logger.error('StockOverview', 'fetchMarketStats', error, { date });
@@ -430,16 +403,6 @@ const StockOverview = () => {
heatmapChart.current.on('click', function(params) {
// 只有点击个股有code的节点才跳转
if (params.data && params.data.code && !params.data.children) {
const stock = {
code: params.data.code,
name: params.data.name,
change_percent: params.data.change
};
const marketCapRange = getMarketCapRange(params.data.value);
// 🎯 追踪热力图股票点击
trackHeatmapStockClicked(stock, marketCapRange);
navigate(`/company?scode=${params.data.code}`);
}
});
@@ -449,7 +412,7 @@ const StockOverview = () => {
});
// ❌ 移除热力图渲染失败 toast非关键操作
}
}, [colorMode, goldColor, navigate, trackHeatmapStockClicked]); // ✅ 添加追踪函数依赖
}, [colorMode, goldColor, navigate]); // ✅ 移除 toast 依赖
// 获取市值区间
const getMarketCapRange = (cap) => {
@@ -464,12 +427,6 @@ const StockOverview = () => {
const handleSearchChange = (e) => {
const value = e.target.value;
setSearchQuery(value);
// 🎯 追踪搜索开始(首次输入时)
if (value && !searchQuery) {
trackSearchInitiated();
}
debounceSearch(value);
};
@@ -481,30 +438,19 @@ const StockOverview = () => {
};
// 选择股票
const handleSelectStock = (stock, index = 0) => {
// 🎯 追踪搜索结果点击
trackSearchResultClicked(stock, index);
const handleSelectStock = (stock) => {
navigate(`/company?scode=${stock.stock_code}`);
handleClearSearch();
};
// 查看概念详情模仿概念中心打开对应HTML页
const handleConceptClick = (concept, rank = 0) => {
// 🎯 追踪概念点击
trackConceptClicked(concept, rank);
const htmlPath = `/htmls/${concept.concept_name}.html`;
const handleConceptClick = (conceptId, conceptName) => {
const htmlPath = `/htmls/${conceptName}.html`;
window.open(htmlPath, '_blank');
};
// 处理日期选择
const handleDateChange = (date) => {
const previousDate = selectedDate;
// 🎯 追踪日期变化
trackDateChanged(date, previousDate);
setSelectedDate(date);
setIsCalendarOpen(false);
// 重新获取数据
@@ -715,7 +661,7 @@ const StockOverview = () => {
p={4}
cursor="pointer"
_hover={{ bg: hoverBg }}
onClick={() => handleSelectStock(stock, index)}
onClick={() => handleSelectStock(stock)}
borderBottomWidth={index < searchResults.length - 1 ? "1px" : "0"}
borderColor={borderColor}
>
@@ -934,7 +880,7 @@ const StockOverview = () => {
}}
transition="all 0.3s"
cursor="pointer"
onClick={() => handleConceptClick(concept, index)}
onClick={() => handleConceptClick(concept.concept_id, concept.concept_name)}
position="relative"
overflow="hidden"
>
@@ -1005,13 +951,6 @@ const StockOverview = () => {
cursor="pointer"
onClick={(e) => {
e.stopPropagation();
// 🎯 追踪概念下的股票标签点击
trackConceptStockClicked({
code: stock.stock_code,
name: stock.stock_name
}, concept.concept_name);
navigate(`/company?scode=${stock.stock_code}`);
}}
>
@@ -1030,7 +969,7 @@ const StockOverview = () => {
rightIcon={<FaChevronRight />}
onClick={(e) => {
e.stopPropagation();
handleConceptClick(concept, index);
handleConceptClick(concept.concept_id, concept.concept_name);
}}
>
查看详情

View File

@@ -28,9 +28,7 @@ import { FiTrendingUp, FiTrendingDown, FiDollarSign, FiPieChart, FiTarget, FiAct
import DonutChart from '../../../components/Charts/DonutChart';
import IconBox from '../../../components/Icons/IconBox';
export default function AccountOverview({ account, tradingEvents }) {
// tradingEvents 已传递,可用于将来添加的账户重置等功能
// 例如: tradingEvents.trackAccountReset(beforeResetData)
export default function AccountOverview({ account }) {
const textColor = useColorModeValue('gray.700', 'white');
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
const profitColor = account?.totalProfit >= 0 ? 'green.500' : 'red.500';

View File

@@ -64,38 +64,20 @@ const calculateChange = (currentPrice, avgPrice) => {
return { change, changePercent };
};
export default function PositionsList({ positions, account, onSellStock, tradingEvents }) {
export default function PositionsList({ positions, account, onSellStock }) {
const [selectedPosition, setSelectedPosition] = useState(null);
const [sellQuantity, setSellQuantity] = useState(0);
const [orderType, setOrderType] = useState('MARKET');
const [limitPrice, setLimitPrice] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [hasTracked, setHasTracked] = React.useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();
const toast = useToast();
const cardBg = useColorModeValue('white', 'gray.800');
const textColor = useColorModeValue('gray.700', 'white');
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
// 🎯 追踪持仓查看 - 组件加载时触发一次
React.useEffect(() => {
if (!hasTracked && positions && positions.length > 0 && tradingEvents && tradingEvents.trackSimulationHoldingsViewed) {
const totalMarketValue = positions.reduce((sum, pos) => sum + (pos.marketValue || pos.quantity * pos.currentPrice || 0), 0);
const totalCost = positions.reduce((sum, pos) => sum + (pos.totalCost || pos.quantity * pos.avgPrice || 0), 0);
const totalProfit = positions.reduce((sum, pos) => sum + (pos.profit || 0), 0);
tradingEvents.trackSimulationHoldingsViewed({
count: positions.length,
totalValue: totalMarketValue,
totalCost,
profitLoss: totalProfit,
});
setHasTracked(true);
}
}, [positions, tradingEvents, hasTracked]);
// 格式化货币
const formatCurrency = (amount) => {
return new Intl.NumberFormat('zh-CN', {
@@ -120,17 +102,6 @@ export default function PositionsList({ positions, account, onSellStock, trading
setSelectedPosition(position);
setSellQuantity(position.availableQuantity); // 默认全部可卖数量
setLimitPrice(position.currentPrice?.toString() || position.avgPrice.toString());
// 🎯 追踪卖出按钮点击
if (tradingEvents && tradingEvents.trackSellButtonClicked) {
tradingEvents.trackSellButtonClicked({
stockCode: position.stockCode,
stockName: position.stockName,
quantity: position.quantity,
profitLoss: position.profit || 0,
}, 'holdings');
}
onOpen();
};
@@ -139,8 +110,6 @@ export default function PositionsList({ positions, account, onSellStock, trading
if (!selectedPosition || sellQuantity <= 0) return;
setIsLoading(true);
const price = orderType === 'LIMIT' ? parseFloat(limitPrice) : selectedPosition.currentPrice || selectedPosition.avgPrice;
try {
const result = await onSellStock(
selectedPosition.stockCode,
@@ -157,20 +126,6 @@ export default function PositionsList({ positions, account, onSellStock, trading
orderType,
orderId: result.orderId
});
// 🎯 追踪卖出成功
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
tradingEvents.trackSimulationOrderPlaced({
stockCode: selectedPosition.stockCode,
stockName: selectedPosition.stockName,
direction: 'sell',
quantity: sellQuantity,
price,
orderType,
success: true,
});
}
toast({
title: '卖出成功',
description: `已卖出 ${selectedPosition.stockName} ${sellQuantity}`,
@@ -187,21 +142,6 @@ export default function PositionsList({ positions, account, onSellStock, trading
quantity: sellQuantity,
orderType
});
// 🎯 追踪卖出失败
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
tradingEvents.trackSimulationOrderPlaced({
stockCode: selectedPosition.stockCode,
stockName: selectedPosition.stockName,
direction: 'sell',
quantity: sellQuantity,
price,
orderType,
success: false,
errorMessage: error.message,
});
}
toast({
title: '卖出失败',
description: error.message,

View File

@@ -34,31 +34,18 @@ import {
import { FiSearch, FiFilter, FiClock, FiTrendingUp, FiTrendingDown } from 'react-icons/fi';
import { logger } from '../../../utils/logger';
export default function TradingHistory({ history, onCancelOrder, tradingEvents }) {
export default function TradingHistory({ history, onCancelOrder }) {
const [filterType, setFilterType] = useState('ALL'); // ALL, BUY, SELL
const [filterStatus, setFilterStatus] = useState('ALL'); // ALL, FILLED, PENDING, CANCELLED
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('createdAt'); // createdAt, stockCode, amount
const [sortOrder, setSortOrder] = useState('desc'); // desc, asc
const [hasTracked, setHasTracked] = React.useState(false);
const toast = useToast();
const cardBg = useColorModeValue('white', 'gray.800');
const textColor = useColorModeValue('gray.700', 'white');
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
// 🎯 追踪历史记录查看 - 组件加载时触发一次
React.useEffect(() => {
if (!hasTracked && history && history.length > 0 && tradingEvents && tradingEvents.trackSimulationHistoryViewed) {
tradingEvents.trackSimulationHistoryViewed({
count: history.length,
filterBy: 'all',
dateRange: 'all',
});
setHasTracked(true);
}
}, [history, tradingEvents, hasTracked]);
// 格式化货币
const formatCurrency = (amount) => {
return new Intl.NumberFormat('zh-CN', {

View File

@@ -55,7 +55,7 @@ import { FiSearch, FiTrendingUp, FiTrendingDown, FiDollarSign, FiZap, FiTarget }
// 导入现有的高质量组件
import IconBox from '../../../components/Icons/IconBox';
export default function TradingPanel({ account, onBuyStock, onSellStock, searchStocks, tradingEvents }) {
export default function TradingPanel({ account, onBuyStock, onSellStock, searchStocks }) {
const [activeTab, setActiveTab] = useState(0); // 0: 买入, 1: 卖出
const [searchTerm, setSearchTerm] = useState('');
const [selectedStock, setSelectedStock] = useState(null);
@@ -87,7 +87,7 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
const results = await searchStocks(searchTerm);
// 转换为组件需要的格式
const formattedResults = results.map(stock => [
stock.stock_code,
stock.stock_code,
{
name: stock.stock_name,
price: stock.current_price || 0, // 使用后端返回的真实价格
@@ -97,20 +97,10 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
]);
setFilteredStocks(formattedResults);
setShowStockList(true);
// 🎯 追踪股票搜索
if (tradingEvents && tradingEvents.trackSimulationStockSearched) {
tradingEvents.trackSimulationStockSearched(searchTerm, formattedResults.length);
}
} catch (error) {
logger.error('TradingPanel', 'handleStockSearch', error, { searchTerm });
setFilteredStocks([]);
setShowStockList(false);
// 🎯 追踪搜索无结果
if (tradingEvents && tradingEvents.trackSimulationStockSearched) {
tradingEvents.trackSimulationStockSearched(searchTerm, 0);
}
}
} else {
setFilteredStocks([]);
@@ -119,7 +109,7 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
}, 300); // 300ms 防抖
return () => clearTimeout(searchDebounced);
}, [searchTerm, searchStocks, tradingEvents]);
}, [searchTerm, searchStocks]);
// 选择股票
const handleSelectStock = (code, stock) => {
@@ -179,9 +169,6 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
if (!validateForm()) return;
setIsLoading(true);
const price = orderType === 'LIMIT' ? parseFloat(limitPrice) : selectedStock.price;
const direction = activeTab === 0 ? 'buy' : 'sell';
try {
let result;
if (activeTab === 0) {
@@ -210,19 +197,6 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
orderType
});
// 🎯 追踪下单成功
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
tradingEvents.trackSimulationOrderPlaced({
stockCode: selectedStock.code,
stockName: selectedStock.name,
direction,
quantity,
price,
orderType,
success: true,
});
}
// ✅ 保留交易成功toast关键用户操作反馈
toast({
title: activeTab === 0 ? '买入成功' : '卖出成功',
@@ -243,20 +217,6 @@ export default function TradingPanel({ account, onBuyStock, onSellStock, searchS
orderType
});
// 🎯 追踪下单失败
if (tradingEvents && tradingEvents.trackSimulationOrderPlaced) {
tradingEvents.trackSimulationOrderPlaced({
stockCode: selectedStock.code,
stockName: selectedStock.name,
direction,
quantity,
price,
orderType,
success: false,
errorMessage: error.message,
});
}
// ✅ 保留交易失败toast关键用户操作错误反馈
toast({
title: activeTab === 0 ? '买入失败' : '卖出失败',

View File

@@ -1,303 +0,0 @@
// src/views/TradingSimulation/hooks/useTradingSimulationEvents.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 {Object} options.portfolio - 账户信息
* @param {number} options.portfolio.totalValue - 总资产
* @param {number} options.portfolio.availableCash - 可用资金
* @param {number} options.portfolio.holdingsCount - 持仓数量
* @param {Function} options.navigate - 路由导航函数
* @returns {Object} 事件追踪处理函数集合
*/
export const useTradingSimulationEvents = ({ portfolio, navigate } = {}) => {
const { track } = usePostHogTrack();
// 🎯 页面浏览事件 - 页面加载时触发
useEffect(() => {
track(RETENTION_EVENTS.TRADING_SIMULATION_ENTERED, {
total_value: portfolio?.totalValue || 0,
available_cash: portfolio?.availableCash || 0,
holdings_count: portfolio?.holdingsCount || 0,
has_holdings: Boolean(portfolio?.holdingsCount && portfolio.holdingsCount > 0),
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '🎮 Trading Simulation Entered', {
totalValue: portfolio?.totalValue,
holdingsCount: portfolio?.holdingsCount,
});
}, [track, portfolio]);
/**
* 追踪股票搜索(模拟盘内)
* @param {string} query - 搜索关键词
* @param {number} resultCount - 搜索结果数量
*/
const trackSimulationStockSearched = useCallback((query, resultCount = 0) => {
if (!query) return;
track(RETENTION_EVENTS.SIMULATION_STOCK_SEARCHED, {
query,
result_count: resultCount,
has_results: resultCount > 0,
timestamp: new Date().toISOString(),
});
// 如果没有搜索结果,额外追踪
if (resultCount === 0) {
track(RETENTION_EVENTS.SEARCH_NO_RESULTS, {
query,
context: 'trading_simulation',
timestamp: new Date().toISOString(),
});
}
logger.debug('useTradingSimulationEvents', '🔍 Simulation Stock Searched', {
query,
resultCount,
});
}, [track]);
/**
* 追踪下单操作
* @param {Object} order - 订单信息
* @param {string} order.stockCode - 股票代码
* @param {string} order.stockName - 股票名称
* @param {string} order.direction - 买卖方向 ('buy' | 'sell')
* @param {number} order.quantity - 数量
* @param {number} order.price - 价格
* @param {string} order.orderType - 订单类型 ('market' | 'limit')
* @param {boolean} order.success - 是否成功
*/
const trackSimulationOrderPlaced = useCallback((order) => {
if (!order || !order.stockCode) {
logger.warn('useTradingSimulationEvents', 'Order object is required');
return;
}
track(RETENTION_EVENTS.SIMULATION_ORDER_PLACED, {
stock_code: order.stockCode,
stock_name: order.stockName || '',
direction: order.direction,
quantity: order.quantity,
price: order.price,
order_type: order.orderType || 'market',
order_value: order.quantity * order.price,
success: order.success,
error_message: order.errorMessage || null,
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '📝 Simulation Order Placed', {
stockCode: order.stockCode,
direction: order.direction,
quantity: order.quantity,
success: order.success,
});
}, [track]);
/**
* 追踪持仓查看
* @param {Object} holdings - 持仓信息
* @param {number} holdings.count - 持仓数量
* @param {number} holdings.totalValue - 持仓总市值
* @param {number} holdings.totalCost - 持仓总成本
* @param {number} holdings.profitLoss - 总盈亏
*/
const trackSimulationHoldingsViewed = useCallback((holdings = {}) => {
track(RETENTION_EVENTS.SIMULATION_HOLDINGS_VIEWED, {
holdings_count: holdings.count || 0,
total_value: holdings.totalValue || 0,
total_cost: holdings.totalCost || 0,
profit_loss: holdings.profitLoss || 0,
profit_loss_percent: holdings.totalCost ? ((holdings.profitLoss / holdings.totalCost) * 100).toFixed(2) : 0,
has_profit: holdings.profitLoss > 0,
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '💼 Simulation Holdings Viewed', {
count: holdings.count,
profitLoss: holdings.profitLoss,
});
}, [track]);
/**
* 追踪持仓股票点击
* @param {Object} holding - 持仓对象
* @param {string} holding.stockCode - 股票代码
* @param {string} holding.stockName - 股票名称
* @param {number} holding.profitLoss - 盈亏金额
* @param {number} position - 在列表中的位置
*/
const trackHoldingClicked = useCallback((holding, position = 0) => {
if (!holding || !holding.stockCode) {
logger.warn('useTradingSimulationEvents', 'Holding object is required');
return;
}
track(RETENTION_EVENTS.STOCK_CLICKED, {
stock_code: holding.stockCode,
stock_name: holding.stockName || '',
source: 'simulation_holdings',
profit_loss: holding.profitLoss || 0,
position,
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '🎯 Holding Clicked', {
stockCode: holding.stockCode,
position,
});
}, [track]);
/**
* 追踪历史交易记录查看
* @param {Object} history - 历史记录信息
* @param {number} history.count - 交易记录数量
* @param {string} history.filterBy - 筛选条件 ('all' | 'buy' | 'sell')
* @param {string} history.dateRange - 日期范围
*/
const trackSimulationHistoryViewed = useCallback((history = {}) => {
track(RETENTION_EVENTS.SIMULATION_HISTORY_VIEWED, {
history_count: history.count || 0,
filter_by: history.filterBy || 'all',
date_range: history.dateRange || 'all',
has_history: Boolean(history.count && history.count > 0),
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '📜 Simulation History Viewed', {
count: history.count,
filterBy: history.filterBy,
});
}, [track]);
/**
* 追踪买入按钮点击
* @param {Object} stock - 股票对象
* @param {string} stock.code - 股票代码
* @param {string} stock.name - 股票名称
* @param {number} stock.price - 当前价格
* @param {string} source - 来源 ('search' | 'holdings' | 'stock_detail')
*/
const trackBuyButtonClicked = useCallback((stock, source = 'search') => {
if (!stock || !stock.code) {
logger.warn('useTradingSimulationEvents', 'Stock object is required');
return;
}
track('Simulation Buy Button Clicked', {
stock_code: stock.code,
stock_name: stock.name || '',
current_price: stock.price || 0,
source,
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '🟢 Buy Button Clicked', {
stockCode: stock.code,
source,
});
}, [track]);
/**
* 追踪卖出按钮点击
* @param {Object} holding - 持仓对象
* @param {string} holding.stockCode - 股票代码
* @param {string} holding.stockName - 股票名称
* @param {number} holding.quantity - 持有数量
* @param {number} holding.profitLoss - 盈亏金额
* @param {string} source - 来源 ('holdings' | 'stock_detail')
*/
const trackSellButtonClicked = useCallback((holding, source = 'holdings') => {
if (!holding || !holding.stockCode) {
logger.warn('useTradingSimulationEvents', 'Holding object is required');
return;
}
track('Simulation Sell Button Clicked', {
stock_code: holding.stockCode,
stock_name: holding.stockName || '',
quantity: holding.quantity || 0,
profit_loss: holding.profitLoss || 0,
source,
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '🔴 Sell Button Clicked', {
stockCode: holding.stockCode,
source,
});
}, [track]);
/**
* 追踪账户重置
* @param {Object} beforeReset - 重置前的账户信息
* @param {number} beforeReset.totalValue - 总资产
* @param {number} beforeReset.profitLoss - 总盈亏
*/
const trackAccountReset = useCallback((beforeReset = {}) => {
track('Simulation Account Reset', {
total_value_before: beforeReset.totalValue || 0,
profit_loss_before: beforeReset.profitLoss || 0,
holdings_count_before: beforeReset.holdingsCount || 0,
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '🔄 Account Reset', {
totalValueBefore: beforeReset.totalValue,
});
}, [track]);
/**
* 追踪标签页切换
* @param {string} tabName - 标签名称 ('trading' | 'holdings' | 'history')
*/
const trackTabClicked = useCallback((tabName) => {
if (!tabName) {
logger.warn('useTradingSimulationEvents', 'Tab name is required');
return;
}
track('Simulation Tab Clicked', {
tab_name: tabName,
timestamp: new Date().toISOString(),
});
logger.debug('useTradingSimulationEvents', '📑 Tab Clicked', {
tabName,
});
}, [track]);
return {
// 搜索事件
trackSimulationStockSearched,
// 交易事件
trackSimulationOrderPlaced,
trackBuyButtonClicked,
trackSellButtonClicked,
// 持仓事件
trackSimulationHoldingsViewed,
trackHoldingClicked,
// 历史记录事件
trackSimulationHistoryViewed,
// 账户管理事件
trackAccountReset,
// UI交互事件
trackTabClicked,
};
};
export default useTradingSimulationEvents;

View File

@@ -49,7 +49,6 @@ import LineChart from '../../components/Charts/LineChart';
// 模拟盘账户管理 Hook
import { useTradingAccount } from './hooks/useTradingAccount';
import { useTradingSimulationEvents } from './hooks/useTradingSimulationEvents';
export default function TradingSimulation() {
// ========== 1. 所有 Hooks 必须放在最顶部,不能有任何条件判断 ==========
@@ -77,15 +76,6 @@ 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');
@@ -281,14 +271,9 @@ export default function TradingSimulation() {
</Box>
{/* 主要功能区域 - 放在上面 */}
<Tabs
index={activeTab}
onChange={(index) => {
setActiveTab(index);
// 🎯 追踪 Tab 切换
const tabNames = ['trading', 'holdings', 'history', 'margin'];
tradingEvents.trackTabClicked(tabNames[index]);
}}
<Tabs
index={activeTab}
onChange={setActiveTab}
variant="soft-rounded"
colorScheme="blue"
size="lg"
@@ -303,31 +288,28 @@ export default function TradingSimulation() {
<TabPanels>
{/* 交易面板 */}
<TabPanel px={0}>
<TradingPanel
<TradingPanel
account={account}
onBuyStock={buyStock}
onSellStock={sellStock}
searchStocks={searchStocks}
tradingEvents={tradingEvents}
/>
</TabPanel>
{/* 我的持仓 */}
<TabPanel px={0}>
<PositionsList
<PositionsList
positions={positions}
account={account}
onSellStock={sellStock}
tradingEvents={tradingEvents}
/>
</TabPanel>
{/* 交易历史 */}
<TabPanel px={0}>
<TradingHistory
<TradingHistory
history={tradingHistory}
onCancelOrder={cancelOrder}
tradingEvents={tradingEvents}
/>
</TabPanel>
@@ -349,7 +331,7 @@ export default function TradingSimulation() {
<Heading size="lg" mb={4} color={useColorModeValue('gray.700', 'white')}>
📊 账户统计分析
</Heading>
<AccountOverview account={account} tradingEvents={tradingEvents} />
<AccountOverview account={account} />
</Box>
{/* 资产走势图表 - 只在有数据时显示 */}