这节,我们来写管理端的这两个页面:


很明显,它们是和这几个管理页面平级的,点击用户图标的时候打开:

所以,我们在它平级添加个路由:

{
path: "/user",
element: <ModifyMenu></ModifyMenu>,
children: [
{
path: 'info_modify',
element: <InfoModify/>
},
{
path: 'password_modify',
element: <PasswordModify/>
},
]
},
然后创建这几个对应的组件:
src/pages/ModifyMenu/ModifyMenu.stx
import { Outlet } from "react-router-dom";
import { Menu as AntdMenu, MenuProps } from "antd";
import "./menu.css";
const items: MenuProps["items"] = [
{
key: "1",
label: "信息修改",
},
{
key: "2",
label: "密码修改",
},
];
export function ModifyMenu() {
return (
<div id="menu-container">
<div className="menu-area">
<AntdMenu defaultSelectedKeys={["1"]} items={items} />
</div>
<div className="content-area">
<Outlet></Outlet>
</div>
</div>
);
}
用到的 menu.css:
#menu-container {
display: flex;
flex-direction: row;
}
#menu-container .menu-area {
width: 200px;
}
然后是
src/pages/InfoModify/InfoModify.tsx
export function InfoModify() {
return <div>InfoModify</div>;
}
src/pages/PasswordModify/PasswordModify.tsx
export function PasswordModify() {
return <div>PasswordModify</div>;
}
在 index.tsx 引入,然后跑一下:


没啥问题。
但是现在点击菜单是没反应的,我们给它加上 click 事件。

const handleMenuItemClick: MenuClickEventHandler = (info) => {
if (info.key === "1") {
router.navigate("/user/info_modify");
} else {
router.navigate("/user/password_modify");
}
};
这里用到的 router 要在 index.tsx 里导出:

测试下:

点击菜单可以切换路由了。
但现在有个问题,页面一刷新,选中的菜单项就变了:

我们需要根据当前路由来决定选中哪个:

这里用到了 react-router 的 useLocation 的 hook 来拿到当前地址:
location.pathname === "/user/info_modify" ? ["1"] : ["2"];
这样,刷新之后选中的菜单项也是对的:

改下 Index 组件,添加两个链接:

<div className="header">
<Link to="/" className="sys_name">
<h1>会议室预定系统-后台管理</h1>
</Link>
<Link to="/user/info_modify">
<UserOutlined className="icon"/>
</Link>
</div>
并且添加它的样式:
#index-container .sys_name {
text-decoration: none;
color: #000;
}
这样就可以方便跳转对应的路由了:

然后,我们来实现信息修改页面:

