feat(个人信息): 新增个人信息页面

- 实现用户头像展示和上传功能
- 添加个人统计数据展示卡片
- 支持基本信息编辑功能
- 集成账号安全设置模块
- 优化页面布局和响应式设计
This commit is contained in:
gaoziman 2025-11-06 00:42:25 +08:00
parent 02c00222d9
commit c781657003
2 changed files with 496 additions and 0 deletions

View File

@ -0,0 +1,415 @@
import React, { useState } from 'react';
import {
Card,
Form,
Input,
Button,
Avatar,
Upload,
Space,
Typography,
Divider,
Grid,
Message,
Select,
DatePicker,
Radio,
Descriptions,
Tag,
Statistic,
} from '@arco-design/web-react';
import {
IconUser,
IconEmail,
IconPhone,
IconLocation,
IconEdit,
IconCamera,
IconCheck,
IconClose,
} from '@arco-design/web-react/icon';
import styles from './style/index.module.less';
const { Title, Paragraph, Text } = Typography;
const { Row, Col } = Grid;
const FormItem = Form.Item;
const Option = Select.Option;
const RadioGroup = Radio.Group;
/**
*
*/
function UserInfo() {
const [form] = Form.useForm();
const [isEditing, setIsEditing] = useState(false);
const [loading, setLoading] = useState(false);
// 模拟用户数据
const [userInfo, setUserInfo] = useState({
avatar:
'https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
name: '张三',
username: 'zhangsan',
email: 'zhangsan@example.com',
phone: '138****8888',
gender: 'male',
birthday: '1990-01-01',
department: '技术部',
position: '高级工程师',
location: '北京市朝阳区',
introduction:
'热爱技术,专注于前端开发领域,有丰富的 React 和 TypeScript 开发经验。',
joinDate: '2020-01-15',
employeeId: 'EMP001',
});
// 统计数据
const statistics = {
projectCount: 28,
taskCompleted: 156,
contribution: 892,
teamSize: 12,
};
// 初始化表单值
React.useEffect(() => {
form.setFieldsValue(userInfo);
}, [userInfo, form]);
// 保存用户信息
const handleSave = async () => {
try {
const values = await form.validate();
setLoading(true);
// 模拟保存
await new Promise((resolve) => setTimeout(resolve, 1000));
setUserInfo({ ...userInfo, ...values });
setIsEditing(false);
Message.success('保存成功');
} catch (error) {
console.error('表单验证失败:', error);
} finally {
setLoading(false);
}
};
// 取消编辑
const handleCancel = () => {
form.setFieldsValue(userInfo);
setIsEditing(false);
};
// 上传头像
const handleAvatarChange = (fileList) => {
if (fileList && fileList.length > 0) {
const file = fileList[0];
if (file.originFile) {
const reader = new FileReader();
reader.onload = (e) => {
setUserInfo({ ...userInfo, avatar: e.target?.result as string });
Message.success('头像上传成功');
};
reader.readAsDataURL(file.originFile);
}
}
};
return (
<div className={styles.container}>
{/* 顶部信息卡片 */}
<Card className={styles['profile-card']}>
<Row gutter={24}>
{/* 左侧头像区域 */}
<Col span={6}>
<div className={styles['avatar-section']}>
<div className={styles['avatar-wrapper']}>
<Avatar size={120} className={styles.avatar}>
<img src={userInfo.avatar} alt="avatar" />
</Avatar>
{isEditing && (
<Upload
accept="image/*"
showUploadList={false}
onChange={handleAvatarChange}
action="/"
>
<div className={styles['avatar-upload']}>
<IconCamera style={{ fontSize: 20 }} />
<div></div>
</div>
</Upload>
)}
</div>
<Title heading={5} style={{ marginTop: 16, textAlign: 'center' }}>
{userInfo.name}
</Title>
<Paragraph
style={{ textAlign: 'center', color: 'var(--color-text-3)' }}
>
{userInfo.position}
</Paragraph>
<div style={{ textAlign: 'center', marginTop: 8 }}>
<Tag color="blue">{userInfo.department}</Tag>
</div>
</div>
</Col>
{/* 右侧统计信息 */}
<Col span={18}>
<div className={styles['stats-section']}>
<Row gutter={16}>
<Col span={6}>
<Statistic
title="参与项目"
value={statistics.projectCount}
suffix="个"
/>
</Col>
<Col span={6}>
<Statistic
title="完成任务"
value={statistics.taskCompleted}
suffix="个"
/>
</Col>
<Col span={6}>
<Statistic
title="代码贡献"
value={statistics.contribution}
suffix="次"
/>
</Col>
<Col span={6}>
<Statistic
title="团队人数"
value={statistics.teamSize}
suffix="人"
/>
</Col>
</Row>
<Divider />
<Descriptions
column={2}
data={[
{
label: '工号',
value: userInfo.employeeId,
},
{
label: '入职时间',
value: userInfo.joinDate,
},
{
label: '邮箱',
value: userInfo.email,
},
{
label: '联系电话',
value: userInfo.phone,
},
]}
/>
</div>
</Col>
</Row>
</Card>
{/* 详细信息卡片 */}
<Card
style={{ marginTop: 20 }}
title="个人信息"
extra={
!isEditing ? (
<Button
type="primary"
icon={<IconEdit />}
onClick={() => setIsEditing(true)}
>
</Button>
) : (
<Space>
<Button icon={<IconClose />} onClick={handleCancel}>
</Button>
<Button
type="primary"
icon={<IconCheck />}
loading={loading}
onClick={handleSave}
>
</Button>
</Space>
)
}
>
<Form
form={form}
layout="vertical"
disabled={!isEditing}
autoComplete="off"
>
<Row gutter={24}>
<Col span={12}>
<FormItem
label="姓名"
field="name"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input
placeholder="请输入姓名"
prefix={<IconUser />}
disabled={!isEditing}
/>
</FormItem>
</Col>
<Col span={12}>
<FormItem
label="用户名"
field="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
placeholder="请输入用户名"
prefix={<IconUser />}
disabled={!isEditing}
/>
</FormItem>
</Col>
<Col span={12}>
<FormItem
label="邮箱"
field="email"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入正确的邮箱格式' },
]}
>
<Input
placeholder="请输入邮箱"
prefix={<IconEmail />}
disabled={!isEditing}
/>
</FormItem>
</Col>
<Col span={12}>
<FormItem
label="手机号"
field="phone"
rules={[{ required: true, message: '请输入手机号' }]}
>
<Input
placeholder="请输入手机号"
prefix={<IconPhone />}
disabled={!isEditing}
/>
</FormItem>
</Col>
<Col span={12}>
<FormItem label="性别" field="gender">
<RadioGroup disabled={!isEditing}>
<Radio value="male"></Radio>
<Radio value="female"></Radio>
</RadioGroup>
</FormItem>
</Col>
<Col span={12}>
<FormItem label="生日" field="birthday">
<DatePicker
style={{ width: '100%' }}
placeholder="请选择生日"
disabled={!isEditing}
/>
</FormItem>
</Col>
<Col span={12}>
<FormItem label="部门" field="department">
<Select placeholder="请选择部门" disabled={!isEditing}>
<Option value="技术部"></Option>
<Option value="产品部"></Option>
<Option value="设计部"></Option>
<Option value="运营部"></Option>
<Option value="市场部"></Option>
</Select>
</FormItem>
</Col>
<Col span={12}>
<FormItem label="职位" field="position">
<Input placeholder="请输入职位" disabled={!isEditing} />
</FormItem>
</Col>
<Col span={24}>
<FormItem label="所在地" field="location">
<Input
placeholder="请输入所在地"
prefix={<IconLocation />}
disabled={!isEditing}
/>
</FormItem>
</Col>
<Col span={24}>
<FormItem label="个人简介" field="introduction">
<Input.TextArea
placeholder="请输入个人简介"
rows={4}
disabled={!isEditing}
maxLength={200}
showWordLimit
/>
</FormItem>
</Col>
</Row>
</Form>
</Card>
{/* 账号安全卡片 */}
<Card style={{ marginTop: 20 }} title="账号安全">
<Descriptions
column={1}
data={[
{
label: '登录密码',
value: (
<Space>
<Text></Text>
<Button type="text" size="small">
</Button>
</Space>
),
},
{
label: '密保手机',
value: (
<Space>
<Text>{userInfo.phone}</Text>
<Button type="text" size="small">
</Button>
</Space>
),
},
{
label: '密保邮箱',
value: (
<Space>
<Text>{userInfo.email}</Text>
<Button type="text" size="small">
</Button>
</Space>
),
},
]}
/>
</Card>
</div>
);
}
export default UserInfo;

View File

@ -0,0 +1,81 @@
.container {
padding: 20px;
background: var(--color-bg-2);
min-height: calc(100vh - 60px);
}
.profile-card {
:global(.arco-card-body) {
padding: 32px;
}
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-wrapper {
position: relative;
display: inline-block;
.avatar {
border: 4px solid var(--color-border-2);
box-shadow: 0 2px 12px rgba(0, 0, 0, 10%);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.avatar-upload {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 50%);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s;
font-size: 12px;
&:hover {
opacity: 1;
}
}
}
.stats-section {
:global(.arco-statistic-title) {
color: var(--color-text-3);
font-size: 14px;
}
:global(.arco-statistic-value) {
color: var(--color-text-1);
font-weight: 600;
}
}
// 响应式布局
@media (max-width: 768px) {
.container {
padding: 12px;
}
.profile-card {
:global(.arco-card-body) {
padding: 16px;
}
}
}