李金
7 months ago
18 changed files with 639 additions and 417 deletions
-
5package.json
-
24src/App.tsx
-
111src/components/error-boundary/index.tsx
-
25src/components/error/403.tsx
-
25src/components/error/404.tsx
-
37src/components/error/error.tsx
-
13src/components/page-loading/index.tsx
-
16src/layout/EmptyLayout.tsx
-
255src/layout/RootLayout.tsx
-
18src/layout/_authenticated.tsx
-
30src/pages/dashboard/index.tsx
-
15src/pages/login/index.tsx
-
19src/request.ts
-
403src/routes.tsx
-
11src/store/system.ts
-
8src/types.d.ts
-
7src/utils/auth.ts
-
34yarn.lock
@ -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 |
@ -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: '' } |
||||
} |
} |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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> |
||||
|
) |
||||
} |
} |
@ -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, |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
}, |
||||
|
}) |
@ -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 |
@ -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 |
@ -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 layoutRoute = createRoute({ |
|
||||
getParentRoute: () => rootRoute, |
|
||||
id: '/layout', |
|
||||
component: RootLayout, |
|
||||
|
const authRoute = AuthenticatedImport.update({ |
||||
|
getParentRoute: () => rootRoute, |
||||
|
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({ |
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> |
|
||||
} |
|
||||
|
export const RootProvider = memo((props: { context: Partial<IRootContext> }) => { |
||||
|
|
||||
if (isPending) { |
|
||||
return <div>Loading...</div> |
|
||||
} |
|
||||
|
|
||||
router.update({ |
|
||||
context: { |
|
||||
queryClient, |
|
||||
menuData: data, |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
return ( |
|
||||
<QueryClientProvider client={queryClient}> |
|
||||
<RouterProvider router={router}/> |
|
||||
</QueryClientProvider> |
|
||||
) |
|
||||
} |
|
||||
|
return ( |
||||
|
<QueryClientProvider client={queryClient}> |
||||
|
<RouterProvider router={router} context={{ ...props.context, queryClient }}/> |
||||
|
</QueryClientProvider> |
||||
|
) |
||||
|
}) |
@ -0,0 +1,7 @@ |
|||||
|
import { getToken } from '@/store/system.ts' |
||||
|
|
||||
|
|
||||
|
export const isAuthenticated = () => { |
||||
|
const token = getToken() |
||||
|
return !!token |
||||
|
} |
@ -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" |
||||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue