本文来编写最后一个模块 —— 个人中心,功能如下图所示:
个人中心模块分几个功能点,首先是头部的用户信息展示,包括头像、用户昵称、个人签名。其次是账号相关的操作,如用户信息修改、密码重置等。最后是退出登录,将其放置于页面底部,并且设置二次确认弹窗,避免误触。
头部信息展示
修改 container/User/index.jsx 代码如下:
import React from 'react';
import s from './style.module.less';
const User = () => {
return <div className={s.user}>
<div className={s.head}>
<div className={s.info}>
<span>昵称:测试</span>
<span>
<img style={{ width: 30, height: 30, verticalAlign: '-10px' }} src="//s.yezgea02.com/1615973630132/geqian.png" alt="" />
<b>个性签名</b>
</span>
</div>
<img className={s.avatar} style={{ width: 60, height: 60, borderRadius: 8 }} src={'//s.yezgea02.com/1624959897466/avatar.jpeg'} alt="" />
</div>
</div>
}
export default User
这里给 .head 一个背景图片,介绍一下顶部的布局思路,如下所示:
在 .head 内通过 flex 实现左右布局,在 .info 内通过 flex 的 flex-direction 设置为 column 实现上下布局。
.head 底部留出的位置,用于放置后续的操作。
完成布局之后,将数据填上,通过 /api/user/get_userinfo 接口,获取用户信息,添加代码如下:
import React, { useState, useEffect } from 'react';
import { get } from '@/utils';
import s from './style.module.less';
const User = () => {
const [user, setUser] = useState({});
useEffect(() => {
getUserInfo();
}, []);
// 获取用户信息
const getUserInfo = async () => {
const { data } = await get('/api/user/get_userinfo');
setUser(data);
setAvatar(data.avatar)
};
return <div className={s.user}>
<div className={s.head}>
<div className={s.info}>
<span>昵称:{user.username || '--'}</span>
<span>
<img style={{ width: 30, height: 30, verticalAlign: '-10px' }} src="//s.yezgea02.com/1615973630132/geqian.png" alt="" />
<b>{user.signature || '暂无个签'}</b>
</span>
</div>
<img className={s.avatar} style={{ width: 60, height: 60, borderRadius: 8 }} src={user.avatar || ''} alt="" />
</div>
</div>
}
export default User
用户信息相关操作
紧接着,我们需要布局用户相关操作的内容,在上述基础上添加如下代码:
...
import { useHistory } from 'react-router-dom';
import { Cell, } from 'zarm';
const User = () => {
...
const history = useHistory();
return <div className={s.user}>
...
<div className={s.content}>
<Cell
hasArrow
title="用户信息修改"
onClick={() => history.push('/userinfo')}
icon={<img style={{ width: 20, verticalAlign: '-7px' }} src="//s.yezgea02.com/1615974766264/gxqm.png" alt="" />}
/>
<Cell
hasArrow
title="重制密码"
onClick={() => history.push('/account')}
icon={<img style={{ width: 20, verticalAlign: '-7px' }} src="//s.yezgea02.com/1615974766264/zhaq.png" alt="" />}
/>
<Cell
hasArrow
title="关于我们"
onClick={() => history.push('/about')}
icon={<img style={{ width: 20, verticalAlign: '-7px' }} src="//s.yezgea02.com/1615975178434/lianxi.png" alt="" />}
/>
</div>
</div>
};
这里有三个列表跳转项,分别是 userinfo、account、about。
用户信息修改
在 container 目录下新建一个 UserInfo 目录。
添加 index.js 和 style.module.less,并且在 router/index.js 内添加相对应的路由配置项。
文件上传组件分析
我们先对 Zarm 的文件上传组件进行分析,我们尝试编写如下代码:
import React from 'react';
import { FilePicker, Button } from 'zarm';
import s from './style.module.less';
const UserInfo = () => {
const handleSelect = (file) => {
console.log('file', file)
}
return <div className={s.userinfo}>
<FilePicker onChange={handleSelect} accept="image/*">
<Button theme='primary' size='xs'>点击上传</Button>
</FilePicker>
</div>
};
export default UserInfo;
点击按钮,上传一张图片,我们查看回调函数 handleSelect 的执行结果:
此时,我们需要的是上传资源的原始文件,在上述返回对象中,file 属性为 File 文件类型,它是浏览器返回的原生对象,我们需要通过下列代码,将其改造成一个 form-data 对象:
const handleSelect = (file) => {
console.log('file', file)
let formData = new FormData()
formData.append('file', file.file)
}
再将 formData 通过 axios 上传到服务器,服务端通过 ctx.request.files[0] 获取到前端上传的 文件原始对象,并将其读取,存入服务器内部。这样就完成了一套前端上传资源,服务端存储并返回路径的一个过程。
接下来进行完整代码的编写,如下所示:
import React, { useEffect, useState } from 'react';
import { Button, FilePicker, Input, Toast } from 'zarm';
import { useHistory } from 'react-router-dom';
import Header from '@/components/Header'; // 由于是内页,使用到公用头部
import axios from 'axios'; // // 由于采用 form-data 传递参数,所以直接只用 axios 进行请求
import { get, post } from '@/utils';
import { baseUrl } from 'config'; // 由于直接使用 axios 进行请求,统一封装了请求 baseUrl
import s from './style.module.less';
const UserInfo = () => {
const history = useHistory(); // 路由实例
const [user, setUser] = useState({}); // 用户
const [avatar, setAvatar] = useState(''); // 头像
const [signature, setSignature] = useState(''); // 个签
const token = localStorage.getItem('token'); // 登录令牌
useEffect(() => {
getUserInfo(); // 初始化请求
}, []);
// 获取用户信息
const getUserInfo = async () => {
const { data } = await get('/api/user/get_userinfo');
setUser(data);
setAvatar(data.avatar)
setSignature(data.signature)
};
// 获取图片回调
const handleSelect = (file) => {
console.log('file.file', file.file)
if (file && file.file.size > 200 * 1024) {
Toast.show('上传头像不得超过 200 KB!!')
return
}
let formData = new FormData()
// 生成 form-data 数据类型
formData.append('file', file.file)
// 通过 axios 设置 'Content-Type': 'multipart/form-data', 进行文件上传
axios({
method: 'post',
url: `${baseUrl}/upload`,
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': token
}
}).then(res => {
// 返回图片地址
setAvatar(res.data)
})
}
// 编辑用户信息方法
const save = async () => {
const { data } = await post('/api/user/edit_userinfo', {
signature,
avatar
});
Toast.show('修改成功')
// 成功后回到个人中心页面
history.goBack()
}
return <>
<Header title='用户信息' />
<div className={s.userinfo}>
<h1>个人资料</h1>
<div className={s.item}>
<div className={s.title}>头像</div>
<div className={s.avatar}>
<img className={s.avatarUrl} src={avatar} alt=""/>
<div className={s.desc}>
<span>支持 jpg、png、jpeg 格式大小 200KB 以内的图片</span>
<FilePicker className={s.filePicker} onChange={handleSelect} accept="image/*">
<Button className={s.upload} theme='primary' size='xs'>点击上传</Button>
</FilePicker>
</div>
</div>
</div>
<div className={s.item}>
<div className={s.title}>个性签名</div>
<div className={s.signature}>
<Input
clearable
type="text"
value={signature}
placeholder="请输入个性签名"
onChange={(value) => setSignature(value)}
/>
</div>
</div>
<Button onClick={save} style={{ marginTop: 50 }} block theme='primary'>保存</Button>
</div>
</>
};
export default UserInfo;
浏览器展示效果如下:
通过请求,得到的路径是这样的,因为我们在服务端返回的地址就是一个相对路径,所以我们需要给路径加上 host,要注意如果你是本地启动的服务端代码,这里的 host 就是你的服务端代码启动的 host,如 locahost:7001,而我目前使用的是在线接口,所以我们在 utils/index.js 下新增一个图片地址转换的方法,如下所示:
// utils/index.js
import { baseUrl } from 'config'
const MODE = import.meta.env.MODE // 环境变量
...
export const imgUrlTrans = (url) => {
if (url && url.startsWith('http')) {
return url
} else {
url = `${MODE == 'development' ? 'http://api.chennick.wang' : baseUrl}${url}`
return url
}
}
然后在 UserInfo/index.jsx 中引入 imgUrlTrans 并如下使用:
// 获取用户信息
const getUserInfo = async () => {
const { data } = await get('/api/user/get_userinfo');
setUser(data);
setAvatar(imgUrlTrans(data.avatar))
setSignature(data.signature)
};
...
// 返回图片地址
setAvatar(imgUrlTrans(res.data))
修改密码
在 container 目录下新建 Account 目录,在内部分别新建 index.jsx 和 style.module.less。
首先我们需要安装 rc-form 作为本次页面的表单组件,因为 Zarm 没有提供表单组件,包括 Antd Mobile 这样的组件,也没有提供表单相关的组件,所以这里我们需要使用 rc-form 自己编写表单相关验证方法,它也是 antd 官方使用的表单组件。
npm i rc-form -S
为 Account/index.jsx 添加如下代码:
// Account/index.jsx
import React from 'react';
import { Cell, Input, Button, Toast } from 'zarm';
import { createForm } from 'rc-form';
import Header from '@/components/Header'
import { post } from '@/utils'
import s from './style.module.less'
const Account = (props) => {
// Account 通过 createForm 高阶组件包裹之后,可以在 props 中获取到 form 属性
const { getFieldProps, getFieldError } = props.form;
// 提交修改方法
const submit = () => {
// validateFields 获取表单属性元素
props.form.validateFields(async (error, value) => {
// error 表单验证全部通过,为 false,否则为 true
if (!error) {
console.log(value)
if (value.newpass != value.newpass2) {
Toast.show('新密码输入不一致');
return
}
await post('/api/user/modify_pass', {
old_pass: value.oldpass,
new_pass: value.newpass,
new_pass2: value.newpass2
})
Toast.show('修改成功')
}
});
}
return <>
<Header title="重制密码" />
<div className={s.account}>
<div className={s.form}>
<Cell title="原密码">
<Input
clearable
type="text"
placeholder="请输入原密码"
{...getFieldProps('oldpass', { rules: [{ required: true }] })}
/>
</Cell>
<Cell title="新密码">
<Input
clearable
type="text"
placeholder="请输入新密码"
{...getFieldProps('newpass', { rules: [{ required: true }] })}
/>
</Cell>
<Cell title="确认密码">
<Input
clearable
type="text"
placeholder="请再此输入新密码确认"
{...getFieldProps('newpass2', { rules: [{ required: true }] })}
/>
</Cell>
</div>
<Button className={s.btn} block theme="primary" onClick={submit}>提交</Button>
</div>
</>
};
export default createForm()(Account);
这里要注意,Account 在抛出去的时候,需要用 createForm() 高阶组件进行包裹,这样在 Account 的内部能接收到 form 属性,它的内部提供了 getFieldProps 方法,对 Input 组件进行表单设置,Input 的 onChange 方法会被代理,最终可以通过 form.validateFields 以回到函数的形式拿到 Input 内的值,并且可以加以验证。
在路由配置项中添加相应的路由:
// router/index.js
...
import Account from '@/container/Account'
...
{
path: "/account",
component: Account
}
退出登陆
退出登录操作,我的处理方式是将本地的 token 清除,并且回到登录页面
在 User/index.jsx 下添加代码如下:
const User = () => {
// 退出登录
const logout = async () => {
localStorage.removeItem('token');
history.push('/login');
};
return <div className={s.user}>
...
<Button className={s.logout} block theme="danger" onClick={logout}>退出登录</Button>
</div>
}
当点击退出登陆,再次点击登陆的时候发现没有自动跳转首页,需要修改一下Login/index.jsx中的逻辑:
const { data } = await post('/api/user/login', {
username,
password
});
console.log('data', data)
localStorage.setItem('token', data.token);
window.location.href = '/'
这里之所以用 window.location.href 的原因是,utils/axios.js 内部需要再次被执行,才能通过 localStorage.getItem 拿到最新的 token。如果只是用 history.push 跳转页面的话,页面是不会被刷新,那么 axios.js 的 token 就无法设置。
总结
实战部分到此结束,我们完成了一个基础的记账本所有的基础功能。