本文来编写最后一个模块 —— 个人中心,功能如下图所示:


个人中心模块分几个功能点,首先是头部的用户信息展示,包括头像、用户昵称、个人签名。其次是账号相关的操作,如用户信息修改、密码重置等。最后是退出登录,将其放置于页面底部,并且设置二次确认弹窗,避免误触。
头部信息展示
修改 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}><CellhasArrowtitle="用户信息修改"onClick={() => history.push('/userinfo')}icon={<img style={{ width: 20, verticalAlign: '-7px' }} src="//s.yezgea02.com/1615974766264/gxqm.png" alt="" />}/><CellhasArrowtitle="重制密码"onClick={() => history.push('/account')}icon={<img style={{ width: 20, verticalAlign: '-7px' }} src="//s.yezgea02.com/1615974766264/zhaq.png" alt="" />}/><CellhasArrowtitle="关于我们"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 进行请求,统一封装了请求 baseUrlimport 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}><Inputclearabletype="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.jsimport { 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.jsximport 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,否则为 trueif (!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="原密码"><Inputclearabletype="text"placeholder="请输入原密码"{...getFieldProps('oldpass', { rules: [{ required: true }] })}/></Cell><Cell title="新密码"><Inputclearabletype="text"placeholder="请输入新密码"{...getFieldProps('newpass', { rules: [{ required: true }] })}/></Cell><Cell title="确认密码"><Inputclearabletype="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 就无法设置。
总结
实战部分到此结束,我们完成了一个基础的记账本所有的基础功能。
