Browse Source

完善框架

main
李金 5 months ago
parent
commit
6e94f1cd04
  1. 5
      package.json
  2. 24
      src/App.tsx
  3. 111
      src/components/error-boundary/index.tsx
  4. 25
      src/components/error/403.tsx
  5. 25
      src/components/error/404.tsx
  6. 37
      src/components/error/error.tsx
  7. 13
      src/components/page-loading/index.tsx
  8. 16
      src/layout/EmptyLayout.tsx
  9. 255
      src/layout/RootLayout.tsx
  10. 18
      src/layout/_authenticated.tsx
  11. 30
      src/pages/dashboard/index.tsx
  12. 15
      src/pages/login/index.tsx
  13. 19
      src/request.ts
  14. 403
      src/routes.tsx
  15. 11
      src/store/system.ts
  16. 8
      src/types.d.ts
  17. 7
      src/utils/auth.ts
  18. 34
      yarn.lock

5
package.json

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host --port 3000 --debug",
"dev": "vite --host --port 3000",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
@ -22,10 +22,13 @@
"antd": "^5.16.1", "antd": "^5.16.1",
"axios": "^1.6.8", "axios": "^1.6.8",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"jotai": "^2.8.0", "jotai": "^2.8.0",
"jotai-tanstack-query": "^0.8.5", "jotai-tanstack-query": "^0.8.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"wonka": "^6.3.4" "wonka": "^6.3.4"
}, },
"devDependencies": { "devDependencies": {

24
src/App.tsx

@ -1,16 +1,26 @@
import { appAtom, appStore, menuDataAtom } from '@/store/system.ts'
import { Provider, useAtom, useAtomValue } from 'jotai'
import './App.css' import './App.css'
import { RootProvider } from './routes.tsx' import { RootProvider } from './routes.tsx'
import { Provider } from 'jotai'
import { appStore } from '@/store/system.ts'
function App() { function App() {
const [ , ] = useAtom(appAtom)
const { data, isError, isPending } = useAtomValue(menuDataAtom)
return (
<Provider store={appStore}>
<RootProvider/>
</Provider>
)
if (isError) {
return <div>Error</div>
}
if (isPending) {
return <div>Loading...</div>
}
return (
<Provider store={appStore}>
<RootProvider context={{ menuData: data }}/>
</Provider>
)
} }
export default App export default App

111
src/components/error-boundary/index.tsx

@ -1,64 +1,65 @@
import React, { ErrorInfo } from 'react'
import { Button, Result } from 'antd' import { Button, Result } from 'antd'
import React, { ErrorInfo } from 'react'
export class ErrorBoundary extends React.Component< export class ErrorBoundary extends React.Component<
Record<string, any>,
{ hasError: boolean; errorInfo: string }
Record<string, any>,
{ hasError: boolean; errorInfo: string }
> { > {
state = { hasError: false, errorInfo: '' }
static getDerivedStateFromError(error: Error) {
return { hasError: true, errorInfo: error.message }
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, errorInfo: error.message }
}
componentDidCatch(error: any, errorInfo: ErrorInfo) {
// You can also log the error to an error reporting service
// eslint-disable-next-line no-console
console.log(error, errorInfo)
componentDidCatch(error: any, errorInfo: ErrorInfo) {
// You can also log the error to an error reporting service
// eslint-disable-next-line no-console
console.log(error, errorInfo)
}
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<Result
style={{
height: '100%',
background: '#fff',
}}
title="错误信息"
extra={
<>
<div
style={{
maxWidth: 620,
textAlign: 'start',
backgroundColor: 'rgba(255,229,100,0.3)',
borderInlineStartColor: '#ffe564',
borderInlineStartWidth: '9px',
borderInlineStartStyle: 'solid',
padding: '20px 45px 20px 26px',
margin: 'auto',
marginBlockEnd: '30px',
marginBlockStart: '20px',
}}
>
<p>{this.state.errorInfo}</p>
</div>
<Button
danger
type="primary"
onClick={() => {
window.location.reload()
}}
>
</Button>
</>
}
/>
)
}
return this.props.children
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<Result
style={{
height: '100%',
background: '#fff',
}}
title="错误信息"
extra={
<>
<div
style={{
maxWidth: 620,
textAlign: 'start',
backgroundColor: 'rgba(255,229,100,0.3)',
borderInlineStartColor: '#ffe564',
borderInlineStartWidth: '9px',
borderInlineStartStyle: 'solid',
padding: '20px 45px 20px 26px',
margin: 'auto',
marginBlockEnd: '30px',
marginBlockStart: '20px',
}}
>
<p>{this.state.errorInfo}</p>
</div>
<Button
danger
type="primary"
onClick={() => {
window.location.reload()
}}
>
</Button>
</>
}
/>
)
} }
return this.props.children
}
state = { hasError: false, errorInfo: '' }
} }

