前言
# 前言
行文至此,万里长征已经快要走到头了。本章节带同学们来编写最后一个模块 —— 个人中心。
个人中心模块分几个功能点,首先是头部的用户信息展示,包括头像、用户昵称、个人签名。其次是一些账号相关的操作,如用户信息修改、密码重置等。最后是退出登录,将其放置于页面底部,并且设置二次确认弹窗,避免误触。
# 知识点
- 图片资源上传格式处理。
- 原生表单插件
rc-form
的使用。 - 底部导航栏定位。
# 正文
# 头部信息展示
修改 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
文末已为同学们提供下本章节 demo 代码,样式部分不再详细说明。
这里给 .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
/api/user/get_userinfo
接口返回字段分析:
- avatar:头像地址,这里要注意,我目前采用的线上接口,如果是本地开发的情况,需要修改你的
host
。 - signature:个性签名。
- username:用户登录名称。
浏览器展示如下所示:
# 用户信息相关操作
紧接着,我们需要布局用户相关操作的内容,在上述基础上添加如下代码:
...
import { useNavigate } from 'react-router-dom';
import { Cell, } from 'zarm';
const User = () => {
...
const navigateTo = useNavigate();
return <div className={s.user}>
...
<div className={s.content}>
<Cell
hasArrow
title="用户信息修改"
onClick={() => navigateTo('/userinfo')}
icon={<img style={{ width: 20, verticalAlign: '-7px' }} src="//s.yezgea02.com/1615974766264/gxqm.png" alt="" />}
/>
<Cell
hasArrow
title="重制密码"
onClick={() => navigateTo('/account')}
icon={<img style={{ width: 20, verticalAlign: '-7px' }} src="//s.yezgea02.com/1615974766264/zhaq.png" alt="" />}
/>
<Cell
hasArrow
title="关于我们"
onClick={() => navigateTo('/about')}
icon={<img style={{ width: 20, verticalAlign: '-7px' }} src="//s.yezgea02.com/1615975178434/lianxi.png" alt="" />}
/>
</div>
</div>
};
添加样式:
...
.content {
width: 90%;
position: absolute;
top: 120px;
left: 50%;
transform: translateX(-50%);
box-shadow: 3px 2px 20px 10px rgba(0, 0, 0, .1);
border-radius: 10px;
overflow: hidden;
}
代码部分,直接采用 Zarm
组件库提供的 Cell
组件,它适用于列表布局,官方文档 (opens new window)提供了很多列表布局的例子,可以直接在内部拷贝代码进行二次加工。能不用自己写样式,尽量就不要写。用组件库的目的,就是减少工作量,提高布局的效率。
浏览器展示效果如下:
这里有三个列表跳转项,分别是 userinfo
、account
、about
。我们逐一击破。
首先我们在 container
目录下新建一个 UserInfo
目录,如下所示:
添加 index.js
和 style.module.less
,并且在 router/index.js
内添加相对应的路由配置项。
于是我们尝试点击「修改用户信息」,如下所示:
成功之后,我们便可在 UserInfo
中编写编辑用户相关信息的操作,在编写正式代码之前,我们先对 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 { useNavigate } 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 navigateTo = useNavigate(); // 路由实例
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('修改成功')
// 成功后回到个人中心页面
navigateTo(-1)
}
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;
详细的注释信息,已经在上述代码中表明,需要注意的是,本次请求直接使用了 axios
方法,所以我们需要将 baseUrl
单独封装到一个配置文件中,便于后续使用,在 src
目录下新建 config/index.js
,添加如下代码:
const MODE = import.meta.env.MODE // 环境变量
export const baseUrl = MODE == 'development' ? '/api' : 'http://api.chennick.wang';
MODE
作为 vite
运行时的环境变量,可以通过它来配置开发环境和生成环境的一些变量差异。
然后需要在 vite.config.js
中修改如下:
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'), // src 路径
'utils': path.resolve(__dirname, 'src/utils'), // src 路径
'config': path.resolve(__dirname, 'src/config') // src 路径
}
},
配置好 config
,便可以直接在代码中通过:
import { baseUrl } from 'config';
上述形式来获取 config
中的变量信息。
重启项目,浏览器展示效果如下:
通过请求,得到的路径是这样的,因为我们在服务端返回的地址就是一个相对路径,所以我们需要给路径加上 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 {
padding: 0 12px;
.form {
:global {
.za-cell:after {
left: unset;
border-top: unset;
border-bottom: 1PX solid #e9e9e9;
}
}
}
.btn {
margin-top: 50px;
}
}
这里要注意,Account
在抛出去的时候,需要用 createForm()
高阶组件进行包裹,这样在 Account
的内部能接收到 form
属性,它的内部提供了 getFieldProps
方法,对 Input
组件进行表单设置,Input
的 onChange
方法会被代理,最终可以通过 form.validateFields
以回到函数的形式拿到 Input
内的值,并且可以加以验证。
别忘记在路由配置项中添加相应的路由:
// router/index.js
...
import Account from '@/container/Account'
...
{
path: "/account",
component: Account
}
页面展示如下:
这里为了方便查看效果,输入框就不以密码的形式隐藏输入了,点击「提交」按钮之后,接口调用成功,但是我为 admin
账户在服务端设置了不能修改密码的权限,这里方便大家测试页面方便,不能随意修改密码。
测试账号:admin,密码:111111
# 退出登录
退出登录操作,我的处理方式是将本地的 token
清除,并且回到登录页面,简单粗暴了一些,但也不失为一个解决方案。
在 User/index.jsx
下添加代码如下:
const User = () => {
// 退出登录
const logout = async () => {
localStorage.removeItem('token');
navigateTo('/login');
};
return <div className={s.user}>
...
<Button className={s.logout} block theme="danger" onClick={logout}>退出登录</Button>
</div>
}
样式如下:
.logout {
width: 90%;
position: absolute;
bottom: 70px;
left: 50%;
transform: translateX(-50%);
}
通过绝对定位将按钮定位在底部,我们尝试点击它,如下所示:
再次点击登录,发现没有自动前往首页,我们这里对登录页面进行修改,打开 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
。如果只是用 navigateTo
跳转页面的话,页面是不会被刷新,那么 axios.js
的 token
就无法设置。
# 总结
实战部分到此结束,同学们若是在实战中遇到了问题,请前往课程的官方交流群,群里有与你志同道合的同学,以及本人也会在群里为大家排忧解难。