教育平台前端-侧边栏及个人信息

126 阅读4分钟

页面效果

image.png

image.png

用户信息上下文

  1. 返回一个userInfo、设置userInfo函数,为了缓存userInfo
  2. 对象、函数通过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

  1. 当访问/路径就重定向到/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

  1. 退出登录时,清空本地存储的信息
  2. 通过useContext拿到userInfo
  3. 使用<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

  1. 初始化页面请求最新的userInfo,保存到useContext并回显表单
  2. 提交最新数据并更新到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;

上传图片组件

  1. 初始化组件获取上传参数信息保存到OSSData
  2. getExtraData返回加工后参数信息
  3. 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;
}

结果

修改前 image.png 修改后 image.png