25
src/components/error/403.tsx

@ -0,0 +1,25 @@
import { useNavigate } from '@tanstack/react-router'
import { Button, Result } from 'antd'
const NotPermission = () => {
const navigate = useNavigate()
return (
<Result
className="no-permission-page"
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
extra={
<Button type="primary" onClick={() => navigate({
to: '../'
})}>
Go Back
</Button>
}
/>
)
}
export default NotPermission

25
src/components/error/404.tsx

@ -0,0 +1,25 @@
import { useNavigate } from '@tanstack/react-router'
import { Button, Result } from 'antd'
const NotFound = () => {
const navigate = useNavigate()
return (
<Result
className="error-page"
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Button type="primary" onClick={() => navigate({
to: '../'
})}>
Go Back
</Button>
}
/>
)
}
export default NotFound

37
src/components/error/error.tsx

@ -0,0 +1,37 @@
import { Result } from 'antd'
const ErrorPage = ({ error }: { error: any, reset?: string }) => {
return (
<Result
style={{
height: '100%',
background: '#fff',
}}
status={'error'}
title="错误信息"
extra={
<>
<div
style={{
maxWidth: 620,
textAlign: 'start',
backgroundColor: 'rgba(255,229,100,0.3)',
borderInlineStartColor: '#ffe564',
borderInlineStartWidth: '9px',
borderInlineStartStyle: 'solid',
padding: '20px 45px 20px 26px',
margin: 'auto',
marginBlockEnd: '30px',
marginBlockStart: '20px',
}}
>
<p>{error?.message}</p>
</div>
</>
}
/>
)
}
export default ErrorPage

13
src/components/page-loading/index.tsx

@ -0,0 +1,13 @@
import { Spin } from 'antd'
const PageLoading = () => {
return (
<>
<Spin spinning={true}>
<div style={{ height: '100vh', width: '100vh' }}></div>
</Spin>
</>
)
}
export default PageLoading

16
src/layout/EmptyLayout.tsx

@ -1,11 +1,15 @@
import { Outlet } from '@tanstack/react-router'
import ErrorPage from '@/components/error/error.tsx'
import { CatchBoundary, Outlet } from '@tanstack/react-router'
const EmptyLayout = () => { const EmptyLayout = () => {
return (
<>
<Outlet/>
</>
)
return (
<CatchBoundary
getResetKey={() => 'reset-layout'}
errorComponent={ErrorPage}
>
<Outlet/>
</CatchBoundary>
)
} }
export default EmptyLayout export default EmptyLayout

255
src/layout/RootLayout.tsx

