https://www.html.cn/create-react-app/
# 现在
npx create-react-app react-admin-app --template typescript
熟悉目录结构
- react-admin-app
-node_modules
-public
-src
App.css
App.test.tsx App.tsx的测试文件 npm run test 查看测试结果
App.tsx
index.css
index.tsx react应用程序的入口文件
logo.svg
react-app-env.d.ts // 声明文件 // 指令声明对包的依赖关系
reportWebVitals.ts // 测试性能
seupTests.ts // 使用jest做为测试工具
.gitignore
package-lock.json
package.json
README.md
tsconfig.json
*.d.ts 代表ts的声明文件
src
api
components
layout
store
router
utils
views
App.tsx
index.tsx
logo.svg
react-app-env.d.ts
reportWebVitals.ts
seupTests.ts
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
);
reportWebVitals();
// src/App.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const App: FC = (props) => {
return (
<>App>
)
}
export default App
两种方式:
本项目推荐使用第二种方式
$ cnpm i @craco/craco @types/node -D
https://www.npmjs.com/package/@craco/craco
项目根目录创建 craco.config.js
,代码如下:
// craco.config.js
const path = require('path')
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
}
为了使 TS 文件引入时的别名路径能够正常解析,需要配置 tsconifg.json
,在 compilerOptions
选项里添加 path 等属性。为了防止配置被覆盖,需要单独创建一个文件 tsconfig.path.json
,添加以下代码
// tsconfig.path.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": [
"node"
]
}
}
在 tsconifg.json
引入配置文件:
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"extends": "./tsconfig.path.json",
"include": [
"src"
]
}
修改 package.json
如下:
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test"
},
$ npm run start
根据项目需求 任选其一即可
$ cnpm i redux -S
$ cnpm i redux react-redux -S
$ cnpm i redux react-redux redux-thunk -S
$ cnpm i redux react-redux redux-saga -S
$ cnpm i redux react-redux redux-thunk immutable redux-immutable -S
$ cnpm i redux react-redux redux-saga immutable redux-immutable -S
$ cnpm i mobx mobx-react -S
本项目不采用之前的状态管理模式,使用 rtk 技术
cnpm i @reduxjs/toolkit redux react-redux -S
2021年11月4日 发布了 react-router-dom的v6.0.0版本:https://reactrouter.com/
如需使用v5版本:https://v5.reactrouter.com/web/guides/quick-start cnpm i react-router-dom@5 -S
本项目采用 V6版本
cnpm i react-router-dom -S
思考,有没有必要安装 prop-types ?
cnpm i prop-types -S
本项目其实没有必要安装,因为所有的数据都是基于ts,而ts需要指定类型注解
cnpm i axios -S
以前版本中 cnpm i @types/axios -S
Ts 中 @types/* 为声明文件
官网地址:https://ant.design/index-cn 5.2.0
国内官方镜像地址:https://ant-design.antgroup.com/index-cn
国内gitee镜像地址:https://ant-design.gitee.io/index-cn
cnpm i antd @ant-design/icons -S
src/index.tsx
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
);
reportWebVitals();
测试组件库
// src/App.tsx
import React, { FC } from 'react';
import { Button } from 'antd';
interface IAppProps {
}
const App: FC = (props) => {
return (
<>
App
>
)
}
export default App
浏览器查看发现测试通过
https://ant-design.antgroup.com/docs/react/use-in-typescript-cn
antd 内建了深色主题和紧凑主题,你可以参照 使用暗色主题和紧凑主题 进行接入。
可以定制的变量列表如下:
@primary-color: #1890ff; // 全局主色
@link-color: #1890ff; // 链接色
@success-color: #52c41a; // 成功色
@warning-color: #faad14; // 警告色
@error-color: #f5222d; // 错误色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color: rgba(0, 0, 0, 0.65); // 主文本色
@text-color-secondary: rgba(0, 0, 0, 0.45); // 次文本色
@disabled-color: rgba(0, 0, 0, 0.25); // 失效色
@border-radius-base: 2px; // 组件/浮层圆角
@border-color-base: #d9d9d9; // 边框色
@box-shadow-base: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 9px 28px 8px rgba(0, 0, 0, 0.05); // 浮层阴影
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
<React.StrictMode>
<ConfigProvider
theme = { {
token: {
colorPrimary: '#1890ff'
}
} }
>
<App />
</ConfigProvider>
</React.StrictMode>
);
reportWebVitals();
https://www.lodashjs.com/
Lodash 工具包,项目必装,它提供了很多使用的函数
$ cnpm i lodash -S
$ cnpm i @types/lodash -D
import _ from 'lodash'
var users = [
{ 'user': 'barney', 'active': false },
{ 'user': 'fred', 'active': false },
{ 'user': 'pebbles', 'active': true }
];
console.log(_.findIndex(users, (item) => item.user === 'pebbles'))
console.log(users.findIndex((item) => item.user === 'pebbles'))
预览模板:https://pro.ant.design/zh-CN/
src/layout/Index.tsx 作为后台管理系统的主页面布局(包含左侧的菜单栏,顶部,底部等)
https://ant-design.gitee.io/components/layout-cn/#components-layout-demo-custom-trigger
不要照着代码敲,直接复制即可,给 Layout 组件添加 id为
admin-app
// src/layout/Index.tsx
import React, { useState } from 'react';
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu, theme } from 'antd';
const { Header, Sider, Content } = Layout;
const App: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const {
token: { colorBgContainer },
} = theme.useToken();
return (
,
label: 'nav 1',
},
{
key: '2',
icon: ,
label: 'nav 2',
},
{
key: '3',
icon: ,
label: 'nav 3',
},
]}
/>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => setCollapsed(!collapsed),
})}
Content
);
};
export default App;
主组件引入 主界面的布局文件
// src/App.tsx
import React, { FC } from 'react';
import Index from '@/layout/Index'
import './App.css'
interface IAppProps {
}
const App: FC = (props) => {
return (
<>
>
)
}
export default App
查看浏览器,预览运行结果
发现页面并不是全屏。审查元素设置 root以及 components-layout-demo-custom-trigger 高度为 100%
/* src/App.css */
#root, #components-layout-demo-custom-trigger { height: 100%;}
#components-layout-demo-custom-trigger .trigger {
padding: 0 24px;
font-size: 18px;
line-height: 64px;
cursor: pointer;
transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
color: #1890ff;
}
#components-layout-demo-custom-trigger .logo {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
}
先拆分左侧的菜单栏组件
// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import {
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';
const { Sider } = Layout;
const App: React.FC = () => {
const [collapsed] = useState(false);
return (
,
label: 'nav 1',
},
{
key: '2',
icon: ,
label: 'nav 2',
},
{
key: '3',
icon: ,
label: 'nav 3',
},
]}
/>
);
};
export default App;
// src/layout/components/AppHeader.tsx
import React, { useState } from 'react';
import {
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme } from 'antd';
const { Header } = Layout;
const App: React.FC = () => {
const [collapsed, setCollapsed] = useState(false);
const {
token: { colorBgContainer },
} = theme.useToken();
return (
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => setCollapsed(!collapsed),
})}
);
};
export default App;
// src/layout/components/AppMain.tsx
import React from 'react';
import { Layout, theme } from 'antd';
const { Content } = Layout;
const App: React.FC = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
return (
Content
);
};
export default App;
整和组件资源
// src/layout/components/index.ts
export { default as SideBar } from './SideBar'
export { default as AppHeader } from './AppHeader'
export { default as AppMain } from './AppMain'
// src/layout/Index.tsx
import React from 'react';
import { Layout } from 'antd';
// import SideBar from './components/SideBar'
// import AppHeader from './components/AppHeader'
// import AppMain from './components/AppMain'
import { SideBar, AppHeader, AppMain } from './components'
const App: React.FC = () => {
return (
);
};
export default App;
此时点击头部的控制器,发现只有头部组件的 图标在切换,但是并没有影响左侧菜单的收缩
建议使用状态管理器管理控制的这个状态
http://cn.redux.js.org/
参考链接:http://cn.redux.js.org/tutorials/typescript-quick-start
// src/store/index.ts
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({
reducer: {}
})
// 导出类型注解
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatch
export default store
构建app的模块用于管理 头部和 左侧菜单的共同的状态
虽然可以将RootState
andAppDispatch
类型导入到每个组件中,但最好创建useDispatch
and useSelector
hooks 的类型化版本以在您的应用程序中使用。
// src/store/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './index'
// 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
创建状态管理
// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'
interface IAppState {
collapsed: boolean
}
const initialState: IAppState = {
collapsed: false
}
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
changeCollapsed (state) {
state.collapsed = !state.collapsed
}
}
})
export const { changeCollapsed } = appSlice.actions
export default appSlice.reducer
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import app from './modules/app'
const store = configureStore({
reducer: {
app
}
})
// 导出类型注解
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatch
export default store
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'
import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './store'
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
);
reportWebVitals();
// src/layout/components/SideBar.tsx
import React from 'react';
import {
UploadOutlined,
UserOutlined,
VideoCameraOutlined,
} from '@ant-design/icons';
import { Layout, Menu } from 'antd';
import { useAppSelector } from '@/store/hooks'
// import { useSelector } from 'react-redux'
// import type { RootState } from '@/store'
const { Sider } = Layout;
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
// const collapsed = useSelector((state: RootState) => state.app.collapsed)
return (
,
label: 'nav 1',
},
{
key: '2',
icon: ,
label: 'nav 2',
},
{
key: '3',
icon: ,
label: 'nav 3',
},
]}
/>
);
};
export default App;
// src/layout/components/AppHeader.tsx
import React from 'react';
import {
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
const { Header } = Layout;
const App: React.FC = () => {
// const [collapsed, setCollapsed] = useState(false);
const collapsed = useAppSelector(state => state.app.collapsed)
const dispatch = useAppDispatch()
const {
token: { colorBgContainer },
} = theme.useToken();
return (
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
// onClick: () => setCollapsed(!collapsed),
onClick: () => dispatch(changeCollapsed())
})}
);
};
export default App;
永久存储 用户习惯
数据持久化: redux-persist
此时发现 头部的 按钮可以控制左侧菜单栏了,但是还没有满足需求
需求如下:保留用户的使用习惯
// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'
interface IAppState {
collapsed: boolean
}
const initialState: IAppState = {
// collapsed: false
collapsed: localStorage.getItem('collapsed') === 'true'
}
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
changeCollapsed (state) {
state.collapsed = !state.collapsed
localStorage.setItem('collapsed', String(state.collapsed))
}
}
})
export const { changeCollapsed } = appSlice.actions
export default appSlice.reducer
$ cnpm i store2 -S
https://www.npmjs.com/package/store2
推荐一个好用的永久存储的 类 localStorage 的工具 store2
// src/store/modules/app.ts
import { createSlice } from '@reduxjs/toolkit'
import store2 from 'store2'
interface IAppState {
collapsed: boolean
}
const initialState: IAppState = {
// collapsed: false
// collapsed: localStorage.getItem('collapsed') === 'true'
collapsed: store2.get('collapsed') === 'true'
}
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
changeCollapsed (state) {
state.collapsed = !state.collapsed
// localStorage.setItem('collapsed', String(state.collapsed))
store2.set('collapsed', String(state.collapsed))
}
}
})
export const { changeCollapsed } = appSlice.actions
export default appSlice.reducer
https://ant-design.gitee.io/components/menu-cn/#components-menu-demo-sider-current
Antd 4.20以上版本直接实现 递归
antd 4.20版本以下需要手动实现
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
type MenuItem = Required<MenuProps>['items'][number];
// 扩展固有的类型
type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string // 多级菜单的默认地址
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: <HomeOutlined />
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: <HomeOutlined />,
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: <HomeOutlined />,
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: <HomeOutlined />,
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: <HomeOutlined />,
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: <HomeOutlined />,
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: <HomeOutlined />,
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: <HomeOutlined />,
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: <HomeOutlined />,
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: <HomeOutlined />,
}
]
}
]
export default menus
左侧菜单栏的头部设定logo以及后台管理系统名称
// src/layout/components/SideBar.tsx
import React from 'react';
import { Layout, Menu, Image } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
const { Sider } = Layout;
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
return (
{ !collapsed && 嗨购后台管理系统 }
);
};
export default App;
以上菜单项的设置在antd 4.20.0
版本以上好使,如果在4.20.0
版本以下,应该使用 递归组件实现
// src/layout/components/SideBar.tsx
import React from 'react';
import { Layout, Menu, Image } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
const { Sider } = Layout;
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
// 自定义左侧菜单栏 - 递归
const renderMenus = (menus: any[]) => {
return menus.map(item => {
if (item.children) {
return (
{ renderMenus(item.children) }
)
} else {
return { item.label }
}
})
}
return (
{ !collapsed && 嗨购后台管理系统 }
);
};
export default App;
组件形式渲染左侧菜单目前并不推荐使用
如果左侧菜单栏数据过于庞大,每个管理项里又有很多项,需要只展开一个菜单项
// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import { Layout, Menu, Image } from 'antd';
import type { MenuProps } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
const { Sider } = Layout;
// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
if (item.children) {
rootSubmenuKeys.push(item.key as string)
}
})
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
const [openKeys, setOpenKeys] = useState(['sub1']);
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
return (
{ !collapsed && 嗨购后台管理系统 }
);
};
export default App;
https://reactrouter.com/
|-src
| |- ...
| |-views
| |- banner
| |- List.tsx #首页轮播图
| | |- Add.tsx #添加轮播图
| |- home
| | |- Index.tsx #系统首页
| |- pro
| | |- List.tsx #产品管理
| | |- Search.tsx #筛选列表
| |- account
| | |- User.tsx #用户列表
| | |- Admin.tsx#管理员列表
// src/views/home/Index.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
系统首页
)
}
export default Com
// src/views/account/Admin.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
管理员列表
)
}
export default Com
// src/views/account/User.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
用户列表
)
}
export default Com
// src/views/banner/Add.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
添加轮播图
)
}
export default Com
// src/views/banner/List.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
轮播图列表
)
}
export default Com
// src/views/pro/List.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
产品列表
)
}
export default Com
// src/views/pro/Search.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
筛选列表
)
}
export defa
v6的路由通过 element 属性定义匹配的组件
因此menus中可以添加一个 element 属性,值就为组件的引用即可
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
type MenuItem = Required<MenuProps>['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: <HomeOutlined />,
element: <Home />
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: <HomeOutlined />,
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: <HomeOutlined />,
element: <BannerList />
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: <HomeOutlined />,
element: <BannerAdd />
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: <HomeOutlined />,
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: <HomeOutlined />,
element: <ProList />
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: <HomeOutlined />,
element: <SearchList />
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: <HomeOutlined />,
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: <HomeOutlined />,
element: <UserList />
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: <HomeOutlined />,
element: <AdminList />
}
]
}
]
export default menus
在根组件添加 BrowserRouter
或者 HashRouter
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
);
reportWebVitals();
在menu.tsx
里已经定义好了请求的路径(其实就是数据中key属性)和路径对应组件(其实就是数据中的element属性),剩下就是定义路由组件了
组件渲染的区域 AppMain
组件
// src/layout/components/AppMain.tsx
import React from 'react';
import { Layout, theme } from 'antd';
import { Routes, Route, Navigate } from 'react-router-dom';
// import BannerAdd from '@/views/banner/Add'
import { IMyMenuItem } from '@/router/menu';
import menus from '@/router/menu'
const { Content } = Layout;
const App: React.FC = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
const renderRoute: any = (menus: IMyMenuItem[]) => {
return menus.map(item => {
if (item.children) {
// React.Fragment 也为空标签,可以设置 key 属性
// 实现 重定向
return (
} />
{
renderRoute(item.children!)
}
)
} else {
return
}
})
}
return (
{/* } /> */}
{/* } /> */}
{ renderRoute(menus) }
);
};
export default App;
可以在地址栏输入路径,测试是否正常
http://localhost:3000/ #系统首页
http://localhost:3000/banner #轮播图管理
http://localhost:3000/banner/list #轮播图列表
http://localhost:3000/banner/add #添加轮播图
http://localhost:3000/pro #产品管理
http://localhost:3000/pro/search #筛选列表
http://localhost:3000/pro/list #产品列表
http://localhost:3000/account #账户管理
http://localhost:3000/account/user #用户列表
http://localhost:3000/account/admin #管理员列表
// src/views/error/Page404.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
404
)
}
export default Com
// src/layout/components/AppMain.tsx
import React from 'react';
import { Layout, theme } from 'antd';
import { Routes, Route, Navigate } from 'react-router-dom';
// import BannerAdd from '@/views/banner/Add'
import Page404 from '@/views/error/Page404'
import { IMyMenuItem } from '@/router/menu';
import menus from '@/router/menu'
const { Content } = Layout;
const App: React.FC = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
const renderRoute: any = (menus: IMyMenuItem[]) => {
return menus.map(item => {
if (item.children) {
// React.Fragment 也为空标签,可以设置 key 属性
// 实现 重定向
return (
} />
{
renderRoute(item.children!)
}
)
} else {
return
}
})
}
return (
{/* } /> */}
{/* } /> */}
{ renderRoute(menus) }
} />
);
};
export default App;
上述项目中,切换路由都是手动输入的,实际上应该点击左侧菜单栏进行路由导航。
左侧菜单的逻辑交互,前面已经生成了(openKeys 以及 onOpenChanges 实现)
现在通过点击事件来切换导航
// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import { Layout, Menu, Image } from 'antd';
import type { MenuProps } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useNavigate } from 'react-router-dom';
const { Sider } = Layout;
// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
if (item.children) {
rootSubmenuKeys.push(item.key as string)
}
})
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
const [openKeys, setOpenKeys] = useState(['']);
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
// console.log('keys', keys)
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
// console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
const navigate = useNavigate()
const changeUrl = ({ key }: { key: string }) => {
console.log(key)
navigate(key)
}
return (
{ !collapsed && 嗨购后台管理系统 }
);
};
export default App;
当页面刷新时,需要保证当前二级路由是展开的,且当前路由是被选中的状态
// src/layout/components/SideBar.tsx
import React, { useState } from 'react';
import { Layout, Menu, Image } from 'antd';
import type { MenuProps } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useLocation, useNavigate } from 'react-router-dom';
const { Sider } = Layout;
// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
if (item.children) {
rootSubmenuKeys.push(item.key as string)
}
})
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
// /pro/search
const { pathname } = useLocation() // /pro/search
// console.log(location)
const [selectedKeys, setSelectedKeys] = useState([ pathname ]) // ['/pro/search']
const [openKeys, setOpenKeys] = useState(['/' + pathname.split('/')[1] ]); // ['/pro']
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
// console.log('keys', keys)
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
// console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
const navigate = useNavigate()
const changeUrl = ({ key }: { key: string }) => {
// console.log(key)
navigate(key)
setSelectedKeys([key]) // 点击时需要告诉程序哪一项被选中
}
return (
{ !collapsed && 嗨购后台管理系统 }
);
};
export default App;
通过案例项目,得知 面包屑组件应该包含在 页面的头部 https://vvbin.cn/next/#/feat/breadcrumb/flat
参照组件库的面包屑 https://ant-design.gitee.io/components/breadcrumb-cn/#components-breadcrumb-demo-react-router
头部组件加入了面包屑导航组件,尽可能不动原来的布局
// src/layout/components/AppHeader.tsx
import React from 'react';
import {
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { useLocation, Link } from 'react-router-dom'
import menus, { IMyMenuItem } from '@/router/menu'
const { Header } = Layout;
// const breadcrumbNameMap: any = {
// '/': '系统首页',
// '/banner': '轮播图管理',
// '/banner/list': '轮播图列表',
// '/banner/add': '添加轮播图',
// '/pro': '产品管理',
// '/pro/list': '产品列表',
// '/pro/search': '筛选列表',
// '/account': '账户管理',
// '/account/user': '用户列表',
// '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}
function getBreadcrumbNameMap (menus: any[]) {
menus.forEach(item => {
if (item.children) {
breadcrumbNameMap[item.path] = item.label
getBreadcrumbNameMap(item.children)
} else {
breadcrumbNameMap[item.path] = item.label
}
})
}
console.log(breadcrumbNameMap)
getBreadcrumbNameMap(menus)
const App: React.FC = () => {
// const [collapsed, setCollapsed] = useState(false);
const collapsed = useAppSelector(state => state.app.collapsed)
const dispatch = useAppDispatch()
const {
token: { colorBgContainer },
} = theme.useToken();
const location = useLocation(); // /pro/list
const pathSnippets = location.pathname.split('/').filter((i) => i);
console.log(pathSnippets) // ['pro', 'list']
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
console.log(url) // /pro /pro/list
return (
{breadcrumbNameMap[url]}
);
});
const breadcrumbItems = [
系统首页
,
].concat(extraBreadcrumbItems);
return (
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
// onClick: () => setCollapsed(!collapsed),
onClick: () => dispatch(changeCollapsed())
})}
{breadcrumbItems}
);
};
export default App;
随之而来的问题就是,当点击面包屑导航时,地址栏的路由已经发生了跳转,但是左侧菜单栏数据效果没有实时更新(左侧菜单栏组件早就创建完毕,选中和打开的选项已经做了固定, 点击面包屑没有引起左侧菜单栏组件的状态以及属性的更新,左侧菜单栏不会重新渲染)
此时可以在左侧菜单栏组件监听 路由的变化 – -useEffect
// src/layout/components/SideBar.tsx
import React, { useEffect, useState } from 'react';
import { Layout, Menu, Image } from 'antd';
import type { MenuProps } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useLocation, useNavigate } from 'react-router-dom';
const { Sider } = Layout;
// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
if (item.children) {
rootSubmenuKeys.push(item.key as string)
}
})
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
// /pro/search
const { pathname } = useLocation() // /pro/search
// console.log(location)
const [selectedKeys, setSelectedKeys] = useState([ pathname ]) // ['/pro/search']
const [openKeys, setOpenKeys] = useState(['/' + pathname.split('/')[1] ]); // ['/pro']
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
// console.log('keys', keys)
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
// console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
const navigate = useNavigate()
const changeUrl = ({ key }: { key: string }) => {
// console.log(key)
navigate(key)
setSelectedKeys([key]) // 点击时需要告诉程序哪一项被选中
}
useEffect(() => { // ++++++++++++
setSelectedKeys([pathname])
setOpenKeys(['/' + pathname.split('/')[1] ])
}, [pathname])
return (
{ !collapsed && 嗨购后台管理系统 }
);
};
export default App;
https://panjiachen.gitee.io/vue-element-admin/#/charts/line
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-upxVnLXz-1678063667812)(assets/image-20221025113910072.png)]
// src/layout/components/AppTabs.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
AppTabs
)
}
export default Com
// src/layout/components/index.ts
export { default as SideBar } from './SideBar'
export { default as AppHeader } from './AppHeader'
export { default as AppMain } from './AppMain'
export { default as AppTabs } from './AppTabs'
// src/layout/Index.tsx
import React from 'react';
import { Layout } from 'antd';
// import SideBar from './components/SideBar'
// import AppHeader from './components/AppHeader'
// import AppMain from './components/AppMain'
import { SideBar, AppHeader, AppMain, AppTabs } from './components'
const App: React.FC = () => {
return (
);
};
export default App;
后期 监听地址栏 从tabsArr 中提取数据
const tabsArr = [{"label":"系统首页","key":"/"},
{"label":"轮播图列表","key":"/banner/list"},
{"label":"添加轮播图","key":"/banner/add"},
{"label":"产品列表","key":"/pro/list"},
{"label":"筛选列表","key":"/pro/search"},
{"label":"用户列表","key":"/account/user"},
{"label":"管理员列表","key":"/account/admin"}]
// src/layout/components/AppTabs.tsx
import React, { FC, useEffect, useState } from 'react';
import menus from '@/router/menu'
import { useLocation, useNavigate } from 'react-router-dom';
import { Tag } from 'antd'
interface IAppProps {
}
// 需要的原始数据
const tabAttr: { label: any; key: any; }[] = []
function getTabAttrs (menus: any[]) {
menus.forEach(item => {
if (item.children) {
getTabAttrs(item.children)
} else {
tabAttr.push({
label: item.label,
key: item.key
})
}
})
}
getTabAttrs(menus)
// console.log('tabAttr', tabAttr)
const Com: FC<IAppProps> = (props) => {
// 当前地址栏的地址
const { pathname } = useLocation()
// 快捷导航的数组
const [arr, setArr] = useState([{ label: '系统首页', key: '/' }])
// 选中的索引值 - 加样式
const [current, setCurrent] = useState(0)
const [num, setNum] = useState(0) // 为了获取最新的数据
useEffect(() => {
// 判断当前的路由在不在快捷导航的数组中
const index = arr.findIndex(item => item.key === pathname)
if (index !== -1) {
// 如果在,拿到索引值,添加样式
setCurrent(index)
} else {
// 如果不在快捷导航数组中
// 从原始数据中获取值
const item = tabAttr.find(item => item.key === pathname)
const newArr = arr
item && newArr.push(item)
// 修改状态
setArr(newArr)
setCurrent(arr.length - 1)
}
}, [pathname, arr, num]) // 一旦num发生变化 一定会获取到最新的数据
const navigate = useNavigate()
return (
<div style={{ height: 40, background: '#fff', borderTop: '1px solid #ccc', paddingLeft: 16, display: 'flex', flexWrap: 'nowrap', overflowX: 'auto' }}>
{
arr && arr.map((item, index) => {
return (
<Tag
style = {{ height: 26, lineHeight: '26px', marginTop: 7, borderRadius: 0, cursor: 'pointer'}}
onClose={ () => {
// 当前选中的这一项删除
if (current === index) {
// 选中上一个数据,跳转页面
navigate(arr[index - 1].key)
setCurrent(current - 1)
} else {
// 未选中删除
if (index < current) { // 删除选中左边
// 索引值减一
setCurrent(current - 1)
} else {
// 让组件的状态发生改变
// console.log('current', current)
// console.log('arr', arr)
// console.log('index', index)
setNum(Math.random()) // 初始获取最新数据
}
}
// 删除数据
const deleteArr = arr
deleteArr.splice(index, 1)
setArr(deleteArr)
}}
onClick = { () => {
// console.log('test', index)
navigate(arr[index].key)
}}
closable = { index !== 0 } key = { item.key } color = { current === index ? '#108ee9': '#ccc' }>
{ item.label }
</Tag>
)
})
}
</div>
)
}
export default Com
// src/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'
import store2 from 'store2'
const isDev = process.env.NODE_ENV === 'development'
const ins = axios.create({
baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin'
})
ins.interceptors.request.use(config => {
config.headers!.token = store2.get('token') || ''
return config
}, error => Promise.reject(error))
ins.interceptors.response.use(response => {
if (response.data.code === '10119') {
store2.remove('token')
store2.remove('adminname')
window.location.href = "/login"
}
return response
}, error => Promise.reject(error))
// 自定义各种常用的restful api的请求
// axios.get('url', { params: { key: value } })
// axios.post('url', { key: value })
// axios({ url: '', method: 'GET', params: { key: value }})
// axios({ url: '', method: 'POST', data: { key: value }})
export default function request( config: AxiosRequestConfig ) {
// 接口请求 必须参数 url method data headers
const { url = '', method = 'GET', data = {}, headers = {} } = config
// 区分不同的数据请求 为了执行时传入的数据请求方式统一性 GEt GeT get GET
switch (method.toUpperCase()) {
case 'GET':
return ins.get(url, { params: data })
case 'POST':
// 可能数据请求方式 表单提交 文件提交 默认json
// 表单提交
if (headers['content-type'] === 'application/x-www-form-url-encoded') {
// 转换参数 URLSearchParams / 第三方库 qs
const p = new URLSearchParams()
for (const key in data) {
p.append(key, data[key])
}
return ins.post(url, p, { headers })
}
// 文件提交
if (headers['content-type'] === 'multipart/form-data') {
const p = new FormData()
for (const key in data) {
p.append(key, data[key])
}
return ins.post(url, p, { headers })
}
// 默认 application/json
return ins.post(url, data)
// 修改数据 - 所有的数据的更新
case 'PUT':
return ins.put(url, data)
// 删除数据
case 'DELETE':
return ins.delete(url, { data })
// 修改数据 - 部分的数据的更新
case 'PATCH':
return ins.patch(url, data)
default:
return ins(config)
}
}
按照思维来看,此时需要请求以及渲染轮播图管理相关功能,但是查看后端接口,发现基本所有的借口都需要基于 token,那么需要首先完成登录功能
接口文档:http://121.89.205.189:3000/admindoc/
https://ant-design.gitee.io/components/form-cn/#components-form-demo-normal-login
// src/api/admin.ts
import request from '@/utils/request'
export interface IAdminLoginParams {
adminname: string
password: string
}
export function loginFn (params: IAdminLoginParams) {
return request({
url: '/admin/login',
method: 'POST',
data: params
})
}
// src/views/login/Index.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
登录
)
}
export default Com
// src/App.tsx
import React, { FC } from 'react';
import { Routes, Route } from 'react-router-dom'
import Index from '@/layout/Index'
import Login from '@/views/login/Index'
import './App.css'
interface IAppProps {
}
const App: FC = (props) => {
return (
} />
} />
{/* */}
)
}
export default App
地址栏访问 http://localhost:3000/login
即可看到登录页面出现,其余路由还保持和之前一致
/* src/views/login/login.module.css */
.loginBox {
width: 100%;
height: 100%;
background-color: #2d3a4b;
display: flex;
justify-content: center;
align-items: center;
}
.loginForm {
width: 460px;
height: 350px;
/* background-color: #fff; */
}
.loginTitle {
text-align: center;
color: #fff;
font-size: 26px;
margin-bottom: 30px;
}
.myInput {
height: 47px;
background-color: #2d3a4b;
}
.myInput input {
background-color: #2d3a4b;
color: #fff;
}
.myInput input::-webkit-input-placeholder{
color:#fff;
}
.loginBtn {
height: 36px;
}
.tip {
display: flex;
color: #fff;
width: 50%;
}
.tip div {
flex: 1;
}
// src/views/login/Index.tsx
import { IAdminLoginParams } from '@/api/admin';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Form, Input } from 'antd';
import React, { FC } from 'react';
import style from './login.module.css'
interface IAppProps {
}
const Com: FC = (props) => {
const onFinish = (values: IAdminLoginParams) => {
console.log('Received values of form: ', values);
};
return (
系统登录
} placeholder="管理员账户" />
}
type="password"
placeholder="密码"
/>
账户:admin
密码:123456
)
}
export default Com
使用状态管理器,异步操作可以在组件,也可以在状态管理器
使用状态管理器(RTK)管理登录信息。
// src/store/modules/admin.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import store2 from 'store2'
interface IAppState {
loginState: boolean
adminname: string
token: string
}
const initialState: IAppState = {
loginState: Boolean(store2.get('loginState')) || false,
adminname: store2.get('adminname') || '',
token: store2.get('token') || ''
}
export const appSlice = createSlice({
name: 'admin',
initialState,
reducers: {
changeLoginState (state, action: PayloadAction<boolean>) {
state.loginState = action.payload
store2.set('loginState', action.payload)
},
changeAdminName (state, action: PayloadAction<string>) {
state.adminname = action.payload
store2.set('adminname', String(state.adminname))
},
changeToken (state, action: PayloadAction<string>) {
state.token = action.payload
store2.set('token', String(state.token))
}
}
})
export const { changeLoginState, changeAdminName, changeToken } = appSlice.actions
export default appSlice.reducer
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import app from './modules/app'
import admin from './modules/admin'
const store = configureStore({
reducer: {
app,
admin
}
})
// 导出类型注解
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>
// 推断出类型
export type AppDispatch = typeof store.dispatch
export default store
// src/views/login/Index.tsx
import { IAdminLoginParams, loginFn } from '@/api/admin';
import { useAppDispatch } from '@/store/hooks';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Form, Input, message } from 'antd';
import React, { FC } from 'react';
import { changeAdminName, changeLoginState, changeToken } from '@/store/modules/admin'
import style from './login.module.css'
import { useNavigate } from 'react-router-dom';
interface IAppProps {
}
const Com: FC = (props) => {
const dispatch = useAppDispatch()
const navigate = useNavigate()
const onFinish = (values: IAdminLoginParams) => {
console.log('Received values of form: ', values);
loginFn(values).then(res => {
if (res.data.code === '10003') {
message.warning('密码输入错误')
} else if (res.data.code === '10005') {
message.error('账户不存在')
} else {
message.success('登录成功')
console.log(res.data.data)
dispatch(changeLoginState(true))
dispatch(changeAdminName(res.data.data.adminname))
dispatch(changeToken(res.data.data.token))
navigate('/')
}
})
};
return (
系统登录
} placeholder="管理员账户" />
}
type="password"
placeholder="密码"
/>
账户:admin
密码:123456
)
}
export default Com
当前路由在登录页面,判断用户的登录状态,如果登录,则跳转到系统的首页,如果未登录,显示登录页面
当前路由在非登录页面,判断用户的登录状态,如果登录,则显示非登录页面,如果未登录,跳转到登录页面
// src/App.tsx
import React, { FC } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom'
import Index from '@/layout/Index'
import Login from '@/views/login/Index'
import './App.css'
import { useAppSelector } from './store/hooks';
interface IAppProps {
}
const App: FC = (props) => {
const loginState = useAppSelector(state => state.admin.loginState)
return (
: } />
: } />
{/* */}
)
}
export default App
封装axios时已经实现 — 响应拦截器
后台管理系统都需要请求数据,而请求数据 都需要添加token字段
// src/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'
import store2 from 'store2'
const isDev = process.env.NODE_ENV === 'development'
const ins = axios.create({
baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin'
})
ins.interceptors.request.use(config => {
config.headers!.token = store2.get('token') || ''
return config
}, error => Promise.reject(error))
ins.interceptors.response.use(response => {
if (response.data.code === '10119') {
store2.remove('token')
store2.remove('adminname')
store2.remove('loginState')
window.location.href = "/login"
}
return response
}, error => Promise.reject(error))
// 自定义各种常用的restful api的请求
// axios.get('url', { params: { key: value } })
// axios.post('url', { key: value })
// axios({ url: '', method: 'GET', params: { key: value }})
// axios({ url: '', method: 'POST', data: { key: value }})
export default function request(config: AxiosRequestConfig) {
// 接口请求 必须参数 url method data headers
const { url = '', method = 'GET', data = {}, headers = {} } = config
// 区分不同的数据请求 为了执行时传入的数据请求方式统一性 GEt GeT get GET
switch (method.toUpperCase()) {
case 'GET':
return ins.get(url, { params: data })
case 'POST':
// 可能数据请求方式 表单提交 文件提交 默认json
// 表单提交
if (headers['content-type'] === 'application/x-www-form-url-encoded') {
// 转换参数 URLSearchParams / 第三方库 qs
const p = new URLSearchParams()
for (const key in data) {
p.append(key, data[key])
}
return ins.post(url, p, { headers })
}
// 文件提交
if (headers['content-type'] === 'multipart/form-data') {
const p = new FormData()
for (const key in data) {
p.append(key, data[key])
}
return ins.post(url, p, { headers })
}
// 默认 application/json
return ins.post(url, data)
// 修改数据 - 所有的数据的更新
case 'PUT':
return ins.put(url, data)
// 删除数据
case 'DELETE':
return ins.delete(url, { data })
// 修改数据 - 部分的数据的更新
case 'PATCH':
return ins.patch(url, data)
default:
return ins(config)
}
}
https://ant-design.gitee.io/components/dropdown-cn/#components-dropdown-demo-trigger
// src/layout/components/AppHeader.tsx
import React from 'react';
import {
DownOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;
// const breadcrumbNameMap: any = {
// '/': '系统首页',
// '/banner': '轮播图管理',
// '/banner/list': '轮播图列表',
// '/banner/add': '添加轮播图',
// '/pro': '产品管理',
// '/pro/list': '产品列表',
// '/pro/search': '筛选列表',
// '/account': '账户管理',
// '/account/user': '用户列表',
// '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}
function getBreadcrumbNameMap (menus: any[]) {
menus.forEach(item => {
if (item.children) {
breadcrumbNameMap[item.path] = item.label
getBreadcrumbNameMap(item.children)
} else {
breadcrumbNameMap[item.path] = item.label
}
})
}
// console.log(breadcrumbNameMap)
getBreadcrumbNameMap(menus)
const App: React.FC = () => {
// const [collapsed, setCollapsed] = useState(false);
const collapsed = useAppSelector(state => state.app.collapsed)
const dispatch = useAppDispatch()
const {
token: { colorBgContainer },
} = theme.useToken();
const location = useLocation(); // /pro/list
const pathSnippets = location.pathname.split('/').filter((i) => i);
// console.log(pathSnippets) // ['pro', 'list']
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
// console.log(url) // /pro /pro/list
return (
{breadcrumbNameMap[url]}
);
});
const breadcrumbItems = [
系统首页
,
].concat(extraBreadcrumbItems);
const items: MenuProps['items'] = [
{
label: '个人中心',
key: '/center',
},
{
type: 'divider',
},
{
label: '退出',
key: '/logout',
},
];
const navigate = useNavigate()
const onClick: MenuProps['onClick'] = ({ key }) => {
// console.log(key)
// navigate(key)
if (key === '/logout') {
store2.remove('loginState')
store2.remove('adminname')
store2.remove('token')
dispatch(changeLoginState(false)) // 只需要修改 loginState
navigate('/login')
}
}
return (
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
// onClick: () => setCollapsed(!collapsed),
onClick: () => dispatch(changeCollapsed())
})}
{breadcrumbItems}
e.preventDefault()}>
);
};
export default App;
先获取退出登陆时 路由的地址
// src/layout/components/AppHeader.tsx
import React from 'react';
import {
DownOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;
// const breadcrumbNameMap: any = {
// '/': '系统首页',
// '/banner': '轮播图管理',
// '/banner/list': '轮播图列表',
// '/banner/add': '添加轮播图',
// '/pro': '产品管理',
// '/pro/list': '产品列表',
// '/pro/search': '筛选列表',
// '/account': '账户管理',
// '/account/user': '用户列表',
// '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}
function getBreadcrumbNameMap (menus: any[]) {
menus.forEach(item => {
if (item.children) {
breadcrumbNameMap[item.path] = item.label
getBreadcrumbNameMap(item.children)
} else {
breadcrumbNameMap[item.path] = item.label
}
})
}
// console.log(breadcrumbNameMap)
getBreadcrumbNameMap(menus)
const App: React.FC = () => {
// const [collapsed, setCollapsed] = useState(false);
const collapsed = useAppSelector(state => state.app.collapsed)
const dispatch = useAppDispatch()
const {
token: { colorBgContainer },
} = theme.useToken();
const location = useLocation(); // /pro/list
const pathSnippets = location.pathname.split('/').filter((i) => i);
// console.log(pathSnippets) // ['pro', 'list']
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
// console.log(url) // /pro /pro/list
return (
{breadcrumbNameMap[url]}
);
});
const breadcrumbItems = [
系统首页
,
].concat(extraBreadcrumbItems);
const items: MenuProps['items'] = [
{
label: '个人中心',
key: '/center',
},
{
type: 'divider',
},
{
label: '退出',
key: '/logout',
},
];
const navigate = useNavigate()
const { pathname } = useLocation()
const onClick: MenuProps['onClick'] = ({ key }) => {
// console.log(key)
// navigate(key)
if (key === '/logout') {
store2.remove('loginState')
store2.remove('adminname')
store2.remove('token')
dispatch(changeLoginState(false)) // 只需要修改 loginState
// navigate('/login')
navigate('/login?r=' + pathname)
}
}
return (
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
// onClick: () => setCollapsed(!collapsed),
onClick: () => dispatch(changeCollapsed())
})}
{breadcrumbItems}
e.preventDefault()}>
);
};
export default App;
正常考虑问题思路是,在登陆时,登录成功之后 判断有没有退出时的记录地址,然后跳转
但实际上程序运行的思路是,当你登录成功之后,已经修改了登录状态,状态的改变引起视图的二次渲染,所以真正决定跳转地址的是App.tsx组件
// src/App.tsx
import React, { FC } from 'react';
import { Routes, Route, Navigate, useSearchParams, useLocation } from 'react-router-dom'
import Index from '@/layout/Index'
import Login from '@/views/login/Index'
import './App.css'
import { useAppSelector } from './store/hooks';
interface IAppProps {
}
const App: FC = (props) => {
const loginState = useAppSelector(state => state.admin.loginState)
// 1
// const [params] = useSearchParams()
// console.log('params', params.get('r') as string)
// const url = params.get('r') as string
// 2
const location = useLocation()
console.log('location', location.search)
const url = location.search.split('?r=')[1]
return (
: } />
: } />
{/* */}
)
}
export default App
添加一个设置页面
// src/views/set/Index.tsx
import React, { FC } from 'react';
interface IAppProps {
}
const Com: FC = (props) => {
return (
设置
)
}
export default Com
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
import Set from '@/views/set/Index'
type MenuItem = Required['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: ,
element:
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: ,
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: ,
element:
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: ,
element:
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: ,
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: ,
element:
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: ,
element:
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: ,
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: ,
element:
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: ,
element:
}
]
},
{
path: '/set',
label: '设置',
key: '/set',
icon: ,
element:
},
]
export default menus
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xNgpluG4-1678063667814)(assets/image-20230215160041756.png)]
给router/menu.tsx中不需要出现的 添加 hidden
给添加轮播图以及设置选项添加 hidden 属性
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
import Set from '@/views/set/Index'
type MenuItem = Required<MenuProps>['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode;
hidden?: number
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: <HomeOutlined />,
element: <Home />
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: <HomeOutlined />,
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: <HomeOutlined />,
element: <BannerList />
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: <HomeOutlined />,
element: <BannerAdd />,
hidden: 1
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: <HomeOutlined />,
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: <HomeOutlined />,
element: <ProList />
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: <HomeOutlined />,
element: <SearchList />
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: <HomeOutlined />,
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: <HomeOutlined />,
element: <UserList />
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: <HomeOutlined />,
element: <AdminList />
}
]
},
{
path: '/set',
label: '设置',
key: '/set',
icon: <HomeOutlined />,
element: <Set />,
hidden: 1
},
]
export default menus
渲染左侧菜单栏数据时,可以过滤数据,将有hidden: 1
子菜单删除掉
// src/layout/components/AppHeader.tsx
import React from 'react';
import {
DownOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined
} from '@ant-design/icons';
import { Layout, theme, Breadcrumb, Dropdown, Space, MenuProps, Image } from 'antd';
import { useAppSelector, useAppDispatch } from '@/store/hooks'
import { changeCollapsed } from '@/store/modules/app'
import { changeLoginState } from '@/store/modules/admin'
import { useLocation, Link, useNavigate } from 'react-router-dom'
import menus from '@/router/menu'
import store2 from 'store2'
const { Header } = Layout;
// const breadcrumbNameMap: any = {
// '/': '系统首页',
// '/banner': '轮播图管理',
// '/banner/list': '轮播图列表',
// '/banner/add': '添加轮播图',
// '/pro': '产品管理',
// '/pro/list': '产品列表',
// '/pro/search': '筛选列表',
// '/account': '账户管理',
// '/account/user': '用户列表',
// '/account/admin': '管理员列表'
// }
let breadcrumbNameMap: any = {}
function getBreadcrumbNameMap (menus: any[]) {
menus.forEach(item => {
if (item.children) {
breadcrumbNameMap[item.path] = item.label
getBreadcrumbNameMap(item.children)
} else {
breadcrumbNameMap[item.path] = item.label
}
})
}
// console.log(breadcrumbNameMap)
getBreadcrumbNameMap(menus)
const App: React.FC = () => {
// const [collapsed, setCollapsed] = useState(false);
const collapsed = useAppSelector(state => state.app.collapsed)
const dispatch = useAppDispatch()
const {
token: { colorBgContainer },
} = theme.useToken();
const location = useLocation(); // /pro/list
const pathSnippets = location.pathname.split('/').filter((i) => i);
// console.log(pathSnippets) // ['pro', 'list']
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
// console.log(url) // /pro /pro/list
return (
{breadcrumbNameMap[url]}
);
});
const breadcrumbItems = [
系统首页
,
].concat(extraBreadcrumbItems);
const items: MenuProps['items'] = [
{
label: '个人中心',
key: '/center',
},
{
label: '设置',
key: '/set',
},
{
type: 'divider',
},
{
label: '退出',
key: '/logout',
},
];
const navigate = useNavigate()
const { pathname } = useLocation()
const onClick: MenuProps['onClick'] = ({ key }) => {
// console.log(key)
// navigate(key)
if (key === '/logout') {
store2.remove('loginState')
store2.remove('adminname')
store2.remove('token')
dispatch(changeLoginState(false)) // 只需要修改 loginState
// navigate('/login')
navigate('/login?r=' + pathname)
} else if (key === '/set') {
navigate(key)
}
}
return (
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
// onClick: () => setCollapsed(!collapsed),
onClick: () => dispatch(changeCollapsed())
})}
{breadcrumbItems}
e.preventDefault()}>
);
};
export default App;
隐藏子菜单使用 hidden 属性,如果使用的不是hidden属性,那么需要自行过滤数据
以下代码是过滤算法,本项目不需要
function getData (menus: IMenuProps[]) { // ++++++++++
const items:IMenuProps[] = []
menus.forEach(item => {
if (item.children) {
if (!item.hidden) {
items.push({...item}) // 只提取二级菜单项中的第一层级
}
} else {
if (!item.hidden) {
items.push({...item}) // 一级菜单提取出来
}
}
})
items.forEach(item => { // 因为上面只提取了第一层级的数据
if(item.children) {
let a = getData(item.children)
item.children = a
}
})
return items
}
// src/api/admin.ts
import request from '@/utils/request'
export interface IAdminLoginParams {
adminname: string
password: string
}
// 登录
export function loginFn (params: IAdminLoginParams) {
return request({
url: '/admin/login',
method: 'POST',
data: params,
// headers: {
// 'content-type': 'application/json'
// }
})
}
// 获取管理员列表数据
export function getAdminList () {
return request({
url: '/admin/list',
// method: 'GET'
})
}
// 获取管理员信息
export function getAdminDetail (params: { adminname: string }) {
return request({
url: '/admin/detail',
// method: 'GET',
data: params
})
}
export interface IAddAdminParams {
adminname: string
password: string
role: number
checkedKeys: any[]
}
// 添加管理员
export function addAdmin (params: IAddAdminParams) {
return request({
url: '/admin/add',
method: 'POST',
data: params
})
}
export interface IUpdateParams {
adminname: string
role: number
checkedKeys: any[]
}
// 修改管理员信息
export function updateAdmin (params: IUpdateParams) {
return request({
url: '/admin/update',
method: 'POST',
data: params
})
}
// 删除
export function deleteAdmin (params: { adminid: string }) {
return request({
url: '/admin/delete',
method: 'POST',
data: params
})
}
// src/views/account/Admin.tsx
import { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const Com: FC = (props) => {
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render () {
return (
} />
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
useEffect(() => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}, [])
return (
<>
>
)
}
export default Com
如果屏幕比较小,默认展示的都是10条数据,就容易超出固定容器大小,此时可以通过 限制表格的滚动属性解决问题
height scroll
// src/views/account/Admin.tsx
import { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const Com: FC = (props) => {
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render () {
return (
} />
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
useEffect(() => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
return (
)
}
export default Com
优化数据表格(分页器优化 - 序号-分页之后需要要连贯)
// src/views/account/Admin.tsx
import { getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const Com: FC = (props) => {
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { (current - 1) * pageSize + index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render () {
return (
} />
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
useEffect(() => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
return (
`共有 ${total} 条数据`
} }
/>
)
}
export default Com
19.5 添加中文包
由于 antd 组件的默认文案是英文,所以需要修改为中文
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6wk2s3h3-1678063667815)(assets/image-20221026163808513.png)]
https://ant-design.gitee.io/docs/react/getting-started-cn
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import { Provider } from 'react-redux'
import zhCN from 'antd/locale/zh_CN';
import { BrowserRouter } from 'react-router-dom'
import App from './App';
import reportWebVitals from './reportWebVitals';
import store from './store'
import 'antd/dist/reset.css'; // antd重置样式表
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLDivElement
);
root.render(
);
reportWebVitals()
19.6删除管理员
// src/views/account/Admin.tsx
import { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Popconfirm, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useState } from 'react';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const Com: FC = (props) => {
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { (current - 1) * pageSize + index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render (text: any, record: IAdmin) {
return (
} />
{
deleteAdmin({ adminid: record.adminid }).then(() => {
getAdminListData()
})
}}
onCancel={() => {}}
okText="删除"
cancelText="取消"
>
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
const getAdminListData = () => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}
useEffect(() => {
getAdminListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
return (
`共有 ${total} 条数据`
} }
/>
)
}
export default Com
19.7 如何批量删除管理员数据
https://ant-design.gitee.io/components/table-cn/#components-table-demo-row-selection-custom
// src/views/account/Admin.tsx
import { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Popconfirm, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const Com: FC = (props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
const flag = useMemo(() => {
return selectedRowKeys.length > 0
}, [selectedRowKeys])
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { (current - 1) * pageSize + index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render (text: any, record: IAdmin) {
return (
} />
{
deleteAdmin({ adminid: record.adminid }).then(() => {
getAdminListData()
})
}}
onCancel={() => {}}
okText="删除"
cancelText="取消"
>
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
const getAdminListData = () => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}
useEffect(() => {
getAdminListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const deleteMany = () => {
// promise.all
const arr: any = []
selectedRowKeys.forEach(item => {
arr.push(deleteAdmin({ adminid: String(item) }))
})
Promise.all(arr).then(() => {
getAdminListData()
setSelectedRowKeys([])
})
}
return (
{ flag ? : null }
`共有 ${total} 条数据`
} }
rowSelection={rowSelection}
/>
)
}
export default Com
19.9.添加管理员
19.9.1 设置添加管理员的抽屉效果(无树形控件)
// src/views/account/Admin.tsx
import { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const Com: FC = (props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
const flag = useMemo(() => {
return selectedRowKeys.length > 0
}, [selectedRowKeys])
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { (current - 1) * pageSize + index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render (text: any, record: IAdmin) {
return (
} />
{
deleteAdmin({ adminid: record.adminid }).then(() => {
getAdminListData()
})
}}
onCancel={() => {}}
okText="删除"
cancelText="取消"
>
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
const getAdminListData = () => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}
useEffect(() => {
getAdminListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const deleteMany = () => {
// promise.all
const arr: any = []
selectedRowKeys.forEach(item => {
arr.push(deleteAdmin({ adminid: String(item) }))
})
Promise.all(arr).then(() => {
getAdminListData()
setSelectedRowKeys([])
})
}
const [open, setOpen] = useState(false)
return (
{ flag ? : null }
`共有 ${total} 条数据`
} }
rowSelection={rowSelection}
/>
{ setOpen(false)} } open={open}>
)
}
export default Com
19.9.2 修改菜单数据 添加了keyid字段
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
import Set from '@/views/set/Index'
type MenuItem = Required['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode;
hidden?: number;
keyid: string
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: ,
element: ,
keyid: '0-0'
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: ,
keyid: '0-1',
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: ,
element: ,
keyid: '0-1-0'
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: ,
element: ,
hidden: 1,
keyid: '0-1-1'
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: ,
keyid: '0-2',
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: ,
element: ,
keyid: '0-2-0'
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: ,
element: ,
keyid: '0-2-1'
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: ,
keyid: '0-3',
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: ,
element: ,
keyid: '0-3-0'
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: ,
element: ,
keyid: '0-3-1'
}
]
},
{
path: '/set',
label: '设置',
key: '/set',
icon: ,
element: ,
hidden: 1,
keyid: '0-4'
},
]
export default menus
19.9.3 添加管理员时选择该管理员权限
// src/views/account/Admin.tsx
import { deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus, { IMyMenuItem } from '@/router/menu'
import type { DataNode } from 'antd/es/tree';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const getTreeData = (menus: any[] ) => {
const arr: DataNode[] = []
menus.forEach(item => {
let obj: DataNode = {
key: '',
title: ''
}
if (item.children) {
obj = {
key: item.keyid,
title: item.label,
children: getTreeData(item.children)
}
} else {
obj = {
key: item.keyid,
title: item.label
}
}
arr.push(obj)
})
return arr
}
const Com: FC = (props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
const flag = useMemo(() => {
return selectedRowKeys.length > 0
}, [selectedRowKeys])
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { (current - 1) * pageSize + index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render (text: any, record: IAdmin) {
return (
} />
{
deleteAdmin({ adminid: record.adminid }).then(() => {
getAdminListData()
})
}}
onCancel={() => {}}
okText="删除"
cancelText="取消"
>
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
const getAdminListData = () => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}
useEffect(() => {
getAdminListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const deleteMany = () => {
// promise.all
const arr: any = []
selectedRowKeys.forEach(item => {
arr.push(deleteAdmin({ adminid: String(item) }))
})
Promise.all(arr).then(() => {
getAdminListData()
setSelectedRowKeys([])
})
}
const [open, setOpen] = useState(false)
const [checkedKeys, setCheckedKeys] = useState(['0-0'])
const [adminname, setAdminname] = useState('')
const [password, setPassword] = useState('')
const [role, setRole] = useState(1)
return (
{ flag ? : null }
`共有 ${total} 条数据`
} }
rowSelection={rowSelection}
/>
{ setOpen(false)} } open={open}>
setAdminname(event.target.value)} placeholder="管理员账户" />
setPassword(event.target.value)} placeholder="密码" />
)
}
export default Com
19.9.4 添加管理员
添加完毕一定要记得重置(表单,权限)
// src/views/account/Admin.tsx
import { addAdmin, deleteAdmin, getAdminList } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const getTreeData = (menus: any[] ) => {
const arr: DataNode[] = []
menus.forEach(item => {
let obj: DataNode = {
key: '',
title: ''
}
if (item.children) {
obj = {
key: item.keyid,
title: item.label,
children: getTreeData(item.children)
}
} else {
obj = {
key: item.keyid,
title: item.label
}
}
arr.push(obj)
})
return arr
}
const Com: FC = (props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
const flag = useMemo(() => {
return selectedRowKeys.length > 0
}, [selectedRowKeys])
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { (current - 1) * pageSize + index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render (text: any, record: IAdmin) {
return (
} />
{
deleteAdmin({ adminid: record.adminid }).then(() => {
getAdminListData()
})
}}
onCancel={() => {}}
okText="删除"
cancelText="取消"
>
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
const getAdminListData = () => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}
useEffect(() => {
getAdminListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const deleteMany = () => {
// promise.all
const arr: any = []
selectedRowKeys.forEach(item => {
arr.push(deleteAdmin({ adminid: String(item) }))
})
Promise.all(arr).then(() => {
getAdminListData()
setSelectedRowKeys([])
})
}
const [open, setOpen] = useState(false)
const [checkedKeys, setCheckedKeys] = useState(['0-0'])
const [adminname, setAdminname] = useState('')
const [password, setPassword] = useState('')
const [role, setRole] = useState(1)
return (
{ flag ? : null }
`共有 ${total} 条数据`
} }
rowSelection={rowSelection}
/>
{
setAdminname('')
setPassword('')
setRole(1)
setCheckedKeys(['0-0'])
setOpen(false)
} } open={open}>
setAdminname(event.target.value)} placeholder="管理员账户" />
setPassword(event.target.value)} placeholder="密码" />
)
}
export default Com
19.10管理员修改
修改不重新生成新的页面,还在这个页面,使用模态框实现
// src/views/account/Admin.tsx
import { addAdmin, deleteAdmin, getAdminList, updateAdmin } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, Modal, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const getTreeData = (menus: any[] ) => {
const arr: DataNode[] = []
menus.forEach(item => {
let obj: DataNode = {
key: '',
title: ''
}
if (item.children) {
obj = {
key: item.keyid,
title: item.label,
children: getTreeData(item.children)
}
} else {
obj = {
key: item.keyid,
title: item.label
}
}
arr.push(obj)
})
return arr
}
const Com: FC = (props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
const flag = useMemo(() => {
return selectedRowKeys.length > 0
}, [selectedRowKeys])
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { (current - 1) * pageSize + index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render (text: any, record: IAdmin) {
return (
)
}
}
]
const [adminList, setAdminList] = useState([])
const getAdminListData = () => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}
useEffect(() => {
getAdminListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const deleteMany = () => {
// promise.all
const arr: any = []
selectedRowKeys.forEach(item => {
arr.push(deleteAdmin({ adminid: String(item) }))
})
Promise.all(arr).then(() => {
getAdminListData()
setSelectedRowKeys([])
})
}
const [open, setOpen] = useState(false)
const [checkedKeys, setCheckedKeys] = useState(['0-0'])
const [adminname, setAdminname] = useState('')
const [password, setPassword] = useState('')
const [role, setRole] = useState(1)
const [isModalOpen, setIsModalOpen] = useState(false)
return (
setOpen(true) }>添加管理员
{ flag ? 批量删除 : null }
`共有 ${total} 条数据`
} }
rowSelection={rowSelection}
/>
{
setAdminname('')
setPassword('')
setRole(1)
setCheckedKeys(['0-0'])
setOpen(false)
} } open={open}>
setAdminname(event.target.value)} placeholder="管理员账户" />
setPassword(event.target.value)} placeholder="密码" />
{
setIsModalOpen(false)
setAdminname('')
setRole(1)
setCheckedKeys(['0-0'])
}}>
setAdminname(event.target.value)} placeholder="管理员账户" />
)
}
export default Com
20 系统首页数据统计
// src/api/home.ts
import request from '@/utils/request'
export function getUserTotalNum () {
return request({
url: '/statistic/user'
})
}
export function getShopTotalNum () {
return request({
url: '/statistic/product'
})
}
// src/views/home/Index.tsx
import { getShopTotalNum, getUserTotalNum } from '@/api/home';
import { Col, Row, Statistic } from 'antd';
import React, { FC, useEffect, useState } from 'react';
import CountUp from 'react-countup';
interface IAppProps {
}
const formatter: any = (value: number) => ;
const Com: FC = (props) => {
const [usersLen, setUsersLen] = useState(0)
const [prosLen, setProsLen] = useState(0)
useEffect(() => {
getUserTotalNum().then(res => setUsersLen(res.data.data))
getShopTotalNum().then(res => setProsLen(res.data.data))
}, [])
return (
)
}
export default Com
21 左侧菜单栏的权限
21.1 思路
- 当用户登录的时候,可以获取到该用户的
checkedKeys
数据
- 使用这个数据从
router/menu.tsx
中提取匹配的数据,
- 生成左侧菜单栏组件(目前是直接渲染
router/menu.tsx
)
21.2 算法过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-agoi8npW-1678063667816)(assets/image-20221027151803006.png)]
从一个数组['0-0', '0-1-0', '0-2-0-0', '0-2-0-1', '0-3-2']
触发,筛选 router/menu.tsx
,获取到满足条件的数据
21.3 算法实现
算法1:
- 从[‘0-0’, ‘0-1-0’, ‘0-2-0-0’, ‘0-2-0-1’, ‘0-3-2’] 到
- [‘0-0’, ‘0-1’, ‘0-1-0’, ‘0-2’, ‘0-2-0’, ‘0-2-0-0’, ‘0-2-0-1’, ‘0-3’, ‘0-3-2’]
// ['0-0', '0-1-0', '0-2-0-0', '0-2-0-1', '0-3-2']
// ['0-0', '0-1', '0-1-0', '0-2', '0-2-0', '0-2-0-0', '0-2-0-1', '0-3', '0-3-2']
let arr = ['0-0','0-1-0','0-2-0-0','0-2-0-1','0-3-2', '0-4-0-0-2', '0-5-1-2-0-1']
// 0-0
// 0-1 0-1-0
// 0-2 0-2-0 0-2-0-0
// 0-2 0-2-0 0-2-0-1
// 0-3 0-3-2
// 0-4 0-4-0 0-4-0-0 0-4-0-0-2
// 0-5 0-5-1 0-5-1-2 0-5-1-2-0 0-5-1-2-0-1
// let brr = []
// for(let i = 0; i < arr.length; i++){
// for(let j = 0; j < arr[i].length; j += 2){
// brr.push(arr[i].substring(0, j + 3))
// }
// }
// console.log(new Set(brr));
let brr = new Set()
for(let i = 0; i < arr.length; i++){
for(let j = 0; j < arr[i].length; j += 2){
brr.add(arr[i].substring(0, j + 3))
}
}
console.log(brr);
算法2:
[‘0-0’, ‘0-1’, ‘0-1-0’, ‘0-2’, ‘0-2-0’, ‘0-2-0-0’, ‘0-2-0-1’, ‘0-3’, ‘0-3-2’] 提取数据
import { IMyMenuItem } from "./menu"
// src/router/utils.tsx
export function getCheckedKeysArr (arr: string[]) {
const brr: Set<string> = new Set()
for(let i = 0; i < arr.length; i++){
for(let j = 0; j < arr[i].length; j += 2){
brr.add(arr[i].substring(0, j + 3))
}
}
return [...brr] // 修改tsconfig.json中 "target": "es6",
}
// menus 原始数据
// checkedKeys转换后的数据 ['0-0', '0-1', '0-1-0', '0-2', '0-2-0', '0-2-0-0', '0-2-0-1', '0-3', '0-3-2']
export function getPermissionMenu (menus: IMyMenuItem[], checkedKeys: string[]) {
let arr: IMyMenuItem[] = []
// 处理第一级数据
checkedKeys.forEach(value => {
menus.forEach(item => {
if (item.keyid === value) { // 这项数据又
arr.push({...item})
}
})
})
// 处理子数据
arr.forEach(item => {
if (item.children) {
let newArr = getPermissionMenu(item.children, checkedKeys)
item.children = newArr
}
})
return arr
}
此时提示 ts配置中target 需要更改为 ‘es2015’
// tsconfig.json
{
"compilerOptions": {
"target": "es2015",
...
}
}
21.4 生成动态的左侧菜单项
状态管理器拿用户名,使用接口获取 权限 数据,整合权限数据,提取菜单数据
admin账户 渲染 原始的 menus 数据
// src/layout/components/SideBar.tsx
import React, { useEffect, useState } from 'react';
import { Layout, Menu, Image } from 'antd';
import type { MenuProps } from 'antd';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks'
import logo from '@/logo.svg'
import { useLocation, useNavigate } from 'react-router-dom';
import { getAdminDetail } from '@/api/admin';
import { getCheckedKeysArr, getPermissionMenu } from '@/router/utils';
const { Sider } = Layout;
// 获取哪些项具有二级菜单
const rootSubmenuKeys: string[] = []
menus.forEach(item => {
if (item.children) {
rootSubmenuKeys.push(item.key as string)
}
})
const App: React.FC = () => {
const collapsed = useAppSelector(state => state.app.collapsed)
// /pro/search
const { pathname } = useLocation() // /pro/search
// console.log(location)
const [selectedKeys, setSelectedKeys] = useState([ pathname ]) // ['/pro/search']
const [openKeys, setOpenKeys] = useState(['/' + pathname.split('/')[1] ]); // ['/pro']
const onOpenChange: MenuProps['onOpenChange'] = (keys) => {
// console.log('keys', keys)
const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1);
// console.log('latestOpenKey', latestOpenKey) // /banner /pro /account
if (rootSubmenuKeys.indexOf(latestOpenKey!) === -1) {
setOpenKeys(keys);
} else {
setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
}
};
const navigate = useNavigate()
const changeUrl = ({ key }: { key: string }) => {
// console.log(key)
navigate(key)
setSelectedKeys([key]) // 点击时需要告诉程序哪一项被选中
}
useEffect(() => {
setSelectedKeys([pathname])
setOpenKeys(['/' + pathname.split('/')[1] ])
}, [pathname])
const adminname = useAppSelector(state => state.admin.adminname)
const [showMenu, setShowMenu] = useState([])
useEffect(() => {
getAdminDetail({ adminname }).then(res => {
// console.log(res.data.data)
const oldCheckedKeys = res.data.data[0].checkedKeys
const checkedKeysArr = getCheckedKeysArr(oldCheckedKeys)
const newMenus = adminname === 'admin' ? menus : getPermissionMenu(menus, checkedKeysArr)
setShowMenu(newMenus)
})
}, [adminname])
return (
{ !collapsed && 嗨购后台管理系统 }
);
};
export default App;
有的公司在登录之后,会直接返回类似router/menus.tsx
的数据
22、页面权限
也称之为路由权限
一个有权限访问页面A的人,把整个链接地址发给了没有权限访问的另外一个人
根据数据库中存储的字段,提取当前用户需要的 menus 的数据
如果用户访问的当前路由在 总路由中但是不在当前用户的路由中,显示无权限页面,否则显示404页面
核心思想:
-
当前路由在不在权限路由 - 生成路由时使用 当前权限路由 (getPermissionMenu)
-
当前的路由在不在所有的路由
import { IMyMenuItem } from "./menu"
// src/router/utils.tsx
export function getCheckedKeysArr (arr: string[]) {
// console.log('arr', arr)
const brr: Set<string> = new Set()
for(let i = 0; i < arr.length; i++){
for(let j = 0; j < arr[i].length; j += 2){
brr.add(arr[i].substring(0, j + 3))
}
}
return [...brr] // 修改tsconfig.json中 "target": "es6",
}
// menus 原始数据
// checkedKeys转换后的数据 ['0-0', '0-1', '0-1-0', '0-2', '0-2-0', '0-2-0-0', '0-2-0-1', '0-3', '0-3-2']
export function getPermissionMenu (menus: IMyMenuItem[], checkedKeys: string[]) {
let arr: IMyMenuItem[] = []
// 处理第一级数据
checkedKeys.forEach(value => {
menus.forEach(item => {
if (item.keyid === value) { // 这项数据又
arr.push({...item})
}
})
})
// 处理子数据
arr.forEach(item => {
if (item.children) {
let newArr = getPermissionMenu(item.children, checkedKeys)
item.children = newArr
}
})
return arr
}
// 判断当前请求的地址是不是在路由系统中
export function isContainMenus (menus: IMyMenuItem[], pathname: string) { // ++++++++++
let bool = menus.some(item => {
if (item.children) {
if (item.key === pathname) {
return true
} else {
return item.children.some(it => it!.key === pathname)
}
} else {
return item.key === pathname
}
})
return bool
}
// src/layout/components/AppMain.tsx
import React, { useEffect, useState } from 'react';
import { Layout, theme } from 'antd';
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
// import BannerAdd from '@/views/banner/Add'
import Page404 from '@/views/error/Page404'
import { IMyMenuItem } from '@/router/menu';
import menus from '@/router/menu'
import { useAppSelector } from '@/store/hooks';
import { getAdminDetail } from '@/api/admin';
import { getCheckedKeysArr, getPermissionMenu, isContainMenus } from '@/router/utils';
const { Content } = Layout;
const App: React.FC = () => {
const {
token: { colorBgContainer },
} = theme.useToken();
const adminname = useAppSelector(state => state.admin.adminname)
const [showMenu, setShowMenu] = useState([])
useEffect(() => {
getAdminDetail({ adminname }).then(res => {
// console.log(res.data.data)
const oldCheckedKeys = res.data.data[0].checkedKeys
const checkedKeysArr = getCheckedKeysArr(oldCheckedKeys)
const newMenus = adminname === 'admin' ? menus : getPermissionMenu(menus, checkedKeysArr)
setShowMenu(newMenus)
})
}, [adminname])
const renderRoute: any = (menus: IMyMenuItem[]) => {
return menus.map(item => {
if (item.children) {
// React.Fragment 也为空标签,可以设置 key 属性
// 实现 重定向
return (
} />
{
renderRoute(item.children!)
}
)
} else {
return
}
})
}
const { pathname } = useLocation()
return (
{/* } /> */}
{/* } /> */}
{/* { renderRoute(menus) } */}
{ renderRoute(showMenu) }
无权限 : } />
);
};
export default App;
23、按钮权限
超级管理员才可以批量删除
// src/views/account/Admin.tsx
import { addAdmin, deleteAdmin, getAdminDetail, getAdminList, updateAdmin } from '@/api/admin';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { Button, Drawer, Input, message, Modal, Popconfirm, Select, Space, Table, Tag, Tree } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import menus from '@/router/menu'
import type { DataNode } from 'antd/es/tree';
import { useAppSelector } from '@/store/hooks';
interface IAppProps {
}
interface IAdmin {
adminid: string
adminname: string
password: string
role: number
checkedKeys: any[]
}
const getTreeData = (menus: any[] ) => {
const arr: DataNode[] = []
menus.forEach(item => {
let obj: DataNode = {
key: '',
title: ''
}
if (item.children) {
obj = {
key: item.keyid,
title: item.label,
children: getTreeData(item.children)
}
} else {
obj = {
key: item.keyid,
title: item.label
}
}
arr.push(obj)
})
return arr
}
const Com: FC = (props) => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys changed: ', newSelectedRowKeys);
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
const flag = useMemo(() => {
return selectedRowKeys.length > 0
}, [selectedRowKeys])
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const columns = [
{
title: '序号',
render (text: any, record: IAdmin, index: number) {
return <> { (current - 1) * pageSize + index + 1 }>
}
},
{
title: '管理员账户',
dataIndex: 'adminname'
},
{
title: '管理员角色',
dataIndex: 'role',
render (text: number) { // 自定义列信息
return (
<>
{
text === 2 ? 超级管理员 :
普通管理员
}
>
)
}
},
{
title: '操作',
render (text: any, record: IAdmin) {
return (
{
setIsModalOpen(true)
setAdminname(record.adminname)
setRole(record.role)
setCheckedKeys(record.checkedKeys)
}} shape="circle" icon={ } />
{
deleteAdmin({ adminid: record.adminid }).then(() => {
getAdminListData()
})
}}
onCancel={() => {}}
okText="删除"
cancelText="取消"
>
} />
)
}
}
]
const [adminList, setAdminList] = useState([])
const getAdminListData = () => {
getAdminList().then(res => {
console.log(res.data)
setAdminList(res.data.data)
})
}
useEffect(() => {
getAdminListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const deleteMany = () => {
if (deleteRole < 2) {
message.error('暂无权限');
} else {
// promise.all
const arr: any = []
selectedRowKeys.forEach(item => {
arr.push(deleteAdmin({ adminid: String(item) }))
})
Promise.all(arr).then(() => {
getAdminListData()
setSelectedRowKeys([])
})
}
}
const [open, setOpen] = useState(false)
const [checkedKeys, setCheckedKeys] = useState(['0-0'])
const [adminname, setAdminname] = useState('')
const [password, setPassword] = useState('')
const [role, setRole] = useState(1)
const [isModalOpen, setIsModalOpen] = useState(false)
const name = useAppSelector(state => state.admin.adminname)
const [deleteRole, setDeleteRole] = useState(1)
useEffect(() => {
getAdminDetail({ adminname: name }).then(res => {
setDeleteRole(res.data.data[0].role)
})
})
return (
setOpen(true) }>添加管理员
{ flag ? 批量删除 : null }
`共有 ${total} 条数据`
} }
rowSelection={rowSelection}
/>
{
setAdminname('')
setPassword('')
setRole(1)
setCheckedKeys(['0-0'])
setOpen(false)
} } open={open}>
setAdminname(event.target.value)} placeholder="管理员账户" />
setPassword(event.target.value)} placeholder="密码" />
{
setIsModalOpen(false)
setAdminname('')
setRole(1)
setCheckedKeys(['0-0'])
}}>
setAdminname(event.target.value)} placeholder="管理员账户" />
)
}
export default Com
24、轮播图管理
24.1 封装接口
// src/api/banner.ts
import request from '@/utils/request'
export interface IAddBannerParams {
img: string
alt: string
link: string
}
export function addBanner (params: IAddBannerParams) {
return request({
url: '/banner/add',
method: 'POST',
data: params
})
}
export function getBannerList () {
return request({
url: '/banner/list'
})
}
export function deleteBanner (params: { bannerid: string }) {
return request({
url: '/banner/delete',
data: params
})
}
24.2 轮播图页面渲染
// src/views/banner/List.tsx
import { deleteBanner, getBannerList } from '@/api/banner';
import { DeleteOutlined } from '@ant-design/icons';
import { Button, Image, Popconfirm, Table } from 'antd';
import React, { FC, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface IAppProps {
}
const { Column } = Table;
const Com: FC = (props) => {
const [bannerList, setBannerList] = useState([])
const getBannerListData = () => {
getBannerList().then(res => setBannerList(res.data.data))
}
useEffect(() => {
getBannerListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const navigate = useNavigate()
return (
navigate('/banner/add') }>添加轮播图
`共有 ${total} 条数据`
} }
>
{
return { (current - 1) * pageSize + index + 1 }
}} />
{
return
}} />
{
return {
deleteBanner({ bannerid: record.bannerid }).then(() => {
getBannerListData()
})
}}
onCancel={() => {}}
okText="删除"
cancelText="取消"
>
} />
}} />
)
}
export default Com
23.3 添加轮播图
// src/views/banner/Add.tsx
import { addBanner } from '@/api/banner';
import { Input, Space, Image, Button } from 'antd';
import React, { FC, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
interface IAppProps {
}
const Com: FC = (props) => {
const navigate = useNavigate()
const [link, setLink] = useState('')
const [alt, setAlt] = useState('')
const [img, setImg] = useState('')
const file = useRef()
const onChange = () => {
const myFile = file.current.input.files[0]
console.log(myFile)
const reader = new FileReader()
reader.readAsDataURL(myFile) // base64地址
reader.onload = function () {
setImg(this.result)
}
}
const flag = useMemo(() => {
return alt === '' || link === '' || img === ''
}, [alt, img, link])
const onAdd = () => {
addBanner({
alt, link, img
}).then(() => {
navigate(-1)
})
}
return (
setLink(e.target.value)}/>
setAlt(e.target.value)}/>
setImg(e.target.value)}/>
添加
)
}
export default Com
25.产品管理
25.1 封装接口
// src/api/pro.ts
import request from '@/utils/request'
export function getProList () {
return request({
url: '/pro/list'
})
}
export function getSearchList (params?: { category?: string, search?: string}) {
return request({
url: '/pro/searchPro',
method: 'POST',
data: params
})
}
export function getCategoryList () {
return request({
url: '/pro/getCategory'
})
}
25.2 产品列表
// src/views/pro/List.tsx
import { getProList } from '@/api/pro';
import { DeleteOutlined } from '@ant-design/icons';
import { Table, Image, Popconfirm, Button } from 'antd';
import React, { FC, useEffect, useState } from 'react';
interface IAppProps {
}
const { Column } = Table;
const Com: FC = (props) => {
const [proList, setProList] = useState([])
const getProListData = () => {
getProList().then(res => setProList(res.data.data))
}
useEffect(() => {
getProListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
return (
`共有 ${total} 条数据`
} }
>
{
return { (current - 1) * pageSize + index + 1 }
}} />
{
return
}} />
{
return } />
}} />
)
}
export default Com
25.3 筛选列表
// src/views/pro/Search.tsx
import { getCategoryList, getProList, getSearchList } from '@/api/pro';
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { Table, Image, Button, Space, Select, Input } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
interface IAppProps {
}
const { Column } = Table;
const Com: FC = (props) => {
const [proList, setProList] = useState([])
const getProListData = () => {
getProList().then(res => setProList(res.data.data))
}
useEffect(() => {
getProListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const [categoryList, setCategoryList] = useState([])
const [category, setCategory] = useState('')
const [search, setSearch] = useState('')
// const arr = [{ value: '', label: '全部' }]
useEffect(() => {
getCategoryList().then(res => {
console.log(res.data.data)
setCategoryList(res.data.data)
})
}, [])
const arr = useMemo(() => {
const brr: any = [{ value: '', label: '全部' }]
categoryList.forEach((item: any) => {
brr.push({
value: item,
label: item
})
})
return brr
}, [categoryList])
return (
`共有 ${total} 条数据`
} }
>
{
return { (current - 1) * pageSize + index + 1 }
}} />
{
return
}} />
{
return } />
}} />
)
}
export default Com
26.数据可视化
方案:
echarts: https://echarts.apache.org/zh/index.html
ts中使用 echarts : https://echarts.apache.org/handbook/zh/basics/import#%E5%9C%A8-typescript-%E4%B8%AD%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5
highCharts:https://www.hcharts.cn/
Antv: https://antv.gitee.io/zh/
g2:https://antv-g2.gitee.io/zh/
g2plot:https://g2plot.antv.vision/zh/
react中使用g2:https://charts.ant.design/zh-CN
D3:视频地址:链接: https://pan.baidu.com/s/1SVS36TjtcR27Rqj_HURDZA 密码: p9ur
1.echarts
添加页面以及配置路由
// src/views/data/Echarts.tsx
const Com = () => {
return (
<>
echarts
>
)
}
export default Com
// src/views/data/HighCharts.tsx
const Com = () => {
return (
<>
HighCharts
>
)
}
export default Com
// src/views/data/Antv.tsx
const Com = () => {
return (
<>
Antv
>
)
}
export default Com
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
import Set from '@/views/set/Index'
import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'
type MenuItem = Required<MenuProps>['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode;
hidden?: number;
keyid: string
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: <HomeOutlined />,
element: <Home />,
keyid: '0-0'
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: <HomeOutlined />,
keyid: '0-1',
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: <HomeOutlined />,
element: <BannerList />,
keyid: '0-1-0'
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: <HomeOutlined />,
element: <BannerAdd />,
hidden: 1,
keyid: '0-1-1'
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: <HomeOutlined />,
keyid: '0-2',
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: <HomeOutlined />,
element: <ProList />,
keyid: '0-2-0'
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: <HomeOutlined />,
element: <SearchList />,
keyid: '0-2-1'
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: <HomeOutlined />,
keyid: '0-3',
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: <HomeOutlined />,
element: <UserList />,
keyid: '0-3-0'
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: <HomeOutlined />,
element: <AdminList />,
keyid: '0-3-1'
}
]
},
{
path: '/set',
label: '设置',
key: '/set',
icon: <HomeOutlined />,
element: <Set />,
hidden: 1,
keyid: '0-4'
},
{
path: '/data',
label: '数据可视化',
key: '/data',
redirect: '/data/echarts',
icon: <HomeOutlined />,
keyid: '0-5',
children: [
{
path: '/data/echarts',
key: '/data/echarts',
label: 'echarts',
icon: <HomeOutlined />,
element: <Echarts />,
keyid: '0-5-0'
},
{
path: '/data/HighCharts',
key: '/data/HighCharts',
label: 'HighCharts',
icon: <HomeOutlined />,
element: <HighCharts />,
keyid: '0-5-1'
},
{
path: '/data/antv',
key: '/data/antv',
label: 'antv',
icon: <HomeOutlined />,
element: <Antv />,
keyid: '0-5-2'
}
]
},
]
export default menus
cnpm install echarts --save
// src/api/data.ts
import request from './../utils/request'
export function getData () {
return request({
url: '/data/simpleData'
})
}
处理数据
自适应
// src/views/data/Echarts.tsx
import { getServerData } from "@/api/data";
import { Button, Col, Row } from "antd"
import * as echarts from 'echarts';
import { useEffect } from "react";
const Com = () => {
useEffect(() => {
var BarChart = echarts.init(document.getElementById('barCharts') as HTMLElement);
BarChart.setOption({
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
})
}, [])
useEffect(() => {
var BarChart = echarts.init(document.getElementById('lineCharts') as HTMLElement);
BarChart.setOption({
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'line',
data: [5, 20, 36, 10, 10, 20]
}
]
})
}, [])
useEffect(() => {
var BarChart = echarts.init(document.getElementById('randomCharts') as HTMLElement);
BarChart.setOption({
title: {
text: 'Stacked Line'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
name: 'Email',
type: 'line',
stack: 'Total',
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: 'Union Ads',
type: 'line',
stack: 'Total',
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: 'Video Ads',
type: 'line',
stack: 'Total',
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: 'Direct',
type: 'line',
stack: 'Total',
data: [320, 332, 301, 334, 390, 330, 320]
},
{
name: 'Search Engine',
type: 'line',
stack: 'Total',
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
})
}, [])
useEffect(() => {
getServerData().then(res => {
const arr: any =[]
const brr: any = []
res.data.data.forEach((item: { x: any; val: any; }) => {
arr.push(item.x)
brr.push(item.val)
})
var BarChart = echarts.init(document.getElementById('serverCharts') as HTMLElement);
BarChart.setOption({
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: arr
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: brr
}
]
})
})
}, [])
return (
<>
echarts
柱状图
折线图
随意图形
服务器数据
>
)
}
export default Com
2.Highcharts
vue: https://www.highcharts.com.cn/docs/highcharts-vue
react: https://www.highcharts.com.cn/docs/highcharts-react
cnpm install highcharts highcharts-react-official -S
// src/views/data/HighCharts.tsx
import React, { FC, useEffect, useState } from 'react';
import * as Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
console.log(HighchartsReact)
interface IHighChartsProps {
};
const HighCharts:FC = (props: HighchartsReact.Props) => {
const [option, setOption] = useState({
title: {
text: '2010 ~ 2016 年太阳能行业就业人员发展情况'
},
subtitle: {
text: '数据来源:thesolarfoundation.com'
},
yAxis: {
title: {
text: '就业人数'
}
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'middle'
},
plotOptions: {
series: {
label: {
connectorAllowed: false
},
pointStart: 2010
}
},
series: [{
name: '安装,实施人员',
data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175]
}, {
name: '工人',
data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434]
}, {
name: '销售',
data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387]
}, {
name: '项目开发',
data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227]
}, {
name: '其他',
data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111]
}],
responsive: {
rules: [{
condition: {
maxWidth: 500
},
chartOptions: {
legend: {
layout: 'horizontal',
align: 'center',
verticalAlign: 'bottom'
}
}
}]
}
})
useEffect(() => {
window.addEventListener('resize',() => {
setOption({
title: {
text: '2010 ~ 2016 年太阳能行业就业人员发展情况'
},
subtitle: {
text: '数据来源:thesolarfoundation.com'
},
yAxis: {
title: {
text: '就业人数'
}
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'middle'
},
plotOptions: {
series: {
label: {
connectorAllowed: false
},
pointStart: 2010
}
},
series: [{
name: '安装,实施人员',
data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175]
}, {
name: '工人',
data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434]
}, {
name: '销售',
data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387]
}, {
name: '项目开发',
data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227]
}, {
name: '其他',
data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111]
}],
responsive: {
rules: [{
condition: {
maxWidth: 500
},
chartOptions: {
legend: {
layout: 'horizontal',
align: 'center',
verticalAlign: 'bottom'
}
}
}]
}
})
})
}, [])
return (
<>
HighCharts
>
)
};
export default HighCharts;
3.antv - g2
https://antv-g2.gitee.io/zh
cnpm i @antv/g2 @antv/data-set -S
// src/views/data/Antv.tsx
import React, { FC, useEffect } from 'react';
import { Chart } from '@antv/g2';
interface IAntvProps {
};
const Antv: FC = () => {
useEffect(() => {
const data = [
{ year: '1951 年', sales: 38 },
{ year: '1952 年', sales: 52 },
{ year: '1956 年', sales: 61 },
{ year: '1957 年', sales: 145 },
{ year: '1958 年', sales: 48 },
{ year: '1959 年', sales: 38 },
{ year: '1960 年', sales: 38 },
{ year: '1962 年', sales: 38 },
];
const chart = new Chart({
container: 'antv',
autoFit: true,
height: 500,
});
chart.data(data);
chart.scale('sales', {
nice: true,
});
chart.tooltip({
showMarkers: false
});
chart.interaction('active-region');
chart.interval().position('year*sales');
chart.render();
}, [])
return (
<>
Antv
>
)
};
export default Antv;
27.编辑器
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
import Set from '@/views/set/Index'
import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'
import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'
type MenuItem = Required<MenuProps>['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode;
hidden?: number;
keyid: string
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: <HomeOutlined />,
element: <Home />,
keyid: '0-0'
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: <HomeOutlined />,
keyid: '0-1',
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: <HomeOutlined />,
element: <BannerList />,
keyid: '0-1-0'
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: <HomeOutlined />,
element: <BannerAdd />,
hidden: 1,
keyid: '0-1-1'
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: <HomeOutlined />,
keyid: '0-2',
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: <HomeOutlined />,
element: <ProList />,
keyid: '0-2-0'
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: <HomeOutlined />,
element: <SearchList />,
keyid: '0-2-1'
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: <HomeOutlined />,
keyid: '0-3',
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: <HomeOutlined />,
element: <UserList />,
keyid: '0-3-0'
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: <HomeOutlined />,
element: <AdminList />,
keyid: '0-3-1'
}
]
},
{
path: '/set',
label: '设置',
key: '/set',
icon: <HomeOutlined />,
element: <Set />,
hidden: 1,
keyid: '0-4'
},
{
path: '/data',
label: '数据可视化',
key: '/data',
redirect: '/data/echarts',
icon: <HomeOutlined />,
keyid: '0-5',
children: [
{
path: '/data/echarts',
key: '/data/echarts',
label: 'echarts',
icon: <HomeOutlined />,
element: <Echarts />,
keyid: '0-5-0'
},
{
path: '/data/HighCharts',
key: '/data/HighCharts',
label: 'HighCharts',
icon: <HomeOutlined />,
element: <HighCharts />,
keyid: '0-5-1'
},
{
path: '/data/antv',
key: '/data/antv',
label: 'antv',
icon: <HomeOutlined />,
element: <Antv />,
keyid: '0-5-2'
}
]
},
{
path: '/braft',
label: '父文本编辑器',
key: '/braft',
icon: <HomeOutlined />,
element: <Braft />,
keyid: '0-6'
},
{
path: '/md',
label: 'markDown编辑器',
key: '/md',
icon: <HomeOutlined />,
element: <Md />,
keyid: '0-7'
},
]
export default menus
1.富文本编辑器
react版本: https://braft.margox.cn/demos/basic
cnpm i braft-editor -S
// src/views/edit/Braft.tsx
import React, { useState } from 'react';
import 'braft-editor/dist/index.css'
import BraftEditor from 'braft-editor'
type Props = {}
const Com = (props: Props) => {
const [editorState, setEditorState] = useState('')
const [html, setHtml] = useState('')
const handleChange = (editorState: any) => {
console.log(editorState.toHTML())
setEditorState(editorState)
setHtml(editorState.toHTML())
}
return (
<>
>
);
}
export default Com
2.markDown编辑器
阅读器:https://www.npmjs.com/package/react-markdown
编辑器:https://www.npmjs.com/package/react-markdown-editor-lite
cnpm i react-markdown react-markdown-editor-lite -S
// src/views/edit/Md.tsx
import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown' // 阅读器
import MdEditor from 'react-markdown-editor-lite'; // 编辑器
// import style manually
import 'react-markdown-editor-lite/lib/index.css'; // 样式
type Props = {}
const Com = (props: Props) => {
const [content, setContent] = useState('')
return (
<>
Markdown展示
{
return { text }
}} onChange={ ( { html, text }: { html: any; text: any}) => {
setContent(html)
}} />
>
);
}
export default Com
28.导入以及导出
// src/views/excel/Import.tsx
import React from 'react';
type ComProps = {}
const Com = (props: ComProps) => (
<>
导入
>
);
export default Com
// src/views/excel/Export.tsx
import React from 'react';
type ComProps = {}
const Com = (props: ComProps) => (
<>
导出
>
);
export default Co
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
import Set from '@/views/set/Index'
import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'
import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'
import Import from '@/views/excel/Import'
import Export from '@/views/excel/Export'
type MenuItem = Required['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode;
hidden?: number;
keyid: string
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: ,
element: ,
keyid: '0-0'
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: ,
keyid: '0-1',
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: ,
element: ,
keyid: '0-1-0'
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: ,
element: ,
hidden: 1,
keyid: '0-1-1'
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: ,
keyid: '0-2',
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: ,
element: ,
keyid: '0-2-0'
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: ,
element: ,
keyid: '0-2-1'
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: ,
keyid: '0-3',
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: ,
element: ,
keyid: '0-3-0'
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: ,
element: ,
keyid: '0-3-1'
}
]
},
{
path: '/set',
label: '设置',
key: '/set',
icon: ,
element: ,
hidden: 1,
keyid: '0-4'
},
{
path: '/data',
label: '数据可视化',
key: '/data',
redirect: '/data/echarts',
icon: ,
keyid: '0-5',
children: [
{
path: '/data/echarts',
key: '/data/echarts',
label: 'echarts',
icon: ,
element: ,
keyid: '0-5-0'
},
{
path: '/data/HighCharts',
key: '/data/HighCharts',
label: 'HighCharts',
icon: ,
element: ,
keyid: '0-5-1'
},
{
path: '/data/antv',
key: '/data/antv',
label: 'antv',
icon: ,
element: ,
keyid: '0-5-2'
}
]
},
{
path: '/braft',
label: '父文本编辑器',
key: '/braft',
icon: ,
element: ,
keyid: '0-6'
},
{
path: '/md',
label: 'markDown编辑器',
key: '/md',
icon: ,
element: ,
keyid: '0-7'
},
{
path: '/excel',
label: '导入以及导出',
key: '/excel',
redirect: '/excel/export',
icon: ,
keyid: '0-8',
children: [
{
path: '/excel/import',
key: '/excel/import',
label: '导入',
icon: ,
element: ,
keyid: '0-8-0'
},
{
path: '/excel/export',
key: '/excel/export',
label: '导出',
icon: ,
element: ,
keyid: '0-8-1'
}
]
},
]
export default menus
1.导出
cnpm i js-export-excel -S
src/views/excel/test.d.ts
declare module 'js-export-excel'
本案例导出产品筛选列表的数据
// src/views/excel/Export.tsx
import { getCategoryList, getProList, getSearchList } from '@/api/pro';
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons';
import { Table, Image, Button, Space, Select, Input } from 'antd';
import React, { FC, useEffect, useMemo, useState } from 'react';
import ExportJsonExcel from 'js-export-excel'
interface IAppProps {
}
interface DataType {
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
img2: string
img3: string
img4: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proid: string
proname: string
sales: number
stock: number
}
const { Column } = Table;
const Com: FC = (props) => {
const [proList, setProList] = useState([])
const getProListData = () => {
getProList().then(res => setProList(res.data.data))
}
useEffect(() => {
getProListData()
}, [])
const [height] = useState(document.body.offsetHeight) // 计算body的高度
const [current, setCurrent] = useState(1)
const [pageSize, setPageSize] = useState(10)
const onChange = (page: number, pageSize: number) => {
setCurrent(page)
setPageSize(pageSize)
}
const [categoryList, setCategoryList] = useState([])
const [category, setCategory] = useState('')
const [search, setSearch] = useState('')
// const arr = [{ value: '', label: '全部' }]
useEffect(() => {
getCategoryList().then(res => {
console.log(res.data.data)
setCategoryList(res.data.data)
})
}, [])
const arr = useMemo(() => {
const brr: any = [{ value: '', label: '全部' }]
categoryList.forEach((item: any) => {
brr.push({
value: item,
label: item
})
})
return brr
}, [categoryList])
return (
`共有 ${total} 条数据`
}}
>
{
return {(current - 1) * pageSize + index + 1}
}} />
{
return
}} />
{
return } />
}} />
)
}
export default Com
以上方案为纯前端的导出,实际上还有其余的导出方法,比如通过接口实现,前端可以通过a的href属性实现
2.导入
数据在 src/views/excel/pro.xlsx
cnpm install xlsx
// src/views/excel/Import.tsx
import { Button, Table, Image, Switch } from 'antd';
import React, { useState } from 'react';
import * as XLSX from 'xlsx';
type ComProps = {}
interface DataType {
banners: string[]
brand: string
category: string
desc: string
discount: number
img1: string
img2: string
img3: string
img4: string
isrecommend: number
issale: number
isseckill: number
originprice: number
proid: string
proname: string
sales: number
stock: number
}
const Com = (props: ComProps) => {
const [proList, setProList] = useState([])
const importExcel = () => { // 导入数据
const file = (document.getElementById('fileRef') as HTMLInputElement).files![0]
const reader = new FileReader()
reader.readAsBinaryString(file!) // 转成 二进制格式
reader.onload = function () {
const workbook = XLSX.read(this.result, { type: 'binary' });
const t = workbook.Sheets['list'] // 拿到表格数据
// console.log(t)
const r: any = XLSX.utils.sheet_to_json(t) // 转换成json格式
// console.log(r)
setProList(r)
// 将r的数据上传至服务器
}
}
return (
<>
导入
{ // 触发文件选择器
(document.getElementById('fileRef') as HTMLInputElement).click()
}}>导入数据
{ return { index + 1 }}}
>
{return }}
>
a.originprice - b.originprice}
>
a.discount - b.discount}
>
a.sales - b.sales}
>
a.stock - b.stock}
>
{return }}
>
{return }}
>
{return }}
>
>
)
};
export default Com
如果在nodejs环境中,通过接口实现
const xlsx = require('node-xlsx').default;
// 导入excel表格的数据
router.get('/uploadPro', (req, res, next) => {
const originData = xlsx.parse(`${__dirname}/pro.xlsx`);
const firstData = originData[0].data
const arr = []
for (var i = 0; i < firstData.length; i++) {
if (i !== 0) {
arr.push({
proid: 'pro_'+ uuid.v4(),
category: firstData[i][0],
brand: firstData[i][1],
proname: firstData[i][2],
banners: firstData[i][3],
originprice: firstData[i][4],
sales: firstData[i][5],
stock: firstData[i][6],
desc: firstData[i][7],
issale: firstData[i][8],
isrecommend: firstData[i][9],
discount: firstData[i][10],
isseckill: firstData[i][11],
img1: firstData[i][12],
img2: firstData[i][13],
img3: firstData[i][14],
img4: firstData[i][15],
})
}
}
// 拿到 arr 的数据,先清空 产品的表格数据,然后再插入
mysql.delete(Product, {}, 1).then(() => { // 不要忘记写1,因为1 代表的是删除多条数据
// 所有的数据已删除完成
// 插入数据
mysql.insert(Product, arr).then(() => {
// 重定向到 商品的管理的页面路由
res.send('导入数据成功') // 相当于浏览器自动跳转到了 /pro 的路由
})
})
})
29.地图
https://huiyan.baidu.com/github/react-bmapgl/#/%E5%BF%AB%E9%80%9F%E5%85%A5%E9%97%A8
https://lbsyun.baidu.com/
// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';
interface IBaiduProps {
};
const Baidu:FC = () => {
return (
<>
百度地图
>
)
};
export default Baidu;
// src/views/map/Gaode.tsx
import React, { FC, useEffect } from 'react';
interface IBaiduProps {
};
const Baidu:FC = () => {
return (
<>
高德地图
>
)
};
export default Baidu;
// src/router/menus.tsx
import type { MenuProps } from 'antd';
import { HomeOutlined } from '@ant-design/icons'
import { ReactNode } from 'react';
import Home from '@/views/home/Index'
import BannerList from '@/views/banner/List'
import BannerAdd from '@/views/banner/Add'
import ProList from '@/views/pro/List'
import SearchList from '@/views/pro/Search'
import UserList from '@/views/account/User'
import AdminList from '@/views/account/Admin'
import Set from '@/views/set/Index'
import Echarts from '@/views/data/Echarts'
import HighCharts from '@/views/data/HighCharts'
import Antv from '@/views/data/Antv'
import Braft from '@/views/edit/Braft'
import Md from '@/views/edit/Md'
import Import from '@/views/excel/Import'
import Export from '@/views/excel/Export'
import Baidu from '@/views/map/Baidu'
import Gaode from '@/views/map/Gaode'
type MenuItem = Required['items'][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode;
hidden?: number;
keyid: string
}
const menus: IMyMenuItem[] = [
{
path: '/',
label: '系统首页',
key: '/',
icon: ,
element: ,
keyid: '0-0'
},
{
path: '/banner',
label: '轮播图管理',
key: '/banner',
redirect: '/banner/list',
icon: ,
keyid: '0-1',
children: [
{
path: '/banner/list',
key: '/banner/list',
label: '轮播图列表',
icon: ,
element: ,
keyid: '0-1-0'
},
{
path: '/banner/add',
key: '/banner/add',
label: '添加轮播图',
icon: ,
element: ,
hidden: 1,
keyid: '0-1-1'
}
]
},
{
path: '/pro',
label: '产品管理',
key: '/pro',
redirect: '/pro/list',
icon: ,
keyid: '0-2',
children: [
{
path: '/pro/list',
key: '/pro/list',
label: '产品列表',
icon: ,
element: ,
keyid: '0-2-0'
},
{
path: '/pro/search',
key: '/pro/search',
label: '筛选列表',
icon: ,
element: ,
keyid: '0-2-1'
}
]
},
{
path: '/account',
label: '账户管理',
key: '/account',
redirect: '/account/user',
icon: ,
keyid: '0-3',
children: [
{
path: '/account/user',
key: '/account/user',
label: '用户列表',
icon: ,
element: ,
keyid: '0-3-0'
},
{
path: '/account/admin',
key: '/account/admin',
label: '管理员列表',
icon: ,
element: ,
keyid: '0-3-1'
}
]
},
{
path: '/set',
label: '设置',
key: '/set',
icon: ,
element: ,
hidden: 1,
keyid: '0-4'
},
{
path: '/data',
label: '数据可视化',
key: '/data',
redirect: '/data/echarts',
icon: ,
keyid: '0-5',
children: [
{
path: '/data/echarts',
key: '/data/echarts',
label: 'echarts',
icon: ,
element: ,
keyid: '0-5-0'
},
{
path: '/data/HighCharts',
key: '/data/HighCharts',
label: 'HighCharts',
icon: ,
element: ,
keyid: '0-5-1'
},
{
path: '/data/antv',
key: '/data/antv',
label: 'antv',
icon: ,
element: ,
keyid: '0-5-2'
}
]
},
{
path: '/braft',
label: '父文本编辑器',
key: '/braft',
icon: ,
element: ,
keyid: '0-6'
},
{
path: '/md',
label: 'markDown编辑器',
key: '/md',
icon: ,
element: ,
keyid: '0-7'
},
{
path: '/excel',
label: '导入以及导出',
key: '/excel',
redirect: '/excel/export',
icon: ,
keyid: '0-8',
children: [
{
path: '/excel/import',
key: '/excel/import',
label: '导入',
icon: ,
element: ,
keyid: '0-8-0'
},
{
path: '/excel/export',
key: '/excel/export',
label: '导出',
icon: ,
element: ,
keyid: '0-8-1'
}
]
},
{
path: '/map',
label: '地图',
key: '/map',
redirect: '/map/baidu',
icon: ,
keyid: '0-8',
children: [
{
path: '/map/baidu',
key: '/map/baidu',
label: '百度地图',
icon: ,
element: ,
keyid: '0-9-0'
},
{
path: '/map/gaode',
key: '/map/gaode',
label: '高德地图',
icon: ,
element: ,
keyid: '0-9-1'
}
]
},
]
export default menus
DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React Apptitle>
<script type="text/javascript" src="https://api.map.baidu.com/api?v=1.0&&type=webgl&ak=17qecKvCwmMWGwzVqPQvG9GQkRSPZHc8">
script>
head>
<body>
<noscript>You need to enable JavaScript to run this app.noscript>
<div id="root">div>
body>
html>
// src/views/map/map.d.ts.
interface Window {
BMapGL: any
}
// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';
interface IBaiduProps {
};
const Baidu:FC = () => {
useEffect(() => {
var map = new window.BMapGL.Map("allmap");
map.centerAndZoom(new window.BMapGL.Point(116.280190, 40.049191), 19);
map.enableScrollWheelZoom(true);
map.setHeading(64.5);
map.setTilt(73);
}, [])
return (
<>
百度地图
>
)
};
export default Baidu;
30.项目打包发布
https://blog.csdn.net/daxunshuo/article/details/102976306?spm=1001.2014.3001.5501
$ cnpm run build
# 估计需要一点时间请耐心等待
打包完毕项目 build 文件夹即为 项目打包出来的文件,只需要把build文件夹上传至服务器,一般情况下,都需要修改build文件夹的名字
如果打开 build/index.html 文件,发现 css js 文件的引入使用的都是绝对路径
如果服务器只部署这一个项目,那么可以把 build文件夹的内容替换了 服务器的静态资源文件 — 绝对路径
如果服务器部署多个项目,那么可以把修改过后的 build的文件夹上传到 服务器的静态资源文件 — 项目路径
如何打包项目时使用相对路径
package.json
{
"homepage": '.'
}
http://121.89.205.189:2207/
port Home from ‘@/views/home/Index’
import BannerList from ‘@/views/banner/List’
import BannerAdd from ‘@/views/banner/Add’
import ProList from ‘@/views/pro/List’
import SearchList from ‘@/views/pro/Search’
import UserList from ‘@/views/account/User’
import AdminList from ‘@/views/account/Admin’
import Set from ‘@/views/set/Index’
import Echarts from ‘@/views/data/Echarts’
import HighCharts from ‘@/views/data/HighCharts’
import Antv from ‘@/views/data/Antv’
import Braft from ‘@/views/edit/Braft’
import Md from ‘@/views/edit/Md’
import Import from ‘@/views/excel/Import’
import Export from ‘@/views/excel/Export’
import Baidu from ‘@/views/map/Baidu’
import Gaode from ‘@/views/map/Gaode’
type MenuItem = Required[‘items’][number];
// 扩展固有的类型
export type IMyMenuItem = MenuItem & {
path: string; // 如果后期使用key值为跳转地址时,可以不添加 path 属性
children?: IMyMenuItem[];
redirect?: string; // 多级菜单的默认地址
element?: ReactNode;
hidden?: number;
keyid: string
}
const menus: IMyMenuItem[] = [
{
path: ‘/’,
label: ‘系统首页’,
key: ‘/’,
icon: ,
element: ,
keyid: ‘0-0’
},
{
path: ‘/banner’,
label: ‘轮播图管理’,
key: ‘/banner’,
redirect: ‘/banner/list’,
icon: ,
keyid: ‘0-1’,
children: [
{
path: ‘/banner/list’,
key: ‘/banner/list’,
label: ‘轮播图列表’,
icon: ,
element: ,
keyid: ‘0-1-0’
},
{
path: ‘/banner/add’,
key: ‘/banner/add’,
label: ‘添加轮播图’,
icon: ,
element: ,
hidden: 1,
keyid: ‘0-1-1’
}
]
},
{
path: ‘/pro’,
label: ‘产品管理’,
key: ‘/pro’,
redirect: ‘/pro/list’,
icon: ,
keyid: ‘0-2’,
children: [
{
path: ‘/pro/list’,
key: ‘/pro/list’,
label: ‘产品列表’,
icon: ,
element: ,
keyid: ‘0-2-0’
},
{
path: ‘/pro/search’,
key: ‘/pro/search’,
label: ‘筛选列表’,
icon: ,
element: ,
keyid: ‘0-2-1’
}
]
},
{
path: ‘/account’,
label: ‘账户管理’,
key: ‘/account’,
redirect: ‘/account/user’,
icon: ,
keyid: ‘0-3’,
children: [
{
path: ‘/account/user’,
key: ‘/account/user’,
label: ‘用户列表’,
icon: ,
element: ,
keyid: ‘0-3-0’
},
{
path: ‘/account/admin’,
key: ‘/account/admin’,
label: ‘管理员列表’,
icon: ,
element: ,
keyid: ‘0-3-1’
}
]
},
{
path: ‘/set’,
label: ‘设置’,
key: ‘/set’,
icon: ,
element: ,
hidden: 1,
keyid: ‘0-4’
},
{
path: ‘/data’,
label: ‘数据可视化’,
key: ‘/data’,
redirect: ‘/data/echarts’,
icon: ,
keyid: ‘0-5’,
children: [
{
path: ‘/data/echarts’,
key: ‘/data/echarts’,
label: ‘echarts’,
icon: ,
element: ,
keyid: ‘0-5-0’
},
{
path: ‘/data/HighCharts’,
key: ‘/data/HighCharts’,
label: ‘HighCharts’,
icon: ,
element: ,
keyid: ‘0-5-1’
},
{
path: ‘/data/antv’,
key: ‘/data/antv’,
label: ‘antv’,
icon: ,
element: ,
keyid: ‘0-5-2’
}
]
},
{
path: ‘/braft’,
label: ‘父文本编辑器’,
key: ‘/braft’,
icon: ,
element: ,
keyid: ‘0-6’
},
{
path: ‘/md’,
label: ‘markDown编辑器’,
key: ‘/md’,
icon: ,
element: ,
keyid: ‘0-7’
},
{
path: ‘/excel’,
label: ‘导入以及导出’,
key: ‘/excel’,
redirect: ‘/excel/export’,
icon: ,
keyid: ‘0-8’,
children: [
{
path: ‘/excel/import’,
key: ‘/excel/import’,
label: ‘导入’,
icon: ,
element: ,
keyid: ‘0-8-0’
},
{
path: ‘/excel/export’,
key: ‘/excel/export’,
label: ‘导出’,
icon: ,
element: ,
keyid: ‘0-8-1’
}
]
},
{
path: ‘/map’,
label: ‘地图’,
key: ‘/map’,
redirect: ‘/map/baidu’,
icon: ,
keyid: ‘0-8’,
children: [
{
path: ‘/map/baidu’,
key: ‘/map/baidu’,
label: ‘百度地图’,
icon: ,
element: ,
keyid: ‘0-9-0’
},
{
path: ‘/map/gaode’,
key: ‘/map/gaode’,
label: ‘高德地图’,
icon: ,
element: ,
keyid: ‘0-9-1’
}
]
},
]
export default menus
```html
React App
// src/views/map/map.d.ts.
interface Window {
BMapGL: any
}
// src/views/map/Baidu.tsx
import React, { FC, useEffect } from 'react';
interface IBaiduProps {
};
const Baidu:FC = () => {
useEffect(() => {
var map = new window.BMapGL.Map("allmap");
map.centerAndZoom(new window.BMapGL.Point(116.280190, 40.049191), 19);
map.enableScrollWheelZoom(true);
map.setHeading(64.5);
map.setTilt(73);
}, [])
return (
<>
百度地图
>
)
};
export default Baidu;
30.项目打包发布
https://blog.csdn.net/daxunshuo/article/details/102976306?spm=1001.2014.3001.5501
$ cnpm run build
# 估计需要一点时间请耐心等待
打包完毕项目 build 文件夹即为 项目打包出来的文件,只需要把build文件夹上传至服务器,一般情况下,都需要修改build文件夹的名字
如果打开 build/index.html 文件,发现 css js 文件的引入使用的都是绝对路径
如果服务器只部署这一个项目,那么可以把 build文件夹的内容替换了 服务器的静态资源文件 — 绝对路径
如果服务器部署多个项目,那么可以把修改过后的 build的文件夹上传到 服务器的静态资源文件 — 项目路径
如何打包项目时使用相对路径
package.json
{
"homepage": '.'
}
http://121.89.205.189:2207/
你可能感兴趣的:(react,javascript,前端,react.js)