页面效果
用户信息上下文
- 返回一个
userInfo、设置userInfo函数,为了缓存userInfo - 对象、函数通过
userContext传递需要加useMemo、useCallback,防止多次执行
import { IUserInfo } from '@/apis/types';
import {
createContext, useCallback, useMemo, useRef, useState,
} from 'react';
export type UserInfoReturnType = {
userInfo: IUserInfo
setUserInfoCallback: (user: IUserInfo, isLogout?: boolean) => void
};
// 用于获取用户信息
export const useUserInfo = (): UserInfoReturnType => {
// 使用useRef创建一个autoLogin变量,用于存储本地存储的autoLogin值
const autoLogin = useRef(localStorage.getItem('autoLogin'));
// 使用useState创建一个userInfo变量,用于存储用户信息
const [userInfo, setUserInfo] = useState<IUserInfo>(() => {
// 获取用户信息
const userInfoJson = autoLogin.current
? localStorage.getItem('userInfo')
: sessionStorage.getItem('userInfo');
return userInfoJson
? JSON.parse(userInfoJson)
: {
avatar: '',
desc: '',
id: '',
name: '',
tel: '',
};
});
// 使用useCallback创建一个setUserInfoCallback函数,用于更新用户信息
const setUserInfoCallback = useCallback(
(user: IUserInfo, isLogout = false) => {
// 更新用户信息
setUserInfo(user);
// 更新autoLogin变量
autoLogin.current = localStorage.getItem('autoLogin');
// 如果isLogout为true,则从本地存储中移除userInfo
if (isLogout) {
localStorage.removeItem('userInfo');
sessionStorage.removeItem('userInfo');
// 否则,如果autoLogin变量为true,则将userInfo存储到本地存储中
} else if (autoLogin.current) {
localStorage.setItem('userInfo', JSON.stringify(user));
// 否则,将userInfo存储到sessionStorage中
} else {
sessionStorage.setItem('userInfo', JSON.stringify(user));
}
},
// 传入的参数
[],
);
// 使用useMemo Hook,将userInfo和setUserInfoCallback函数作为返回值
const contextValue = useMemo(
() => ({ userInfo, setUserInfoCallback }),
[userInfo, setUserInfoCallback],
);
// 返回contextValue
return contextValue;
};
export const UserInfoContext = createContext<any>(null);
src\App.tsx
import { useRoutes } from 'react-router-dom';
import { UserInfoContext, useUserInfo } from './hooks';
import { routes } from './router';
function App() {
const contextValue = useUserInfo();
return (
<UserInfoContext.Provider value={contextValue}>
{useRoutes(routes)}
</UserInfoContext.Provider>
);
}
export default App;
导航栏
src\router\index.tsx
- 当访问
/路径就重定向到/home路径
import Layout from '@/components/layout';
import { lazy } from 'react';
import { Navigate, RouteObject } from 'react-router-dom';
const Home = lazy(() => import('@/views/home'));
export const routes: RouteObject[] = [
{
path: '/',
element: <Layout />,
children: [
{
// 重定向
path: '/',
element: <Navigate to="/home" />,
},
{
path: 'home',
element: <Home />,
},
],
},
];
src\components\layout\index.tsx
- 退出登录时,清空本地存储的信息
- 通过
useContext拿到userInfo - 使用
<Outlet />显示页面内容,Suspense防止路由加了lazy白屏
import { UserInfoContext, UserInfoReturnType } from '@/hooks';
import { MenuRoutes } from '@/router/menus';
import { removeLocalStorageItems, removeSessionStorageItems } from '@/utils';
import { LogoutOutlined } from '@ant-design/icons';
import { MenuDataItem, ProLayout } from '@ant-design/pro-components';
import { Dropdown } from 'antd';
import { Suspense, useContext } from 'react';
import { Link, useNavigate, Outlet } from 'react-router-dom';
import style from './index.module.less';
const menuItemRender = (item: MenuDataItem, dom: React.ReactNode) => (
<Link to={item.path || '/'}>{dom}</Link>
);
const Layout = () => {
const nav = useNavigate();
const { userInfo, setUserInfoCallback } = useContext<UserInfoReturnType>(UserInfoContext);
const handlerLogout = () => {
removeSessionStorageItems(['accessToken', 'refreshToken', 'userInfo']);
removeLocalStorageItems([
'accessToken',
'refreshToken',
'userInfo',
'autoLogin',
]);
setUserInfoCallback(
{
avatar: '',
desc: '',
id: '',
name: '',
tel: '',
},
true,
);
nav('/login');
};
return (
<ProLayout
className={style.container}
layout="mix"
// 设置头像属性
avatarProps={{
src: userInfo.avatar,
title: userInfo.name,
size: 'small',
onClick: () => nav('/my'),
// 渲染头像
render: (props, dom) => (
<Dropdown
menu={{
items: [
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
onClick: handlerLogout,
},
],
}}
>
{dom}
</Dropdown>
),
}}
// 设置标题
title="教育平台"
// 设置路由
route={{
path: '/',
routes: MenuRoutes,
}}
// 设置菜单项渲染函数
menuItemRender={menuItemRender}
// 点击菜单头部时触发的函数
onMenuHeaderClick={() => nav('/')}
>
<Suspense fallback="">
<Outlet />
</Suspense>
</ProLayout>
);
};
export default Layout;
src\router\menus.tsx
导航栏数据
export const MenuRoutes = [
{
path: 'home',
name: '首页',
icon: <HomeOutlined />,
},
];
src/apis/user.ts
import { get, post } from '.';
import { IUpdateUserParams, IUserInfo } from './types';
export const updateUser = (id:string, params: IUpdateUserParams) => post(`user/${id}`, params);
export const findUser = (tel: string) => get<IUserInfo>(`user/${tel}`);
src/apis/types.ts
export interface IUserInfo {
id: string
desc: string
name: string
tel: string
avatar: string
}
export type IUpdateUserParams = Omit<IUserInfo, 'id'>;
个人信息
src/router/menus.tsx
个人信息页面不需要显示在侧边栏,所以hideInMenu设置为true
export const MenuRoutes = [
{
path: 'my',
name: '个人信息',
icon: <HomeOutlined />,
hideInMenu: true,
},
];
src/views/my/index.tsx
- 初始化页面请求最新的
userInfo,保存到useContext并回显表单 - 提交最新数据并更新到
useContext
import { findUser, updateUser } from '@/apis/user';
import OSSUpload from '@/components/ossImageUpload';
import { UserInfoContext, UserInfoReturnType } from '@/hooks';
import {
PageContainer,
ProForm,
ProFormInstance,
ProFormText,
ProFormTextArea,
} from '@ant-design/pro-components';
import {
Col, Form, message, Row,
} from 'antd';
import { useContext, useEffect, useRef } from 'react';
const My = () => {
const formRef = useRef<ProFormInstance>();
const { userInfo, setUserInfoCallback } = useContext<UserInfoReturnType>(UserInfoContext);
const handleFindUser = async () => {
try {
const res = await findUser(userInfo.tel);
if (res.code === 200) {
setUserInfoCallback(res.data);
const {
tel, desc, avatar, name,
} = res.data;
formRef.current?.setFieldsValue({
tel,
desc,
avatar: {
url: avatar,
},
name,
});
}
} catch (error) {
console.log('error', error);
}
};
// 处理查找用户
useEffect(() => {
handleFindUser();
}, []);
return (
<PageContainer>
<ProForm
formRef={formRef}
// 设置表单布局为水平
layout="horizontal"
// 设置提交按钮的属性
submitter={{
resetButtonProps: {
// 隐藏重置按钮
style: {
display: 'none',
},
},
}}
// 表单提交时触发的回调函数
onFinish={async (values) => {
try {
await updateUser(userInfo.id, {
...values,
avatar: values.avatar?.url || '',
});
message.success('修改成功');
const res = await findUser(userInfo.tel);
if (res.code === 200) {
setUserInfoCallback(res.data);
}
} catch (error) {
console.log('error', error);
}
}}
>
<Row gutter={20}>
<Col>
<ProFormText
name="tel"
label="手机号"
tooltip="不能修改"
disabled
/>
<ProFormText name="name" label="昵称" placeholder="请输入昵称" />
<ProFormTextArea
name="desc"
label="简介"
placeholder="请输入简介信息"
/>
</Col>
<Col>
<Form.Item name="avatar">
<OSSUpload />
</Form.Item>
</Col>
</Row>
</ProForm>
</PageContainer>
);
};
export default My;
上传图片组件
- 初始化组件获取上传参数信息保存到
OSSData getExtraData返回加工后参数信息handleChange状态是removed就直接退出,否则就重新生成file对象
import React, {
FC, useEffect, useRef, useState,
} from 'react';
import ImgCrop from 'antd-img-crop';
import type { UploadProps } from 'antd';
import { message, Upload } from 'antd';
import type { UploadFile } from 'antd/es/upload/interface';
import { getOSSInfo } from '@/apis/oss';
import { IOSSRes } from '@/apis/types';
interface OSSUploadProps {
value?: UploadFile;
onChange?: (file?: UploadFile) => void;
}
const OSSUpload: FC = ({ value, onChange }: OSSUploadProps) => {
const [OSSData, setOSSData] = useState<IOSSRes>();
const key = useRef('');
const init = async () => {
try {
const result = await getOSSInfo();
setOSSData(result.data);
} catch (error:any) {
message.error(error);
}
};
useEffect(() => {
init();
}, []);
const handleChange: UploadProps['onChange'] = ({ file }) => {
if (file.status === 'removed') {
onChange?.();
return;
}
const newFile = {
...file,
url: `${OSSData?.host}/${key.current}`,
};
onChange?.(newFile);
};
const getExtraData: UploadProps['data'] = (file) => {
// 获取文件后缀
const suffix = file.name.slice(file.name.lastIndexOf('.'));
// 获取文件名
const filename = Date.now() + suffix;
// 获取文件路径
key.current = `${OSSData?.dir}${filename}`;
return {
key: key.current,
OSSAccessKeyId: OSSData?.accessId,
policy: OSSData?.policy,
Signature: OSSData?.signature,
};
};
const beforeUpload: UploadProps['beforeUpload'] = async (file) => {
if (!OSSData) return false;
const expire = Number(OSSData.expire) * 1000;
if (expire < Date.now()) {
await init();
}
return file;
};
return (
<ImgCrop rotationSlider>
<Upload
name="file"
fileList={value ? [value] : []}
action={OSSData?.host}
onChange={handleChange}
data={getExtraData}
beforeUpload={beforeUpload}
listType="picture-card"
>
+ 替换头像
</Upload>
</ImgCrop>
);
};
OSSUpload.defaultProps = {
value: null,
onChange: () => {},
};
export default OSSUpload;
src/apis/types.ts
export interface IOSSRes {
dir: string;
expire: string;
host: string;
accessId: string;
policy: string;
signature: string;
}
结果
修改前
修改后