Browse Source

完善框架

main
李金 7 months ago
parent
commit
6e94f1cd04
  1. 5
      package.json
  2. 16
      src/App.tsx
  3. 7
      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. 10
      src/layout/EmptyLayout.tsx
  9. 21
      src/layout/RootLayout.tsx
  10. 18
      src/layout/_authenticated.tsx
  11. 4
      src/pages/dashboard/index.tsx
  12. 5
      src/pages/login/index.tsx
  13. 19
      src/request.ts
  14. 141
      src/routes.tsx
  15. 11
      src/store/system.ts
  16. 6
      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",
"type": "module",
"scripts": {
"dev": "vite --host --port 3000 --debug",
"dev": "vite --host --port 3000",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
@ -22,10 +22,13 @@
"antd": "^5.16.1",
"axios": "^1.6.8",
"dayjs": "^1.11.10",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"jotai": "^2.8.0",
"jotai-tanstack-query": "^0.8.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.0",
"wonka": "^6.3.4"
},
"devDependencies": {

16
src/App.tsx

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

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

@ -1,12 +1,10 @@
import React, { ErrorInfo } from 'react'
import { Button, Result } from 'antd'
import React, { ErrorInfo } from 'react'
export class ErrorBoundary extends React.Component<
Record<string, any>,
{ hasError: boolean; errorInfo: string }
> {
state = { hasError: false, errorInfo: '' }
static getDerivedStateFromError(error: Error) {
return { hasError: true, errorInfo: error.message }
}
@ -15,6 +13,7 @@ export class ErrorBoundary extends React.Component<
// You can also log the error to an error reporting service
// eslint-disable-next-line no-console
console.log(error, errorInfo)
}
render() {
@ -61,4 +60,6 @@ export class ErrorBoundary extends React.Component<
}
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

10
src/layout/EmptyLayout.tsx

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

21
src/layout/RootLayout.tsx

@ -1,15 +1,13 @@
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 { useState } from 'react'
import defaultProps from './_defaultProps'
import { Link, Outlet, useRouteContext } from '@tanstack/react-router'
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所需的数据
@ -54,6 +52,10 @@ export default () => {
// overflow: 'auto',
}}
>
<CatchBoundary
getResetKey={() => 'reset-page'}
errorComponent={ErrorPage}
>
<ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
@ -139,6 +141,7 @@ export default () => {
</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,
},
})
}
},
})

4
src/pages/dashboard/index.tsx