之前用户端修改信息页面也是类似的,我们直接拿过来就行:
import { Button, Form, Input, message } from "antd";
import { useForm } from "antd/es/form/Form";
import { useCallback, useEffect, useState } from "react";
import "./info_modify.css";
import { useNavigate } from "react-router-dom";
import { HeadPicUpload } from "./HeadPicUpload";
export interface UserInfo {
username: string;
headPic: string;
nickName: string;
email: string;
captcha: string;
}
const layout1 = {
labelCol: { span: 6 },
wrapperCol: { span: 18 },
};
export function InfoModify() {
const [form] = useForm();
const navigate = useNavigate();
const onFinish = useCallback(async (values: UserInfo) => {}, []);
const sendCaptcha = useCallback(async function () {}, []);
useEffect(() => {
async function query() {}
query();
}, []);
return (
<div id="updateInfo-container">
<Form
form={form}
{...layout1}
onFinish={onFinish}
colon={false}
autoComplete="off">
<Form.Item
label="头像"
name="headPic"
rules={[{ required: true, message: "请输入头像!" }]}
shouldUpdate>
<HeadPicUpload></HeadPicUpload>
</Form.Item>
<Form.Item
label="昵称"
name="nickName"
rules={[{ required: true, message: "请输入昵称!" }]}>
<Input />
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[
{ required: true, message: "请输入邮箱!" },
{ type: "email", message: "请输入合法邮箱地址!" },
]}>
<Input disabled />
</Form.Item>
<div className="captcha-wrapper">
<Form.Item
label="验证码"
name="captcha"
rules={[{ required: true, message: "请输入验证码!" }]}>
<Input />
</Form.Item>
<Button type="primary" onClick={sendCaptcha}>
发送验证码
</Button>
</div>
<Form.Item {...layout1} label=" ">
<Button className="btn" type="primary" htmlType="submit">
修改
</Button>
</Form.Item>
</Form>
</div>
);
}
css 部分如下:
#updateInfo-container {
width: 400px;
margin: 50px auto 0 auto;
text-align: center;
}
#updateInfo-container .btn {
width: 100%;
}
#updateInfo-container .captcha-wrapper {
display: flex;
justify-content: flex-end;
}
用到的 HeadPicUpload 组件如下:
import { InboxOutlined } from "@ant-design/icons";
import { message } from "antd";
import Dragger, { DraggerProps } from "antd/es/upload/Dragger";
interface HeadPicUploadProps {
value?: string;
onChange?: Function;
}
let onChange: Function;
const props: DraggerProps = {
name: "file",
action: "http://localhost:3005/user/upload",
onChange(info) {
const { status } = info.file;
if (status === "done") {
onChange(info.file.response.data);
message.success(`${info.file.name} 文件上传成功`);
} else if (status === "error") {
message.error(`${info.file.name} 文件上传失败`);
}
},
};
const dragger = (
<Dragger {...props}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">点击或拖拽文件到这个区域来上传</p>
</Dragger>
);
export function HeadPicUpload(props: HeadPicUploadProps) {
onChange = props.onChange!;
return props?.value ? (
<div>
<img
src={"http://localhost:3005/" + props.value}
alt="头像"
width="100"
height="100"
/>
{dragger}
</div>
) : (
<div>{dragger}</div>
);
}
这些都是我们前面写过一遍的。
渲染出来是这样的: 
上传功能也是可用的:

然后我们还要加上回显接口、发送验证码接口、更新接口。
在 interfaces.tsx 加上这三个接口:
export async function getUserInfo() {
return await axiosInstance.get("/user/info");
}
export async function updateInfo(data: UserInfo) {
return await axiosInstance.post("/user/admin/update", data);
}
export async function updateUserInfoCaptcha() {
return await axiosInstance.get("/user/update/captcha");
}
然后先调用下回显接口:

async function query() {
const res = await getUserInfo();
const { data } = res.data;
if (res.status === 201 || res.status === 200) {
form.setFieldValue("headPic", data.headPic);
form.setFieldValue("nickName", data.nickName);
form.setFieldValue("email", data.email);
}
}

可以看到,正确回显了数据。
然后是发送验证码接口:
const sendCaptcha = useCallback(async function () {
const res = await updateUserInfoCaptcha();
if (res.status === 201 || res.status === 200) {
message.success(res.data.data);
} else {
message.error("系统繁忙,请稍后再试");
}
}, []);
不过现在的邮箱地址不是真实的,我们手动去数据库里改一下:

改完点击 apply。
然后需要重新登录一遍,因为现在后端会直接从 jwt 里取邮箱地址,重新登录才会更新。

邮箱收到了验证码:

然后加上更新用户信息的接口:
const onFinish = useCallback(async (values: UserInfo) => {
const res = await updateInfo(values);
if (res.status === 201 || res.status === 200) {
const { message: msg, data } = res.data;
if (msg === "success") {
message.success("用户信息更新成功");
} else {
message.error(data);
}
} else {
message.error("系统繁忙,请稍后再试");
}
}, []);
上传头像,点击发送验证码,填入收到的验证码,点击修改:

修改成功后,刷新页面,可以看到依然是修改后的数据,就代表修改成功了:

接下来是密码修改页面:

