Files
vf_react/src/views/Profile/ProfilePage.js
zdl e4db19e607 feat: 已完成的工作:
-  创建了4个P1优先级Hook(搜索、导航、个人资料、订阅)
  -  将其中3个Hook集成到5个组件中
  -  在个人资料、设置、搜索、订阅流程中添加了15+个追踪点
  -  覆盖了完整的收入漏斗(支付发起 → 成功 → 订阅创建)
  -  添加了留存追踪(个人资料更新、设置修改、搜索查询)

  影响:
  - 完整的用户订阅旅程可见性
  - 个人资料/设置参与度追踪
  - 搜索行为分析
  - 完整的支付漏斗追踪(微信支付)
2025-10-29 12:29:41 +08:00

683 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/Profile/ProfilePage.js
import React, { useState, useRef } from 'react';
import {
Box,
Container,
VStack,
HStack,
Text,
Heading,
Avatar,
Button,
Input,
Textarea,
FormControl,
FormLabel,
SimpleGrid,
Card,
CardBody,
CardHeader,
Stat,
StatLabel,
StatNumber,
StatHelpText,
Badge,
Divider,
Select,
useToast,
IconButton,
Flex,
Progress,
Tag,
TagLabel,
TagCloseButton,
Wrap,
WrapItem,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure
} from '@chakra-ui/react';
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();
const toast = useToast();
// 表单数据状态
const [formData, setFormData] = useState({
nickname: user?.nickname || '',
bio: user?.bio || '',
location: user?.location || '',
gender: user?.gender || '',
birth_date: user?.birth_date || '',
trading_experience: user?.trading_experience || '',
investment_style: user?.investment_style || '',
risk_preference: user?.risk_preference || '',
investment_amount: user?.investment_amount || '',
preferred_markets: user?.preferred_markets ? user.preferred_markets.split(',') : [],
creator_tags: user?.creator_tags ? user.creator_tags.split(',') : []
});
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSaveProfile = async () => {
setIsLoading(true);
try {
// 这里应该调用后端API更新用户信息
const updatedData = {
...formData,
preferred_markets: formData.preferred_markets.join(','),
creator_tags: formData.creator_tags.join(',')
};
logger.debug('ProfilePage', '保存个人资料', { userId: user?.id });
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
updateUser(updatedData);
setIsEditing(false);
// 🎯 追踪个人资料更新成功
const updatedFields = Object.keys(formData).filter(
key => user?.[key] !== formData[key]
);
profileEvents.trackProfileUpdated(updatedFields, updatedData);
// ✅ 保留关键操作提示
toast({
title: "个人资料更新成功",
status: "success",
duration: 3000,
isClosable: true,
});
} catch (error) {
logger.error('ProfilePage', 'handleSaveProfile', error, { userId: user?.id });
// 🎯 追踪个人资料更新失败
const attemptedFields = Object.keys(formData);
profileEvents.trackProfileUpdateFailed(attemptedFields, error.message);
// ✅ 保留错误提示
toast({
title: "更新失败",
description: error.message,
status: "error",
duration: 3000,
isClosable: true,
});
} finally {
setIsLoading(false);
}
};
const handleAvatarUpload = (event) => {
const file = event.target.files[0];
if (file) {
logger.debug('ProfilePage', '上传头像', { fileName: file.name, fileSize: file.size });
// 这里应该上传文件到服务器
const reader = new FileReader();
reader.onload = (e) => {
updateUser({ avatar_url: e.target.result });
// 🎯 追踪头像上传
profileEvents.trackAvatarUploaded('file_upload', file.size);
// ✅ 保留关键操作提示
toast({
title: "头像更新成功",
status: "success",
duration: 3000,
isClosable: true,
});
};
reader.readAsDataURL(file);
}
};
const addMarketTag = () => {
if (newTag && !formData.preferred_markets.includes(newTag)) {
setFormData(prev => ({
...prev,
preferred_markets: [...prev.preferred_markets, newTag]
}));
setNewTag('');
}
};
const removeMarketTag = (tagToRemove) => {
setFormData(prev => ({
...prev,
preferred_markets: prev.preferred_markets.filter(tag => tag !== tagToRemove)
}));
};
const getProgressColor = (score) => {
if (score >= 800) return 'green';
if (score >= 500) return 'blue';
if (score >= 200) return 'yellow';
return 'red';
};
return (
<Container maxW="container.xl" py={8}>
<VStack spacing={8} align="stretch">
{/* 页面标题 */}
<HStack justify="space-between">
<Heading size="lg" color="gray.800">个人资料</Heading>
{!isEditing ? (
<Button
leftIcon={<EditIcon />}
colorScheme="blue"
onClick={() => setIsEditing(true)}
>
编辑资料
</Button>
) : (
<HStack>
<Button
leftIcon={<CheckIcon />}
colorScheme="green"
onClick={handleSaveProfile}
isLoading={isLoading}
>
保存
</Button>
<Button
leftIcon={<CloseIcon />}
variant="outline"
onClick={() => {
setIsEditing(false);
setFormData({
nickname: user?.nickname || '',
bio: user?.bio || '',
location: user?.location || '',
gender: user?.gender || '',
birth_date: user?.birth_date || '',
trading_experience: user?.trading_experience || '',
investment_style: user?.investment_style || '',
risk_preference: user?.risk_preference || '',
investment_amount: user?.investment_amount || '',
preferred_markets: user?.preferred_markets ? user.preferred_markets.split(',') : [],
creator_tags: user?.creator_tags ? user.creator_tags.split(',') : []
});
}}
>
取消
</Button>
</HStack>
)}
</HStack>
<SimpleGrid columns={{ base: 1, lg: 3 }} spacing={8}>
{/* 左侧:基本信息 */}
<Box gridColumn={{ base: "1", lg: "1 / 3" }}>
<Card>
<CardHeader>
<Heading size="md">基本信息</Heading>
</CardHeader>
<CardBody>
<VStack spacing={6}>
{/* 头像和基本信息 */}
<HStack spacing={6} align="start" w="full">
<VStack>
<Avatar
size="2xl"
src={user?.avatar_url}
name={user?.nickname || user?.username}
/>
{isEditing && (
<Button
size="sm"
onClick={() => fileInputRef.current?.click()}
>
更换头像
</Button>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleAvatarUpload}
/>
</VStack>
<VStack flex="1" align="start" spacing={4}>
<HStack w="full">
<Badge colorScheme="blue" variant="subtle">
用户名: {user?.username}
</Badge>
{user?.is_verified && (
<Badge colorScheme="green">已实名认证</Badge>
)}
{user?.has_wechat && (
<Badge colorScheme="green">微信已绑定</Badge>
)}
{user?.is_creator && (
<Badge colorScheme="purple">创作者</Badge>
)}
</HStack>
<SimpleGrid columns={2} spacing={4} w="full">
<FormControl>
<FormLabel>昵称</FormLabel>
{isEditing ? (
<Input
name="nickname"
value={formData.nickname}
onChange={handleInputChange}
placeholder="请输入昵称"
/>
) : (
<Text>{user?.nickname || '未设置'}</Text>
)}
</FormControl>
<FormControl>
<FormLabel>性别</FormLabel>
{isEditing ? (
<Select
name="gender"
value={formData.gender}
onChange={handleInputChange}
>
<option value="">请选择</option>
<option value="male"></option>
<option value="female"></option>
<option value="other">其他</option>
</Select>
) : (
<Text>
{user?.gender === 'male' ? '男' :
user?.gender === 'female' ? '女' :
user?.gender === 'other' ? '其他' : '未设置'}
</Text>
)}
</FormControl>
<FormControl>
<FormLabel>所在地</FormLabel>
{isEditing ? (
<Input
name="location"
value={formData.location}
onChange={handleInputChange}
placeholder="请输入所在地"
/>
) : (
<Text>{user?.location || '未设置'}</Text>
)}
</FormControl>
<FormControl>
<FormLabel>生日</FormLabel>
{isEditing ? (
<Input
name="birth_date"
type="date"
value={formData.birth_date}
onChange={handleInputChange}
/>
) : (
<Text>{user?.birth_date || '未设置'}</Text>
)}
</FormControl>
</SimpleGrid>
<FormControl>
<FormLabel>个人简介</FormLabel>
{isEditing ? (
<Textarea
name="bio"
value={formData.bio}
onChange={handleInputChange}
placeholder="介绍一下自己..."
rows={3}
/>
) : (
<Text color="gray.600">
{user?.bio || '这个人很懒,什么都没留下...'}
</Text>
)}
</FormControl>
</VStack>
</HStack>
<Divider />
{/* 投资偏好 */}
<Box w="full">
<Heading size="sm" mb={4}>投资偏好</Heading>
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={4}>
<FormControl>
<FormLabel>交易经验</FormLabel>
{isEditing ? (
<Select
name="trading_experience"
value={formData.trading_experience}
onChange={handleInputChange}
>
<option value="">请选择</option>
<option value="beginner">新手 (0-1)</option>
<option value="intermediate">中级 (1-3)</option>
<option value="advanced">高级 (3-5)</option>
<option value="expert">专家 (5年以上)</option>
</Select>
) : (
<Text>
{user?.trading_experience === 'beginner' ? '新手 (0-1年)' :
user?.trading_experience === 'intermediate' ? '中级 (1-3年)' :
user?.trading_experience === 'advanced' ? '高级 (3-5年)' :
user?.trading_experience === 'expert' ? '专家 (5年以上)' : '未设置'}
</Text>
)}
</FormControl>
<FormControl>
<FormLabel>投资风格</FormLabel>
{isEditing ? (
<Select
name="investment_style"
value={formData.investment_style}
onChange={handleInputChange}
>
<option value="">请选择</option>
<option value="conservative">保守型</option>
<option value="moderate">稳健型</option>
<option value="aggressive">积极型</option>
<option value="speculative">投机型</option>
</Select>
) : (
<Text>
{user?.investment_style === 'conservative' ? '保守型' :
user?.investment_style === 'moderate' ? '稳健型' :
user?.investment_style === 'aggressive' ? '积极型' :
user?.investment_style === 'speculative' ? '投机型' : '未设置'}
</Text>
)}
</FormControl>
<FormControl>
<FormLabel>风险偏好</FormLabel>
{isEditing ? (
<Select
name="risk_preference"
value={formData.risk_preference}
onChange={handleInputChange}
>
<option value="">请选择</option>
<option value="low">低风险</option>
<option value="medium">中等风险</option>
<option value="high">高风险</option>
</Select>
) : (
<Text>
{user?.risk_preference === 'low' ? '低风险' :
user?.risk_preference === 'medium' ? '中等风险' :
user?.risk_preference === 'high' ? '高风险' : '未设置'}
</Text>
)}
</FormControl>
<FormControl>
<FormLabel>投资金额</FormLabel>
{isEditing ? (
<Select
name="investment_amount"
value={formData.investment_amount}
onChange={handleInputChange}
>
<option value="">请选择</option>
<option value="under_10k">1万以下</option>
<option value="10k_50k">1-5</option>
<option value="50k_100k">5-10</option>
<option value="100k_500k">10-50</option>
<option value="over_500k">50万以上</option>
</Select>
) : (
<Text>
{user?.investment_amount === 'under_10k' ? '1万以下' :
user?.investment_amount === '10k_50k' ? '1-5万' :
user?.investment_amount === '50k_100k' ? '5-10万' :
user?.investment_amount === '100k_500k' ? '10-50万' :
user?.investment_amount === 'over_500k' ? '50万以上' : '未设置'}
</Text>
)}
</FormControl>
</SimpleGrid>
{/* 偏好市场标签 */}
<FormControl mt={4}>
<FormLabel>偏好市场</FormLabel>
<Wrap>
{formData.preferred_markets.map((market, index) => (
<WrapItem key={index}>
<Tag size="md" variant="solid" colorScheme="blue">
<TagLabel>{market}</TagLabel>
{isEditing && (
<TagCloseButton onClick={() => removeMarketTag(market)} />
)}
</Tag>
</WrapItem>
))}
{isEditing && (
<WrapItem>
<HStack>
<Input
size="sm"
placeholder="添加市场"
value={newTag}
onChange={(e) => setNewTag(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addMarketTag()}
/>
<IconButton
size="sm"
icon={<AddIcon />}
onClick={addMarketTag}
/>
</HStack>
</WrapItem>
)}
</Wrap>
{formData.preferred_markets.length === 0 && !isEditing && (
<Text color="gray.500" fontSize="sm">暂未设置偏好市场</Text>
)}
</FormControl>
</Box>
</VStack>
</CardBody>
</Card>
</Box>
{/* 右侧:统计数据 */}
<VStack spacing={6}>
{/* 社区统计 */}
<Card w="full">
<CardHeader>
<Heading size="md">社区统计</Heading>
</CardHeader>
<CardBody>
<VStack spacing={4}>
<Stat textAlign="center">
<StatLabel>用户等级</StatLabel>
<StatNumber color="blue.500">Lv.{user?.user_level || 1}</StatNumber>
</Stat>
<SimpleGrid columns={2} spacing={4} w="full">
<Stat textAlign="center">
<StatLabel>声誉分数</StatLabel>
<StatNumber fontSize="lg">{user?.reputation_score || 0}</StatNumber>
</Stat>
<Stat textAlign="center">
<StatLabel>贡献点数</StatLabel>
<StatNumber fontSize="lg">{user?.contribution_point || 0}</StatNumber>
</Stat>
<Stat textAlign="center">
<StatLabel>发帖数</StatLabel>
<StatNumber fontSize="lg">{user?.post_count || 0}</StatNumber>
</Stat>
<Stat textAlign="center">
<StatLabel>评论数</StatLabel>
<StatNumber fontSize="lg">{user?.comment_count || 0}</StatNumber>
</Stat>
<Stat textAlign="center">
<StatLabel>关注者</StatLabel>
<StatNumber fontSize="lg">{user?.follower_count || 0}</StatNumber>
</Stat>
<Stat textAlign="center">
<StatLabel>关注中</StatLabel>
<StatNumber fontSize="lg">{user?.following_count || 0}</StatNumber>
</Stat>
</SimpleGrid>
<Box w="full">
<Text fontSize="sm" color="gray.600" mb={2}>声誉等级</Text>
<Progress
value={(user?.reputation_score || 0) / 10}
colorScheme={getProgressColor(user?.reputation_score || 0)}
borderRadius="md"
/>
<Text fontSize="xs" color="gray.500" mt={1}>
{user?.reputation_score || 0} / 1000
</Text>
</Box>
</VStack>
</CardBody>
</Card>
{/* 账户信息 */}
<Card w="full">
<CardHeader>
<Heading size="md">账户信息</Heading>
</CardHeader>
<CardBody>
<VStack spacing={3} align="start">
<HStack justify="space-between" w="full">
<Text fontSize="sm" color="gray.600">邮箱</Text>
<VStack align="end" spacing={0}>
<Text fontSize="sm">{user?.email}</Text>
{user?.email_confirmed && (
<Badge size="xs" colorScheme="green">已验证</Badge>
)}
</VStack>
</HStack>
{user?.phone && (
<HStack justify="space-between" w="full">
<Text fontSize="sm" color="gray.600">手机号</Text>
<VStack align="end" spacing={0}>
<Text fontSize="sm">{user.phone}</Text>
{user?.phone_confirmed && (
<Badge size="xs" colorScheme="green">已验证</Badge>
)}
</VStack>
</HStack>
)}
<HStack justify="space-between" w="full">
<Text fontSize="sm" color="gray.600">微信</Text>
{user?.has_wechat ? (
<Badge size="xs" colorScheme="green">已绑定</Badge>
) : (
<Badge size="xs" colorScheme="gray">未绑定</Badge>
)}
</HStack>
<HStack justify="space-between" w="full">
<Text fontSize="sm" color="gray.600">注册时间</Text>
<Text fontSize="sm">
{user?.created_at ? new Date(user.created_at).toLocaleDateString() : '未知'}
</Text>
</HStack>
<HStack justify="space-between" w="full">
<Text fontSize="sm" color="gray.600">最后活跃</Text>
<Text fontSize="sm">
{user?.last_seen ? new Date(user.last_seen).toLocaleDateString() : '未知'}
</Text>
</HStack>
<HStack justify="space-between" w="full">
<Text fontSize="sm" color="gray.600">账户状态</Text>
<Badge colorScheme={user?.status === 'active' ? 'green' : 'gray'}>
{user?.status === 'active' ? '正常' : '未激活'}
</Badge>
</HStack>
</VStack>
</CardBody>
</Card>
{/* 实名认证 */}
{!user?.is_verified && (
<Card w="full">
<CardHeader>
<Heading size="md">实名认证</Heading>
</CardHeader>
<CardBody>
<VStack spacing={4}>
<Text fontSize="sm" color="gray.600" textAlign="center">
完成实名认证获得更高权限和信任度
</Text>
<Button colorScheme="orange" size="sm" onClick={onOpen}>
立即认证
</Button>
</VStack>
</CardBody>
</Card>
)}
</VStack>
</SimpleGrid>
</VStack>
{/* 实名认证模态框 */}
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>实名认证</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<VStack spacing={4}>
<FormControl>
<FormLabel>真实姓名</FormLabel>
<Input placeholder="请输入真实姓名" />
</FormControl>
<FormControl>
<FormLabel>身份证号</FormLabel>
<Input placeholder="请输入身份证号" />
</FormControl>
<Text fontSize="sm" color="gray.500">
您的个人信息将严格保密仅用于身份验证
</Text>
<Button colorScheme="blue" w="full">
提交认证
</Button>
</VStack>
</ModalBody>
</ModalContent>
</Modal>
</Container>
);
}