@ -1,5 +1,5 @@
import {ProCard} from '@ant-design/pro-components'
import { createLazyRoute } from '@tanstack/react-router'
import {createFileRoute} from '@tanstack/react-router'
const Index = () => {
@ -19,7 +19,7 @@ const Index = () => {
)
}
export const Route = createLazyRoute('/welcome')({
export const Route = createFileRoute('/dashboard')({
component: Index,
})
export default Index

5
src/pages/login/index.tsx

@ -1,14 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
const Login = () => {
return (
<div>
Login
{}
</div>
)
}
export const Route = createFileRoute("/login")({
export const Route = createFileRoute('/login')({
component: 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 { message } from 'antd'
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
export type { AxiosRequestConfig }
@ -22,19 +22,10 @@ request.interceptors.request.use((config) => {
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
}, (error) => {
console.log('error', error)
return Promise.reject(error)
})

141
src/routes.tsx

@ -1,19 +1,27 @@
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 {
createRouter,
AnyRoute,
createLazyRoute,
createRootRouteWithContext,
createRoute,
RouterProvider, AnyRoute, redirect, createRootRouteWithContext, createLazyRoute, Outlet,
createRouter,
Outlet,
redirect,
RouterProvider,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { memo } from 'react'
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 { appAtom, menuDataAtom } from './store/system.ts'
import { useAtom } from 'jotai/index'
import EmptyLayout from '@/layout/EmptyLayout.tsx'
export const queryClient = new QueryClient({
@ -34,75 +42,119 @@ const rootRoute = createRootRouteWithContext<IRootContext>()({
),
beforeLoad: ({ location }) => {
if (location.pathname === '/') {
return redirect({ to: '/welcome' })
return redirect({ to: '/dashboard' })
}
},
notFoundComponent: () => <div>404 Not Found</div>,
notFoundComponent: NotFound,
pendingComponent: PageLoading,
errorComponent: ({ error }) => <ErrorPage error={error}/>,
})
const emptyRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/empty',
id: '/_empty',
component: EmptyLayout,
})
const layoutRoute = createRoute({
const authRoute = AuthenticatedImport.update({
getParentRoute: () => rootRoute,
id: '/layout',
id: '/_authenticated',
} as any)
const layoutNormalRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/_normal_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({
path: '/login',
getParentRoute: () => emptyRoute,
} as any)
const menusRoute = createRoute({
getParentRoute: () => layoutRoute,
getParentRoute: () => layoutAuthRoute,
path: '/system/menus',
}).lazy(async () => await import('@/pages/system/menus').then(d => d.Route))
const departmentsRoute = createRoute({
getParentRoute: () => layoutRoute,
getParentRoute: () => layoutAuthRoute,
path: '/system/departments',
}).lazy(async () => await import('@/pages/system/departments').then(d => d.Route))
const usersRoute = createRoute({
getParentRoute: () => layoutRoute,
getParentRoute: () => layoutAuthRoute,
path: '/system/users',
}).lazy(async () => await import('@/pages/system/users').then(d => d.Route))
const rolesRoute = createRoute({
getParentRoute: () => layoutRoute,
getParentRoute: () => layoutAuthRoute,
path: '/system/roles',
}).lazy(async () => await import('@/pages/system/roles').then(d => d.Route))
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 layoutRoute
parentRoute: typeof layoutAuthRoute
},
'/system/departments': {
preLoaderRoute: typeof departmentsRoute
parentRoute: typeof layoutRoute
parentRoute: typeof layoutAuthRoute
},
'/system/users': {
preLoaderRoute: typeof usersRoute
parentRoute: typeof layoutRoute
parentRoute: typeof layoutAuthRoute
},
'/system/roles': {
preLoaderRoute: typeof rolesRoute
parentRoute: typeof layoutRoute
parentRoute: typeof layoutAuthRoute
},
'/welcome': {
preLoaderRoute: typeof rootRoute
parentRoute: typeof layoutRoute
parentRoute: typeof layoutAuthRoute
},
}
}
@ -110,9 +162,20 @@ declare module '@tanstack/react-router' {
const routeTree = rootRoute.addChildren(
[
//非Layout
loginRoute,
emptyRoute,
layoutRoute.addChildren(
//不带权限Layout
layoutNormalRoute.addChildren([
notAuthRoute,
]),
//带权限Layout
dashboardRoute,
authRoute.addChildren(
[
layoutAuthRoute.addChildren(
[
menusRoute,
departmentsRoute,
@ -122,6 +185,8 @@ const routeTree = rootRoute.addChildren(
),
]
)
]
)
export const generateDynamicRoutes = (menuData: MenuItem[]) => {
@ -207,33 +272,15 @@ export const generateDynamicRoutes = (menuData: MenuItem[]) => {
const router = createRouter({
routeTree,
context: { queryClient, menuData: undefined },
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}/>
<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 { formatMenuData } from '@/utils'
import { isAuthenticated } from '@/utils/auth.ts'
import { atom, createStore } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
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(() => ({
queryKey: [ 'menus' ],
queryFn: async () => {
if (window.location.pathname === '/login') return []
if (!isAuthenticated()) {
return []
}
return await systemServ.menus.list()
},
select: data => formatMenuData(data as any ?? []),

6
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 { Router } from '@tanstack/react-router'
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'
@ -38,7 +38,7 @@ export type Props = Attributes & {
};
export interface IRootContext {
menuData?: MenuItem[];
menuData: MenuItem[];
queryClient: QueryClient;
}

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:
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:
version "5.3.1"
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"
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:
version "16.13.1"
resolved "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -3231,6 +3260,11 @@ vite@^5.2.0:
optionalDependencies:
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:
version "4.0.3"
resolved "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"

Loading…
Cancel
Save