Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref

This commit is contained in:
2025-11-19 19:41:59 +08:00
9 changed files with 63 additions and 156 deletions

View File

@@ -44,7 +44,7 @@
**前端** **前端**
- **核心框架**: React 18.3.1 - **核心框架**: React 18.3.1
- **类型系统**: TypeScript 5.9.3(渐进式接入中,支持 JS/TS 混合开发) - **类型系统**: TypeScript 5.9.3(渐进式接入中,支持 JS/TS 混合开发)
- **UI 组件库**: Chakra UI 2.8.2(主要) + Ant Design 5.27.4(表格/表单) - **UI 组件库**: Chakra UI 2.10.9(主要) + Ant Design 5.27.4(表格/表单)
- **状态管理**: Redux Toolkit 2.9.2 - **状态管理**: Redux Toolkit 2.9.2
- **路由**: React Router v6.30.1 配合 React.lazy() 实现代码分割 - **路由**: React Router v6.30.1 配合 React.lazy() 实现代码分割
- **构建系统**: CRACO 7.1.0 + 激进的 webpack 5 优化 - **构建系统**: CRACO 7.1.0 + 激进的 webpack 5 优化

View File

@@ -6,9 +6,9 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.0.0", "@ant-design/icons": "^6.0.0",
"@asseinfo/react-kanban": "^2.2.0", "@asseinfo/react-kanban": "^2.2.0",
"@chakra-ui/icons": "^2.1.1", "@chakra-ui/icons": "^2.2.6",
"@chakra-ui/react": "^2.8.2", "@chakra-ui/react": "^2.10.9",
"@chakra-ui/theme-tools": "^1.3.6", "@chakra-ui/theme-tools": "^2.2.6",
"@emotion/cache": "^11.4.0", "@emotion/cache": "^11.4.0",
"@emotion/react": "^11.4.0", "@emotion/react": "^11.4.0",
"@emotion/styled": "^11.3.0", "@emotion/styled": "^11.3.0",

View File

