前言
行文至此,万里长征已经快要走到头了。本章节带同学们来编写最后一个模块 —— 个人中心。
个人中心模块分几个功能点,首先是头部的用户信息展示,包括头像、用户昵称、个人签名。其次是一些账号相关的操作,如用户信息修改、密码重置等。最后是退出登录,将其放置于页面底部,并且设置二次确认弹窗,避免误触。
知识点
- 图片资源上传格式处理。
- 原生表单插件
rc-form
的使用。
- 底部导航栏定位。
正文
头部信息展示
修改 container/User/index.jsx
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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={" alt="" /> </div> </div> ); };
export default User;
|
文末已为同学们提供下本章节 demo 代码,样式部分不再详细说明。
这里给 .head
一个背景图片,介绍一下顶部的布局思路,如下所示:
在 .head
内通过 flex
实现左右布局,在 .info
内通过 flex
的 flex-direction
设置为 column
实现上下布局。
.head
底部留出的位置,用于放置后续的操作。
完成布局之后,将数据填上,通过 /api/user/get_userinfo
接口,获取用户信息,添加代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| 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:用户登录名称。
浏览器展示如下所示:
用户信息相关操作
紧接着,我们需要布局用户相关操作的内容,在上述基础上添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| ... 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> };
|
添加样式:
1 2 3 4 5 6 7 8 9 10
| ... .content { width: 90%; position: absolute; top: 120px; left: 50%; transform: translateX(-50%); box-shadow: 3px 2px 20px 10px rgba(0, 0, 0, 0.1); border-radius: 10px; overflow: hidden; }
|
代码部分,直接采用 Zarm
组件库提供的 Cell
组件,它适用于列表布局,官方文档提供了很多列表布局的例子,可以直接在内部拷贝代码进行二次加工。能不用自己写样式,尽量就不要写。用组件库的目的,就是减少工作量,提高布局的效率。
浏览器展示效果如下:
这里有三个列表跳转项,分别是 userinfo
、account
、about
。我们逐一击破。
首先我们在 container
目录下新建一个 UserInfo
目录,如下所示:
添加 index.js
和 style.module.less
,并且在 router/index.js
内添加相对应的路由配置项。
于是我们尝试点击「修改用户信息」,如下所示:
成功之后,我们便可在 UserInfo
中编写编辑用户相关信息的操作,在编写正式代码之前,我们先对 Zarm
的上传组件进行分析,我们尝试编写如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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
对象:
1 2 3 4 5
| const handleSelect = (file) => { console.log("file", file); let formData = new FormData(); formData.append("file", file.file); };
|
再将 formData
通过 axios
上传到服务器,服务端通过 ctx.request.files[0]
获取到前端上传的 文件原始对象,并将其读取,存入服务器内部。这样就完成了一套前端上传资源,服务端存储并返回路径的一个过程。
接下来进行完整代码的编写,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| 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"; import { get, post } from "@/utils"; import { baseUrl } from "config"; 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(); formData.append("file", file.file); 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
,添加如下代码:
1 2 3 4
| const MODE = import.meta.env.MODE;
export const baseUrl = MODE == "development" ? "/api" : "http://api.chennick.wang";
|
MODE
作为 vite
运行时的环境变量,可以通过它来配置开发环境和生成环境的一些变量差异。
然后需要在 vite.config.js
中修改如下:
1 2 3 4 5 6 7
| resolve: { alias: { '@': path.resolve(__dirname, 'src'), 'utils': path.resolve(__dirname, 'src/utils'), 'config': path.resolve(__dirname, 'src/config') } },
|
配置好 config
,便可以直接在代码中通过:
1
| import { baseUrl } from "config";
|
上述形式来获取 config
中的变量信息。
重启项目,浏览器展示效果如下:
通过请求,得到的路径是这样的,因为我们在服务端返回的地址就是一个相对路径,所以我们需要给路径加上 host
,要注意如果你是本地启动的服务端代码,这里的 host
就是你的服务端代码启动的 host
,如 locahost:7001
,而我目前使用的是在线接口,所以我们在 utils/index.js
下新增一个图片地址转换的方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12
| 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
并如下使用:
1 2 3 4 5 6 7 8 9 10 11 12
| 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
官方使用的表单组件。
我们为 Account/index.jsx
添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| 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) => { const { getFieldProps, getFieldError } = props.form;
const submit = () => { props.form.validateFields(async (error, value) => { 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);
|
样式代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| .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
内的值,并且可以加以验证。
别忘记在路由配置项中添加相应的路由:
1 2 3 4 5 6 7 8 9
| ... import Account from '@/container/Account'
... { path: "/account", component: Account }
|
页面展示如下:
这里为了方便查看效果,输入框就不以密码的形式隐藏输入了,点击「提交」按钮之后,接口调用成功,但是我为 admin
账户在服务端设置了不能修改密码的权限,这里方便大家测试页面方便,不能随意修改密码。
测试账号:admin,密码:111111
退出登录
退出登录操作,我的处理方式是将本地的 token
清除,并且回到登录页面,简单粗暴了一些,但也不失为一个解决方案。
在 User/index.jsx
下添加代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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> ); };
|
样式如下:
1 2 3 4 5 6 7
| .logout { width: 90%; position: absolute; bottom: 70px; left: 50%; transform: translateX(-50%); }
|
通过绝对定位将按钮定位在底部,我们尝试点击它,如下所示:
再次点击登录,发现没有自动前往首页,我们这里对登录页面进行修改,打开 Login/index.jsx
,做如下修改:
1 2 3 4 5 6 7
| 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
就无法设置。