代码如下:
import { Button, Form, Input, message } from "antd";
import { useForm } from "antd/es/form/Form";
import "./password_modify.css";
import { useCallback } from "react";
import { Link, useNavigate } from "react-router-dom";
export interface UpdatePassword {
email: string;
captcha: string;
password: string;
confirmPassword: string;
}
const layout1 = {
labelCol: { span: 6 },
wrapperCol: { span: 18 },
};
const layout2 = {
labelCol: { span: 0 },
wrapperCol: { span: 24 },
};
export function PasswordModify() {
const [form] = useForm();
const navigate = useNavigate();
const onFinish = useCallback(async (values: UpdatePassword) => {}, []);
const sendCaptcha = useCallback(async function () {}, []);
return (
<div id="updatePassword-container">
<Form
form={form}
{...layout1}
onFinish={onFinish}
colon={false}
autoComplete="off">
<Form.Item
label="密码"
name="password"
rules={[{ required: true, message: "请输入密码!" }]}>
<Input.Password />
</Form.Item>
<Form.Item
label="确认密码"
name="confirmPassword"
rules={[{ required: true, message: "请输入确认密码!" }]}>
<Input.Password />
</Form.Item>
<Form.Item
label="邮箱"
name="email"
rules={[
{ required: true, message: "请输入邮箱!" },
{ type: "email", message: "请输入合法邮箱地址!" },
]}>
<Input />
</Form.Item>
<div className="captcha-wrapper">
<Form.Item
label="验证码"
name="captcha"
rules={[{ required: true, message: "请输入验证码!" }]}>
<Input />
</Form.Item>
<Button type="primary" onClick={sendCaptcha}>
发送验证码
</Button>
</div>
<Form.Item {...layout1} label=" ">
<Button className="btn" type="primary" htmlType="submit">
修改密码
</Button>
</Form.Item>
</Form>
</div>
);
}
用到的 password_modify.css:
#updatePassword-container {
width: 400px;
margin: 40px auto;
text-align: center;
}
#updatePassword-container .btn {
width: 100%;
}
#updatePassword-container .captcha-wrapper {
display: flex;
justify-content: flex-end;
}
渲染出来是这样的:

然后在 interfaces.ts 加上用到的发送验证码、修改密码这两个接口:
export async function updatePasswordCaptcha(email: string) {
return await axiosInstance.get("/user/update_password/captcha", {
params: {
address: email,
},
});
}
export async function updatePassword(data: UpdatePassword) {
return await axiosInstance.post("/user/admin/update_password", data);
}
然后先在页面调用下回显接口:

useEffect(() => {
async function query() {
const res = await getUserInfo();
const { data } = res.data;
if (res.status === 201 || res.status === 200) {
form.setFieldValue("username", data.username);
form.setFieldValue("email", data.email);
}
}
query();
}, []);
并把邮箱 Input 设置为 disabled

这样邮箱地址就会回显,并且只读:

然后调用发送验证码接口:
const sendCaptcha = useCallback(async function () {
const address = form.getFieldValue("email");
if (!address) {
return message.error("邮箱地址为空");
}
const res = await updatePasswordCaptcha(address);
if (res.status === 201 || res.status === 200) {
message.success(res.data.data);
} else {
message.error("系统繁忙,请稍后再试");
}
}, []);
点击发送验证码:

邮箱收到了对应的验证码:
然后加上修改密码接口:
const onFinish = useCallback(async (values: UpdatePassword) => {
if (values.password !== values.confirmPassword) {
return message.error("两次密码不一致");
}
const res = await updatePassword({
...values,
username: form.getFieldValue("username"),
});
const { message: msg, data } = res.data;
if (res.status === 201 || res.status === 200) {
message.success("密码修改成功");
} else {
message.error(data || "系统繁忙,请稍后再试");
}
}, []);
提示密码修改成功:

我们可以去登录页面,用老密码试试:

再用新密码试试:

这样,管理端的用户相关的页面就完成了。
案例代码上传了小册仓库。
总结
这节我们实现了管理端的用户信息修改和密码修改的页面。
首先添加了一个和管理页面平级的二级路由,然后添加了两个组件。
这两个页面都是表单,涉及到回显数据、发送验证码、上传文件、更新接口。
这也是管理系统的常见功能。
下节开始,我们就开始写会议室管理的功能了。