@ -1,144 +1,147 @@
import {
ProConfigProvider,
ProLayout,
} from '@ant-design/pro-components'
import PageBreadcrumb from '@/components/breadcrumb'
import { ErrorBoundary } from '@/components/error-boundary'
import ErrorPage from '@/components/error/error.tsx'
import { MenuItem } from '@/types'
import { ProConfigProvider, ProLayout, } from '@ant-design/pro-components'
import { CatchBoundary, Link, Outlet, useRouteContext } from '@tanstack/react-router'
import { ConfigProvider, Dropdown } from 'antd' import { ConfigProvider, Dropdown } from 'antd'
import { useState } from 'react' import { useState } from 'react'
import defaultProps from './_defaultProps'
import { Link, Outlet, useRouteContext } from '@tanstack/react-router'
import Icon from '../components/icon' import Icon from '../components/icon'
import { MenuItem } from '@/types'
import PageBreadcrumb from '@/components/breadcrumb'
import { ErrorBoundary } from '@/components/error-boundary'
import defaultProps from './_defaultProps'
//根据menuData生成Breadcrumb所需的数据 //根据menuData生成Breadcrumb所需的数据
const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => { const getBreadcrumbData = (menuData: MenuItem[], pathname: string) => {
const breadcrumbData: any[] = []
const findItem = (menuData: any[], pathname: string) => {
for (let i = 0; i < menuData.length; i++) {
if (menuData[i].path === pathname) {
breadcrumbData.push(menuData[i])
return true
}
if (menuData[i].children) {
if (findItem(menuData[i].children, pathname)) {
breadcrumbData.push(menuData[i])
return true
}
}
const breadcrumbData: any[] = []
const findItem = (menuData: any[], pathname: string) => {
for (let i = 0; i < menuData.length; i++) {
if (menuData[i].path === pathname) {
breadcrumbData.push(menuData[i])
return true
}
if (menuData[i].children) {
if (findItem(menuData[i].children, pathname)) {
breadcrumbData.push(menuData[i])
return true
} }
return false
}
} }
findItem(menuData, pathname)
return breadcrumbData.reverse()
return false
}
findItem(menuData, pathname)
return breadcrumbData.reverse()
} }
export default () => { export default () => {
const { menuData } = useRouteContext({
from: undefined,
strict: false,
select: (state) => state
})
const { menuData } = useRouteContext({
from: undefined,
strict: false,
select: (state) => state
})
const items = getBreadcrumbData(menuData, location.pathname)
const items = getBreadcrumbData(menuData, location.pathname)
const [ pathname, setPathname ] = useState(location.pathname)
const [ pathname, setPathname ] = useState(location.pathname)
return (
<div
id="crazy-pro-layout"
style={{
height: '100vh',
// overflow: 'auto',
}}
>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
return document.getElementById('crazy-pro-layout') || document.body
}}
return (
<div
id="crazy-pro-layout"
style={{
height: '100vh',
// overflow: 'auto',
}}
>
<CatchBoundary
getResetKey={() => 'reset-page'}
errorComponent={ErrorPage}
>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
return document.getElementById('crazy-pro-layout') || document.body
}}
>
<ProLayout
headerContentRender={() => <PageBreadcrumb
className={'top-breadcrumb'}
showIcon={false}
items={items}/>}
title="Crazy Pro"
{...defaultProps}
route={{
path: '/',
routes: menuData
}}
location={{
pathname,
}}
token={{
header: {
colorBgMenuItemSelected: 'rgba(0,0,0,0.04)',
},
}}
menu={{
collapsedShowGroupTitle: true,
}}
avatarProps={{
src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
size: 'small',
title: '管理员',
render: (_, dom) => {
return (
<Dropdown
menu={{
items: [
{
key: 'logout',
icon: <Icon type={'Logout'}/>,
label: '退出登录',
},
],
}}
> >
<ProLayout
headerContentRender={() => <PageBreadcrumb
className={'top-breadcrumb'}
showIcon={false}
items={items}/>}
title="Crazy Pro"
{...defaultProps}
route={{
path: '/',
routes: menuData
}}
location={{
pathname,
}}
token={{
header: {
colorBgMenuItemSelected: 'rgba(0,0,0,0.04)',
},
}}
menu={{
collapsedShowGroupTitle: true,
}}
avatarProps={{
src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
size: 'small',
title: '管理员',
render: (_, dom) => {
return (
<Dropdown
menu={{
items: [
{
key: 'logout',
icon: <Icon type={'Logout'}/>,
label: '退出登录',
},
],
}}
>
{dom}
</Dropdown>
)
},
}}
actionsRender={(props) => {
if (props.isMobile) return []
if (typeof window === 'undefined') return []
return []
}}
menuRender={(_, defaultDom) => (
<>
{defaultDom}
</>
)}
menuItemRender={(item, dom) => {
return <div onClick={() => {
setPathname(item.path || '/welcome')
}}
>
<Link to={item.path} target={item.type === 3 ? '_blank': '_self' }>
{dom}
</Link>
</div>
}}
{...{
'layout': 'mix',
'navTheme': 'light',
'contentWidth': 'Fluid',
'fixSiderbar': true,
'colorPrimary': '#1677FF',
'siderMenuType': 'group',
// layout: 'side',
}}
ErrorBoundary={ErrorBoundary}
>
<Outlet/>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</div>
)
{dom}
</Dropdown>
)
},
}}
actionsRender={(props) => {
if (props.isMobile) return []
if (typeof window === 'undefined') return []
return []
}}
menuRender={(_, defaultDom) => (
<>
{defaultDom}
</>
)}
menuItemRender={(item, dom) => {
return <div onClick={() => {
setPathname(item.path || '/welcome')
}}
>
<Link to={item.path} target={item.type === 3 ? '_blank' : '_self'}>
{dom}
</Link>
</div>
}}
{...{
'layout': 'mix',
'navTheme': 'light',
'contentWidth': 'Fluid',
'fixSiderbar': true,
'colorPrimary': '#1677FF',
'siderMenuType': 'group',
// layout: 'side',
}}
ErrorBoundary={ErrorBoundary}
>
<Outlet/>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</CatchBoundary>
</div>
)
} }

