Umi项目搭建
umijs是一个基于react的前端框, 无论是用于前台还是后台,都是一个不错的选择
下面根据umijs的官方文档,来搭建一个umi项目, 并且添加一些常用的配置
安装umi
可查看官网: umijs
执行以下命令安装umi
:
npx create-umi@latest
会提示是否安装umi, 如图, 此时的umi版本是4.3.11, 我们选择Y
安装
接下来要求我们输入项目名称, 选择 Ant Design Pro 模板,就能使用 @umijs/max 来创建项目了。输入后继续操作
下面就是根据情况选择了, 我选了npm
, 源选择taobao
, 回车继续, 等待依赖安装完成
看到下面,说明项目创建成功了
运行项目:
npm run dev
优化项目
进来之后就是这个页面, 但是这个页面是umijs的默认页面, 我们需要对项目进行优化
表格组件修改
默认的表格属性可以进行优化, 如图, 去掉表格左上角的标题, 以及选择时出现的已选择xx项 取消选择
的提示
在ProTable组件中添加如下属性:
headerTitle = "查询表格"
// 取消当选择行数据时,表格上方的'取消选择'提示框
tableAlertRender = { false }
// 取消当选择行数据时,表格上方的'已选择*项'提示框
tableAlertOptionRender = { false }
<ProTable<API.UserInfo>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
search={{
labelWidth: 120,
}}
// 取消当选择行数据时,表格上方的'已选择*项 取消选择'提示框
tableAlertRender={false}
tableAlertOptionRender={false}
toolBarRender={() => [
<Button
key="1"
type="primary"
onClick={() => handleModalVisible(true)}
>
新建
</Button>,
]}
columns={columns}
rowSelection={{
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
}}
/>
一下是一些表格属性, 供参考
<EditableProTable<any>
headerTitle={config?.headerTitle}
actionRef={actionRef}
rowKey={config?.rowKey}
editable={{
type: 'multiple',
onlyAddOneLineAlertMessage: '只能新增一行',
onlyOneLineEditorAlertMessage: '只能编辑一行',
onSave: async (key, row) => {
if (saveRowData && typeof row.id === 'number') {
await saveRowData(row)
actionRef.current?.cancelEditable(key)
actionRef.current?.reload()
}
if (addRowData && row.id?.startsWith('add')) {
await addRowData(row)
actionRef.current?.reload()
}
},
onDelete: async (key, row) => {
await handleAction('deleted', row)
},
// 当同时开启多行编辑时,保存之前编辑的行(或者取消之前添加的行)
onChange: async (keys) => {
// 如果同时编辑多行,先将之前的保存
if (keys.length > 1) {
// 如果是新增的行, 取消编辑状态
if (String(keys[0])?.startsWith('add')) {
actionRef.current?.cancelEditable(keys[0])
} else {
actionRef.current?.saveEditable(keys[0])
}
setEditRowKeys(keys.slice(-1))
} else {
setEditRowKeys(keys)
}
},
}}
// 参考文档 https://procomponents.ant.design/components/editable-table#recordcreatorprops
recordCreatorProps={{
position: 'top',
record: () => ({ id: (Math.random() * 1000000).toFixed(0) }),
style: {
display: 'none',
},
}}
options={{
setting: {
listsHeight: 400,
},
fullScreen: true,
}}
scroll={{ x: true }}
search={
search === false
? search
: {
labelWidth: 'auto',
span: {
xs: 12,
sm: 12,
md: 12,
lg: 6,
xl: 6,
xxl: 6,
},
}
}
toolBarRender={toolBarRender}
toolbar={toolBar}
params={{ ...params, ...useParams() }}
request={request}
columns={columns}
columnsState={{
persistenceKey: location.pathname?.toString() + '/columns',
persistenceType: 'localStorage',
// option操作栏默认固定在右侧
defaultValue: {
option: {
fixed: 'right',
},
},
}}
pagination={{
...pagination,
showSizeChanger: true,
showQuickJumper: true,
pageSizeOptions: [
'3',
'5',
'10',
'20',
'50',
'100',
'300',
'500',
'800',
'1000',
'5000',
],
onChange: (page, pageSize) => {
setPagination({ current: page, pageSize })
sessionStorage.setItem(
location.pathname?.toString() + '/pagination',
JSON.stringify({ current: page, pageSize }),
)
},
}}
rowSelection={{
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
}}
// 取消当选择行数据时,表格上方的'已选择*项 取消选择'提示框
tableAlertRender={false}
tableAlertOptionRender={false}
/>
.umirc.ts
这是构建时配置, 用于配置构建时的一些配置, 比如代理, 路由, 主题等
import { defineConfig } from '@umijs/max'
// 项目中全部的路由配置
import { routes } from './src/routes'
export default defineConfig({
antd: {
configProvider: {},
// 开启暗色主题
dark: false,
// 开启紧凑主题
compact: false,
style: 'less',
theme: {},
appConfig: {},
// 是否使用 moment.js 作为日期处理工具
momentPicker: false,
// 配置 antd 的 StyleProvider 组件,该组件用于兼容低版本浏览器
styleProvider: {
hashPriority: 'high',
legacyTransformer: true,
},
},
dva: {},
access: {},
model: {},
initialState: {},
request: {
dataField: 'data',
},
locale: {
default: 'zh-CN',
antd: true,
title: true,
// 浏览器语言检测
baseNavigator: true,
baseSeparator: '-',
},
layout: {
locale: true,
},
routes,
npmClient: 'npm',
tailwindcss: {},
})
app.ts
包含布局, 动态路由, 请求相关, 全局数据重新获取 等的配置
动态路由的实现思路:
1.在layout
配置中的menu属性中配置request
属性
2.通过pro-components
提供的getMenuData
方法传入设定好的路由,然后得到菜单数据
import Logo from '@/components/Logo'
import RightRender from '@/components/RightRender'
import { dataSourceMap, defaultDataSource } from '@/config'
import {
DATA_SOURCE_STORAGE,
LOGIN_PATH,
USER_TOKEN_STORAGE,
} from '@/constants'
import { asyncRoutes } from '@/routes/asyncRoutes'
import { filterAdminRoutes, menuRender } from '@/routes/handleRouteRender'
import { reqUserInfo } from '@/services/userController'
import { getToken } from '@/utils/authorization'
import { extractDomainSuffix } from '@/utils/getNationCode'
import { getMenuData } from '@ant-design/pro-components'
import { RunTimeLayoutConfig } from '@umijs/max'
import { message } from 'antd'
import type { RequestConfig } from 'umi'
import { getDvaApp, getIntl, matchRoutes } from 'umi'
import { GLOBAL } from '../typings'
// 运行时配置 全局初始化数据配置,用于 Layout 用户信息和权限初始化
export async function getInitialState(): Promise<{
userInfo: API.UserInfo
dataSource: keyof typeof dataSourceMap
}> {
// 获取用户信息
const token = localStorage.getItem(USER_TOKEN_STORAGE)
let data = {} as API.UserInfo
try {
if (token) {
data = (await reqUserInfo()).data
}
} catch (error) {
}
return {
userInfo: data,
dataSource: defaultDataSource,
}
}
// 运行时配置, 用于配置全局的 layout
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
const intl = getIntl()
return {
title: intl.formatMessage({ id: 'site.title' }),
logo: <Logo />,
menu: {
locale: true,
// 每当 initialState?.dataSource 发生修改时重新执行 request
params: {
dataSource: initialState?.dataSource,
},
request: async (params, defaultMenuData) => {
const menuData = getMenuData(
filterAdminRoutes(
asyncRoutes[initialState!.dataSource],
initialState?.userInfo,
),
{ locale: true },
intl.formatMessage,
menuRender,
)
return menuData.menuData
},
},
layout: 'mix',
logout: () => {
// 跳转到登录页
window.location.href = LOGIN_PATH
},
// 右上角渲染
rightRender: () => <RightRender />,
}
}
export const request: RequestConfig = {
timeout: 30000,
baseURL: dataSourceMap[defaultDataSource].baseUrl,
errorConfig: {
errorThrower() {
},
errorHandler: (error: any) => {
const { formatMessage } = getIntl()
// token失效跳转登录页
if (error.response?.status === 401) {
message
.open({
key: 'login',
content: formatMessage({ id: 'site.unauthorized.title' }),
duration: 3,
})
.then()
// 清除token
localStorage.removeItem(USER_TOKEN_STORAGE)
setTimeout(() => {
window.location.href = LOGIN_PATH
}, 100)
} else {
message.error(error.message).then()
}
},
},
requestInterceptors: [
(url: any, options: any) => {
return {
url,
options: {
...options,
// 上传进度条和其他配置
...options.config,
headers: {
Authorization: getToken(),
},
params: {
...options.params,
},
},
}
},
],
responseInterceptors: [
// 对状态码检查
(response: any) => {
if (response.data.code && response.data.code !== 200) {
message.error(response?.data?.msg).then()
}
return response
},
],
}
export function onRouteChange({
clientRoutes,
location,
}: {
clientRoutes: GLOBAL.IRouteObject[]
location: Location
}) {
const route = matchRoutes(clientRoutes, location.pathname)?.pop()?.route as
| GLOBAL.IRouteObject
| undefined
if (route) {
const dispatch = getDvaApp()._store.dispatch
const userStore = getDvaApp()._store.getState().user
// 如果用户信息不存在则请求用户信息
if (!userStore.userInfo?.username) {
}
}
}
处理路由渲染和权限判断的方法
// @/routes/handleRouteRender.ts
import * as Icon from '@ant-design/icons'
import { MenuDataItem } from '@ant-design/pro-components'
import { cloneDeep } from 'lodash'
import React, { ReactNode } from 'react'
import { GLOBAL } from '../../typings'
/**
* @description antd-pro的路由渲染方法
* */
export const menuRender = (menuData: MenuDataItem[]) => {
return menuData.map((menu) => {
return {
...menu,
icon: renderIcon(menu.icon),
}
})
}
/**
* @description 渲染路由的图标, 用于展示在菜单栏
* @param icon 路由的图标
* @returns ReactNode
* */
export const renderIcon = (icon: string | ReactNode | undefined) => {
// 如果是ReactNode则直接返回
if (React.isValidElement(icon)) {
return icon
}
// 如果是字符串则渲染对应的图标
if (typeof icon === 'string') {
// 先去掉 '<' 和 '/>'
const iconIndex = icon.replace(/<|\/>/g, '').trim()
const IconComponent = (Icon as any)[iconIndex as string]
if (!IconComponent) {
return null
}
return React.createElement(IconComponent)
}
// 其他情况返回null
return null
}
/**
* @description 过滤最高权限路由, 当用户名为administrator时, 且路由标志admin=true时, 返回该路由
* @param routes 路由列表
* @param userInfo 用户信息
* @returns 过滤后的路由列表
* */
export const filterAdminRoutes = (
routes: GLOBAL.IRouteObject[],
userInfo: API.UserInfo | undefined,
) => {
// 如果当前用户为管理员, 则返回所有路由
if (userInfo && userInfo?.username?.toLowerCase() === 'administrator') {
return routes
}
// 否则返回非admin路由
return routes.filter((route) => !route.admin)
}
/**
* @description 根据用户的menuTree过滤路由, 只返回用户有权限的路由
* @param routes 路由列表
* @param menuTree 用户的菜单树
* @returns 过滤后的路由列表
* */
export const filterRoutesByMenuTree = (
routes: any[],
menuTree: API.UserMenuItem[],
) => {
// 如果用户没有菜单树, 则返回空
if (!menuTree || !menuTree.length) {
return []
}
// 将routes和menuTree的name字段进行递归比对, 如果相同则返回
return cloneDeep(routes).filter((route) => {
const menu = menuTree.find((item) => item.name === route.name)
if (menu) {
if (route.routes) {
route.routes = filterRoutesByMenuTree(route.routes, menu.children || [])
}
return true
}
return false
})
}
路由权限access判断
// '@/permission/router.ts
import { GLOBAL } from '../../typings'
/**
* @description 路由全部路径 采用递归的形式 将所有父级路由和子路由的路径拼接成字符串
* @param routes 路由配置 - 可传本地路由配置 或者 用户信息中的权限路由配置
* @param {string} basePath 基础路径
* @returns {string[]} 返回所有路由的路径
* */
export const getAllRoutePaths = (
routes: GLOBAL.IRouteObject[],
basePath: string = '',
): string[] => {
if (!Array.isArray(routes)) {
return []
}
let allRoutePaths: string[] = []
routes.forEach((route) => {
const fullPath = basePath + route.path
if (route.routes) {
const nestedRoutePaths = getAllRoutePaths(route.routes, fullPath + '/')
allRoutePaths.push(...nestedRoutePaths)
} else {
allRoutePaths.push(fullPath)
}
})
return allRoutePaths
}
/**
* @description 获取路由元信息, 即获取原始路由配置的所有信息
* @param routes 路由配置
* @param path 路由路径
* */
export const getRouteMeta = (
routes: GLOBAL.IRouteObject[],
path: string = '',
): GLOBAL.IRouteObject | undefined => {
if (Array.isArray(routes) && path) {
return routes.find((route) => route.path === path)
}
return undefined
}
/**
* @description 最高权限路由判断, 路由标志admin=true时, 且用户名为administrator时 返回true, admin为false时返回true
* @param routes 路由配置
* @param path 路由路径
* @param userInfo 用户信息
* */
export const canAccessAdminRoute = (
routes: GLOBAL.IRouteObject[],
path: string = '',
userInfo: API.UserInfo | undefined,
) => {
// 获取路由元信息
const routeMeta = getRouteMeta(routes, path)
// 只判断一种情况, 当路由标志admin=true时, 且用户名不为administrator时 false, 其他情况返回true
if (routeMeta && routeMeta.admin) {
return !!(userInfo && userInfo.username.toLowerCase() === 'administrator')
}
return true
}
/**
* @description 判断当前路由是否有权限访问
* @param routes 路由配置
* */
export const canRouteAccess = (routes: GLOBAL.IRouteObject[]) => {
return (
currentPath: string,
userInfo: API.UserInfo | undefined,
path: string | undefined,
) => {
// 获取全部的路由的完整路径
const allRoutePaths: string[] = getAllRoutePaths(routes)
// 判断当前路径是否是超级管理员权限
const accessAdminRoute = canAccessAdminRoute(routes, path, userInfo)
// 检查当前路径是否在权限配置中, 同时检查是否存在动态参数
return (
accessAdminRoute &&
allRoutePaths.some((path) => {
if (path.includes(':')) {
const pathReg = new RegExp(
path.replace(/:\w+/g, '\\w+').replace(/\//g, '\\/'),
)
return pathReg.test(currentPath)
}
return path.indexOf(currentPath) !== -1
})
)
}
}
access.ts
项目权限控制文件
import { USER_TOKEN_STORAGE } from '@/constants'
import { canRouteAccess } from '@/permission/router'
import { asyncRoutes } from '@/routes/asyncRoutes'
interface InitialState {
userInfo: API.UserInfo
}
export default (initialState: InitialState) => {
// 是否登录
const isLogin = !!localStorage.getItem(USER_TOKEN_STORAGE)
// 当前路由是否有权限访问
const isRouteAccess = canRouteAccess(
asyncRoutes[initialState.dataSource] || [],
)
return {
isLogin,
isRouteAccess,
}
}
动态切换baseURL
import { getRequestInstance } from 'umi'
/**
* 更新请求的基础路径
* @param baseURL 基础路径
* @returns void
* @example
* import { updateRequestBaseURL } from '@/utils/updateRequestBaseURL'
* updateRequestBaseURL('http://localhost:8080')
* */
export function updateRequestBaseURL(baseURL: string) {
// 更新运行时配置
const instance = getRequestInstance()
instance.defaults.baseURL = baseURL
}
右上角渲染组件
import DataSourceSelect from '@/components/DataSource'
import { dataSourceMap } from '@/config'
import { DATA_SOURCE_STORAGE, LOGIN_PATH } from '@/constants'
import { updateRequestBaseURL } from '@/utils/updateRequestBaseURL'
import { useModel } from '@@/exports'
import type { MenuProps } from 'antd'
import { Avatar, Dropdown, Switch, theme } from 'antd'
import { useState } from 'react'
import {
SelectLang,
useAntdConfig,
useAntdConfigSetter,
useIntl,
useNavigate,
} from 'umi'
const { darkAlgorithm, defaultAlgorithm } = theme
/**
* 右上角的组件
* 1. 退出登录
* 2. 语言选择
* 3. 主题选择
* 4. 数据源选择
* */
const RightRender = () => {
const { initialState } = useModel('@@initialState')
const { formatMessage } = useIntl()
const navigate = useNavigate()
const setAntdConfig = useAntdConfigSetter()
const antdConfig = useAntdConfig()
const [dataSource, setDataSource] = useState<keyof typeof dataSourceMap>(
initialState!.dataSource,
)
// 选择数据源
const handleSelectDataSource = (value: keyof typeof dataSourceMap) => {
setDataSource(value)
localStorage.setItem(DATA_SOURCE_STORAGE, value)
// 修改request请求的baseURL
updateRequestBaseURL(dataSourceMap[value as keyof typeof dataSourceMap])
// 触发刷新
window.location.reload()
}
const logout = () => {
navigate(LOGIN_PATH)
}
const items: MenuProps['items'] = [
{
key: '1',
label: (
<span onClick={logout}>{formatMessage({ id: 'site.logout' })}</span>
),
},
]
return (
<div className="flex items-center space-x-1">
{/*主题*/}
<Switch
checkedChildren={formatMessage({ id: 'site.theme.dark' })}
unCheckedChildren={formatMessage({ id: 'site.theme.light' })}
checked={
Array.isArray(antdConfig?.theme?.algorithm) &&
antdConfig?.theme?.algorithm.includes(darkAlgorithm)
}
onChange={(data) => {
// 此配置会与原配置深合并
setAntdConfig({
theme: {
algorithm: [data ? darkAlgorithm : defaultAlgorithm],
},
})
}}
/>
{/*数据源*/}
<DataSourceSelect
dataSource={dataSource}
onChange={handleSelectDataSource}
/>
{/*切换语言*/}
<SelectLang reload={false} />
{/*退出登录*/}
<Dropdown menu={{ items }} placement="bottomLeft">
<Avatar
style={{ cursor: 'pointer' }}
src={
initialState?.userInfo?.avatar_path ||
'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg'
}
size="small"
/>
</Dropdown>
</div>
)
}
export default RightRender
环境变量
UMI4这个环境变量属实巨坑...
现在看看是如何在UMI4中使用环境变量的吧......
cross-env
首先我们安装这个东西cross-env
:
npm install cross-env -D
修改启动命令
假设环境有uat和prd两个环境
在package.json里面修改如下:
"scripts": {
"dev:uat": "cross-env UMI_ENV=uat max build",
"dev:prd": "cross-env UMI_ENV=prd max build",
"build:uat": "cross-env UMI_ENV=uat max build",
"build:prd": "cross-env UMI_ENV=prd max build",
},
UMI_ENV=uat指的是p
的值为uat
如果使用的是普通的UMI,则将max build
替换为umi dev
或umi build
创建配置文件
在UMI中,每个环境会对应一个配置文件(这个文件在项目的根目录,不在src里面)
这个配置文件指的是.umirc.ts
或config.ts
,取决于项目中使用哪一个
由于我们使用了两个环境,uat
和prd
因此我们需要在.umirc.ts的基础上再创建两个文件:
分别是.umirc.uat.ts
和.umirc.prd.ts
, 如果使用config.ts,同理
查看官网,配置文件的加载规则如下:
因此我们自定义的环境不能包含dev和prod,test这三个默认的环境
创建环境变量
最后一步,当我们创建好了对应环境的配置文件之后,我可们可以在该配置文件中定义环境变量:
在.umirc.uat.ts
文件中:
export default defineConfig({
// ... 其他配置
tailwindcss: {},
define: {
"process.env.UMI_ENV" : process.env.UMI_ENV,
"process.env.UMI_APP_BASE_VIETNAM_API" : 'https://uat.a.in:8081/api/v1/',
"process.env.UMI_APP_BASE_THAILAND_API" : 'https://uat.a.in:8081/api/v1/',
"process.env.UMI_APP_BASE_INDONESIA_API" : 'https://uat.a.in:8081/api/v1/',
"process.env.UMI_APP_BASE_MALAYSIA_API" : 'https://uat.a.in:8081/api/v1/',
},
})
在.umirc.prd.ts
文件中:
export default defineConfig({
// ... 其他配置
tailwindcss: {},
define: {
"process.env.UMI_ENV" : process.env.UMI_ENV,
"process.env.UMI_APP_BASE_VIETNAM_API" : 'https://prd.a.in:8081/api/v1/',
"process.env.UMI_APP_BASE_THAILAND_API" : 'https://prd.a.in:8081/api/v1/',
"process.env.UMI_APP_BASE_INDONESIA_API" : 'https://prd.a.in:8081/api/v1/',
"process.env.UMI_APP_BASE_MALAYSIA_API" : 'https://prd.a.in:8081/api/v1/',
},
})
使用环境变量
在项目的任意位置使用p
即可读取到对应的环境变量
其它变量使用方法同上
实现layout和tab切换
由于UMI不提供layout和tab切换的功能(至少我没发现),因此我们需要自己实现
先说一下实现后的效果:
- 可以通过tabs标签页组件进行切换
- 通过tab切换的组件会保留该组件的状态 (比如表单数据, 表格数据等)
- 可右键关闭标签页
以下是效果图
实现思路
- 自定义
layout
组件 - 实现
tabs
组件(antd) - 监听路由的变化, 设置Tabs的items值, 达到切换的效果
- 实现组件缓存, 保留组件的状态
- 右键关闭标签页
- 国际化(可选)
自定义layout组件
layout目录结构如下:
├── layout
│ ├── RightClickMenu
│ │ ├── index.tsx
│ ├── style
│ │ ├── tabs.less
│ ├── index.tsx
代码
layout本体layout/index.tsx
import useMenuName from '@/hooks/useMenuName'
import RightClickMenu from '@/layouts/RightClickMenu'
import { Tabs, TabsProps } from 'antd'
import { useEffect, useRef, useState } from 'react'
import { useOutlet } from 'react-router-dom'
import { useLocation } from 'umi'
import './style/index.less'
/**
* 全局 Layout 组件
* 用于缓存组件、处理路由变化、设置当前激活的 tab
* */
const Layout = () => {
const location = useLocation()
const componentList = useRef(new Map())
const outlet = useOutlet()
const { getMenuName } = useMenuName()
const [tabs, setTabs] = useState<TabsProps['items']>([])
const [activeKey, setActiveKey] = useState<string>('')
useEffect(() => {
// 缓存组件
setAliveComponent(location.pathname)
// 处理路由变化
handleRouteChange(location.pathname)
// 设置当前激活的 tab
setActiveKey(location.pathname)
}, [location.pathname])
// 缓存组件
const setAliveComponent = (key: string) => {
// 如果缓存中没有当前组件,则添加
if (!componentList.current.has(key)) {
componentList.current.set(key, outlet)
}
}
// 当路径变化时,更新 tabs
const handleRouteChange = (path: string) => {
// 如果已存在,则不添加
if (tabs?.find((item) => item.key === location.pathname)) {
return
}
// 添加新 tab
const newTab = {
key: path,
label: getMenuName(path).sourceName,
children: componentList.current.get(path),
closable: tabs?.length !== 1,
}
setTabs((prev) => [...prev!, newTab])
}
// 关闭 其它/左边/右边 标签
const handleTabCloseOther = (
type: 'other' | 'left' | 'right' | 'self',
key: string,
) => {
if (!key || tabs?.length === 1 || !tabs) {
return
}
const currentIndex = tabs?.findIndex((item) => item.key === key)
// 缓存的组件列表
const cacheList = componentList.current
if (type === 'other') {
setTabs([tabs[currentIndex!]])
// 删除缓存
cacheList.forEach((_, k) => {
if (k !== key) {
cacheList.delete(k)
}
})
} else if (type === 'left') {
setTabs(tabs.slice(currentIndex))
cacheList.forEach((_, k) => {
if (
k !== key &&
tabs.findIndex((item) => item.key === k) < currentIndex
) {
cacheList.delete(k)
}
})
} else if (type === 'right') {
setTabs(tabs.slice(0, currentIndex! + 1))
cacheList.forEach((_, k) => {
if (
k !== key &&
tabs.findIndex((item) => item.key === k) > currentIndex
) {
cacheList.delete(k)
}
})
} else if (type === 'self') {
setTabs((prev) => prev!.filter((item) => item.key !== key))
cacheList.delete(key)
}
}
return (
<Tabs
size="small"
type="editable-card"
hideAdd={true}
tabBarStyle={{
paddingLeft: 40,
}}
onEdit={(key) =>
handleTabCloseOther('self', typeof key === 'string' ? key : '')
}
activeKey={activeKey}
onChange={(key) => setActiveKey(key)}
>
{/* 为了实现动态可关闭按钮的启用和关闭 以及右键菜单 使用JSX方式渲染tab */}
{tabs?.map((tab) => (
<Tabs.TabPane
key={tab.key}
tab={
<RightClickMenu
clickTab={tab}
handleTabCloseOther={handleTabCloseOther}
/>
}
closable={tabs.length !== 1 && tab.key !== activeKey}
>
{tab.children}
</Tabs.TabPane>
))}
</Tabs>
)
}
export default Layout
右键菜单组件layout/RightClickMenu/index.tsx
import { Dropdown, MenuProps } from 'antd'
import { Tab } from 'rc-tabs/lib/interface'
import { useIntl } from 'umi'
interface RightClickMenuProps {
clickTab: Tab
handleTabCloseOther: (key: 'other' | 'left' | 'right', tabKey: string) => void
}
const RightClickMenu = (props: RightClickMenuProps) => {
const { clickTab, handleTabCloseOther } = props
const { formatMessage } = useIntl()
const rightClickMenus: MenuProps['items'] = [
{
key: 'left',
label: formatMessage({ id: 'layout.right.menu.left' }),
},
{
key: 'right',
label: formatMessage({ id: 'layout.right.menu.right' }),
},
{
key: 'other',
label: formatMessage({ id: 'layout.right.menu.other' }),
},
]
return (
<Dropdown
menu={{
items: rightClickMenus,
onClick: (e) => {
handleTabCloseOther(e.key as 'other' | 'left' | 'right', clickTab.key)
},
}}
trigger={['contextMenu']}
>
{/* 如果tab的label设为了ReactNode 这里需要修改 ! */}
<span>{formatMessage({ id: clickTab.label as string })}</span>
</Dropdown>
)
}
export default RightClickMenu