@@ -356,24 +356,22 @@ export default function AuthFormContent() {
// 更新session // 更新session
await checkSession(); await checkSession();
// ✅ 兼容后端两种命名格式camelCase (isNewUser) 和 snake_case (is_new_user)
const isNewUser = data.isNewUser ?? data.is_new_user ?? false;
// 追踪登录成功并识别用户 // 追踪登录成功并识别用户
authEvents.trackLoginSuccess(data.user, 'phone', data.isNewUser); authEvents.trackLoginSuccess(data.user, 'phone', isNewUser);
// ✅ 保留登录成功 toast关键操作提示 // ✅ 保留登录成功 toast关键操作提示
toast({ toast({
title: data.isNewUser ? '注册成功' : '登录成功', title: isNewUser ? '注册成功' : '登录成功',
description: config.successDescription, description: config.successDescription,
status: "success", status: "success",
duration: 2000, duration: 2000,
}); });
logger.info('AuthFormContent', '登录成功', {
isNewUser: data.isNewUser,
userId: data.user?.id
});
// 检查是否为新注册用户 // 检查是否为新注册用户
if (data.isNewUser) { if (isNewUser) {
// 新注册用户,延迟后显示昵称设置引导 // 新注册用户,延迟后显示昵称设置引导
setTimeout(() => { setTimeout(() => {
setCurrentPhone(phone); setCurrentPhone(phone);

View File

@@ -1,5 +1,5 @@
// src/components/Auth/AuthModalManager.js // src/components/Auth/AuthModalManager.js
import React from 'react'; import React, { useEffect, useRef } from 'react';
import { import {
Modal, Modal,
ModalOverlay, ModalOverlay,
@@ -10,6 +10,8 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAuthModal } from '../../hooks/useAuthModal'; import { useAuthModal } from '../../hooks/useAuthModal';
import AuthFormContent from './AuthFormContent'; import AuthFormContent from './AuthFormContent';
import { trackEventAsync } from '@lib/posthog';
import { ACTIVATION_EVENTS } from '@lib/constants';
/** /**
* 全局认证弹窗管理器 * 全局认证弹窗管理器
@@ -21,6 +23,27 @@ export default function AuthModalManager() {
closeModal closeModal
} = useAuthModal(); } = useAuthModal();
// ✅ 追踪弹窗打开次数(用于漏斗分析)
const hasTrackedOpen = useRef(false);
useEffect(() => {
if (isAuthModalOpen && !hasTrackedOpen.current) {
// ✅ 使用异步追踪,不阻塞渲染
trackEventAsync(ACTIVATION_EVENTS.LOGIN_PAGE_VIEWED, {
timestamp: new Date().toISOString(),
modal_type: 'auth_modal',
trigger_source: 'user_action', // 可以通过 props 传递更精确的来源
});
hasTrackedOpen.current = true;
}
// ✅ 弹窗关闭时重置标记(允许再次追踪)
if (!isAuthModalOpen) {
hasTrackedOpen.current = false;
}
}, [isAuthModalOpen]);
// 响应式尺寸配置 // 响应式尺寸配置
const modalSize = useBreakpointValue({ const modalSize = useBreakpointValue({
base: "md", // 移动端md不占满全屏 base: "md", // 移动端md不占满全屏

View File

@@ -5,6 +5,7 @@ import { useToast } from '@chakra-ui/react';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { useNotification } from '../contexts/NotificationContext'; import { useNotification } from '../contexts/NotificationContext';
import { identifyUser, resetUser, trackEvent } from '@lib/posthog'; import { identifyUser, resetUser, trackEvent } from '@lib/posthog';
import { SPECIAL_EVENTS } from '@lib/constants';
// 创建认证上下文 // 创建认证上下文
const AuthContext = createContext(); const AuthContext = createContext();
@@ -220,25 +221,10 @@ export const AuthProvider = ({ children }) => {
setUser(data.user); setUser(data.user);
setIsAuthenticated(true); setIsAuthenticated(true);
// ✅ 追踪登录事件 // ❌ 过时的追踪代码已移除(新代码在组件中使用 useAuthEvents 追踪)
trackEvent('user_logged_in', { // 正确的事件追踪在 AuthFormContent.js 中调用 authEvents.trackLoginSuccess()
loginType, // 事件名:'User Logged In' 或 'User Signed Up'
timestamp: new Date().toISOString() // 属性名login_method (不是 loginType)
});
// ✅ 首次登录追踪
const firstLoginKey = `first_login_${data.user.id}`;
const hasLoggedInBefore = localStorage.getItem(firstLoginKey);
if (!hasLoggedInBefore) {
trackEvent('first_login', {
user_id: data.user.id,
login_type: loginType,
timestamp: new Date().toISOString()
});
localStorage.setItem(firstLoginKey, 'true');
}
// ⚡ 移除toast让调用者处理UI反馈避免并发更新冲突 // ⚡ 移除toast让调用者处理UI反馈避免并发更新冲突
// toast({ // toast({
@@ -294,20 +280,10 @@ export const AuthProvider = ({ children }) => {
setUser(data.user); setUser(data.user);
setIsAuthenticated(true); setIsAuthenticated(true);
// ✅ 识别用户身份到 PostHog // ❌ 过时的追踪代码已移除(新代码在组件中使用 useAuthEvents 追踪)
identifyUser(data.user.id, { // 正确的事件追踪在 AuthFormContent.js 中调用 authEvents.trackLoginSuccess()
email: data.user.email, // 事件名:'User Signed Up'(不是 'user_registered'
username: data.user.username, // 属性名login_method不是 method
subscription_tier: data.user.subscription_tier,
role: data.user.role,
registration_date: data.user.created_at
});
// ✅ 追踪注册事件
trackEvent('user_registered', {
method: 'phone',
timestamp: new Date().toISOString()
});
toast({ toast({
title: "注册成功", title: "注册成功",
@@ -332,73 +308,6 @@ export const AuthProvider = ({ children }) => {
} }
}; };
// 邮箱注册
const registerWithEmail = async (email, code, username, password) => {
try {
setIsLoading(true);
const response = await fetch(`/api/auth/register/email`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
email,
code,
username,
password
})
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || '注册失败');
}
// 注册成功后自动登录
setUser(data.user);
setIsAuthenticated(true);
// ✅ 识别用户身份到 PostHog
identifyUser(data.user.id, {
email: data.user.email,
username: data.user.username,
subscription_tier: data.user.subscription_tier,
role: data.user.role,
registration_date: data.user.created_at
});
// ✅ 追踪注册事件
trackEvent('user_registered', {
method: 'email',
timestamp: new Date().toISOString()
});
toast({
title: "注册成功",
description: "欢迎加入价值前沿!",
status: "success",
duration: 3000,
isClosable: true,
});
// ⚡ 注册成功后显示欢迎引导延迟2秒
setTimeout(() => {
showWelcomeGuide();
}, 2000);
return { success: true };
} catch (error) {
logger.error('AuthContext', 'registerWithEmail', error);
return { success: false, error: error.message };
} finally {
setIsLoading(false);
}
};
// 发送手机验证码 // 发送手机验证码
const sendSmsCode = async (phone) => { const sendSmsCode = async (phone) => {
try { try {
@@ -428,35 +337,6 @@ export const AuthProvider = ({ children }) => {
} }
}; };
// 发送邮箱验证码
const sendEmailCode = async (email) => {
try {
const response = await fetch(`/api/auth/send-email-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ email })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '发送失败');
}
// ❌ 移除成功 toast
logger.info('AuthContext', '邮箱验证码已发送', { email: email.substring(0, 3) + '***@***' });
return { success: true };
} catch (error) {
// ❌ 移除错误 toast
logger.error('AuthContext', 'sendEmailCode', error);
return { success: false, error: error.message };
}
};
// 登出方法 // 登出方法
const logout = async () => { const logout = async () => {
try { try {
@@ -467,8 +347,12 @@ export const AuthProvider = ({ children }) => {
}); });
// ✅ 追踪登出事件(必须在 resetUser() 之前,否则会丢失用户身份) // ✅ 追踪登出事件(必须在 resetUser() 之前,否则会丢失用户身份)
trackEvent('user_logged_out', { trackEvent(SPECIAL_EVENTS.USER_LOGGED_OUT, {
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
user_id: user?.id || null,
session_duration_minutes: user?.session_start
? Math.round((Date.now() - new Date(user.session_start).getTime()) / 60000)
: null,
}); });
// ✅ 重置 PostHog 用户会话 // ✅ 重置 PostHog 用户会话
@@ -513,9 +397,7 @@ export const AuthProvider = ({ children }) => {
updateUser, updateUser,
login, login,
registerWithPhone, registerWithPhone,
registerWithEmail,
sendSmsCode, sendSmsCode,
sendEmailCode,
logout, logout,
hasRole, hasRole,
refreshSession, refreshSession,

View File

@@ -124,6 +124,7 @@ async function startApp() {
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
// Render the app with Router wrapper // Render the app with Router wrapper
// ✅ StrictMode 已启用Chakra UI 2.10.9+ 已修复兼容性问题)
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Router <Router

View File

@@ -33,8 +33,8 @@ export const initPostHog = () => {
posthog.init(apiKey, { posthog.init(apiKey, {
api_host: apiHost, api_host: apiHost,
// Pageview tracking - manual control for better accuracy // Pageview tracking - auto-capture for DAU/MAU analytics
capture_pageview: false, // We'll manually capture with custom properties capture_pageview: true, // Auto-capture all page views (required for DAU tracking)
capture_pageleave: true, // Auto-capture when user leaves page capture_pageleave: true, // Auto-capture when user leaves page
// Session Recording Configuration // Session Recording Configuration

View File

@@ -195,9 +195,12 @@ const EnhancedCalendar = ({
onClick={() => onDateChange(date)} onClick={() => onDateChange(date)}
transition="all 0.2s" transition="all 0.2s"
cursor="pointer" cursor="pointer"
display="flex"
alignItems="center"
justifyContent="center"
> >
<Text <Text
fontSize={compact ? 'md' : 'lg'} fontSize={compact ? 'lg' : 'xl'}
fontWeight={isToday || isSelected ? 'bold' : 'normal'} fontWeight={isToday || isSelected ? 'bold' : 'normal'}
color={isSelected ? 'blue.600' : 'gray.700'} color={isSelected ? 'blue.600' : 'gray.700'}
> >
@@ -206,13 +209,13 @@ const EnhancedCalendar = ({
{hasData && ( {hasData && (
<Badge <Badge
position="absolute" position="absolute"
top="2px" top="4px"
right="2px" right="4px"
size={compact ? 'sm' : 'md'} size={compact ? 'sm' : 'md'}
colorScheme={getDateBadgeColor(dateData.count)} colorScheme={getDateBadgeColor(dateData.count)}
fontSize={compact ? '10px' : '11px'} fontSize={compact ? '9px' : '10px'}
px={compact ? 1 : 2} px={compact ? 1 : 2}
minW={compact ? '22px' : '28px'} minW={compact ? '20px' : '24px'}
borderRadius="full" borderRadius="full"
> >
{dateData.count} {dateData.count}
@@ -221,7 +224,7 @@ const EnhancedCalendar = ({
{isToday && ( {isToday && (
<Text <Text
position="absolute" position="absolute"
bottom="2px" bottom="4px"
left="50%" left="50%"
transform="translateX(-50%)" transform="translateX(-50%)"
fontSize={compact ? '9px' : '10px'} fontSize={compact ? '9px' : '10px'}

View File

@@ -444,7 +444,6 @@ export default function LimitAnalyse() {
borderColor="whiteAlpha.300" borderColor="whiteAlpha.300"
backdropFilter="saturate(180%) blur(10px)" backdropFilter="saturate(180%) blur(10px)"
w="full" w="full"
minH="420px"
> >
<CardBody p={4}> <CardBody p={4}>
<EnhancedCalendar <EnhancedCalendar
@@ -453,8 +452,9 @@ export default function LimitAnalyse() {
availableDates={availableDates} availableDates={availableDates}
compact compact
hideSelectionInfo hideSelectionInfo
hideLegend
width="100%" width="100%"
cellHeight={10} cellHeight={16}
/> />
</CardBody> </CardBody>
</Card> </Card>