18
src/layout/_authenticated.tsx

@ -0,0 +1,18 @@
import { isAuthenticated } from '@/utils/auth.ts'
import { createFileRoute,redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
if (!isAuthenticated()) {
throw redirect({
to: '/login',
search: {
// Use the current location to power a redirect after login
// (Do not use `router.state.resolvedLocation` as it can
// potentially lag behind the actual current location)
redirect: location.href,
},
})
}
},
})

30
src/pages/dashboard/index.tsx

@ -1,25 +1,25 @@
import { ProCard } from '@ant-design/pro-components'
import { createLazyRoute } from '@tanstack/react-router'
import {ProCard} from '@ant-design/pro-components'
import {createFileRoute} from '@tanstack/react-router'
const Index = () => { const Index = () => {
return ( return (
<>
<ProCard
style={{
height: '100vh',
minHeight: 800,
}}
>
<>
<ProCard
style={{
height: '100vh',
minHeight: 800,
}}
>
<h1>Dashboard</h1>
<h1>Dashboard</h1>
</ProCard>
</>
</ProCard>
</>
) )
} }
export const Route = createLazyRoute('/welcome')({
component: Index,
})
export const Route = createFileRoute('/dashboard')({
component: Index,
})
export default Index export default Index

15
src/pages/login/index.tsx

@ -1,15 +1,16 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
const Login = () => { const Login = () => {
return (
<div>
Login
</div>
)
return (
<div>
{}
</div>
)
} }
export const Route = createFileRoute("/login")({
component: Login
export const Route = createFileRoute('/login')({
component: Login
}) })
export default Login export default Login

19
src/request.ts

