Merge branch 'feature_2025/251117_pref' of https://git.valuefrontier.cn/vf/vf_react into feature_2025/251117_pref
This commit is contained in:
@@ -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 优化
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(不占满全屏)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user