feat: 开发持仓、股票信息相关接口
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Tabs,
|
||||
Card,
|
||||
Avatar,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Upload,
|
||||
Image,
|
||||
Space,
|
||||
App as AntdApp,
|
||||
Modal,
|
||||
} from 'antd';
|
||||
import type { TabsProps } from 'antd';
|
||||
import { UserOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { authService } from '@/services/auth';
|
||||
import { userService } from '@/services/user';
|
||||
import { storageService } from '@/services/storage';
|
||||
import type { UserInfo } from '@/types/user';
|
||||
import type { UploadFile } from 'antd/es/upload';
|
||||
import type { RcFile } from 'antd/es/upload';
|
||||
import dayjs from 'dayjs';
|
||||
import './UserInfoPage.css';
|
||||
|
||||
/**
|
||||
* 计算使用天数
|
||||
* @param createdAt 创建时间
|
||||
* @returns 使用天数字符串,如 "365天" 或 "1年30天"
|
||||
*/
|
||||
const calculateUsageDays = (createdAt: Date | string): string => {
|
||||
const created = dayjs(createdAt);
|
||||
const now = dayjs();
|
||||
const days = now.diff(created, 'day');
|
||||
|
||||
if (days < 365) {
|
||||
return `${days}天`;
|
||||
}
|
||||
|
||||
const years = Math.floor(days / 365);
|
||||
const remainingDays = days % 365;
|
||||
return `${years}年${remainingDays}天`;
|
||||
};
|
||||
|
||||
const UserInfoPage = () => {
|
||||
const { message: messageApi } = AntdApp.useApp();
|
||||
const [user, setUser] = useState<UserInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [avatarFileList, setAvatarFileList] = useState<UploadFile[]>([]);
|
||||
const [passwordForm] = Form.useForm();
|
||||
const [infoForm] = Form.useForm();
|
||||
const isLoadingRef = useRef(false);
|
||||
|
||||
// 加载用户信息
|
||||
const loadUserInfo = async () => {
|
||||
// 防止重复请求
|
||||
if (isLoadingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentUser = authService.getUser();
|
||||
if (!currentUser) {
|
||||
messageApi.error('未找到用户信息');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingRef.current = true;
|
||||
setLoading(true);
|
||||
try {
|
||||
const userData = await userService.getUserById(currentUser.userId);
|
||||
console.log('userData:', userData);
|
||||
setUser(userData);
|
||||
infoForm.setFieldsValue({
|
||||
nickname: userData.nickname || '',
|
||||
phone: userData.phone || '',
|
||||
email: userData.email || '',
|
||||
});
|
||||
// 设置头像文件列表
|
||||
if (userData.avatarUrl) {
|
||||
setAvatarFileList([
|
||||
{
|
||||
uid: '-1',
|
||||
name: 'avatar',
|
||||
status: 'done',
|
||||
url: userData.avatarUrl,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message || '加载用户信息失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
isLoadingRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUserInfo();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 上传头像
|
||||
const handleAvatarUpload = async (
|
||||
file: RcFile,
|
||||
onSuccess?: (response: any) => void,
|
||||
onError?: (error: any) => void
|
||||
) => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const response = await storageService.uploadAvatar(file);
|
||||
// 更新用户头像
|
||||
if (user) {
|
||||
await userService.updateUser(user.userId, {
|
||||
avatarUrl: response.url,
|
||||
});
|
||||
messageApi.success('头像上传成功');
|
||||
await loadUserInfo();
|
||||
onSuccess?.(response);
|
||||
}
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message || '头像上传失败');
|
||||
onError?.(error);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 头像上传配置
|
||||
const avatarUploadProps = {
|
||||
name: 'file',
|
||||
listType: 'picture' as const,
|
||||
maxCount: 1,
|
||||
fileList: avatarFileList,
|
||||
accept: 'image/*',
|
||||
customRequest: async (options: any) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
await handleAvatarUpload(file as RcFile, onSuccess, onError);
|
||||
},
|
||||
beforeUpload: (file: File) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
if (!isImage) {
|
||||
messageApi.error('只能上传图片文件!');
|
||||
return false;
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
if (!isLt2M) {
|
||||
messageApi.error('图片大小不能超过 2MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onRemove: () => {
|
||||
setAvatarFileList([]);
|
||||
},
|
||||
onChange: ({ fileList: newFileList }: { fileList: UploadFile[] }) => {
|
||||
setAvatarFileList(newFileList);
|
||||
},
|
||||
};
|
||||
|
||||
// 进入编辑模式
|
||||
const handleEdit = () => {
|
||||
if (user) {
|
||||
infoForm.setFieldsValue({
|
||||
nickname: user.nickname || '',
|
||||
phone: user.phone || '',
|
||||
email: user.email || '',
|
||||
});
|
||||
setEditing(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const handleCancelEdit = () => {
|
||||
setEditing(false);
|
||||
infoForm.resetFields();
|
||||
};
|
||||
|
||||
// 处理更新按钮点击(先验证表单,再显示确认框)
|
||||
const handleUpdateClick = async () => {
|
||||
try {
|
||||
// 先验证表单
|
||||
const values = await infoForm.validateFields();
|
||||
if (!user) return;
|
||||
|
||||
// 验证通过,显示确认框
|
||||
Modal.confirm({
|
||||
title: '确认更新',
|
||||
content: '确定要更新个人信息吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await userService.updateUser(user.userId, {
|
||||
nickname: values.nickname || undefined,
|
||||
phone: values.phone || undefined,
|
||||
email: values.email || undefined,
|
||||
});
|
||||
|
||||
messageApi.success('个人信息更新成功');
|
||||
setEditing(false);
|
||||
await loadUserInfo();
|
||||
} catch (error: any) {
|
||||
messageApi.error(error.message || '更新失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
// 验证失败,不显示确认框
|
||||
if (error.errorFields) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 修改密码
|
||||
const handleChangePassword = async () => {
|
||||
try {
|
||||
const values = await passwordForm.validateFields();
|
||||
if (!user) return;
|
||||
|
||||
await userService.changePassword(user.userId, {
|
||||
oldPassword: values.oldPassword,
|
||||
newPassword: values.newPassword,
|
||||
});
|
||||
|
||||
messageApi.success('密码修改成功');
|
||||
passwordForm.resetFields();
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
return;
|
||||
}
|
||||
messageApi.error(error.message || '密码修改失败');
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return <Card loading={loading}>加载中...</Card>;
|
||||
}
|
||||
|
||||
// Tab配置
|
||||
const tabItems: TabsProps['items'] = [
|
||||
{
|
||||
key: 'info',
|
||||
label: '个人信息',
|
||||
children: (
|
||||
<div className="user-info-content">
|
||||
<Form
|
||||
form={infoForm}
|
||||
layout="horizontal"
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
style={{ maxWidth: 600 }}
|
||||
>
|
||||
{/* 头像 */}
|
||||
<Form.Item wrapperCol={{ span: 18, offset: 6 }}>
|
||||
<div className="avatar-wrapper">
|
||||
{user.avatarUrl ? (
|
||||
<Image
|
||||
src={user.avatarUrl}
|
||||
alt="头像"
|
||||
width={100}
|
||||
height={100}
|
||||
style={{ borderRadius: 8 }}
|
||||
fallback="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect width='100' height='100' fill='%23f0f0f0'/%3E%3Cpath d='M50 33c9.4 0 17 7.6 17 17s-7.6 17-17 17-17-7.6-17-17 7.6-17 17-17zm0 40c11 0 33.5 5.5 33.5 16.5v8H16.5v-8C16.5 78.5 39 73 50 73z' fill='%23999'/%3E%3C/svg%3E"
|
||||
/>
|
||||
) : (
|
||||
<Avatar size={100} icon={<UserOutlined />} />
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{editing ? (
|
||||
<Upload {...avatarUploadProps}>
|
||||
<Button
|
||||
icon={<UploadOutlined />}
|
||||
loading={uploading}
|
||||
size="small"
|
||||
>
|
||||
上传头像
|
||||
</Button>
|
||||
</Upload>
|
||||
) : (
|
||||
<span
|
||||
style={{ fontSize: 14, color: 'rgba(0, 0, 0, 0.65)' }}
|
||||
>
|
||||
头像
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
{/* 昵称 */}
|
||||
<Form.Item
|
||||
label="昵称"
|
||||
rules={[{ max: 100, message: '昵称不能超过100个字符' }]}
|
||||
>
|
||||
{editing ? (
|
||||
<Form.Item
|
||||
name="nickname"
|
||||
noStyle
|
||||
rules={[{ max: 100, message: '昵称不能超过100个字符' }]}
|
||||
>
|
||||
<Input placeholder="请输入昵称" />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<span className="info-field-text">{user.nickname || '-'}</span>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
{/* 用户名(不可编辑) */}
|
||||
<Form.Item label="用户名">
|
||||
<span className="info-field-text">{user.username}</span>
|
||||
</Form.Item>
|
||||
|
||||
{/* 电话 */}
|
||||
<Form.Item
|
||||
label="电话"
|
||||
rules={[
|
||||
{
|
||||
pattern: /^[0-9+\-() ]+$/,
|
||||
message: '电话号码格式不正确',
|
||||
},
|
||||
{ max: 20, message: '电话不能超过20个字符' },
|
||||
]}
|
||||
>
|
||||
{editing ? (
|
||||
<Form.Item
|
||||
name="phone"
|
||||
noStyle
|
||||
rules={[
|
||||
{
|
||||
pattern: /^[0-9+\-() ]+$/,
|
||||
message: '电话号码格式不正确',
|
||||
},
|
||||
{ max: 20, message: '电话不能超过20个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入电话" />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<span className="info-field-text">{user.phone || '-'}</span>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
{/* 邮箱 */}
|
||||
<Form.Item
|
||||
label="邮箱"
|
||||
rules={[
|
||||
{ type: 'email', message: '邮箱格式不正确' },
|
||||
{ max: 100, message: '邮箱不能超过100个字符' },
|
||||
]}
|
||||
>
|
||||
{editing ? (
|
||||
<Form.Item
|
||||
name="email"
|
||||
noStyle
|
||||
rules={[
|
||||
{ type: 'email', message: '邮箱格式不正确' },
|
||||
{ max: 100, message: '邮箱不能超过100个字符' },
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入邮箱" />
|
||||
</Form.Item>
|
||||
) : (
|
||||
<span className="info-field-text">{user.email}</span>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
{/* 注册时间(不可编辑) */}
|
||||
<Form.Item label="注册时间">
|
||||
<span className="info-field-text">
|
||||
{dayjs(user.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</span>
|
||||
</Form.Item>
|
||||
|
||||
{/* 使用天数(不可编辑) */}
|
||||
<Form.Item label="使用天数">
|
||||
<span className="info-field-text">
|
||||
{calculateUsageDays(user.createdAt)}
|
||||
</span>
|
||||
</Form.Item>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Form.Item wrapperCol={{ offset: 6, span: 18 }} style={{ marginTop: 24 }}>
|
||||
{editing ? (
|
||||
<Space>
|
||||
<Button type="primary" onClick={handleUpdateClick}>
|
||||
更新个人信息
|
||||
</Button>
|
||||
<Button onClick={handleCancelEdit}>取消</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Button type="primary" onClick={handleEdit}>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: '修改密码',
|
||||
children: (
|
||||
<Form
|
||||
form={passwordForm}
|
||||
layout="horizontal"
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 18 }}
|
||||
style={{ maxWidth: 600 }}
|
||||
onFinish={handleChangePassword}
|
||||
>
|
||||
<Form.Item
|
||||
name="oldPassword"
|
||||
label="原密码"
|
||||
rules={[{ required: true, message: '请输入原密码' }]}
|
||||
>
|
||||
<Input.Password placeholder="请输入原密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="newPassword"
|
||||
label="新密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码长度至少6位' },
|
||||
{ max: 100, message: '密码长度不能超过100位' },
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请输入新密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label="确认密码"
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password placeholder="请再次输入新密码" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
修改密码
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="user-info-page">
|
||||
<Card>
|
||||
<Tabs defaultActiveKey="info" items={tabItems} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserInfoPage;
|
||||
Reference in New Issue
Block a user