Browse Source

完善框架

main
dark 7 months ago
parent
commit
5fd81d1f62
  1. 2
      mock/menus.ts
  2. 2
      package.json
  3. 5
      src/App.tsx
  4. 11
      src/layout/EmptyLayout.tsx
  5. 15
      src/pages/login/index.tsx
  6. 5
      src/pages/system/departments/index.tsx
  7. 6
      src/pages/system/menus/index.tsx
  8. 5
      src/pages/system/roles/index.tsx
  9. 5
      src/pages/system/users/index.tsx
  10. 63
      src/request.ts
  11. 128
      src/routes.tsx
  12. 2
      src/service/base.ts
  13. 3
      src/service/system.ts
  14. 49
      src/store/system.ts
  15. 25
      src/types.d.ts
  16. 18
      src/types/menus.d.ts
  17. 26
      src/utils/index.ts
  18. 6
      tsconfig.json
  19. 4
      vite.config.ts

2
mock/menus.ts

@ -2,7 +2,7 @@ import { MockMethod } from 'vite-plugin-mock'
export default [ export default [
{ {
url: '/api/menus',
url: '/api/v1/menus',
method: 'get', method: 'get',
response: () => { response: () => {
return { return {

2
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",
"dev": "vite --host --port 3000 --debug",
"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"

5
src/App.tsx

@ -1,10 +1,15 @@
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() {
return ( return (
<Provider store={appStore}>
<RootProvider/> <RootProvider/>
</Provider>
) )
} }

11
src/layout/EmptyLayout.tsx

@ -0,0 +1,11 @@
import { Outlet } from '@tanstack/react-router'
const EmptyLayout = () => {
return (
<>
<Outlet/>
</>
)
}
export default EmptyLayout

15
src/pages/login/index.tsx

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

5
src/pages/system/departments/index.tsx

@ -1,4 +1,5 @@
import { PageContainer } from '@ant-design/pro-components' import { PageContainer } from '@ant-design/pro-components'
import { createLazyFileRoute } from '@tanstack/react-router'
const Departments = () => { const Departments = () => {
return ( return (
@ -8,4 +9,8 @@ const Departments = () => {
) )
} }
export const Route = createLazyFileRoute("/system/departments")({
component: Departments
})
export default Departments export default Departments

6
src/pages/system/menus/index.tsx

@ -4,6 +4,7 @@ import { useAtom, useAtomValue } from 'jotai'
import { menuDataAtom, selectedMenuAtom, selectedMenuIdAtom } from '@/store/system.ts' import { menuDataAtom, selectedMenuAtom, selectedMenuIdAtom } from '@/store/system.ts'
import { formatterMenuData } from '@/utils/uuid.ts' import { formatterMenuData } from '@/utils/uuid.ts'
import { CloseOutlined, PlusOutlined } from '@ant-design/icons' import { CloseOutlined, PlusOutlined } from '@ant-design/icons'
import { createLazyFileRoute } from '@tanstack/react-router'
const Menus = () => { const Menus = () => {
@ -57,4 +58,9 @@ const Menus = () => {
) )
} }
export const Route = createLazyFileRoute("/system/menus")({
component: Menus
})
export default Menus export default Menus

5
src/pages/system/roles/index.tsx

@ -1,4 +1,5 @@
import { PageContainer } from '@ant-design/pro-components' import { PageContainer } from '@ant-design/pro-components'
import { createLazyFileRoute } from '@tanstack/react-router'
const Roles = () => { const Roles = () => {
return ( return (
@ -8,4 +9,8 @@ const Roles = () => {
) )
} }
export const Route = createLazyFileRoute("/system/roles")({
component: Roles
})
export default Roles export default Roles

5
src/pages/system/users/index.tsx

@ -1,4 +1,5 @@
import { PageContainer } from '@ant-design/pro-components' import { PageContainer } from '@ant-design/pro-components'
import { createLazyFileRoute } from '@tanstack/react-router'
const Users = () => { const Users = () => {
return ( return (
@ -8,4 +9,8 @@ const Users = () => {
) )
} }
export const Route = createLazyFileRoute("/system/users")({
component: Users
})
export default Users export default Users

63
src/request.ts

@ -1,19 +1,76 @@
import axios, { AxiosRequestConfig } from 'axios'
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { message } from 'antd'
import { getToken, setToken } from '@/store/system.ts'
export type { AxiosRequestConfig } export type { AxiosRequestConfig }
export const request = axios.create({ export const request = axios.create({
baseURL: '/api/v1', baseURL: '/api/v1',
timeout: 1000,
// timeout: 1000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}) })
//拦截request,添加token
request.interceptors.request.use((config) => {
const token = getToken()
if (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
})
//拦截response,返回data //拦截response,返回data
request.interceptors.response.use((response) => {
request.interceptors.response.use((response: AxiosResponse) => {
// console.log('response', response.data) // console.log('response', response.data)
message.destroy()
switch (response.data.code) {
case 200:
//login
if (response.config.url?.includes('/sys/login')) {
setToken(response.data.data.token)
}
return response.data return response.data
case 401:
// 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)}`
break
default:
message.error(response.data.message)
return Promise.reject(response)
}
}, (error) => {
console.log('error', error)
return Promise.reject(error)
}) })
export default request export default request

128
src/routes.tsx

@ -1,16 +1,20 @@
import { import {
createRouter, createRouter,
createRoute, createRoute,
RouterProvider, AnyRoute, redirect, createRootRouteWithContext, createLazyRoute,
RouterProvider, AnyRoute, redirect, createRootRouteWithContext, createLazyRoute, Outlet,
} from '@tanstack/react-router' } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools' import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import RootLayout from './layout/RootLayout' import RootLayout from './layout/RootLayout'
import ListPageLayout from '@/layout/ListPageLayout.tsx' import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { Route as LoginRouteImport } from '@/pages/login'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { IRootContext, MenuItem } from './types' import { IRootContext, MenuItem } from './types'
import { menuDataAtom } from './store/system.ts'
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: { defaultOptions: {
@ -23,12 +27,10 @@ export const queryClient = new QueryClient({
const rootRoute = createRootRouteWithContext<IRootContext>()({ const rootRoute = createRootRouteWithContext<IRootContext>()({
component: () => ( component: () => (
<>
<RootLayout/>
<div>
<Outlet/>
<TanStackRouterDevtools position={'bottom-right'}/> <TanStackRouterDevtools position={'bottom-right'}/>
</>
</div>
), ),
beforeLoad: ({ location }) => { beforeLoad: ({ location }) => {
if (location.pathname === '/') { if (location.pathname === '/') {
@ -38,13 +40,97 @@ const rootRoute = createRootRouteWithContext<IRootContext>()({
notFoundComponent: () => <div>404 Not Found</div>, notFoundComponent: () => <div>404 Not Found</div>,
}) })
const generateDynamicRoutes = (menuData: MenuItem[]) => {
const emptyRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/empty',
component: EmptyLayout,
})
const layoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: '/layout',
component: RootLayout,
})
const loginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => emptyRoute,
} as any)
const menusRoute = createRoute({
getParentRoute: () => layoutRoute,
path: '/system/menus',
}).lazy(async () => await import('@/pages/system/menus').then(d => d.Route))
const departmentsRoute = createRoute({
getParentRoute: () => layoutRoute,
path: '/system/departments',
}).lazy(async () => await import('@/pages/system/departments').then(d => d.Route))
const usersRoute = createRoute({
getParentRoute: () => layoutRoute,
path: '/system/users',
}).lazy(async () => await import('@/pages/system/users').then(d => d.Route))
const rolesRoute = createRoute({
getParentRoute: () => layoutRoute,
path: '/system/roles',
}).lazy(async () => await import('@/pages/system/roles').then(d => d.Route))
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
},
}
}
const routeTree = rootRoute.addChildren(
[
loginRoute,
emptyRoute,
layoutRoute.addChildren(
[
menusRoute,
departmentsRoute,
usersRoute,
rolesRoute,
]
),
]
)
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 path = menu.path?.replace(parentRoute.options?.path, '')
const isLayout = menu.children && menu.children.length > 0 && menu.type === 1
const isLayout = menu.children && menu.children.length > 0 && menu.type === 'menu'
if (isLayout && !menu.component) { if (isLayout && !menu.component) {
//没有component的layout,直接返回 //没有component的layout,直接返回
@ -76,7 +162,7 @@ const generateDynamicRoutes = (menuData: MenuItem[]) => {
// menu.type // menu.type
// 1,组件(页面),2,IFrame,3,外链接,4,按钮 // 1,组件(页面),2,IFrame,3,外链接,4,按钮
if (menu.type === 2) {
if (menu.type === 'iframe') {
component = '@/components/Iframe' component = '@/components/Iframe'
} }
@ -85,7 +171,7 @@ const generateDynamicRoutes = (menuData: MenuItem[]) => {
component: () => <div>404 Not Found</div> component: () => <div>404 Not Found</div>
}) })
} }
/* @vite-ignore */
const d = await import(`${component}`) const d = await import(`${component}`)
if (d.Route) { if (d.Route) {
return d.Route return d.Route
@ -103,7 +189,7 @@ const generateDynamicRoutes = (menuData: MenuItem[]) => {
// 对menuData递归生成路由,只处理type =1 的菜单 // 对menuData递归生成路由,只处理type =1 的菜单
const did = (menus: MenuItem[], parentRoute: AnyRoute) => { const did = (menus: MenuItem[], parentRoute: AnyRoute) => {
return menus.filter((item) => item.type === 1).map((item) => {
return menus.filter((item) => item.type === 'menu').map((item) => {
// 如果有children则递归生成子路由,同样只处理type =1 的菜单 // 如果有children则递归生成子路由,同样只处理type =1 的菜单
const route = generateRoutes(item, parentRoute) const route = generateRoutes(item, parentRoute)
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
@ -119,9 +205,15 @@ const generateDynamicRoutes = (menuData: MenuItem[]) => {
return did(menuData, rootRoute) return did(menuData, rootRoute)
} }
const router = createRouter({
routeTree,
context: { queryClient, menuData: undefined },
defaultPreload: 'intent'
})
export const RootProvider = () => { export const RootProvider = () => {
const [ , ] = useAtom(appAtom)
const { data, isError, isPending } = useAtomValue(menuDataAtom) const { data, isError, isPending } = useAtomValue(menuDataAtom)
if (isError) { if (isError) {
@ -132,13 +224,11 @@ export const RootProvider = () => {
return <div>Loading...</div> return <div>Loading...</div>
} }
const dynamicRoutes = generateDynamicRoutes(data!)
const routeTree = rootRoute.addChildren(dynamicRoutes)
const router = createRouter({
routeTree,
context: { queryClient, menuData: data },
defaultPreload: 'intent'
router.update({
context: {
queryClient,
menuData: data,
}
}) })
return ( return (

2
src/service/base.ts

@ -9,7 +9,7 @@ export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequest
return { return {
list: (params?: TParams & TPage) => { list: (params?: TParams & TPage) => {
return request.get<TResult>(api, { ...options, params })
return request.get<TResult[]>(api, { ...options, params }).then(data=>data.data)
}, },
add: (data: TParams) => { add: (data: TParams) => {
return request.post<TResult>(`${api}/add`, data, options) return request.post<TResult>(`${api}/add`, data, options)

3
src/service/system.ts

@ -1,13 +1,14 @@
import request from '../request.ts' import request from '../request.ts'
import { LoginRequest, LoginResponse } from '@/types/login' import { LoginRequest, LoginResponse } from '@/types/login'
import { createCURD } from '@/service/base.ts' import { createCURD } from '@/service/base.ts'
import { IMenu } from '@/types/menus'
const systemServ = { const systemServ = {
dept: { dept: {
...createCURD('/sys/dept') ...createCURD('/sys/dept')
}, },
menus: { menus: {
...createCURD('/sys/menu')
...createCURD<any, IMenu>('/sys/menu')
}, },
login: (data: LoginRequest) => { login: (data: LoginRequest) => {
return request.post<LoginResponse>('/sys/login', data) return request.post<LoginResponse>('/sys/login', data)

49
src/store/system.ts

@ -1,37 +1,42 @@
import { atomWithQuery } from 'jotai-tanstack-query' import { atomWithQuery } from 'jotai-tanstack-query'
import systemServ from '../service/system.ts' import systemServ from '../service/system.ts'
import { MenuItem } from '@/types'
import { getIcon } from '@/components/icon'
import { atom } from 'jotai/index'
// 格式化菜单数据, 把children转换成routes
export const formatMenuData = (data: MenuItem[]) => {
const result: MenuItem[] = []
for (const item of data) {
if (item.icon && typeof item.icon === 'string') {
item.icon = getIcon(item.icon as string, { size: '14', theme: 'filled' })
}
if (!item.children || !item.children.length) {
result.push(item)
} else {
const { children, ...other } = item
result.push({
...other,
children,
routes: formatMenuData(children),
import { IAppData, MenuItem } from '@/types'
import { atom, createStore } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { formatMenuData } from '@/utils'
/**
* app全局状态
*/
export const appStore = createStore()
export const appAtom = atomWithStorage<Partial<IAppData>>('app', {
name: 'Crazy Pro',
version: '1.0.0',
language: 'zh-CN',
}) })
export const getAppData = () => {
return appStore.get(appAtom)
} }
export const getToken = () => {
return appStore.get(appAtom).token
} }
return result
export const setToken = (token: string) => {
appStore.set(appAtom, { token })
} }
export const menuDataAtom = atomWithQuery(() => ({ export const menuDataAtom = atomWithQuery(() => ({
queryKey: [ 'menus' ], queryKey: [ 'menus' ],
queryFn: async () => { queryFn: async () => {
if (window.location.pathname === '/login') return []
return await systemServ.menus.list() return await systemServ.menus.list()
}, },
select: data => formatMenuData(data.data ?? []),
select: data => formatMenuData(data as any ?? []),
})) }))
export const selectedMenuIdAtom = atom<number>(0) export const selectedMenuIdAtom = atom<number>(0)
@ -39,8 +44,8 @@ export const selectedMenuAtom = atom<MenuItem | unknown>(undefined)
export const byIdMenuAtom = atomWithQuery((get) => ({ export const byIdMenuAtom = atomWithQuery((get) => ({
queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ],
queryFn: async ({ queryKey: [ , id ] }) => { queryFn: async ({ queryKey: [ , id ] }) => {
return await systemServ.menus.info(id as number) return await systemServ.menus.info(id as number)
}, },
select: data => data.data, select: data => data.data,
})) }))

25
src/types.d.ts

@ -2,9 +2,19 @@ import { Attributes, ReactNode } from 'react'
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'
export type LayoutType = 'list' | 'form' | 'tree' | 'normal' export type LayoutType = 'list' | 'form' | 'tree' | 'normal'
export type IAppData = {
name: string;
version: string;
language: string;
baseUrl: string;
token: string;
device: string;
}
export type TRouter = { export type TRouter = {
router: Router & { router: Router & {
context: IRootContext context: IRootContext
@ -28,22 +38,11 @@ export type Props = Attributes & {
}; };
export interface IRootContext { export interface IRootContext {
menuData: MenuItem[];
menuData?: MenuItem[];
queryClient: QueryClient; queryClient: QueryClient;
} }
interface MenuItem {
id?: number | string;
key: string;
title: string;
name: string;
path?: string;
icon?: string | ReactNode;
component?: string;
type: number | string;
order: number;
hideInMenu?: boolean;
children?: MenuItem[];
interface MenuItem extends IMenu{
routes?: MenuItem[]; routes?: MenuItem[];
} }

18
src/types/menus.d.ts

@ -1,21 +1,33 @@
import { ReactNode } from 'react'
export interface MenuButton { export interface MenuButton {
code: string, code: string,
label: string label: string
} }
export interface IMenu { export interface IMenu {
id: number,
parent_id: number, parent_id: number,
parent_path: string,
sort: number, sort: number,
code: string, code: string,
name: string, name: string,
title: string,
component: string,
icon: string | ReactNode,
description: string, description: string,
sequence: number, sequence: number,
type: string, type: string,
path: string, path: string,
properties: string, properties: string,
status: string
buttons: MenuButton[]
status: string,
parent_path: string,
affix: boolean,
redirect: string,
button: MenuButton[],
meta: Meta,
children: IMenu[]
} }
export interface MenuRequest extends IMenu { export interface MenuRequest extends IMenu {

26
src/utils/index.ts

@ -0,0 +1,26 @@
import { IMenu } from '@/types/menus'
import { MenuItem } from '@/types'
import { getIcon } from '@/components/icon'
// 格式化菜单数据, 把children转换成routes
export const formatMenuData = (data: IMenu[]) => {
const result: MenuItem[] = []
for (const item of data) {
if (item.icon && typeof item.icon === 'string') {
item.icon = getIcon(item.icon as string, { size: '14', theme: 'filled' })
}
if (!item.children || !item.children.length) {
result.push(item)
} else {
const { children, ...other } = item
result.push({
...other,
children,
routes: formatMenuData(children),
})
}
}
return result
}

6
tsconfig.json

@ -24,11 +24,11 @@
/* Type checking */ /* Type checking */
"strictNullChecks": true, "strictNullChecks": true,
"files": [ "src/**/*.d.ts"],
"files": [ "./src/**/*.d.ts"],
"paths": { "paths": {
"@/*": ["src/*"]
"@/*": ["./src/*"]
} }
}, },
"include": ["src"],
"include": ["./src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

4
vite.config.ts

@ -17,7 +17,7 @@ export default defineConfig({
'/api': { '/api': {
target: 'https://proapi.devwork.top', target: 'https://proapi.devwork.top',
changeOrigin: true, changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, '')
rewrite: (path) => path
} }
} }
}, },
@ -25,7 +25,7 @@ export default defineConfig({
react(), react(),
viteMockServe({ viteMockServe({
// 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production') // 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production')
enable: true,
enable: false,
// mock 文件的根路径,默认值:'mocks' // mock 文件的根路径,默认值:'mocks'
mockPath: 'mock', mockPath: 'mock',

Loading…
Cancel
Save