@ -1,6 +1,6 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { message } from 'antd'
import { getToken, setToken } from '@/store/system.ts' import { getToken, setToken } from '@/store/system.ts'
import { message } from 'antd'
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
export type { AxiosRequestConfig } export type { AxiosRequestConfig }
@ -22,19 +22,10 @@ request.interceptors.request.use((config) => {
config.headers.Authorization = `Bearer ${token}` config.headers.Authorization = `Bearer ${token}`
} }
if (window.location.pathname === '/login') {
throw new Error('login')
} else {
const search = new URLSearchParams(window.location.search)
let url = `/login?redirect=${encodeURIComponent(window.location.pathname)}`
if (search.toString() !== '') {
url = `/login?redirect=${encodeURIComponent(window.location.pathname + '?=' + search.toString())}`
}
window.location.href = url
}
return config return config
}, (error) => {
console.log('error', error)
return Promise.reject(error)
}) })

403
src/routes.tsx

@ -1,239 +1,286 @@
import NotPermission from '@/components/error/403.tsx'
import NotFound from '@/components/error/404.tsx'
import ErrorPage from '@/components/error/error.tsx'
import PageLoading from '@/components/page-loading'
import { Route as AuthenticatedImport } from '@/layout/_authenticated.tsx'
import EmptyLayout from '@/layout/EmptyLayout.tsx'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { Route as DashboardImport } from '@/pages/dashboard'
import { Route as LoginRouteImport } from '@/pages/login'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { import {
createRouter,
createRoute,
RouterProvider, AnyRoute, redirect, createRootRouteWithContext, createLazyRoute, Outlet,
AnyRoute,
createLazyRoute,
createRootRouteWithContext,
createRoute,
createRouter,
Outlet,
redirect,
RouterProvider,
} from '@tanstack/react-router' } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { memo } from 'react'
import RootLayout from './layout/RootLayout' import RootLayout from './layout/RootLayout'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { Route as LoginRouteImport } from '@/pages/login'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { IRootContext, MenuItem } from './types' import { IRootContext, MenuItem } from './types'
import { appAtom, menuDataAtom } from './store/system.ts'
import { useAtom } from 'jotai/index'
import EmptyLayout from '@/layout/EmptyLayout.tsx'
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
}
defaultOptions: {
queries: {
retry: false,
} }
}
}) })
const rootRoute = createRootRouteWithContext<IRootContext>()({ const rootRoute = createRootRouteWithContext<IRootContext>()({
component: () => (
<div>
<Outlet/>
<TanStackRouterDevtools position={'bottom-right'}/>
</div>
),
beforeLoad: ({ location }) => {
if (location.pathname === '/') {
return redirect({ to: '/welcome' })
}
},
notFoundComponent: () => <div>404 Not Found</div>,
component: () => (
<div>
<Outlet/>
<TanStackRouterDevtools position={'bottom-right'}/>
</div>
),
beforeLoad: ({ location }) => {
if (location.pathname === '/') {
return redirect({ to: '/dashboard' })
}
},
notFoundComponent: NotFound,
pendingComponent: PageLoading,
errorComponent: ({ error }) => <ErrorPage error={error}/>,
}) })
const emptyRoute = createRoute({ const emptyRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/empty',
component: EmptyLayout,
getParentRoute: () => rootRoute,
id: '/_empty',
component: EmptyLayout,
})
const authRoute = AuthenticatedImport.update({
getParentRoute: () => rootRoute,
id: '/_authenticated',
} as any)
const layoutNormalRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/_normal_layout',
component: RootLayout,
}) })
const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/layout',
component: RootLayout,
const layoutAuthRoute = createRoute({
getParentRoute: () => authRoute,
id: '/_auth_layout',
component: RootLayout,
}) })
const notAuthRoute = createRoute({
getParentRoute: () => layoutNormalRoute,
path: '/not-auth',
component: NotPermission
})
const dashboardRoute = DashboardImport.update({
path: '/dashboard',
getParentRoute: () => layoutAuthRoute,
} as any)
const loginRoute = LoginRouteImport.update({ const loginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => emptyRoute,
path: '/login',
getParentRoute: () => emptyRoute,
} as any) } as any)
const menusRoute = createRoute({ const menusRoute = createRoute({
getParentRoute: () => layoutRoute,
path: '/system/menus',
getParentRoute: () => layoutAuthRoute,
path: '/system/menus',
}).lazy(async () => await import('@/pages/system/menus').then(d => d.Route)) }).lazy(async () => await import('@/pages/system/menus').then(d => d.Route))
const departmentsRoute = createRoute({ const departmentsRoute = createRoute({
getParentRoute: () => layoutRoute,
path: '/system/departments',
getParentRoute: () => layoutAuthRoute,
path: '/system/departments',
}).lazy(async () => await import('@/pages/system/departments').then(d => d.Route)) }).lazy(async () => await import('@/pages/system/departments').then(d => d.Route))
const usersRoute = createRoute({ const usersRoute = createRoute({
getParentRoute: () => layoutRoute,
path: '/system/users',
getParentRoute: () => layoutAuthRoute,
path: '/system/users',
}).lazy(async () => await import('@/pages/system/users').then(d => d.Route)) }).lazy(async () => await import('@/pages/system/users').then(d => d.Route))
const rolesRoute = createRoute({ const rolesRoute = createRoute({
getParentRoute: () => layoutRoute,
path: '/system/roles',
getParentRoute: () => layoutAuthRoute,
path: '/system/roles',
}).lazy(async () => await import('@/pages/system/roles').then(d => d.Route)) }).lazy(async () => await import('@/pages/system/roles').then(d => d.Route))
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/login': {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRoute
},
'/system/menus': {
preLoaderRoute: typeof menusRoute
parentRoute: typeof layoutRoute
},
'/system/departments': {
preLoaderRoute: typeof departmentsRoute
parentRoute: typeof layoutRoute
},
'/system/users': {
preLoaderRoute: typeof usersRoute
parentRoute: typeof layoutRoute
},
'/system/roles': {
preLoaderRoute: typeof rolesRoute
parentRoute: typeof layoutRoute
},
'/welcome': {
preLoaderRoute: typeof rootRoute
parentRoute: typeof layoutRoute
},
}
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 routeTree = rootRoute.addChildren( const routeTree = rootRoute.addChildren(
[
loginRoute,
emptyRoute,
layoutRoute.addChildren(
[
menusRoute,
departmentsRoute,
usersRoute,
rolesRoute,
]
),
]
[
//非Layout
loginRoute,
emptyRoute,
//不带权限Layout
layoutNormalRoute.addChildren([
notAuthRoute,
]),
//带权限Layout
dashboardRoute,
authRoute.addChildren(
[
layoutAuthRoute.addChildren(
[
menusRoute,
departmentsRoute,
usersRoute,
rolesRoute,
]
),
]
)
]
) )
export const generateDynamicRoutes = (menuData: MenuItem[]) => { export const generateDynamicRoutes = (menuData: MenuItem[]) => {
// 递归生成路由,如果有routes则递归生成子路由
// 递归生成路由,如果有routes则递归生成子路由
const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => {
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'
const path = menu.path?.replace(parentRoute.options?.path, '')
const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu'
if (isLayout && !menu.component) {
//没有component的layout,直接返回
return createRoute({
getParentRoute: () => parentRoute,
id: path!,
component: ListPageLayout,
})
}
if (isLayout && !menu.component) {
//没有component的layout,直接返回
return createRoute({
getParentRoute: () => parentRoute,
id: path!,
component: ListPageLayout,
})
}
// @ts-ignore 添加menu属性,方便后面获取
const options = {
getParentRoute: () => parentRoute,
menu,
} as any
// @ts-ignore 添加menu属性,方便后面获取
const options = {
getParentRoute: () => parentRoute,
menu,
} as any
if (isLayout) {
options.id = path!
} else {
options.path = path!
}
if (isLayout) {
options.id = path!
} else {
options.path = path!
}
//删除掉parentRoute的path,避免重复
const route = createRoute(options).lazy(async () => {
// @ts-ignore 获取route中的menu属性
const menu = route.options.menu as MenuItem
let component = menu.component
// menu.type
// 1,组件(页面),2,IFrame,3,外链接,4,按钮
if (menu.type === 'iframe') {
component = '@/components/Iframe'
}
if (!component) {
return createLazyRoute(menu.path)({
component: () => <div>404 Not Found</div>
})
}
/* @vite-ignore */
const d = await import(`${component}`)
if (d.Route) {
return d.Route
}
if (d.GenRoute) {
return d.GenRoute(menu.path)
}
return createLazyRoute(menu.path)({
component: d.default || d
})
})
return route
//删除掉parentRoute的path,避免重复
const route = createRoute(options).lazy(async () => {
}
// @ts-ignore 获取route中的menu属性
const menu = route.options.menu as MenuItem
let component = menu.component
// menu.type
// 1,组件(页面),2,IFrame,3,外链接,4,按钮
if (menu.type === 'iframe') {
component = '@/components/Iframe'
}
// 对menuData递归生成路由,只处理type =1 的菜单
const did = (menus: MenuItem[], parentRoute: AnyRoute) => {
return menus.filter((item) => item.type === 'menu').map((item) => {
// 如果有children则递归生成子路由,同样只处理type =1 的菜单
const route = generateRoutes(item, parentRoute)
if (item.children && item.children.length > 0) {
const children = did(item.children, route)
if (children.length > 0) {
route.addChildren(children)
}
}
return route
if (!component) {
return createLazyRoute(menu.path)({
component: () => <div>404 Not Found</div>
}) })
}
}
/* @vite-ignore */
const d = await import(`${component}`)
if (d.Route) {
return d.Route
}
if (d.GenRoute) {
return d.GenRoute(menu.path)
}
return createLazyRoute(menu.path)({
component: d.default || d
})
})
return route
}
// 对menuData递归生成路由,只处理type =1 的菜单
const did = (menus: MenuItem[], parentRoute: AnyRoute) => {
return menus.filter((item) => item.type === 'menu').map((item) => {
// 如果有children则递归生成子路由,同样只处理type =1 的菜单
const route = generateRoutes(item, parentRoute)
if (item.children && item.children.length > 0) {
const children = did(item.children, route)
if (children.length > 0) {
route.addChildren(children)
}
}
return route
})
}
return did(menuData, rootRoute)
return did(menuData, rootRoute)
} }
const router = createRouter({ const router = createRouter({
routeTree,
context: { queryClient, menuData: undefined },
defaultPreload: 'intent'
routeTree,
context: { queryClient, menuData: [] },
defaultPreload: 'intent'
}) })
export const RootProvider = () => {
const [ , ] = useAtom(appAtom)
const { data, isError, isPending } = useAtomValue(menuDataAtom)
if (isError) {
return <div>Error</div>
}
if (isPending) {
return <div>Loading...</div>
}
router.update({
context: {
queryClient,
menuData: data,
}
})
export const RootProvider = memo((props: { context: Partial<IRootContext> }) => {
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router}/>
</QueryClientProvider>
)
}
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} context={{ ...props.context, queryClient }}/>
</QueryClientProvider>
)
})

11
src/store/system.ts

@ -1,9 +1,10 @@
import { atomWithQuery } from 'jotai-tanstack-query'
import systemServ from '../service/system.ts'
import { IAppData, MenuItem } from '@/types' import { IAppData, MenuItem } from '@/types'
import { formatMenuData } from '@/utils'
import { isAuthenticated } from '@/utils/auth.ts'
import { atom, createStore } from 'jotai' import { atom, createStore } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
import { atomWithStorage } from 'jotai/utils' import { atomWithStorage } from 'jotai/utils'
import { formatMenuData } from '@/utils'
import systemServ from '../service/system.ts'
/** /**
@ -33,7 +34,9 @@ export const setToken = (token: string) => {
export const menuDataAtom = atomWithQuery(() => ({ export const menuDataAtom = atomWithQuery(() => ({
queryKey: [ 'menus' ], queryKey: [ 'menus' ],
queryFn: async () => { queryFn: async () => {
if (window.location.pathname === '/login') return []
if (!isAuthenticated()) {
return []
}
return await systemServ.menus.list() return await systemServ.menus.list()
}, },
select: data => formatMenuData(data as any ?? []), select: data => formatMenuData(data as any ?? []),

8
src/types.d.ts

@ -1,8 +1,8 @@
import { Attributes, ReactNode } from 'react'
import { IMenu } from '@/types/menus'
import { QueryClient } from '@tanstack/react-query' import { QueryClient } from '@tanstack/react-query'
import { Router } from '@tanstack/react-router' import { Router } from '@tanstack/react-router'
import { RouteOptions } from '@tanstack/react-router/src/route.ts' import { RouteOptions } from '@tanstack/react-router/src/route.ts'
import { IMenu } from '@/types/menus'
import { Attributes, ReactNode } from 'react'
export type LayoutType = 'list' | 'form' | 'tree' | 'normal' export type LayoutType = 'list' | 'form' | 'tree' | 'normal'
@ -38,11 +38,11 @@ export type Props = Attributes & {
}; };
export interface IRootContext { export interface IRootContext {
menuData?: MenuItem[];
menuData: MenuItem[];
queryClient: QueryClient; queryClient: QueryClient;
} }
interface MenuItem extends IMenu{
interface MenuItem extends IMenu {
routes?: MenuItem[]; routes?: MenuItem[];
} }

7
src/utils/auth.ts

@ -0,0 +1,7 @@
import { getToken } from '@/store/system.ts'
export const isAuthenticated = () => {
const token = getToken()
return !!token
}

34
yarn.lock

@ -2068,6 +2068,27 @@ hoist-non-react-statics@^3.3.2:
dependencies: dependencies:
react-is "^16.7.0" react-is "^16.7.0"
html-parse-stringify@^3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
dependencies:
void-elements "3.1.0"
i18next-browser-languagedetector@^7.2.1:
version "7.2.1"
resolved "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz#1968196d437b4c8db847410c7c33554f6c448f6f"
integrity sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==
dependencies:
"@babel/runtime" "^7.23.2"
i18next@^23.11.2:
version "23.11.2"
resolved "https://registry.npmmirror.com/i18next/-/i18next-23.11.2.tgz#4c0e8192a9ba230fe7dc68b76459816ab601826e"
integrity sha512-qMBm7+qT8jdpmmDw/kQD16VpmkL9BdL+XNAK5MNbNFaf1iQQq35ZbPrSlqmnNPOSUY4m342+c0t0evinF5l7sA==
dependencies:
"@babel/runtime" "^7.23.2"
ignore@^5.2.0, ignore@^5.3.1: ignore@^5.2.0, ignore@^5.3.1:
version "5.3.1" version "5.3.1"
resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
@ -2887,6 +2908,14 @@ react-dom@^18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-i18next@^14.1.0:
version "14.1.0"
resolved "https://registry.npmmirror.com/react-i18next/-/react-i18next-14.1.0.tgz#44da74fbffd416f5d0c5307ef31735cf10cc91d9"
integrity sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==
dependencies:
"@babel/runtime" "^7.23.9"
html-parse-stringify "^3.0.1"
react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0: react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -3231,6 +3260,11 @@ vite@^5.2.0:
optionalDependencies: optionalDependencies:
fsevents "~2.3.3" fsevents "~2.3.3"
[email protected]:
version "3.1.0"
resolved "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
warning@^4.0.3: warning@^4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" resolved "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"

Loading…
Cancel
Save