From f178bbf5dc6a9d08c4980b3a5dfadc945e47d8c7 Mon Sep 17 00:00:00 2001 From: dark Date: Thu, 2 May 2024 00:17:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=99=BB=E5=BD=95=EF=BC=8C?= =?UTF-8?q?=E9=80=80=E5=87=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 58 +++--- src/components/avatar/index.tsx | 87 +++++---- src/pages/login/index.css | 123 ------------ src/pages/login/index.tsx | 26 +-- src/request.ts | 217 +++++++++++---------- src/routes.tsx | 415 +++++++++++++++++++++------------------- 6 files changed, 408 insertions(+), 518 deletions(-) delete mode 100644 src/pages/login/index.css diff --git a/src/App.tsx b/src/App.tsx index bc068d7..cf35203 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,51 +1,37 @@ import { AppContextProvider } from '@/context.ts' import { initI18n } from '@/i18n.ts' import { appAtom, appStore, changeLanguage } from '@/store/system.ts' -import { userMenuDataAtom } from '@/store/user.ts' import { IAppData } from '@/global' import { ConfigProvider } from '@/components/config-provider' -import { Provider, useAtom, useAtomValue } from 'jotai' +import { Provider, useAtom } from 'jotai' import './App.css' import { useEffect } from 'react' import { RootProvider } from './routes.tsx' -import PageLoading from '@/components/page-loading' function App() { - const [ appData, ] = useAtom(appAtom) - const { data = [], isLoading, isFetching, refetch } = useAtomValue(userMenuDataAtom) - - useEffect(() => { - initI18n() - }, []) - - - useEffect(() => { - if (appData.token) { - refetch().then() - } - }, [ appData.token ]) - - - if (isLoading || isFetching) { - return - } - - return ( - - - - - - - - ) + const [ appData, ] = useAtom(appAtom) + + useEffect(() => { + initI18n() + }, []) + + + return ( + + + + + + + + ) } export default App diff --git a/src/components/avatar/index.tsx b/src/components/avatar/index.tsx index 6d85862..d272a21 100644 --- a/src/components/avatar/index.tsx +++ b/src/components/avatar/index.tsx @@ -1,50 +1,63 @@ import Icon from '@/components/icon' import { useTranslation } from '@/i18n.ts' -import { currentUserAtom } from '@/store/user.ts' +import { currentUserAtom, logoutAtom } from '@/store/user.ts' import { Avatar as AntAvatar, Dropdown, Spin } from 'antd' import { useAtomValue } from 'jotai' +import { useNavigate } from '@tanstack/react-router' const Avatar = () => { - const { t } = useTranslation() - const { data, isLoading } = useAtomValue(currentUserAtom) + const { t } = useTranslation() + const { data, isLoading } = useAtomValue(currentUserAtom) + const { mutate: logout } = useAtomValue(logoutAtom) + const navigate = useNavigate() - return ( -
- , - label: {t('app.header.logout')}, - }, - ], - }} - > - - - {!data?.avatar && data?.nickname?.substring(0, 1)} - - + return ( +
+ , + label: {t('app.header.logout')}, + }, + ], + onClick: (e) => { + if (e.key === 'logout') { + logout() + navigate({ + to: '/login', search: { + redirect: window.location.pathname + } + }) + } + }, + }} + > + + + {!data?.avatar && data?.nickname?.substring(0, 1)} + + {data?.nickname} - - -
- ) +
+
+
+ ) } export default Avatar \ No newline at end of file diff --git a/src/pages/login/index.css b/src/pages/login/index.css deleted file mode 100644 index 2586704..0000000 --- a/src/pages/login/index.css +++ /dev/null @@ -1,123 +0,0 @@ -body { - margin: 0; - padding: 0; - font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; - - overflow: hidden; - - .login-container { - display: flex; - align-items: center; - height: 100vh; - background-image: url("@/assets/login.png"); - background-repeat: no-repeat; - background-size: cover; - - - .language { - position: absolute; - top: 10px; - right: 10px; - color: #fff; - font-size: 14px; - cursor: pointer; - - } - - .loginBlock { - width: 100%; - height: 100%; - padding: 40px 0; - display: flex; - align-items: center; - justify-content: center; - } - - .innerBlock { - width: 356px; - margin: 0 auto; - } - - .logo { - height: 30px; - } - - .infoLine { - display: flex; - align-items: center; - justify-content: space-between; - margin: 0; - } - - .infoLeft { - color: #666; - font-size: 14px; - } - - .desc { - margin: 24px 0; - color: #999; - font-size: 16px; - cursor: pointer; - } - - .active { - color: #333; - font-weight: bold; - font-size: 24px; - } - - .line { - display: inline-block; - width: 1px; - height: 12px; - background: #999; - } - - .innerBeforeInput { - margin-left: 10px; - color: #999; - } - - .innerBeforeInput .line { - margin-left: 10px; - } - - .innerAfterInput { - margin-right: 10px; - color: #999; - } - - .innerAfterInput .line { - margin-right: 10px; - vertical-align: middle; - } - - .sendCode { - max-width: 65px; - margin-right: 10px; - } - - .otherLogin { - color: #666; - font-size: 14px; - } - - .icon { - margin-left: 10px; - } - - .link { - color: #5584ff; - font-size: 14px; - text-align: left; - } - - .submitBtn { - width: 100%; - } - } - - -} - diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index 1489b3b..9262330 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -1,22 +1,18 @@ import SelectLang from '@/components/select-lang' -import { createFileRoute, useSearch, useNavigate } from '@tanstack/react-router' -import { Button, Form, Input, message, Space } from 'antd' -import { useAtom } from 'jotai' +import { createFileRoute } from '@tanstack/react-router' +import { Button, Form, Input, Space } from 'antd' +import { useAtom, useAtomValue } from 'jotai' import { useTranslation } from '@/i18n.ts' import { loginAtom, loginFormAtom } from '@/store/user.ts' -import { memo, useEffect, useLayoutEffect } from 'react' -// import './index.css' +import { memo, useLayoutEffect } from 'react' import { useStyles } from './style.ts' const Login = memo(() => { const { styles } = useStyles() - const navigator = useNavigate() - // @ts-ignore 从url中获取redirect参数 - const search = useSearch({ form: '/login' }) const { t } = useTranslation() const [ values, setValues ] = useAtom(loginFormAtom) - const [ { isPending, isSuccess, mutate } ] = useAtom(loginAtom) + const { isPending, mutate } = useAtomValue(loginAtom) const [ form ] = Form.useForm() const handleSubmit = () => { @@ -25,19 +21,11 @@ const Login = memo(() => { }) } - useEffect(() => { - if (isSuccess) { - message.success(t('login.success')) - navigator({ - to: search?.redirect ?? '/' - }) - } - }, [ isSuccess ]) - useLayoutEffect(()=>{ + useLayoutEffect(() => { document.body.className = 'login' - return ()=>{ + return () => { document.body.className = document.body.className.replace('login', '') } diff --git a/src/request.ts b/src/request.ts index 896bf29..d8ab432 100644 --- a/src/request.ts +++ b/src/request.ts @@ -3,8 +3,8 @@ import { IApiResult } from '@/global' import { Record } from '@icon-park/react' import { message } from 'antd' import axios, { - AxiosRequestConfig, - AxiosInstance, AxiosResponse, + AxiosRequestConfig, + AxiosInstance, AxiosResponse, } from 'axios' @@ -16,128 +16,133 @@ interface RequestMethods extends Pick { - const token = getToken() - if (token) { - config.headers.Authorization = `Bearer ${token}` - } + const token = getToken() + if (token) { + config.headers.Authorization = `Bearer ${token}` + } - return config + return config }, (error) => { - console.log('error', error) - return Promise.reject(error) + console.log('error', error) + return Promise.reject(error) }) //拦截response,返回data axiosInstance.interceptors.response.use( - (response) => { - // console.log('response', response.data) - - message.destroy() - - const result = response.data as IApiResult - - switch (result.code) { - case 200: - //login - if (response.config.url?.includes('/sys/login')) { - setToken(result.data.token) - } - return response - case 401: - setToken('') - if (window.location.pathname === '/login') { - return Promise.reject(new Error('to login')) - } - - - // 401: 未登录 - message.error('登录失败,跳转重新登录') - // eslint-disable-next-line no-case-declarations - const search = new URLSearchParams(window.location.search) - // eslint-disable-next-line no-case-declarations - let redirect = window.location.pathname - if (search.toString() !== '') { - redirect = window.location.pathname + '?=' + search.toString() - } - window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` - return Promise.reject(new Error('to login')) - default: - message.error(result.message ?? '请求失败') - return Promise.reject(response) - } - - }, (error) => { - // console.log('error', error) - const { response } = error - if (response) { - switch (response.status) { - case 401: - if (window.location.pathname === '/login') { - return - } - - setToken('') - // 401: 未登录 - message.error('登录失败,跳转重新登录') - // eslint-disable-next-line no-case-declarations - const search = new URLSearchParams(window.location.search) - // eslint-disable-next-line no-case-declarations - let redirect = window.location.pathname - if (search.toString() !== '') { - redirect = window.location.pathname + '?=' + search.toString() - } - window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` - return - default: - message.error(response.data.message) - return Promise.reject(response) - } - } - - return Promise.reject(error) - }) + (response) => { + // console.log('response', response.data) + + message.destroy() + + const result = response.data as IApiResult + + switch (result.code) { + case 200: + //login + if (response.config.url?.includes('/sys/login')) { + setToken(result.data.token) + const search = new URLSearchParams(window.location.search) + // eslint-disable-next-line no-case-declarations + const redirect = search.get('redirect') + if (redirect) { + window.location.href = redirect + } + } + return response + case 401: + setToken('') + if (window.location.pathname === '/login') { + return Promise.reject(new Error('to login')) + } + + // 401: 未登录 + message.error('登录失败,跳转重新登录') + // eslint-disable-next-line no-case-declarations + const search = new URLSearchParams(window.location.search) + // eslint-disable-next-line no-case-declarations + let redirect = window.location.pathname + if (search.toString() !== '') { + redirect = window.location.pathname + '?=' + search.toString() + } + window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` + return Promise.reject(new Error('to login')) + default: + message.error(result.message ?? '请求失败') + return Promise.reject(response) + } + + }, (error) => { + // console.log('error', error) + const { response } = error + if (response) { + switch (response.status) { + case 401: + if (window.location.pathname === '/login') { + return + } + + setToken('') + // 401: 未登录 + message.error('登录失败,跳转重新登录') + // eslint-disable-next-line no-case-declarations + const search = new URLSearchParams(window.location.search) + // eslint-disable-next-line no-case-declarations + let redirect = window.location.pathname + if (search.toString() !== '') { + redirect = window.location.pathname + '?=' + search.toString() + } + window.location.href = `/login?redirect=${encodeURIComponent(redirect)}` + return + default: + message.error(response.data.message) + return Promise.reject(response) + } + } + + return Promise.reject(error) + }) //创建返回IApiResult类型的request export const createFetchMethods = () => { - const methods = {} - - for (const method of Object.keys(axiosInstance)) { - methods[method] = async (url: string, data?: D, config?: AxiosRequestConfig) => { - config = config ?? {} - config.url = url - config.method = method - const isGet = method === 'get' - if (isGet) { - config.params = data - } else { - config.data = data - } - return axiosInstance(config) - .then((response: AxiosResponse>) => { - if (response.data.code !== 200) { - throw new Error(response.data.message) - } - return response.data as IApiResult - }) - .catch((err) => { - throw err - }) + const methods = {} + + for (const method of Object.keys(axiosInstance)) { + methods[method] = async (url: string, data?: D, config?: AxiosRequestConfig) => { + config = config ?? {} + config.url = url + config.method = method + const isGet = method === 'get' + if (isGet) { + config.params = data + } else { + config.data = data + } + return axiosInstance(config) + .then((response: AxiosResponse>) => { + if (response.data.code !== 200) { + throw new Error(response.data.message) + } + return response.data as IApiResult + }) + .catch((err) => { + throw err + }) + } } - } - return methods as Record + return methods as Record } export const request = createFetchMethods() diff --git a/src/routes.tsx b/src/routes.tsx index 82ee837..ff0735c 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -13,85 +13,87 @@ import { Route as LoginRouteImport } from '@/pages/login' import { generateUUID } from '@/utils/uuid.ts' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { - AnyRoute, - createRootRouteWithContext, - createRoute, - createRouter, lazyRouteComponent, - Outlet, - redirect, - RouterProvider, + AnyRoute, + createRootRouteWithContext, + createRoute, + createRouter, lazyRouteComponent, + Outlet, + redirect, + RouterProvider, } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' -import { memo } from 'react' +import { memo, useEffect, useRef } from 'react' import RootLayout from './layout/RootLayout' import { IRootContext, MenuItem } from './global' import { DevTools } from 'jotai-devtools' +import { useAtomValue } from 'jotai' +import { userMenuDataAtom } from '@/store/user.ts' const PageRootLayout = () => { - return - - + return + + } export const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, + defaultOptions: { + queries: { + retry: false, + } } - } }) const rootRoute = createRootRouteWithContext()({ - component: () => ( - - <> - - - - - - ), - beforeLoad: ({ location }) => { - if (location.pathname === '/') { - return redirect({ to: '/dashboard' }) - } - }, - loader: () => { + component: () => ( + + <> + + + + + + ), + beforeLoad: ({ location }) => { + if (location.pathname === '/') { + return redirect({ to: '/dashboard' }) + } + }, + loader: () => { - }, - notFoundComponent: NotFound, - pendingComponent: PageLoading, - errorComponent: ({ error }) => , + }, + notFoundComponent: NotFound, + pendingComponent: PageLoading, + errorComponent: ({ error }) => , }) const emptyRoute = createRoute({ - getParentRoute: () => rootRoute, - id: '/_empty', - component: EmptyLayout, + getParentRoute: () => rootRoute, + id: '/_empty', + component: EmptyLayout, }) const authRoute = AuthenticatedImport.update({ - getParentRoute: () => rootRoute, - id: '/_authenticated', + getParentRoute: () => rootRoute, + id: '/_authenticated', } as any) const layoutNormalRoute = createRoute({ - getParentRoute: () => rootRoute, - id: '/_normal_layout', - component: PageRootLayout, + getParentRoute: () => rootRoute, + id: '/_normal_layout', + component: PageRootLayout, }) const layoutAuthRoute = createRoute({ - getParentRoute: () => authRoute, - id: '/_auth_layout', - component: PageRootLayout, + getParentRoute: () => authRoute, + id: '/_auth_layout', + component: PageRootLayout, }) const notAuthRoute = createRoute({ - getParentRoute: () => layoutNormalRoute, - path: '/not-auth', - component: NotPermission + getParentRoute: () => layoutNormalRoute, + path: '/not-auth', + component: NotPermission }) // const dashboardRoute = DashboardImport.update({ @@ -100,8 +102,8 @@ const notAuthRoute = createRoute({ // } as any) const loginRoute = LoginRouteImport.update({ - path: '/login', - getParentRoute: () => emptyRoute, + path: '/login', + getParentRoute: () => emptyRoute, } as any) // @@ -127,170 +129,189 @@ const loginRoute = LoginRouteImport.update({ declare module '@tanstack/react-router' { - interface FileRoutesByPath { - '/_authenticated': { - preLoaderRoute: typeof AuthenticatedImport - parentRoute: typeof rootRoute - }, - '/_normal_layout': { - preLoaderRoute: typeof layoutNormalRoute - parentRoute: typeof rootRoute - }, - '/_layout': { - preLoaderRoute: typeof layoutAuthRoute - parentRoute: typeof rootRoute - }, - // '/': { - // preLoaderRoute: typeof DashboardImport - // parentRoute: typeof layoutAuthRoute - // }, - // '/dashboard': { - // preLoaderRoute: typeof DashboardImport - // parentRoute: typeof layoutAuthRoute - // }, - '/login': { - preLoaderRoute: typeof LoginRouteImport - parentRoute: typeof rootRoute - }, - // '/system/menus': { - // preLoaderRoute: typeof menusRoute - // parentRoute: typeof layoutAuthRoute - // }, - // '/system/departments': { - // preLoaderRoute: typeof departmentsRoute - // parentRoute: typeof layoutAuthRoute - // }, - // '/system/users': { - // preLoaderRoute: typeof usersRoute - // parentRoute: typeof layoutAuthRoute - // }, - // '/system/roles': { - // preLoaderRoute: typeof rolesRoute - // parentRoute: typeof layoutAuthRoute - // }, - '/welcome': { - preLoaderRoute: typeof rootRoute - parentRoute: typeof layoutAuthRoute - }, - } + interface FileRoutesByPath { + '/_authenticated': { + preLoaderRoute: typeof AuthenticatedImport + parentRoute: typeof rootRoute + }, + '/_normal_layout': { + preLoaderRoute: typeof layoutNormalRoute + parentRoute: typeof rootRoute + }, + '/_layout': { + preLoaderRoute: typeof layoutAuthRoute + parentRoute: typeof rootRoute + }, + // '/': { + // preLoaderRoute: typeof DashboardImport + // parentRoute: typeof layoutAuthRoute + // }, + // '/dashboard': { + // preLoaderRoute: typeof DashboardImport + // parentRoute: typeof layoutAuthRoute + // }, + '/login': { + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRoute + }, + // '/system/menus': { + // preLoaderRoute: typeof menusRoute + // parentRoute: typeof layoutAuthRoute + // }, + // '/system/departments': { + // preLoaderRoute: typeof departmentsRoute + // parentRoute: typeof layoutAuthRoute + // }, + // '/system/users': { + // preLoaderRoute: typeof usersRoute + // parentRoute: typeof layoutAuthRoute + // }, + // '/system/roles': { + // preLoaderRoute: typeof rolesRoute + // parentRoute: typeof layoutAuthRoute + // }, + '/welcome': { + preLoaderRoute: typeof rootRoute + parentRoute: typeof layoutAuthRoute + }, + } } const generateDynamicRoutes = (menuData: MenuItem[], parentRoute: AnyRoute) => { - // 递归生成路由,如果有routes则递归生成子路由 - - const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => { - const path = menu.path?.replace(parentRoute.options?.path, '') - const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu' - - if (isLayout && (!menu.path || !menu.component)) { - //没有component的layout,直接返回 - return createRoute({ - getParentRoute: () => layoutAuthRoute, - id: `/layout-no-path-${generateUUID()}`, - component: EmptyLayout, - }) - } + // 递归生成路由,如果有routes则递归生成子路由 + + const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => { + const path = menu.path?.replace(parentRoute.options?.path, '') + const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu' + + if (isLayout && (!menu.path || !menu.component)) { + //没有component的layout,直接返回 + return createRoute({ + getParentRoute: () => layoutAuthRoute, + id: `/layout-no-path-${generateUUID()}`, + component: EmptyLayout, + }) + } - // @ts-ignore 添加menu属性,方便后面获取 - const options = { - getParentRoute: () => parentRoute, - menu, - } as any - - if (isLayout) { - options.id = path ?? `/layout-${generateUUID()}` - } else { - if (!path) { - console.log(`${menu.name}没有设置视图`) - } else { - options.path = path - } - } + // @ts-ignore 添加menu属性,方便后面获取 + const options = { + getParentRoute: () => parentRoute, + menu, + } as any + + if (isLayout) { + options.id = path ?? `/layout-${generateUUID()}` + } else { + if (!path) { + console.log(`${menu.name}没有设置视图`) + } else { + options.path = path + } + } - let component = menu.component - // menu.type - // 1,组件(页面),2,IFrame,3,外链接,4,按钮 - if (menu.type === 'iframe') { - component = '@/components/Iframe' - } + let component = menu.component + // menu.type + // 1,组件(页面),2,IFrame,3,外链接,4,按钮 + if (menu.type === 'iframe') { + component = '@/components/Iframe' + } - //处理component路径 - component = component.replace(/^\/pages/, '') - component = component.replace(/^\//, '') + //处理component路径 + component = component.replace(/^\/pages/, '') + component = component.replace(/^\//, '') - return createRoute({ - ...options, - component: lazyRouteComponent(() => import(`./pages/${component}`)), - notFoundComponent: NotFound, - }) - } - - // 对menuData递归生成路由,只处理type =1 的菜单 - const did = (menus: MenuItem[], parentRoute: AnyRoute) => { - return menus.filter((item) => item.type === 'menu').map((item, index) => { - // 如果有children则递归生成子路由,同样只处理type =1 的菜单 - const route = generateRoutes(item, parentRoute) - - // console.log(route) - if (item.children && item.children.length > 0) { - const children = did(item.children, route) - if (children.length > 0) { - route.addChildren(children) - } - } - route.init({ originalIndex: index }) - return route - }) - } + return createRoute({ + ...options, + component: lazyRouteComponent(() => import(`./pages/${component}`)), + notFoundComponent: NotFound, + }) + } - const routes = did(menuData, parentRoute) + // 对menuData递归生成路由,只处理type =1 的菜单 + const did = (menus: MenuItem[], parentRoute: AnyRoute) => { + return menus.filter((item) => item.type === 'menu').map((item, index) => { + // 如果有children则递归生成子路由,同样只处理type =1 的菜单 + const route = generateRoutes(item, parentRoute) + + // console.log(route) + if (item.children && item.children.length > 0) { + const children = did(item.children, route) + if (children.length > 0) { + route.addChildren(children) + } + } + route.init({ originalIndex: index }) + return route + }) + } + + const routes = did(menuData, parentRoute) - parentRoute.addChildren(routes) + parentRoute.addChildren(routes) } const routeTree = rootRoute.addChildren( - [ - //非Layout - loginRoute, - emptyRoute, - - //不带权限Layout - layoutNormalRoute.addChildren([ - notAuthRoute, - ]), - - //带权限Layout - // dashboardRoute, - authRoute.addChildren( - [ - layoutAuthRoute - /*.addChildren( - [ - menusRoute, - departmentsRoute, - usersRoute, - rolesRoute, - ] - ),*/ - ]), - ] + [ + //非Layout + loginRoute, + emptyRoute, + + //不带权限Layout + layoutNormalRoute.addChildren([ + notAuthRoute, + ]), + + //带权限Layout + // dashboardRoute, + authRoute.addChildren( + [ + layoutAuthRoute + /*.addChildren( + [ + menusRoute, + departmentsRoute, + usersRoute, + rolesRoute, + ] + ),*/ + ]), + ] ) export const RootProvider = memo((props: { context: Partial }) => { - generateDynamicRoutes(props.context.menuData ?? [], layoutAuthRoute) + const { data: menuData, isLoading, refetch } = useAtomValue(userMenuDataAtom) + + const isFetchRef = useRef(false) + + useEffect(() => { + + if (isFetchRef.current) { + return + } + isFetchRef.current = true + refetch() + + }, []) + + if (isLoading) { + return + } + + generateDynamicRoutes(menuData ?? [], layoutAuthRoute) + + const router = createRouter({ + routeTree, + context: { queryClient, menuData: [] }, + defaultPreload: 'intent', + defaultPendingComponent: () => + }) - const router = createRouter({ - routeTree, - context: { queryClient, menuData: [] }, - defaultPreload: 'intent', - defaultPendingComponent: () => - }) - return ( - - - - ) + return ( + + + + ) }) \ No newline at end of file