From 5fd81d1f62b99aeec996ad6805304a26e96beac2 Mon Sep 17 00:00:00 2001 From: dark Date: Thu, 18 Apr 2024 01:42:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mock/menus.ts | 2 +- package.json | 2 +- src/App.tsx | 7 +- src/layout/EmptyLayout.tsx | 11 +++ src/pages/login/index.tsx | 15 ++++ src/pages/system/departments/index.tsx | 5 ++ src/pages/system/menus/index.tsx | 6 ++ src/pages/system/roles/index.tsx | 5 ++ src/pages/system/users/index.tsx | 5 ++ src/request.ts | 65 +++++++++++++++-- src/routes.tsx | 128 ++++++++++++++++++++++++++++----- src/service/base.ts | 2 +- src/service/system.ts | 3 +- src/store/system.ts | 57 ++++++++------- src/types.d.ts | 25 ++++--- src/types/menus.d.ts | 18 ++++- src/utils/index.ts | 26 +++++++ tsconfig.json | 6 +- vite.config.ts | 8 +-- 19 files changed, 319 insertions(+), 77 deletions(-) create mode 100644 src/layout/EmptyLayout.tsx create mode 100644 src/pages/login/index.tsx create mode 100644 src/utils/index.ts diff --git a/mock/menus.ts b/mock/menus.ts index 83a8e00..433977e 100644 --- a/mock/menus.ts +++ b/mock/menus.ts @@ -2,7 +2,7 @@ import { MockMethod } from 'vite-plugin-mock' export default [ { - url: '/api/menus', + url: '/api/v1/menus', method: 'get', response: () => { return { diff --git a/package.json b/package.json index b817871..1ec5c9a 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite --host --port 3000", + "dev": "vite --host --port 3000 --debug", "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" diff --git a/src/App.tsx b/src/App.tsx index 5bfce9b..a97602c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,15 @@ import './App.css' import { RootProvider } from './routes.tsx' +import { Provider } from 'jotai' +import { appStore } from '@/store/system.ts' function App() { + return ( - + + + ) } diff --git a/src/layout/EmptyLayout.tsx b/src/layout/EmptyLayout.tsx new file mode 100644 index 0000000..bac73dc --- /dev/null +++ b/src/layout/EmptyLayout.tsx @@ -0,0 +1,11 @@ +import { Outlet } from '@tanstack/react-router' + +const EmptyLayout = () => { + return ( + <> + + + ) +} + +export default EmptyLayout \ No newline at end of file diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..fafbe87 --- /dev/null +++ b/src/pages/login/index.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/react-router' + +const Login = () => { + return ( +
+ Login +
+ ) +} + +export const Route = createFileRoute("/login")({ + component: Login +}) + +export default Login \ No newline at end of file diff --git a/src/pages/system/departments/index.tsx b/src/pages/system/departments/index.tsx index 097a8b2..a5b0031 100644 --- a/src/pages/system/departments/index.tsx +++ b/src/pages/system/departments/index.tsx @@ -1,4 +1,5 @@ import { PageContainer } from '@ant-design/pro-components' +import { createLazyFileRoute } from '@tanstack/react-router' const Departments = () => { return ( @@ -8,4 +9,8 @@ const Departments = () => { ) } +export const Route = createLazyFileRoute("/system/departments")({ + component: Departments +}) + export default Departments \ No newline at end of file diff --git a/src/pages/system/menus/index.tsx b/src/pages/system/menus/index.tsx index b1ab2c8..accd27c 100644 --- a/src/pages/system/menus/index.tsx +++ b/src/pages/system/menus/index.tsx @@ -4,6 +4,7 @@ import { useAtom, useAtomValue } from 'jotai' import { menuDataAtom, selectedMenuAtom, selectedMenuIdAtom } from '@/store/system.ts' import { formatterMenuData } from '@/utils/uuid.ts' import { CloseOutlined, PlusOutlined } from '@ant-design/icons' +import { createLazyFileRoute } from '@tanstack/react-router' const Menus = () => { @@ -57,4 +58,9 @@ const Menus = () => { ) } +export const Route = createLazyFileRoute("/system/menus")({ + component: Menus +}) + + export default Menus \ No newline at end of file diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index 614f14b..ee54971 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -1,4 +1,5 @@ import { PageContainer } from '@ant-design/pro-components' +import { createLazyFileRoute } from '@tanstack/react-router' const Roles = () => { return ( @@ -8,4 +9,8 @@ const Roles = () => { ) } +export const Route = createLazyFileRoute("/system/roles")({ + component: Roles +}) + export default Roles \ No newline at end of file diff --git a/src/pages/system/users/index.tsx b/src/pages/system/users/index.tsx index 595ef09..baa64e4 100644 --- a/src/pages/system/users/index.tsx +++ b/src/pages/system/users/index.tsx @@ -1,4 +1,5 @@ import { PageContainer } from '@ant-design/pro-components' +import { createLazyFileRoute } from '@tanstack/react-router' const Users = () => { return ( @@ -8,4 +9,8 @@ const Users = () => { ) } +export const Route = createLazyFileRoute("/system/users")({ + component: Users +}) + export default Users \ No newline at end of file diff --git a/src/request.ts b/src/request.ts index d7486c2..fdf661c 100644 --- a/src/request.ts +++ b/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 const request = axios.create({ baseURL: '/api/v1', - timeout: 1000, + // timeout: 1000, headers: { '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 -request.interceptors.response.use((response) => { +request.interceptors.response.use((response: AxiosResponse) => { // console.log('response', response.data) - return 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 + 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 \ No newline at end of file diff --git a/src/routes.tsx b/src/routes.tsx index cc0d75c..c9cf98f 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,16 +1,20 @@ import { createRouter, createRoute, - RouterProvider, AnyRoute, redirect, createRootRouteWithContext, createLazyRoute, + RouterProvider, AnyRoute, redirect, createRootRouteWithContext, createLazyRoute, Outlet, } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' 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 { 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({ defaultOptions: { @@ -23,12 +27,10 @@ export const queryClient = new QueryClient({ const rootRoute = createRootRouteWithContext()({ component: () => ( - <> - +
+ - - - +
), beforeLoad: ({ location }) => { if (location.pathname === '/') { @@ -38,13 +40,97 @@ const rootRoute = createRootRouteWithContext()({ notFoundComponent: () =>
404 Not Found
, }) -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则递归生成子路由 const generateRoutes = (menu: MenuItem, parentRoute: AnyRoute) => { 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) { //没有component的layout,直接返回 @@ -76,7 +162,7 @@ const generateDynamicRoutes = (menuData: MenuItem[]) => { // menu.type // 1,组件(页面),2,IFrame,3,外链接,4,按钮 - if (menu.type === 2) { + if (menu.type === 'iframe') { component = '@/components/Iframe' } @@ -85,7 +171,7 @@ const generateDynamicRoutes = (menuData: MenuItem[]) => { component: () =>
404 Not Found
}) } - + /* @vite-ignore */ const d = await import(`${component}`) if (d.Route) { return d.Route @@ -103,7 +189,7 @@ const generateDynamicRoutes = (menuData: MenuItem[]) => { // 对menuData递归生成路由,只处理type =1 的菜单 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 的菜单 const route = generateRoutes(item, parentRoute) if (item.children && item.children.length > 0) { @@ -119,9 +205,15 @@ const generateDynamicRoutes = (menuData: MenuItem[]) => { return did(menuData, rootRoute) } +const router = createRouter({ + routeTree, + context: { queryClient, menuData: undefined }, + defaultPreload: 'intent' +}) export const RootProvider = () => { + const [ , ] = useAtom(appAtom) const { data, isError, isPending } = useAtomValue(menuDataAtom) if (isError) { @@ -132,13 +224,11 @@ export const RootProvider = () => { return
Loading...
} - 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 ( diff --git a/src/service/base.ts b/src/service/base.ts index af3224f..953c469 100644 --- a/src/service/base.ts +++ b/src/service/base.ts @@ -9,7 +9,7 @@ export const createCURD = (api: string, options?: AxiosRequest return { list: (params?: TParams & TPage) => { - return request.get(api, { ...options, params }) + return request.get(api, { ...options, params }).then(data=>data.data) }, add: (data: TParams) => { return request.post(`${api}/add`, data, options) diff --git a/src/service/system.ts b/src/service/system.ts index ac45752..04f469f 100644 --- a/src/service/system.ts +++ b/src/service/system.ts @@ -1,13 +1,14 @@ import request from '../request.ts' import { LoginRequest, LoginResponse } from '@/types/login' import { createCURD } from '@/service/base.ts' +import { IMenu } from '@/types/menus' const systemServ = { dept: { ...createCURD('/sys/dept') }, menus: { - ...createCURD('/sys/menu') + ...createCURD('/sys/menu') }, login: (data: LoginRequest) => { return request.post('/sys/login', data) diff --git a/src/store/system.ts b/src/store/system.ts index 41adbb6..f098361 100644 --- a/src/store/system.ts +++ b/src/store/system.ts @@ -1,37 +1,42 @@ import { atomWithQuery } from 'jotai-tanstack-query' 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), - }) - - } - } - return result +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>('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 +} + +export const setToken = (token: string) => { + appStore.set(appAtom, { token }) } export const menuDataAtom = atomWithQuery(() => ({ queryKey: [ 'menus' ], queryFn: async () => { + if (window.location.pathname === '/login') return [] return await systemServ.menus.list() }, - select: data => formatMenuData(data.data ?? []), + select: data => formatMenuData(data as any ?? []), })) export const selectedMenuIdAtom = atom(0) @@ -39,8 +44,8 @@ export const selectedMenuAtom = atom(undefined) export const byIdMenuAtom = atomWithQuery((get) => ({ queryKey: [ 'selectedMenu', get(selectedMenuIdAtom) ], queryFn: async ({ queryKey: [ , id ] }) => { - return await systemServ.menus.info(id as number) }, select: data => data.data, })) + diff --git a/src/types.d.ts b/src/types.d.ts index 7db5f3c..e00ff23 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -2,9 +2,19 @@ import { Attributes, ReactNode } from 'react' 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' 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 = { router: Router & { context: IRootContext @@ -28,22 +38,11 @@ export type Props = Attributes & { }; export interface IRootContext { - menuData: MenuItem[]; + menuData?: MenuItem[]; 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[]; } diff --git a/src/types/menus.d.ts b/src/types/menus.d.ts index 8b88f88..f995485 100644 --- a/src/types/menus.d.ts +++ b/src/types/menus.d.ts @@ -1,21 +1,33 @@ +import { ReactNode } from 'react' export interface MenuButton { code: string, label: string } + + export interface IMenu { + id: number, parent_id: number, - parent_path: string, sort: number, code: string, name: string, + title: string, + component: string, + icon: string | ReactNode, description: string, sequence: number, type: string, path: 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 { diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..9520b08 --- /dev/null +++ b/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 +} diff --git a/tsconfig.json b/tsconfig.json index 3df1f1e..0d1c963 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,11 +24,11 @@ /* Type checking */ "strictNullChecks": true, - "files": [ "src/**/*.d.ts"], + "files": [ "./src/**/*.d.ts"], "paths": { - "@/*": ["src/*"] + "@/*": ["./src/*"] } }, - "include": ["src"], + "include": ["./src"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/vite.config.ts b/vite.config.ts index c3e8652..bd74ce8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,12 +12,12 @@ export default defineConfig({ '@': '/src', }, }, - server:{ - proxy:{ + server: { + proxy: { '/api': { target: 'https://proapi.devwork.top', changeOrigin: true, - // rewrite: (path) => path.replace(/^\/api/, '') + rewrite: (path) => path } } }, @@ -25,7 +25,7 @@ export default defineConfig({ react(), viteMockServe({ // 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production') - enable: true, + enable: false, // mock 文件的根路径,默认值:'mocks' mockPath: 'mock',