Commit 0b626ce3 by zhaochengxiang

init

parents
preview.pro.ant.design
\ No newline at end of file
# Ant Design Pro
This project is initialized with [Ant Design Pro](https://pro.ant.design). Follow is the quick guide for how to use.
## Environment Prepare
Install `node_modules`:
```bash
npm install
```
or
```bash
yarn
```
## Provided Scripts
Ant Design Pro provides some useful script to help you quick start and build with web project, code style check and test.
Scripts provided in `package.json`. It's safe to modify or add additional script:
### Start project
```bash
npm start
```
### Build project
```bash
npm run build
```
### Check code style
```bash
npm run lint
```
You can also use script to auto fix some lint error:
```bash
npm run lint:fix
```
### Test code
```bash
npm test
```
## More
You can view full document on our [official website](https://pro.ant.design). And welcome any feedback in our [github](https://github.com/ant-design/ant-design-pro).
import { IConfig, IPlugin } from 'umi-types';
import defaultSettings from './defaultSettings'; // https://umijs.org/config/
import slash from 'slash2';
import webpackPlugin from './plugin.config';
import routes from './routes';
const baseUrl = '/data-platform/';
const { pwa, primaryColor } = defaultSettings; // preview.pro.ant.design only do not use in your production ;
// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION, LOGIN_HREF } = process.env;
const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site';
const plugins: IPlugin[] = [
[
'umi-plugin-react',
{
antd: true,
dva: {
hmr: true,
},
locale: {
// default false
enable: true,
// default zh-CN
default: 'zh-CN',
// default true, when it is true, will use `navigator.language` overwrite default
baseNavigator: false,
},
dynamicImport: {
loadingComponent: './components/PageLoading/index',
webpackChunkName: true,
level: 3,
},
pwa: pwa
? {
workboxPluginMode: 'InjectManifest',
workboxOptions: {
importWorkboxFrom: 'local',
},
}
: false, // default close dll, because issue https://github.com/ant-design/ant-design-pro/issues/4665
// dll features https://webpack.js.org/plugins/dll-plugin/
// dll: {
// include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
// exclude: ['@babel/runtime', 'netlify-lambda'],
// },
// dll: true,
},
],
[
'umi-plugin-pro-block',
{
moveMock: false,
moveService: false,
modifyRequest: true,
autoAddMenu: true,
},
],
]; // 针对 preview.pro.ant.design 的 GA 统计代码
if (isAntDesignProPreview) {
plugins.push([
'umi-plugin-ga',
{
code: 'UA-72788897-6',
},
]);
plugins.push([
'umi-plugin-pro',
{
serverUrl: 'https://ant-design-pro.netlify.com',
},
]);
}
export default {
base: baseUrl,
publicPath: `${baseUrl}dist/`,
runtimePublicPath: true,
plugins,
block: {
defaultGitUrl: 'https://github.com/ant-design/pro-blocks',
},
hash: true,
targets: {
ie: 11,
},
devtool: isAntDesignProPreview ? 'source-map' : false,
// umi routes: https://umijs.org/zh/guide/router.html
routes,
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
theme: {
'primary-color': primaryColor,
'@text-color': '#000',
'@text-color-secondary': '#000',
},
define: {
BASE_URL: baseUrl,
LOGIN_HREF: LOGIN_HREF || '',
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION:
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION || '', // preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
},
ignoreMomentLocale: true,
lessLoaderOptions: {
javascriptEnabled: true,
},
disableRedirectHoist: true,
cssLoaderOptions: {
modules: true,
getLocalIdent: (
context: {
resourcePath: string;
},
_: string,
localName: string,
) => {
if (
context.resourcePath.includes('node_modules') ||
context.resourcePath.includes('ant.design.pro.less') ||
context.resourcePath.includes('global.less')
) {
return localName;
}
const match = context.resourcePath.match(/src(.*)/);
if (match && match[1]) {
const antdProPath = match[1].replace('.less', '');
const arr = slash(antdProPath)
.split('/')
.map((a: string) => a.replace(/([A-Z])/g, '-$1'))
.map((a: string) => a.toLowerCase());
return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
}
return localName;
},
},
manifest: {
basePath: '/',
},
chainWebpack: webpackPlugin,
/*
proxy: {
'/server/api/': {
target: 'https://preview.pro.ant.design/',
changeOrigin: true,
pathRewrite: { '^/server': '' },
},
},
*/
proxy: {
'/api': {
// target: 'http://139.198.126.96:9011',
// target: 'http://192.168.0.220',
target: 'http://139.198.127.54:18392',
changeOrigin: true,
},
},
treeShaking: true,
} as IConfig;
import { MenuTheme } from 'antd/es/menu/MenuContext';
export type ContentWidth = 'Fluid' | 'Fixed';
export interface DefaultSettings {
/**
* theme for nav menu
*/
navTheme: MenuTheme;
/**
* primary color of ant design
*/
primaryColor: string;
/**
* nav menu position: `sidemenu` or `topmenu`
*/
layout: 'sidemenu' | 'topmenu';
/**
* layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
*/
contentWidth: ContentWidth;
/**
* sticky header
*/
fixedHeader: boolean;
/**
* auto hide header
*/
autoHideHeader: boolean;
/**
* sticky siderbar
*/
fixSiderbar: boolean;
menu: { locale: boolean };
title: string;
pwa: boolean;
// Your custom iconfont Symbol script Url
// eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
// 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理
// Usage: https://github.com/ant-design/ant-design-pro/pull/3517
iconfontUrl: string;
colorWeak: boolean;
}
export default {
navTheme: 'dark',
primaryColor: '#007bff',
layout: 'sidemenu',
contentWidth: 'Fluid',
fixedHeader: false,
autoHideHeader: false,
fixSiderbar: false,
colorWeak: false,
menu: {
locale: true,
},
title: '数据资产平台',
pwa: false,
iconfontUrl: '',
} as DefaultSettings;
// Change theme plugin
// eslint-disable-next-line eslint-comments/abdeils - enable - pair;
/* eslint-disable import/no-extraneous-dependencies */
import ThemeColorReplacer from 'webpack-theme-color-replacer';
import generate from '@ant-design/colors/lib/generate';
import path from 'path';
function getModulePackageName(module: { context: string }) {
if (!module.context) return null;
const nodeModulesPath = path.join(__dirname, '../node_modules/');
if (module.context.substring(0, nodeModulesPath.length) !== nodeModulesPath) {
return null;
}
const moduleRelativePath = module.context.substring(nodeModulesPath.length);
const [moduleDirName] = moduleRelativePath.split(path.sep);
let packageName: string | null = moduleDirName;
// handle tree shaking
if (packageName && packageName.match('^_')) {
// eslint-disable-next-line prefer-destructuring
packageName = packageName.match(/^_(@?[^@]+)/)![1];
}
return packageName;
}
export default (config: any) => {
// preview.pro.ant.design only do not use in your production;
if (
process.env.ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ||
process.env.NODE_ENV !== 'production'
) {
config.plugin('webpack-theme-color-replacer').use(ThemeColorReplacer, [
{
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#1890ff'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
changeSelector(selector: string): string {
switch (selector) {
case '.ant-calendar-today .ant-calendar-date':
return ':not(.ant-calendar-selected-date)' + selector;
case '.ant-btn:focus,.ant-btn:hover':
return '.ant-btn:focus:not(.ant-btn-primary),.ant-btn:hover:not(.ant-btn-primary)';
case '.ant-btn.active,.ant-btn:active':
return '.ant-btn.active:not(.ant-btn-primary),.ant-btn:active:not(.ant-btn-primary)';
default:
return selector;
}
},
// isJsUgly: true,
},
]);
}
// optimize chunks
config.optimization
// share the same chunks across different modules
.runtimeChunk(false)
.splitChunks({
chunks: 'async',
name: 'vendors',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendors: {
test: (module: { context: string }) => {
const packageName = getModulePackageName(module);
if (packageName) {
return ['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0;
}
return false;
},
name(module: { context: string }) {
const packageName = getModulePackageName(module);
if (packageName) {
if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
return 'viz'; // visualization package
}
}
return 'misc';
},
},
},
});
};
const getAntdSerials = (color: string) => {
const lightNum = 9;
const devide10 = 10;
// 淡化(即less的tint)
const lightens = new Array(lightNum).fill(undefined).map((_, i: number) => {
return ThemeColorReplacer.varyColor.lighten(color, i / devide10);
});
const colorPalettes = generate(color);
return lightens.concat(colorPalettes);
};
export default [
{
path: '/login',
component: '../layouts/UserLayout',
routes: [
{
path: './',
name: 'login',
icon: 'appstore',
component: './login',
},
],
},
// {
// path: '/test',
// component: '../layouts/BlankLayout',
// routes: [
// {
// path: './',
// name: 'test-login',
// icon: 'appstore',
// component: './test',
// },
// ],
// },
{
path: '/',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'],
routes: [
{
path: '/',
redirect: 'home',
},
{
path: 'home',
name: 'homepage',
icon: 'home',
component: './home/Home',
exact: true,
},
{
path: 'manage',
name: 'manage',
icon: 'appstore',
routes: [
{
path: './search',
name: 'commonSearch',
icon: 'appstore',
component: './manage/commonSearch',
},
{
path: './assets',
name: 'assets',
icon: 'appstore',
component: './assets',
},
// {
// path: './categories-search',
// name: 'categoriesSearch',
// icon: 'appstore',
// component: './manage/categorieSearch',
// },
// {
// path: './categories',
// name: 'categories',
// icon: 'appstore',
// component: './manage/categories',
// },
{
path: './metasearch',
name: 'metasearch',
icon: 'appstore',
component: './manage/metasearch',
},
{
path: './dataindicator',
name: 'dataindicator',
icon: 'appstore',
component: './data/dataindicator',
},
{
path: './datastandard',
name: 'datastandard',
icon: 'appstore',
component: './data/datastandard',
},
{
path: './dataquality',
name: 'dataquality',
icon: 'appstore',
component: './data/dataquality',
},
],
},
{
path: 'user',
name: 'user',
icon: 'user',
routes: [
{
path: 'subscrible',
name: 'user-subscrible',
icon: 'appstore',
component: './user/subscrible',
},
{
path: 'assets',
name: 'user-assets',
icon: 'appstore',
component: './user/assets',
},
{
path: 'question',
name: 'user-question',
icon: 'appstore',
component: './user/question',
},
// {
// path: 'apply',
// name: 'user-apply',
// icon: 'appstore',
// component: './user/apply',
// // authority: ['admin', 'user'],
// },
],
},
{
component: './404',
},
],
},
{
component: './404',
},
];
// ps https://github.com/GoogleChrome/puppeteer/issues/3120
module.exports = {
launch: {
args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--no-first-run',
'--no-zygote',
'--no-sandbox',
],
},
};
module.exports = {
testURL: 'http://localhost:8000',
preset: 'jest-puppeteer',
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
LOGIN_HREF: false,
},
};
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
import { Request, Response } from 'express';
const getNotices = (req: Request, res: Response) => {
res.json([
{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: 'notification',
},
{
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: 'notification',
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: 'notification',
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: 'notification',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: 'message',
clickClose: true,
},
{
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
type: 'event',
},
{
id: '000000010',
title: '第三方紧急代码变更',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: 'event',
},
{
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
type: 'event',
},
{
id: '000000012',
title: 'ABCD 版本发布',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: 'event',
},
]);
};
export default {
'GET /api/notices': getNotices,
};
export default {
'/api/auth_routes': {
'/form/advanced-form': { authority: ['admin', 'user'] },
},
};
import { Request, Response } from 'express';
// 代码中会兼容本地 service mock 以及部署站点的静态数据
export default {
// 支持值为 Object 和 Array
'GET /api/currentUser': {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
// GET POST 可省略
'GET /api/users': [
{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
},
{
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
},
{
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
},
],
'POST /api/login/account': (req: Request, res: Response) => {
const { password, userName, type } = req.body;
if (password === 'ant.design' && userName === 'admin') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin',
});
return;
}
if (password === 'ant.design' && userName === 'user') {
res.send({
status: 'ok',
type,
currentAuthority: 'user',
});
return;
}
res.send({
status: 'error',
type,
currentAuthority: 'guest',
});
},
'POST /api/register': (req: Request, res: Response) => {
res.send({ status: 'ok', currentAuthority: 'user' });
},
'GET /api/500': (req: Request, res: Response) => {
res.status(500).send({
timestamp: 1513932555104,
status: 500,
error: 'error',
message: 'error',
path: '/base/category/list',
});
},
'GET /api/404': (req: Request, res: Response) => {
res.status(404).send({
timestamp: 1513932643431,
status: 404,
error: 'Not Found',
message: 'No message available',
path: '/base/category/list/2121212',
});
},
'GET /api/403': (req: Request, res: Response) => {
res.status(403).send({
timestamp: 1513932555104,
status: 403,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/401': (req: Request, res: Response) => {
res.status(401).send({
timestamp: 1513932555104,
status: 401,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
};
{
"name": "ant-design-pro",
"version": "1.0.0",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {
"analyze": "cross-env ANALYZE=1 umi build",
"build": "cross-env umi build",
"deploy": "cross-env ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION=site npm run site && npm run gh-pages",
"fetch:blocks": "pro fetch-blocks",
"format-imports": "import-sort --write '**/*.{js,jsx,ts,tsx}'",
"gh-pages": "cp CNAME ./dist/ && gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./",
"lint:prettier": "check-prettier lint",
"lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
"prettier": "prettier -c --write \"**/*\"",
"start": "umi dev",
"start:no-mock": "cross-env MOCK=none umi dev",
"test": "umi test",
"test:all": "node ./tests/run-tests.js",
"test:component": "umi test ./src/components"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-staged"
}
},
"lint-staged": {
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write",
"git add"
],
"**/*.{js,jsx}": "npm run lint-staged:js",
"**/*.{js,ts,tsx}": "npm run lint-staged:js"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"dependencies": {
"@ant-design/pro-layout": "^4.5.16",
"@antv/data-set": "^0.10.2",
"@types/nprogress": "^0.2.0",
"@types/sha1": "^1.1.2",
"antd": "^3.25.0",
"classnames": "^2.2.6",
"dva": "^2.4.1",
"lodash": "^4.17.11",
"lodash-decorators": "^6.0.1",
"memoize-one": "^5.0.4",
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"omit.js": "^1.0.2",
"path-to-regexp": "^3.0.0",
"prop-types": "^15.7.2",
"qs": "^6.7.0",
"react": "^16.8.6",
"react-container-query": "^0.11.0",
"react-copy-to-clipboard": "^5.0.1",
"react-document-title": "^2.0.3",
"react-dom": "^16.8.6",
"react-helmet": "^5.2.1",
"react-media": "^1.9.2",
"react-media-hook2": "^1.0.5",
"redux": "^4.0.1",
"sha1": "^1.1.1",
"umi": "^2.12",
"umi-plugin-pro-block": "^1.3.2",
"umi-plugin-react": "^1.9.5",
"umi-request": "^1.0.8"
},
"devDependencies": {
"@ant-design/colors": "^3.1.0",
"@ant-design/pro-cli": "^1.0.0",
"@types/classnames": "^2.2.7",
"@types/history": "^4.7.2",
"@types/jest": "^24.0.13",
"@types/lodash": "^4.14.133",
"@types/qs": "^6.5.3",
"@types/react": "^16.8.19",
"@types/react-document-title": "^2.0.3",
"@types/react-dom": "^16.8.4",
"@umijs/fabric": "^1.1.0",
"chalk": "^2.4.2",
"check-prettier": "^1.0.3",
"cross-env": "^5.2.0",
"cross-port-killer": "^1.1.1",
"enzyme": "^3.9.0",
"eslint": "^5.16.0",
"gh-pages": "^2.0.1",
"husky": "^3.0.0",
"import-sort-cli": "^6.0.0",
"import-sort-parser-babylon": "^6.0.0",
"import-sort-parser-typescript": "^6.0.0",
"import-sort-style-module": "^6.0.0",
"jest-puppeteer": "^4.2.0",
"lint-staged": "^9.0.0",
"mockjs": "^1.0.1-beta3",
"node-fetch": "^2.6.0",
"prettier": "^1.17.1",
"pro-download": "1.0.1",
"slash2": "^2.0.0",
"stylelint": "^10.1.0",
"umi-plugin-ga": "^1.1.3",
"umi-plugin-pro": "^1.0.2",
"umi-types": "^0.3.8",
"webpack-theme-color-replacer": "^1.2.15"
},
"optionalDependencies": {},
"engines": {
"node": ">=10.0.0"
},
"checkFiles": [
"src/**/*.js*",
"src/**/*.ts*",
"src/**/*.less",
"config/**/*.js*",
"scripts/**/*.js"
]
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
<title>Group 28 Copy 5</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="62.1023273%" y1="0%" x2="108.19718%" y2="37.8635764%" id="linearGradient-1">
<stop stop-color="#4285EB" offset="0%"></stop>
<stop stop-color="#2EC7FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="69.644116%" y1="0%" x2="54.0428975%" y2="108.456714%" id="linearGradient-2">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#148EFF" offset="37.8600687%"></stop>
<stop stop-color="#0A60FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="69.6908165%" y1="-12.9743587%" x2="16.7228981%" y2="117.391248%" id="linearGradient-3">
<stop stop-color="#FA816E" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="41.472606%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
<linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-4">
<stop stop-color="#FA8E7D" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="51.2635191%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="logo" transform="translate(-20.000000, -20.000000)">
<g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)">
<g id="Group-27-Copy-3">
<g id="Group-25" fill-rule="nonzero">
<g id="2">
<path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-1)"></path>
<path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-2)"></path>
</g>
<path d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z" id="Shape" fill="url(#linearGradient-3)"></path>
</g>
<ellipse id="Combined-Shape" fill="url(#linearGradient-4)" cx="100.519339" cy="100.436681" rx="23.6001926" ry="23.580786"></ellipse>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
import React from 'react';
import check, { IAuthorityType } from './CheckPermissions';
import AuthorizedRoute from './AuthorizedRoute';
import Secured from './Secured';
interface AuthorizedProps {
authority: IAuthorityType;
noMatch?: React.ReactNode;
}
export type IAuthorizedType = React.FunctionComponent<AuthorizedProps> & {
Secured: typeof Secured;
check: typeof check;
AuthorizedRoute: typeof AuthorizedRoute;
};
const Authorized: React.FunctionComponent<AuthorizedProps> = ({
children,
authority,
noMatch = null,
}) => {
const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
const dom = check(authority, childrenRender, noMatch);
return <>{dom}</>;
};
export default Authorized as IAuthorizedType;
import { Redirect, Route } from 'umi';
import React from 'react';
import Authorized from './Authorized';
import { IAuthorityType } from './CheckPermissions';
interface AuthorizedRoutePops {
currentAuthority: string;
component: React.ComponentClass<any, any>;
render: (props: any) => React.ReactNode;
redirectPath: string;
authority: IAuthorityType;
}
const AuthorizedRoute: React.SFC<AuthorizedRoutePops> = ({
component: Component,
render,
authority,
redirectPath,
...rest
}) => (
<Authorized
authority={authority}
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
>
<Route
{...rest}
render={(props: any) => (Component ? <Component {...props} /> : render(props))}
/>
</Authorized>
);
export default AuthorizedRoute;
import React from 'react';
import { CURRENT } from './renderAuthorize';
// eslint-disable-next-line import/no-cycle
import PromiseRender from './PromiseRender';
export type IAuthorityType =
| undefined
| string
| string[]
| Promise<boolean>
| ((currentAuthority: string | string[]) => IAuthorityType);
/**
* 通用权限检查方法
* Common check permissions method
* @param { 权限判定 | Permission judgment } authority
* @param { 你的权限 | Your permission description } currentAuthority
* @param { 通过的组件 | Passing components } target
* @param { 未通过的组件 | no pass components } Exception
*/
const checkPermissions = <T, K>(
authority: IAuthorityType,
currentAuthority: string | string[],
target: T,
Exception: K,
): T | K | React.ReactNode => {
// 没有判定权限.默认查看所有
// Retirement authority, return target;
if (!authority) {
return target;
}
// 数组处理
if (Array.isArray(authority)) {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some(item => authority.includes(item))) {
return target;
}
} else if (authority.includes(currentAuthority)) {
return target;
}
return Exception;
}
// string 处理
if (typeof authority === 'string') {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some(item => authority === item)) {
return target;
}
} else if (authority === currentAuthority) {
return target;
}
return Exception;
}
// Promise 处理
if (authority instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
}
// Function 处理
if (typeof authority === 'function') {
try {
const bool = authority(currentAuthority);
// 函数执行后返回值是 Promise
if (bool instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
}
if (bool) {
return target;
}
return Exception;
} catch (error) {
throw error;
}
}
throw new Error('unsupported parameters');
};
export { checkPermissions };
function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
return checkPermissions<T, K>(authority, CURRENT, target, Exception);
}
export default check;
import React from 'react';
import { Spin } from 'antd';
import isEqual from 'lodash/isEqual';
import { isComponentClass } from './Secured';
// eslint-disable-next-line import/no-cycle
interface PromiseRenderProps<T, K> {
ok: T;
error: K;
promise: Promise<boolean>;
}
interface PromiseRenderState {
component: React.ComponentClass | React.FunctionComponent;
}
export default class PromiseRender<T, K> extends React.Component<
PromiseRenderProps<T, K>,
PromiseRenderState
> {
state: PromiseRenderState = {
component: () => null,
};
componentDidMount() {
this.setRenderComponent(this.props);
}
shouldComponentUpdate = (nextProps: PromiseRenderProps<T, K>, nextState: PromiseRenderState) => {
const { component } = this.state;
if (!isEqual(nextProps, this.props)) {
this.setRenderComponent(nextProps);
}
if (nextState.component !== component) return true;
return false;
};
// set render Component : ok or error
setRenderComponent(props: PromiseRenderProps<T, K>) {
const ok = this.checkIsInstantiation(props.ok);
const error = this.checkIsInstantiation(props.error);
props.promise
.then(() => {
this.setState({
component: ok,
});
return true;
})
.catch(() => {
this.setState({
component: error,
});
});
}
// Determine whether the incoming component has been instantiated
// AuthorizedRoute is already instantiated
// Authorized render is already instantiated, children is no instantiated
// Secured is not instantiated
checkIsInstantiation = (
target: React.ReactNode | React.ComponentClass,
): React.FunctionComponent => {
if (isComponentClass(target)) {
const Target = target as React.ComponentClass;
return (props: any) => <Target {...props} />;
}
if (React.isValidElement(target)) {
return (props: any) => React.cloneElement(target, props);
}
return () =>
target as (React.ReactElement<
any,
| string
| ((
props: any,
) => React.ReactElement<
any,
string | any | (new (props: any) => React.Component<any, any, any>)
> | null)
| (new (props: any) => React.Component<any, any, any>)
> | null);
};
render() {
const { component: Component } = this.state;
const { ok, error, promise, ...rest } = this.props;
return Component ? (
<Component {...rest} />
) : (
<div
style={{
width: '100%',
height: '100%',
margin: 'auto',
paddingTop: 50,
textAlign: 'center',
}}
>
<Spin size="large" />
</div>
);
}
}
import React from 'react';
import CheckPermissions from './CheckPermissions';
/**
* 默认不能访问任何页面
* default is "NULL"
*/
const Exception403 = () => 403;
export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => {
if (!component) return false;
const proto = Object.getPrototypeOf(component);
if (proto === React.Component || proto === Function.prototype) return true;
return isComponentClass(proto);
};
// Determine whether the incoming component has been instantiated
// AuthorizedRoute is already instantiated
// Authorized render is already instantiated, children is no instantiated
// Secured is not instantiated
const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
if (isComponentClass(target)) {
const Target = target as React.ComponentClass;
return (props: any) => <Target {...props} />;
}
if (React.isValidElement(target)) {
return (props: any) => React.cloneElement(target, props);
}
return () => target;
};
/**
* 用于判断是否拥有权限访问此 view 权限
* authority 支持传入 string, () => boolean | Promise
* e.g. 'user' 只有 user 用户能访问
* e.g. 'user,admin' user 和 admin 都能访问
* e.g. ()=>boolean 返回true能访问,返回false不能访问
* e.g. Promise then 能访问 catch不能访问
* e.g. authority support incoming string, () => boolean | Promise
* e.g. 'user' only user user can access
* e.g. 'user, admin' user and admin can access
* e.g. () => boolean true to be able to visit, return false can not be accessed
* e.g. Promise then can not access the visit to catch
* @param {string | function | Promise} authority
* @param {ReactNode} error 非必需参数
*/
const authorize = (authority: string, error?: React.ReactNode) => {
/**
* conversion into a class
* 防止传入字符串时找不到staticContext造成报错
* String parameters can cause staticContext not found error
*/
let classError: boolean | React.FunctionComponent = false;
if (error) {
classError = (() => error) as React.FunctionComponent;
}
if (!authority) {
throw new Error('authority is required');
}
return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
const component = CheckPermissions(authority, target, classError || Exception403);
return checkIsInstantiation(component);
};
};
export default authorize;
import Authorized from './Authorized';
import AuthorizedRoute from './AuthorizedRoute';
import Secured from './Secured';
import check from './CheckPermissions';
import renderAuthorize from './renderAuthorize';
Authorized.Secured = Secured;
Authorized.AuthorizedRoute = AuthorizedRoute;
Authorized.check = check;
const RenderAuthorize = renderAuthorize(Authorized);
export default RenderAuthorize;
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable import/no-mutable-exports */
let CURRENT: string | string[] = 'NULL';
type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
/**
* use authority or getAuthority
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = <T>(Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
currentAuthority: CurrentAuthorityType,
): T => {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (
Object.prototype.toString.call(currentAuthority) === '[object String]' ||
Array.isArray(currentAuthority)
) {
CURRENT = currentAuthority as string[];
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default <T>(Authorized: T) => renderAuthorize<T>(Authorized);
@import '~antd/es/style/themes/default.less';
@basicLayout-prefix-cls: ~'@{ant-prefix}-pro-basicLayout-content';
.@{basicLayout-prefix-cls} {
margin: 24px;
padding-top: @layout-header-height;
}
.basicLayout {
.ant-layout {
transition: all 0.2s;
}
}
import './BasicLayout.less';
import React, { useState } from 'react';
import { BreadcrumbProps as AntdBreadcrumbProps } from 'antd/es/breadcrumb';
import { ContainerQuery } from 'react-container-query';
import DocumentTitle from 'react-document-title';
import { Layout } from 'antd';
import classNames from 'classnames';
import useMedia from 'react-media-hook2';
import {
MenuDataItem,
MessageDescriptor,
Route,
RouterTypes,
WithFalse,
} from '@ant-design/pro-layout/es/typings';
import defaultGetPageTitle, { GetPageTitleProps } from '@ant-design/pro-layout/es/getPageTitle';
import defaultSettings, { Settings } from '@ant-design/pro-layout/es/defaultSettings';
import getLocales, { localeType } from '@ant-design/pro-layout/es/locales';
import { getBreadcrumbProps } from '@ant-design/pro-layout/es/utils/getBreadcrumbProps';
import getMenuData from '@ant-design/pro-layout/es/utils/getMenuData';
import { isBrowser } from '@ant-design/pro-layout/es/utils/utils';
import RouteContext from './RouteContext';
import { BaseMenuProps } from './SiderMenu/BaseMenu';
import Footer from './Footer';
import SiderMenu from './SiderMenu';
import { SiderMenuProps } from './SiderMenu/SiderMenu';
import Header, { HeaderViewProps } from './Header';
const { Content } = Layout;
const query = {
'screen-xs': {
maxWidth: 575,
},
'screen-sm': {
minWidth: 576,
maxWidth: 767,
},
'screen-md': {
minWidth: 768,
maxWidth: 991,
},
'screen-lg': {
minWidth: 992,
maxWidth: 1199,
},
'screen-xl': {
minWidth: 1200,
maxWidth: 1599,
},
'screen-xxl': {
minWidth: 1600,
},
};
export interface BasicLayoutProps
extends Partial<RouterTypes<Route>>,
SiderMenuProps,
HeaderViewProps,
Partial<Settings> {
logo?: React.ReactNode | WithFalse<() => React.ReactNode>;
locale?: localeType;
onCollapse?: (collapsed: boolean) => void;
headerRender?: WithFalse<(props: HeaderViewProps) => React.ReactNode>;
footerRender?: WithFalse<
(props: HeaderViewProps, defaultDom: React.ReactNode) => React.ReactNode
>;
menuRender?: WithFalse<(props: HeaderViewProps, defaultDom: React.ReactNode) => React.ReactNode>;
menuItemRender?: BaseMenuProps['menuItemRender'];
pageTitleRender?: WithFalse<typeof defaultGetPageTitle>;
formatMessage?: (message: MessageDescriptor) => string;
menuDataRender?: (menuData: MenuDataItem[]) => MenuDataItem[];
breadcrumbRender?: (routers: AntdBreadcrumbProps['routes']) => AntdBreadcrumbProps['routes'];
itemRender?: AntdBreadcrumbProps['itemRender'];
/**
* 是否禁用移动端模式,有的管理系统不需要移动端模式,此属性设置为true即可
*/
disableMobile?: boolean;
}
const headerRender = (props: BasicLayoutProps): React.ReactNode => {
if (props.headerRender === false) {
return null;
}
return <Header {...props} />;
};
const footerRender = (props: BasicLayoutProps): React.ReactNode => {
if (props.footerRender === false) {
return null;
}
if (props.footerRender) {
return props.footerRender({ ...props }, <Footer />);
}
return <Footer />;
};
const renderSiderMenu = (props: BasicLayoutProps): React.ReactNode => {
const { layout, isMobile, menuRender } = props;
if (props.menuRender === false) {
return null;
}
if (layout === 'topmenu' && !isMobile) {
return null;
}
if (menuRender) {
return menuRender(props, <SiderMenu {...props} />);
}
return <SiderMenu {...props} {...props.menuProps} />;
};
const defaultPageTitleRender = (pageProps: GetPageTitleProps, props: BasicLayoutProps): string => {
const { pageTitleRender } = props;
if (pageTitleRender === false) {
return props.title || '';
}
if (pageTitleRender) {
const title = pageTitleRender(pageProps);
if (typeof title === 'string') {
return title;
}
}
return defaultGetPageTitle(pageProps);
};
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
breadcrumb: { [path: string]: MenuDataItem };
};
function useCollapsed(
collapsed: boolean | undefined,
onCollapse: BasicLayoutProps['onCollapse'],
): [boolean | undefined, BasicLayoutProps['onCollapse']] {
const [nativeCollapsed, setCollapsed] = useState(false);
if (collapsed !== undefined && onCollapse) {
return [collapsed, onCollapse];
}
if (collapsed !== undefined && !onCollapse) {
return [collapsed, undefined];
}
if (collapsed === undefined && onCollapse) {
return [undefined, onCollapse];
}
return [nativeCollapsed, setCollapsed];
}
const getPaddingLeft = (
hasLeftPadding: boolean,
collapsed: boolean | undefined,
siderWidth: number,
): number | undefined => {
if (hasLeftPadding) {
return collapsed ? 80 : siderWidth;
}
return undefined;
};
const BasicLayout: React.FC<BasicLayoutProps> = props => {
const {
children,
onCollapse: propsOnCollapse,
location = { pathname: '/' },
fixedHeader,
fixSiderbar,
navTheme,
layout: PropsLayout,
route = {
routes: [],
},
siderWidth = 256,
menu,
menuDataRender,
} = props;
const formatMessage = ({
id,
defaultMessage,
...rest
}: {
id: string;
defaultMessage?: string;
}): string => {
if (props.formatMessage) {
return props.formatMessage({
id,
defaultMessage,
...rest,
});
}
const locales = getLocales();
if (locales[id]) {
return locales[id];
}
if (defaultMessage) {
return defaultMessage;
}
return id;
};
const { routes = [] } = route;
const { breadcrumb, menuData } = getMenuData(routes, menu, formatMessage, menuDataRender);
/**
* init variables
*/
const isMobile = props.disableMobile
? false
: // eslint-disable-next-line react-hooks/rules-of-hooks
useMedia({
id: 'BasicLayout',
query: '(max-width: 599px)',
targetWindow: window || {
matchMedia: () => true,
},
})[0];
// If it is a fix menu, calculate padding
// don't need padding in phone mode
const hasLeftPadding = fixSiderbar && PropsLayout !== 'topmenu' && !isMobile;
// whether to close the menu
const [collapsed, onCollapse] = useCollapsed(props.collapsed, propsOnCollapse);
// Splicing parameters, adding menuData and formatMessage in props
const defaultProps = {
...props,
formatMessage,
breadcrumb,
};
// gen page title
const pageTitle = defaultPageTitleRender(
{
pathname: location.pathname,
...defaultProps,
},
props,
);
// gen breadcrumbProps, parameter for pageHeader
const breadcrumbProps = getBreadcrumbProps({
...props,
breadcrumb,
});
return (
<DocumentTitle title={pageTitle}>
<ContainerQuery query={query}>
{params => (
<div className={classNames(params, 'ant-design-pro', 'basicLayout')}>
<Layout>
{renderSiderMenu({
...defaultProps,
menuData,
onCollapse,
isMobile,
theme: navTheme,
collapsed,
})}
<Layout
style={{
paddingLeft: getPaddingLeft(!!hasLeftPadding, collapsed, siderWidth),
minHeight: '100vh',
}}
>
{headerRender({
...defaultProps,
menuData,
isMobile,
collapsed,
onCollapse,
})}
<Content
className="ant-pro-basicLayout-content"
style={!fixedHeader ? { paddingTop: 0 } : {}}
>
<RouteContext.Provider
value={{
breadcrumb: breadcrumbProps,
...props,
menuData,
isMobile,
collapsed,
title: pageTitle.split('-')[0].trim(),
}}
>
{children}
</RouteContext.Provider>
</Content>
{footerRender({
isMobile,
collapsed,
...defaultProps,
})}
</Layout>
</Layout>
</div>
)}
</ContainerQuery>
</DocumentTitle>
);
};
BasicLayout.defaultProps = {
logo: 'https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg',
...defaultSettings,
location: isBrowser() ? window.location : undefined,
};
export default BasicLayout;
.copy-block {
position: fixed;
right: 80px;
bottom: 40px;
z-index: 99;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
font-size: 20px;
background: #fff;
border-radius: 40px;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12);
cursor: pointer;
}
.copy-block-view {
position: relative;
.copy-block-code {
display: inline-block;
margin: 0 0.2em;
padding: 0.2em 0.4em 0.1em;
font-size: 85%;
border-radius: 3px;
}
}
import { Icon, Popover, Typography } from 'antd';
import React, { useRef } from 'react';
import { FormattedMessage } from 'umi-plugin-react/locale';
import { connect } from 'dva';
import { isAntDesignPro } from '@/utils/utils';
import styles from './index.less';
const firstUpperCase = (pathString: string): string =>
pathString
.replace('.', '')
.split(/\/|-/)
.map((s): string => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()))
.filter((s): boolean => !!s)
.join('');
// when click block copy, send block url to ga
const onBlockCopy = (label: string) => {
if (!isAntDesignPro()) {
return;
}
const ga = window && window.ga;
if (ga) {
ga('send', 'event', {
eventCategory: 'block',
eventAction: 'copy',
eventLabel: label,
});
}
};
const BlockCodeView: React.SFC<{
url: string;
}> = ({ url }) => {
const blockUrl = `npx umi block add ${firstUpperCase(url)} --path=${url}`;
return (
<div className={styles['copy-block-view']}>
<Typography.Paragraph
copyable={{
text: blockUrl,
onCopy: () => onBlockCopy(url),
}}
style={{
display: 'flex',
}}
>
<pre>
<code className={styles['copy-block-code']}>{blockUrl}</code>
</pre>
</Typography.Paragraph>
</div>
);
};
interface RoutingType {
location: {
pathname: string;
};
}
export default connect(({ routing }: { routing: RoutingType }) => ({
location: routing.location,
}))(({ location }: RoutingType) => {
const url = location.pathname;
const divDom = useRef<HTMLDivElement>(null);
return (
<Popover
title={<FormattedMessage id="app.preview.down.block" defaultMessage="下载此页面到本地项目" />}
placement="topLeft"
content={<BlockCodeView url={url} />}
trigger="click"
getPopupContainer={dom => (divDom.current ? divDom.current : dom)}
>
<div className={styles['copy-block']} ref={divDom}>
<Icon type="download" />
</div>
</Popover>
);
});
import { Icon, Layout } from 'antd';
import React, { Fragment } from 'react';
import GlobalFooter from '@ant-design/pro-layout/es/GlobalFooter';
const { Footer } = Layout;
const defaultLinks = [
{
key: 'Ant Design Pro',
title: 'Ant Design Pro',
href: 'https://pro.ant.design',
blankTarget: true,
},
{
key: 'github',
title: <Icon type="github" />,
href: 'https://github.com/ant-design/ant-design-pro',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
];
const defaultCopyright = '2019 蚂蚁金服体验技术部出品';
export interface FooterProps {
links?: {
key?: string;
title: React.ReactNode;
href: string;
blankTarget?: boolean;
}[];
copyright?: string;
}
const FooterView: React.FC<FooterProps> = ({ links, copyright }: FooterProps) => (
<Footer style={{ padding: 0 }}>
<GlobalFooter
links={links || defaultLinks}
copyright={
<Fragment>
Copyright <Icon type="copyright" /> {copyright || defaultCopyright}
</Fragment>
}
/>
</Footer>
);
export default FooterView;
import { Avatar, Icon, Menu, Spin } from 'antd';
import { ClickParam } from 'antd/es/menu';
import { FormattedMessage } from 'umi-plugin-react/locale';
import React from 'react';
import { connect } from 'dva';
import router from 'umi/router';
import { ConnectProps, ConnectState } from '@/models/connect';
import { CurrentUser } from '@/models/user';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
export interface GlobalHeaderRightProps extends ConnectProps {
currentUser?: CurrentUser;
menu?: boolean;
}
class AvatarDropdown extends React.Component<GlobalHeaderRightProps> {
onMenuClick = (event: ClickParam) => {
const { key } = event;
if (key === 'logout') {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'login/logout',
});
}
return;
}
router.push(`/account/${key}`);
};
render(): React.ReactNode {
const { currentUser, menu } = this.props;
const { userName = '' } = currentUser || {};
if (!menu) {
return (
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} icon="user" alt="avatar" />
<span className={styles.name}>{userName}</span>
</span>
);
}
const menuHeaderDropdown = (
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
{/* <Menu.Item key="center">
<Icon type="user" />
<FormattedMessage id="menu.account.center" defaultMessage="account center" />
</Menu.Item>
<Menu.Item key="settings">
<Icon type="setting" />
<FormattedMessage id="menu.account.settings" defaultMessage="account settings" />
</Menu.Item>
<Menu.Divider /> */}
<Menu.Item key="logout">
<Icon type="logout" />
<FormattedMessage id="menu.account.logout" defaultMessage="logout" />
</Menu.Item>
</Menu>
);
let renderUser = <Spin size="small" style={{ marginLeft: 8, marginRight: 8 }} />;
if (currentUser && currentUser.userName) {
if (LOGIN_HREF === 'dataCatalog') {
renderUser = (
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} icon="user" alt="avatar" />
<span
className={styles.name}
>{`${currentUser.userDName}/${currentUser.userName}`}</span>
</span>
);
} else {
renderUser = (
<HeaderDropdown overlay={menuHeaderDropdown}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} icon="user" alt="avatar" />
<span
className={styles.name}
>{`${currentUser.userDName}/${currentUser.userName}`}</span>
</span>
</HeaderDropdown>
);
}
}
return renderUser;
}
}
export default connect(({ user }: ConnectState) => ({
currentUser: user.currentUser,
}))(AvatarDropdown);
import React, { Component } from 'react';
import { Tag, message } from 'antd';
import { connect } from 'dva';
import { formatMessage } from 'umi-plugin-react/locale';
import groupBy from 'lodash/groupBy';
import moment from 'moment';
import { NoticeItem } from '@/models/global';
import NoticeIcon from '../NoticeIcon';
import { CurrentUser } from '@/models/user';
import { ConnectProps, ConnectState } from '@/models/connect';
import styles from './index.less';
export interface GlobalHeaderRightProps extends ConnectProps {
notices?: NoticeItem[];
currentUser?: CurrentUser;
fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void;
onNoticeClear?: (tabName?: string) => void;
}
class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
componentDidMount() {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'global/fetchNotices',
});
}
}
changeReadState = (clickedItem: NoticeItem): void => {
const { id } = clickedItem;
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'global/changeNoticeReadState',
payload: id,
});
}
};
handleNoticeClear = (title: string, key: string) => {
const { dispatch } = this.props;
message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`);
if (dispatch) {
dispatch({
type: 'global/clearNotices',
payload: key,
});
}
};
getNoticeData = (): { [key: string]: NoticeItem[] } => {
const { notices = [] } = this.props;
if (notices.length === 0) {
return {};
}
const newNotices = notices.map(notice => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime as string).fromNow();
}
if (newNotice.id) {
newNotice.key = newNotice.id;
}
if (newNotice.extra && newNotice.status) {
const color = {
todo: '',
processing: 'blue',
urgent: 'red',
doing: 'gold',
}[newNotice.status];
newNotice.extra = (
<Tag color={color} style={{ marginRight: 0 }}>
{newNotice.extra}
</Tag>
);
}
return newNotice;
});
return groupBy(newNotices, 'type');
};
getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
const unreadMsg: { [key: string]: number } = {};
Object.keys(noticeData).forEach(key => {
const value = noticeData[key];
if (!unreadMsg[key]) {
unreadMsg[key] = 0;
}
if (Array.isArray(value)) {
unreadMsg[key] = value.filter(item => !item.read).length;
}
});
return unreadMsg;
};
render() {
const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
const noticeData = this.getNoticeData();
const unreadMsg = this.getUnreadData(noticeData);
return (
<NoticeIcon
className={styles.action}
count={currentUser && currentUser.unreadCount}
onItemClick={item => {
this.changeReadState(item as NoticeItem);
}}
loading={fetchingNotices}
clearText={formatMessage({ id: 'component.noticeIcon.clear' })}
viewMoreText={formatMessage({ id: 'component.noticeIcon.view-more' })}
onClear={this.handleNoticeClear}
onPopupVisibleChange={onNoticeVisibleChange}
onViewMore={() => message.info('Click on view more')}
clearClose
>
<NoticeIcon.Tab
tabKey="notification"
count={unreadMsg.notification}
list={noticeData.notification}
title={formatMessage({ id: 'component.globalHeader.notification' })}
emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })}
showViewMore
/>
<NoticeIcon.Tab
tabKey="message"
count={unreadMsg.message}
list={noticeData.message}
title={formatMessage({ id: 'component.globalHeader.message' })}
emptyText={formatMessage({ id: 'component.globalHeader.message.empty' })}
showViewMore
/>
<NoticeIcon.Tab
tabKey="event"
title={formatMessage({ id: 'component.globalHeader.event' })}
emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })}
count={unreadMsg.event}
list={noticeData.event}
showViewMore
/>
</NoticeIcon>
);
}
}
export default connect(({ user, global, loading }: ConnectState) => ({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
fetchingNotices: loading.effects['global/fetchNotices'],
notices: global.notices,
}))(GlobalHeaderRight);
// import { Icon, Tooltip } from 'antd';
import React from 'react';
import { connect } from 'dva';
// import { formatMessage } from 'umi-plugin-react/locale';
import { ConnectProps, ConnectState } from '@/models/connect';
import Avatar from './AvatarDropdown';
// import HeaderSearch from '../HeaderSearch';
import SelectLang from '../SelectLang';
import styles from './index.less';
export type SiderTheme = 'light' | 'dark';
export interface GlobalHeaderRightProps extends ConnectProps {
theme?: SiderTheme;
layout: 'sidemenu' | 'topmenu';
}
const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = props => {
const { theme, layout } = props;
let className = styles.right;
if (theme === 'dark' && layout === 'topmenu') {
className = `${styles.right} ${styles.dark}`;
}
return (
<div className={className}>
{/* <HeaderSearch
className={`${styles.action} ${styles.search}`}
placeholder={formatMessage({
id: 'component.globalHeader.search',
})}
dataSource={[
formatMessage({
id: 'component.globalHeader.search.example1',
}),
formatMessage({
id: 'component.globalHeader.search.example2',
}),
formatMessage({
id: 'component.globalHeader.search.example3',
}),
]}
onSearch={value => {
console.log('input', value);
}}
onPressEnter={value => {
console.log('enter', value);
}}
/> */}
{/* <Tooltip
title={formatMessage({
id: 'component.globalHeader.help',
})}
>
<a
target="_blank"
href="https://pro.ant.design/docs/getting-started"
rel="noopener noreferrer"
className={styles.action}
>
<Icon type="question-circle-o" />
</a>
</Tooltip> */}
<Avatar menu {...props} />
<SelectLang className={styles.action} {...props} />
</div>
);
};
export default connect(({ settings }: ConnectState) => ({
theme: settings.navTheme,
layout: settings.layout,
}))(GlobalHeaderRight);
@import '~antd/es/style/themes/default.less';
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
.logo {
display: inline-block;
height: @layout-header-height;
padding: 0 0 0 24px;
font-size: 20px;
line-height: @layout-header-height;
vertical-align: top;
cursor: pointer;
img {
display: inline-block;
vertical-align: middle;
}
}
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
min-width: 160px;
}
}
.trigger {
height: @layout-header-height;
padding: ~'calc((@{layout-header-height} - 20px) / 2)' 24px;
font-size: 20px;
cursor: pointer;
transition: all 0.3s, padding 0s;
&:hover {
background: @pro-header-hover-bg;
}
}
.right {
float: right;
height: 64px;
overflow: hidden;
.action {
display: inline-block;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
> i {
color: @text-color;
vertical-align: middle;
}
&:hover {
background: @pro-header-hover-bg;
}
&:global(.opened) {
background: @pro-header-hover-bg;
}
}
.search {
padding: 0 12px;
&:hover {
background: transparent;
}
}
.account {
.avatar {
margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
margin-right: 8px;
color: @primary-color;
vertical-align: top;
background: rgba(255, 255, 255, 0.85);
}
}
}
.dark {
height: @layout-header-height;
.action {
color: rgba(255, 255, 255, 0.85);
> i {
color: rgba(255, 255, 255, 0.85);
}
&:hover,
&:global(.opened) {
background: @primary-color;
}
:global(.ant-badge) {
color: rgba(255, 255, 255, 0.85);
}
}
}
@media only screen and (max-width: @screen-md) {
:global(.ant-divider-vertical) {
vertical-align: unset;
}
.name {
display: none;
}
i.trigger {
padding: 22px 12px;
}
.logo {
position: relative;
padding-right: 12px;
padding-left: 12px;
}
.right {
position: absolute;
top: 0;
right: 12px;
// background: #fff;
.account {
.avatar {
margin-right: 0;
}
}
}
}
.header :global(.ant-menu) {
background-color: transparent;
}
.header :global(.ant-menu-submenu),
.header :global(.ant-menu-item) > a {
color: #fff;
}
.header {
.right {
.action {
color: #fff;
> i {
color: #fff;
}
}
}
.action {
color: #fff;
> i {
color: #fff;
}
}
}
.action {
display: inline-block;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
> i {
color: @text-color;
// vertical-align: middle;
}
&:hover {
background: @pro-header-hover-bg;
}
&:global(.opened) {
background: @pro-header-hover-bg;
}
}
@import '~antd/es/style/themes/default.less';
@grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content';
.@{grid-content-prefix-cls} {
width: 100%;
height: 100%;
min-height: 100%;
transition: 0.3s;
&.wide {
max-width: 1200px;
margin: 0 auto;
}
}
import './GridContent.less';
import React from 'react';
import { Settings } from '@ant-design/pro-layout/es/defaultSettings';
import RouteContext from '../RouteContext';
interface GridContentProps {
contentWidth?: Settings['contentWidth'];
children: React.ReactNode;
}
const GridContent: React.SFC<GridContentProps> = props => (
<RouteContext.Consumer>
{value => {
const { children, contentWidth: propsContentWidth } = props;
const contentWidth = propsContentWidth || value.contentWidth;
let className = 'ant-pro-grid-content';
if (contentWidth === 'Fixed') {
className = 'ant-pro-grid-content wide';
}
return <div className={className}>{children}</div>;
}}
</RouteContext.Consumer>
);
export default GridContent;
@import '~antd/es/style/themes/default.less';
@fixed-header-prefix-cls: ~'@{ant-prefix}-pro-fixed-header';
.@{fixed-header-prefix-cls} {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: 100%;
transition: width 0.2s;
}
import './Header.less';
import React, { Component } from 'react';
import { Layout } from 'antd';
import GlobalHeader, { GlobalHeaderProps } from '@ant-design/pro-layout/es/GlobalHeader';
import { Settings } from '@ant-design/pro-layout/es/defaultSettings';
import { WithFalse } from '@ant-design/pro-layout/es/typings';
import TopNavHeader from './TopNavHeader';
import { BasicLayoutProps } from './BasicLayout';
const { Header } = Layout;
export interface HeaderViewProps extends Partial<Settings>, GlobalHeaderProps {
isMobile?: boolean;
collapsed?: boolean;
logo?: React.ReactNode;
autoHideHeader?: boolean;
menuRender?: BasicLayoutProps['menuRender'];
headerRender?: BasicLayoutProps['headerRender'];
rightContentRender?: WithFalse<(props: HeaderViewProps) => React.ReactNode>;
siderWidth?: number;
}
interface HeaderViewState {
visible: boolean;
}
class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
static getDerivedStateFromProps(
props: HeaderViewProps,
state: HeaderViewState,
): HeaderViewState | null {
if (!props.autoHideHeader && !state.visible) {
return {
visible: true,
};
}
return null;
}
state = {
visible: true,
};
ticking: boolean = false;
oldScrollTop: number = 0;
componentDidMount(): void {
document.addEventListener('scroll', this.handScroll, { passive: true });
}
componentWillUnmount(): void {
document.removeEventListener('scroll', this.handScroll);
}
getHeadWidth = () => {
const { isMobile, collapsed, fixedHeader, layout, siderWidth = 256 } = this.props;
if (isMobile || !fixedHeader || layout === 'topmenu') {
return '100%';
}
return collapsed ? 'calc(100% - 80px)' : `calc(100% - ${siderWidth}px)`;
};
handScroll = () => {
const { autoHideHeader } = this.props;
const { visible } = this.state;
if (!autoHideHeader) {
return;
}
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop;
if (!this.ticking) {
this.ticking = true;
requestAnimationFrame(() => {
if (this.oldScrollTop > scrollTop) {
this.setState({
visible: true,
});
} else if (scrollTop > 300 && visible) {
this.setState({
visible: false,
});
} else if (scrollTop < 300 && !visible) {
this.setState({
visible: true,
});
}
this.oldScrollTop = scrollTop;
this.ticking = false;
});
}
};
renderContent = () => {
const { isMobile, onCollapse, navTheme, layout, headerRender } = this.props;
const isTop = layout === 'topmenu';
let defaultDom = <GlobalHeader onCollapse={onCollapse} {...this.props} />;
if (isTop && !isMobile) {
defaultDom = (
<TopNavHeader theme={navTheme} mode="horizontal" onCollapse={onCollapse} {...this.props} />
);
}
if (headerRender) {
return headerRender(this.props);
}
return defaultDom;
};
render(): React.ReactNode {
const { fixedHeader } = this.props;
const { visible } = this.state;
const width = this.getHeadWidth();
return visible ? (
<Header
style={{ padding: 0, width, zIndex: 2 }}
className={fixedHeader ? 'ant-pro-fixed-header' : ''}
>
{this.renderContent()}
</Header>
) : null;
}
}
export default HeaderView;
@import '~antd/es/style/themes/default.less';
.container > * {
background-color: #fff;
border-radius: 4px;
box-shadow: @shadow-1-down;
}
@media screen and (max-width: @screen-xs) {
.container {
width: 100% !important;
}
.container > * {
border-radius: 0 !important;
}
}
import { DropDownProps } from 'antd/es/dropdown';
import { Dropdown } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
declare type OverlayFunc = () => React.ReactNode;
export interface HeaderDropdownProps extends DropDownProps {
overlayClassName?: string;
overlay: React.ReactNode | OverlayFunc;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
}
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
);
export default HeaderDropdown;
@import '~antd/es/style/themes/default.less';
.headerSearch {
:global(.anticon-search) {
font-size: 16px;
cursor: pointer;
}
.input {
width: 0;
background: transparent;
border-radius: 0;
transition: width 0.3s, margin-left 0.3s;
:global(.ant-select-selection) {
background: transparent;
}
input {
padding-right: 0;
padding-left: 0;
border: 0;
box-shadow: none !important;
}
&,
&:hover,
&:focus {
border-bottom: 1px solid @border-color-base;
}
&.show {
width: 210px;
margin-left: 8px;
}
}
}
import { AutoComplete, Icon, Input } from 'antd';
import { AutoCompleteProps, DataSourceItemType } from 'antd/es/auto-complete';
import React, { Component } from 'react';
import classNames from 'classnames';
import debounce from 'lodash/debounce';
import styles from './index.less';
export interface HeaderSearchProps {
onPressEnter: (value: string) => void;
onSearch: (value: string) => void;
onChange: (value: string) => void;
onVisibleChange: (b: boolean) => void;
className: string;
placeholder: string;
defaultActiveFirstOption: boolean;
dataSource: DataSourceItemType[];
defaultOpen: boolean;
open?: boolean;
}
interface HeaderSearchState {
value: string;
searchMode: boolean;
}
export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> {
static defaultProps = {
defaultActiveFirstOption: false,
onPressEnter: () => {},
onSearch: () => {},
onChange: () => {},
className: '',
placeholder: '',
dataSource: [],
defaultOpen: false,
onVisibleChange: () => {},
};
static getDerivedStateFromProps(props: HeaderSearchProps) {
if ('open' in props) {
return {
searchMode: props.open,
};
}
return null;
}
private timeout: number | undefined = undefined;
private inputRef: Input | null = null;
constructor(props: HeaderSearchProps) {
super(props);
this.state = {
searchMode: props.defaultOpen,
value: '',
};
this.debouncePressEnter = debounce(this.debouncePressEnter, 500, {
leading: true,
trailing: false,
});
}
componentWillUnmount() {
clearTimeout(this.timeout);
}
onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
const { onPressEnter } = this.props;
const { value } = this.state;
this.timeout = window.setTimeout(() => {
onPressEnter(value); // Fix duplicate onPressEnter
}, 0);
}
};
onChange: AutoCompleteProps['onChange'] = value => {
if (typeof value === 'string') {
const { onSearch, onChange } = this.props;
this.setState({ value });
if (onSearch) {
onSearch(value);
}
if (onChange) {
onChange(value);
}
}
};
enterSearchMode = () => {
const { onVisibleChange } = this.props;
onVisibleChange(true);
this.setState({ searchMode: true }, () => {
const { searchMode } = this.state;
if (searchMode && this.inputRef) {
this.inputRef.focus();
}
});
};
leaveSearchMode = () => {
this.setState({
searchMode: false,
value: '',
});
};
debouncePressEnter = () => {
const { onPressEnter } = this.props;
const { value } = this.state;
onPressEnter(value);
};
render() {
const { className, placeholder, open, ...restProps } = this.props;
const { searchMode, value } = this.state;
delete restProps.defaultOpen; // for rc-select not affected
const inputClass = classNames(styles.input, {
[styles.show]: searchMode,
});
return (
<span
className={classNames(className, styles.headerSearch)}
onClick={this.enterSearchMode}
onTransitionEnd={({ propertyName }) => {
if (propertyName === 'width' && !searchMode) {
const { onVisibleChange } = this.props;
onVisibleChange(searchMode);
}
}}
>
<Icon type="search" key="Icon" />
<AutoComplete
key="AutoComplete"
{...restProps}
className={inputClass}
value={value}
onChange={this.onChange}
>
<Input
ref={node => {
this.inputRef = node;
}}
aria-label={placeholder}
placeholder={placeholder}
onKeyDown={this.onKeyDown}
onBlur={this.leaveSearchMode}
/>
</AutoComplete>
</span>
);
}
}
@import '~antd/es/style/themes/default.less';
.list {
max-height: 400px;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
.item {
padding-right: 24px;
padding-left: 24px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
.meta {
width: 100%;
}
.avatar {
margin-top: 4px;
background: #fff;
}
.iconElement {
font-size: 32px;
}
&.read {
opacity: 0.4;
}
&:last-child {
border-bottom: 0;
}
&:hover {
background: @primary-1;
}
.title {
margin-bottom: 8px;
font-weight: normal;
}
.description {
font-size: 12px;
line-height: @line-height-base;
}
.datetime {
margin-top: 4px;
font-size: 12px;
line-height: @line-height-base;
}
.extra {
float: right;
margin-top: -1.5px;
margin-right: 0;
color: @text-color-secondary;
font-weight: normal;
}
}
.loadMore {
padding: 8px 0;
color: @primary-6;
text-align: center;
cursor: pointer;
&.loadedAll {
color: rgba(0, 0, 0, 0.25);
cursor: unset;
}
}
}
.notFound {
padding: 73px 0 88px;
color: @text-color-secondary;
text-align: center;
img {
display: inline-block;
height: 76px;
margin-bottom: 16px;
}
}
.bottomBar {
height: 46px;
color: @text-color;
line-height: 46px;
text-align: center;
border-top: 1px solid @border-color-split;
border-radius: 0 0 @border-radius-base @border-radius-base;
transition: all 0.3s;
div {
display: inline-block;
width: 50%;
cursor: pointer;
transition: all 0.3s;
user-select: none;
&:hover {
color: @heading-color;
}
&:only-child {
width: 100%;
}
&:not(:only-child):last-child {
border-left: 1px solid @border-color-split;
}
}
}
import { Avatar, List } from 'antd';
import React from 'react';
import classNames from 'classnames';
import { NoticeIconData } from './index';
import styles from './NoticeList.less';
export interface NoticeIconTabProps {
count?: number;
name?: string;
showClear?: boolean;
showViewMore?: boolean;
style?: React.CSSProperties;
title: string;
tabKey: string;
data?: NoticeIconData[];
onClick?: (item: NoticeIconData) => void;
onClear?: () => void;
emptyText?: string;
clearText?: string;
viewMoreText?: string;
list: NoticeIconData[];
onViewMore?: (e: any) => void;
}
const NoticeList: React.SFC<NoticeIconTabProps> = ({
data = [],
onClick,
onClear,
title,
onViewMore,
emptyText,
showClear = true,
clearText,
viewMoreText,
showViewMore = false,
}) => {
if (data.length === 0) {
return (
<div className={styles.notFound}>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
alt="not found"
/>
<div>{emptyText}</div>
</div>
);
}
return (
<div>
<List<NoticeIconData>
className={styles.list}
dataSource={data}
renderItem={(item, i) => {
const itemCls = classNames(styles.item, {
[styles.read]: item.read,
});
// eslint-disable-next-line no-nested-ternary
const leftIcon = item.avatar ? (
typeof item.avatar === 'string' ? (
<Avatar className={styles.avatar} src={item.avatar} />
) : (
<span className={styles.iconElement}>{item.avatar}</span>
)
) : null;
return (
<List.Item
className={itemCls}
key={item.key || i}
onClick={() => onClick && onClick(item)}
>
<List.Item.Meta
className={styles.meta}
avatar={leftIcon}
title={
<div className={styles.title}>
{item.title}
<div className={styles.extra}>{item.extra}</div>
</div>
}
description={
<div>
<div className={styles.description}>{item.description}</div>
<div className={styles.datetime}>{item.datetime}</div>
</div>
}
/>
</List.Item>
);
}}
/>
<div className={styles.bottomBar}>
{showClear ? (
<div onClick={onClear}>
{clearText} {title}
</div>
) : null}
{showViewMore ? (
<div
onClick={e => {
if (onViewMore) {
onViewMore(e);
}
}}
>
{viewMoreText}
</div>
) : null}
</div>
</div>
);
};
export default NoticeList;
@import '~antd/es/style/themes/default.less';
.popover {
position: relative;
width: 336px;
}
.noticeButton {
display: inline-block;
cursor: pointer;
transition: all 0.3s;
}
.icon {
padding: 4px;
vertical-align: middle;
}
.badge {
font-size: 16px;
}
.tabs {
:global {
.ant-tabs-nav-scroll {
text-align: center;
}
.ant-tabs-bar {
margin-bottom: 0;
}
}
}
import { Badge, Icon, Spin, Tabs } from 'antd';
import React, { Component } from 'react';
import classNames from 'classnames';
import NoticeList, { NoticeIconTabProps } from './NoticeList';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
const { TabPane } = Tabs;
export interface NoticeIconData {
avatar?: string | React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
datetime?: React.ReactNode;
extra?: React.ReactNode;
style?: React.CSSProperties;
key?: string | number;
read?: boolean;
}
export interface NoticeIconProps {
count?: number;
bell?: React.ReactNode;
className?: string;
loading?: boolean;
onClear?: (tabName: string, tabKey: string) => void;
onItemClick?: (item: NoticeIconData, tabProps: NoticeIconTabProps) => void;
onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
onTabChange?: (tabTile: string) => void;
style?: React.CSSProperties;
onPopupVisibleChange?: (visible: boolean) => void;
popupVisible?: boolean;
clearText?: string;
viewMoreText?: string;
clearClose?: boolean;
children: React.ReactElement<NoticeIconTabProps>[];
}
export default class NoticeIcon extends Component<NoticeIconProps> {
public static Tab: typeof NoticeList = NoticeList;
static defaultProps = {
onItemClick: (): void => {},
onPopupVisibleChange: (): void => {},
onTabChange: (): void => {},
onClear: (): void => {},
onViewMore: (): void => {},
loading: false,
clearClose: false,
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
};
state = {
visible: false,
};
onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps): void => {
const { onItemClick } = this.props;
if (onItemClick) {
onItemClick(item, tabProps);
}
};
onClear = (name: string, key: string): void => {
const { onClear } = this.props;
if (onClear) {
onClear(name, key);
}
};
onTabChange = (tabType: string): void => {
const { onTabChange } = this.props;
if (onTabChange) {
onTabChange(tabType);
}
};
onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent): void => {
const { onViewMore } = this.props;
if (onViewMore) {
onViewMore(tabProps, event);
}
};
getNotificationBox(): React.ReactNode {
const { children, loading, clearText, viewMoreText } = this.props;
if (!children) {
return null;
}
const panes = React.Children.map(
children,
(child: React.ReactElement<NoticeIconTabProps>): React.ReactNode => {
if (!child) {
return null;
}
const { list, title, count, tabKey, showClear, showViewMore } = child.props;
const len = list && list.length ? list.length : 0;
const msgCount = count || count === 0 ? count : len;
const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
return (
<TabPane tab={tabTitle} key={title}>
<NoticeList
clearText={clearText}
viewMoreText={viewMoreText}
data={list}
onClear={(): void => this.onClear(title, tabKey)}
onClick={(item): void => this.onItemClick(item, child.props)}
onViewMore={(event): void => this.onViewMore(child.props, event)}
showClear={showClear}
showViewMore={showViewMore}
title={title}
{...child.props}
/>
</TabPane>
);
},
);
return (
<>
<Spin spinning={loading} delay={300}>
<Tabs className={styles.tabs} onChange={this.onTabChange}>
{panes}
</Tabs>
</Spin>
</>
);
}
handleVisibleChange = (visible: boolean): void => {
const { onPopupVisibleChange } = this.props;
this.setState({ visible });
if (onPopupVisibleChange) {
onPopupVisibleChange(visible);
}
};
render(): React.ReactNode {
const { className, count, popupVisible, bell } = this.props;
const { visible } = this.state;
const noticeButtonClass = classNames(className, styles.noticeButton);
const notificationBox = this.getNotificationBox();
const NoticeBellIcon = bell || <Icon type="bell" className={styles.icon} />;
const trigger = (
<span className={classNames(noticeButtonClass, { opened: visible })}>
<Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}>
{NoticeBellIcon}
</Badge>
</span>
);
if (!notificationBox) {
return trigger;
}
const popoverProps: {
visible?: boolean;
} = {};
if ('popupVisible' in this.props) {
popoverProps.visible = popupVisible;
}
return (
<HeaderDropdown
placement="bottomRight"
overlay={notificationBox}
overlayClassName={styles.popover}
trigger={['click']}
visible={visible}
onVisibleChange={this.handleVisibleChange}
{...popoverProps}
>
{trigger}
</HeaderDropdown>
);
}
}
@import '~antd/es/style/themes/default.less';
@ant-pro-page-header-wrap: ~'@{ant-prefix}-pro-page-header-wrap';
.@{ant-pro-page-header-wrap}-children-content {
margin: 24px 24px 0;
}
.@{ant-pro-page-header-wrap}-page-header-warp {
background-color: @component-background;
}
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-detail {
display: flex;
}
.@{ant-pro-page-header-wrap}-row {
display: flex;
width: 100%;
}
.@{ant-pro-page-header-wrap}-title-content {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-title,
.@{ant-pro-page-header-wrap}-content {
flex: auto;
}
.@{ant-pro-page-header-wrap}-extraContent,
.@{ant-pro-page-header-wrap}-main {
flex: 0 1 auto;
}
.@{ant-pro-page-header-wrap}-main {
width: 100%;
}
.@{ant-pro-page-header-wrap}-title {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-logo {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-extraContent {
min-width: 242px;
margin-left: 88px;
text-align: right;
}
}
@media screen and (max-width: @screen-xl) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 44px;
}
}
}
@media screen and (max-width: @screen-lg) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 20px;
}
}
}
@media screen and (max-width: @screen-md) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-row {
display: block;
}
.@{ant-pro-page-header-wrap}-action,
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 0;
text-align: left;
}
}
}
@media screen and (max-width: @screen-sm) {
.@{ant-pro-page-header-wrap}-detail {
display: block;
}
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 0;
}
}
import { PageHeader, Tabs } from 'antd';
import React from 'react';
import { TabsProps } from 'antd/es/tabs';
import { PageHeaderProps } from 'antd/es/page-header';
import './index.less';
import GridContent from '../GridContent';
import RouteContext from '../RouteContext';
interface PageHeaderTabConfig {
tabList?: {
key: string;
tab: string;
}[];
tabActiveKey?: TabsProps['activeKey'];
onTabChange?: TabsProps['onChange'];
tabBarExtraContent?: TabsProps['tabBarExtraContent'];
}
interface PageHeaderWrapperProps extends PageHeaderTabConfig, Omit<PageHeaderProps, 'title'> {
title?: React.ReactNode | false;
content?: React.ReactNode;
extraContent?: React.ReactNode;
pageHeaderRender?: (props: PageHeaderWrapperProps) => React.ReactNode;
}
const prefixedClassName = 'ant-pro-page-header-wrap';
/**
* render Footer tabList
* In order to be compatible with the old version of the PageHeader
* basically all the functions are implemented.
*/
const renderFooter: React.SFC<Omit<PageHeaderWrapperProps, 'title'>> = ({
tabList,
tabActiveKey,
onTabChange,
tabBarExtraContent,
}) => {
if (tabList && tabList.length) {
return (
<Tabs
className={`${prefixedClassName}-tabs`}
activeKey={tabActiveKey}
onChange={key => {
if (onTabChange) {
onTabChange(key);
}
}}
tabBarExtraContent={tabBarExtraContent}
>
{tabList.map(item => (
<Tabs.TabPane tab={item.tab} key={item.key} />
))}
</Tabs>
);
}
return null;
};
const renderPageHeader = (
content: React.ReactNode,
extraContent: React.ReactNode,
): React.ReactNode => {
if (!content && !extraContent) {
return null;
}
return (
<div className={`${prefixedClassName}-detail`}>
<div className={`${prefixedClassName}-main`}>
<div className={`${prefixedClassName}-row`}>
{content && <div className={`${prefixedClassName}-content`}>{content}</div>}
{extraContent && (
<div className={`${prefixedClassName}-extraContent`}>{extraContent}</div>
)}
</div>
</div>
</div>
);
};
const defaultPageHeaderRender = (props: PageHeaderWrapperProps): React.ReactNode => {
const { title, content, pageHeaderRender, extraContent, ...restProps } = props;
return (
<RouteContext.Consumer>
{value => {
if (pageHeaderRender) {
return pageHeaderRender({ ...props, ...value });
}
let pageHeaderTitle = title;
if (!title && title !== false) {
pageHeaderTitle = value.title;
}
return (
<PageHeader
{...value}
title={pageHeaderTitle}
{...restProps}
footer={renderFooter(restProps)}
>
{renderPageHeader(content, extraContent)}
</PageHeader>
);
}}
</RouteContext.Consumer>
);
};
const PageHeaderWrapper: React.SFC<PageHeaderWrapperProps> = props => {
const { children } = props;
return (
<div style={{ margin: '-24px -24px 0' }}>
<div className={`${prefixedClassName}-page-header-warp`}>
<GridContent>{defaultPageHeaderRender(props)}</GridContent>
</div>
{children ? (
<GridContent>
<div className={`${prefixedClassName}-children-content`}>{children}</div>
</GridContent>
) : null}
</div>
);
};
export default PageHeaderWrapper;
import React from 'react';
import { Spin } from 'antd';
// loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
const PageLoading: React.FC = () => (
<div style={{ paddingTop: 100, textAlign: 'center' }}>
<Spin size="large" />
</div>
);
export default PageLoading;
import { createContext } from 'react';
import { Settings } from '@ant-design/pro-layout/es/defaultSettings';
import { BreadcrumbListReturn } from '@ant-design/pro-layout/es/utils/getBreadcrumbProps';
import { MenuDataItem } from '@ant-design/pro-layout/es/typings';
interface RouteContextType extends Partial<Settings> {
breadcrumb?: BreadcrumbListReturn;
menuData?: MenuDataItem[];
isMobile?: boolean;
collapsed?: boolean;
title?: string;
}
const routeContext: React.Context<RouteContextType> = createContext({});
export default routeContext;
@import '~antd/es/style/themes/default.less';
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
min-width: 160px;
}
}
.dropDown {
line-height: @layout-header-height;
vertical-align: top;
cursor: pointer;
> i {
font-size: 16px !important;
transform: none !important;
svg {
position: relative;
top: -1px;
}
}
}
import { Icon, Menu } from 'antd';
import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale';
import { ClickParam } from 'antd/es/menu';
import React from 'react';
import classNames from 'classnames';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
interface SelectLangProps {
className?: string;
realReload?: boolean;
}
const SelectLang: React.FC<SelectLangProps> = props => {
const { className, realReload = false } = props;
const selectedLang = getLocale();
const changeLang = ({ key }: ClickParam): void => setLocale(key, realReload);
const locales = ['zh-CN', 'en-US'];
const languageLabels = {
'zh-CN': '简体中文',
// 'zh-TW': '繁体中文',
'en-US': 'English',
// 'pt-BR': 'Português',
};
const languageIcons = {
'zh-CN': '🇨🇳',
// 'zh-TW': '🇭🇰',
'en-US': '🇬🇧',
// 'pt-BR': '🇧🇷',
};
const langMenu = (
<Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}>
{locales.map(locale => (
<Menu.Item key={locale}>
<span role="img" aria-label={languageLabels[locale]}>
{languageIcons[locale]}
</span>{' '}
{languageLabels[locale]}
</Menu.Item>
))}
</Menu>
);
return (
<HeaderDropdown overlay={langMenu} placement="bottomRight">
<span className={classNames(styles.dropDown, className)}>
<Icon
type="global"
title={formatMessage({ id: 'navBar.lang' })}
style={{ color: '#fff' }}
/>
</span>
</HeaderDropdown>
);
};
export default SelectLang;
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable import/no-extraneous-dependencies */
import client from 'webpack-theme-color-replacer/client';
import generate from '@ant-design/colors/lib/generate';
export default {
getAntdSerials(color: string): string[] {
const lightCount = 9;
const divide = 10;
// 淡化(即less的tint)
let lightens = new Array(lightCount).fill(0);
lightens = lightens.map((_, i) => client.varyColor.lighten(color, i / divide));
const colorPalettes = generate(color);
return lightens.concat(colorPalettes);
},
changeColor(color?: string): Promise<void> {
if (!color) {
return Promise.resolve();
}
const options = {
// new colors array, one-to-one corresponde with `matchColors`
newColors: this.getAntdSerials(color),
changeUrl(cssUrl: string): string {
// while router is not `hash` mode, it needs absolute path
return `/${cssUrl}`;
},
};
return client.changer.changeColor(options, Promise);
},
};
import './index.less';
import defaultSettings, { Settings } from '@ant-design/pro-layout/es/defaultSettings';
import { isUrl } from '@ant-design/pro-layout/es/utils/utils';
import { urlToList } from '@ant-design/pro-layout/es/utils/pathTools';
import {
MenuDataItem,
MessageDescriptor,
Route,
RouterTypes,
WithFalse,
} from '@ant-design/pro-layout/es/typings';
import { Icon, Menu, Badge } from 'antd';
import React, { Component } from 'react';
import classNames from 'classnames';
import { MenuMode, MenuProps } from 'antd/es/menu';
import { MenuTheme } from 'antd/es/menu/MenuContext';
import { getMenuMatches } from './SiderMenuUtils';
export interface BaseMenuProps
extends Partial<RouterTypes<Route>>,
Omit<MenuProps, 'openKeys'>,
Partial<Settings> {
className?: string;
collapsed?: boolean;
flatMenuKeys?: string[];
handleOpenChange?: (openKeys: string[]) => void;
isMobile?: boolean;
menuData?: MenuDataItem[];
mode?: MenuMode;
onCollapse?: (collapsed: boolean) => void;
onOpenChange?: (openKeys: string[]) => void;
openKeys?: WithFalse<string[]>;
style?: React.CSSProperties;
theme?: MenuTheme;
formatMessage?: (message: MessageDescriptor) => string;
menuItemRender?: WithFalse<
(
item: MenuDataItem & {
isUrl: boolean;
},
defaultDom: React.ReactNode,
) => React.ReactNode
>;
}
const { SubMenu } = Menu;
let IconFont = Icon.createFromIconfontCN({
scriptUrl: defaultSettings.iconfontUrl,
});
// Allow menu.js config icon as string or ReactNode
// icon: 'setting',
// icon: 'icon-geren' #For Iconfont ,
// icon: 'http://demo.com/icon.png',
// icon: '/favicon.png',
// icon: <Icon type="setting" />,
const getIcon = (icon?: string | React.ReactNode): React.ReactNode => {
if (typeof icon === 'string') {
if (isUrl(icon)) {
return (
<Icon component={() => <img src={icon} alt="icon" className="ant-pro-sider-menu-icon" />} />
);
}
if (icon.startsWith('icon-')) {
return <IconFont type={icon} />;
}
return <Icon type={icon} />;
}
return icon;
};
export default class BaseMenu extends Component<BaseMenuProps> {
public static defaultProps: Partial<BaseMenuProps> = {
flatMenuKeys: [],
onCollapse: () => undefined,
isMobile: false,
openKeys: [],
collapsed: false,
handleOpenChange: () => undefined,
menuData: [],
onOpenChange: () => undefined,
};
warp: HTMLDivElement | undefined;
public constructor(props: BaseMenuProps) {
super(props);
const { iconfontUrl } = props;
// reset IconFont
if (iconfontUrl) {
IconFont = Icon.createFromIconfontCN({
scriptUrl: iconfontUrl,
});
}
}
state = {};
public static getDerivedStateFromProps(props: BaseMenuProps): null {
const { iconfontUrl } = props;
// reset IconFont
if (iconfontUrl) {
IconFont = Icon.createFromIconfontCN({
scriptUrl: iconfontUrl,
});
}
return null;
}
/**
* 获得菜单子节点
*/
getNavMenuItems = (menusData: MenuDataItem[] = []): React.ReactNode[] =>
menusData
.filter(item => item.name && !item.hideInMenu)
.map(item => this.getSubMenuOrItem(item))
.filter(item => item);
// Get the currently selected menu
getSelectedMenuKeys = (pathname?: string): string[] => {
const { flatMenuKeys, selectedKeys } = this.props;
if (selectedKeys !== undefined) {
return selectedKeys;
}
return urlToList(pathname)
.map(itemPath => getMenuMatches(flatMenuKeys, itemPath).pop())
.filter(item => item) as string[];
};
/**
* get SubMenu or Item
*/
getSubMenuOrItem = (item: MenuDataItem): React.ReactNode => {
if (
Array.isArray(item.children) &&
!item.hideChildrenInMenu &&
item.children.some(child => child && !!child.name)
) {
const name = this.getIntlName(item);
let menuTitle = <span>{name}</span>;
if (item.isQuestion) {
menuTitle = (
<Badge dot={item.isQuestion}>
<span style={{ paddingRight: 8 }}>{name}</span>
</Badge>
);
}
return (
<SubMenu
title={
item.icon ? (
<span>
{getIcon(item.icon)}
{menuTitle}
</span>
) : (
name
)
}
key={item.key || item.path}
>
{this.getNavMenuItems(item.children)}
</SubMenu>
);
}
return <Menu.Item key={item.key || item.path}>{this.getMenuItemPath(item)}</Menu.Item>;
};
getIntlName = (item: MenuDataItem) => {
const { name, locale } = item;
const {
menu = {
locale: false,
},
formatMessage,
} = this.props;
if (locale && menu.locale && formatMessage) {
return formatMessage({
id: locale,
defaultMessage: name,
});
}
return name;
};
/**
* 判断是否是http链接.返回 Link 或 a
* Judge whether it is http link.return a or Link
* @memberof SiderMenu
*/
getMenuItemPath = (item: MenuDataItem) => {
const itemPath = this.conversionPath(item.path);
const icon = getIcon(item.icon);
const { location = { pathname: '/' }, isMobile, onCollapse, menuItemRender } = this.props;
const { target } = item;
// if local is true formatMessage all name。
const name = this.getIntlName(item);
let defaultItem = (
<>
{icon}
<span>{name}</span>
</>
);
const isHttpUrl = isUrl(itemPath);
// Is it a http link
if (isHttpUrl) {
defaultItem = (
<a href={itemPath} target={target}>
{icon}
<span>{name}</span>
</a>
);
}
if (menuItemRender) {
return menuItemRender(
{
...item,
isUrl: isHttpUrl,
itemPath,
isMobile,
replace: itemPath === location.pathname,
onClick: () => onCollapse && onCollapse(true),
},
defaultItem,
);
}
return defaultItem;
};
conversionPath = (path: string) => {
if (path && path.indexOf('http') === 0) {
return path;
}
return `/${path || ''}`.replace(/\/+/g, '/');
};
getPopupContainer = (fixedHeader: boolean, layout: string): HTMLElement => {
if (fixedHeader && layout === 'topmenu' && this.warp) {
return this.warp;
}
return document.body;
};
getRef = (ref: HTMLDivElement) => {
this.warp = ref;
};
render(): React.ReactNode {
const {
openKeys,
theme,
mode,
location = {
pathname: '/',
},
className,
collapsed,
handleOpenChange,
style,
fixedHeader = false,
layout = 'sidemenu',
menuData,
selectedKeys: defaultSelectedKeys,
} = this.props;
// if pathname can't match, use the nearest parent's key
let selectedKeys = this.getSelectedMenuKeys(location.pathname);
if (defaultSelectedKeys === undefined && !selectedKeys.length && openKeys) {
selectedKeys = [openKeys[openKeys.length - 1]];
}
let props = {};
if (openKeys && !collapsed && layout === 'sidemenu') {
props = {
openKeys: openKeys.length === 0 ? [...selectedKeys] : openKeys,
};
}
const cls = classNames(className, {
'top-nav-menu': mode === 'horizontal',
});
return (
<>
<Menu
{...props}
key="Menu"
mode={mode}
theme={theme}
onOpenChange={handleOpenChange}
selectedKeys={selectedKeys}
style={style}
className={cls}
getPopupContainer={() => this.getPopupContainer(fixedHeader, layout)}
>
{this.getNavMenuItems(menuData)}
</Menu>
<div ref={this.getRef} />
</>
);
}
}
import React, { Component } from 'react';
import { WithFalse } from '@ant-design/pro-layout/es/typings';
import { Layout } from 'antd';
import classNames from 'classnames';
import './index.less';
import BaseMenu, { BaseMenuProps } from './BaseMenu';
import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
const { Sider } = Layout;
let firstMount = true;
export const defaultRenderLogo = (logo: React.ReactNode): React.ReactNode => {
if (typeof logo === 'string') {
return <img src={logo} alt="logo" />;
}
if (typeof logo === 'function') {
return logo();
}
return logo;
};
export const defaultRenderLogoAndTitle = (
logo: React.ReactNode,
title: React.ReactNode,
menuHeaderRender: SiderMenuProps['menuHeaderRender'],
): React.ReactNode => {
if (menuHeaderRender === false) {
return null;
}
const logoDom = defaultRenderLogo(logo);
const titleDom = <h1>{title}</h1>;
if (menuHeaderRender) {
return menuHeaderRender(logoDom, titleDom);
}
return (
<a href="/">
{logoDom}
{titleDom}
</a>
);
};
export interface SiderMenuProps
extends Pick<BaseMenuProps, Exclude<keyof BaseMenuProps, ['onCollapse']>> {
logo?: React.ReactNode;
siderWidth?: number;
menuHeaderRender?: WithFalse<(logo: React.ReactNode, title: React.ReactNode) => React.ReactNode>;
onMenuHeaderClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
interface SiderMenuState {
pathname?: string;
openKeys?: string[] | false;
flatMenuKeysLen?: number;
}
export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> {
static defaultProps: Partial<SiderMenuProps> = {
flatMenuKeys: [],
isMobile: false,
collapsed: false,
menuData: [],
};
static getDerivedStateFromProps(
props: SiderMenuProps,
state: SiderMenuState,
): SiderMenuState | null {
const { pathname, flatMenuKeysLen } = state;
const { location = { pathname: '/' }, flatMenuKeys = [] } = props;
if (location.pathname !== pathname || flatMenuKeys.length !== flatMenuKeysLen) {
return {
pathname: location.pathname,
flatMenuKeysLen: flatMenuKeys.length,
openKeys: getDefaultCollapsedSubMenus(props),
};
}
return null;
}
constructor(props: SiderMenuProps) {
super(props);
this.state = {
openKeys: getDefaultCollapsedSubMenus(props),
};
}
componentDidMount(): void {
firstMount = false;
}
isMainMenu: (key: string) => boolean = key => {
const { menuData = [] } = this.props;
return menuData.some(item => {
if (key) {
return item.key === key || item.path === key;
}
return false;
});
};
handleOpenChange: (openKeys: string[]) => void = openKeys => {
const { onOpenChange, openKeys: defaultOpenKeys } = this.props;
if (onOpenChange) {
onOpenChange(openKeys);
return;
}
// if defaultOpenKeys existence, don't change
if (defaultOpenKeys !== undefined) {
return;
}
const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
if (moreThanOne) {
this.setState({
openKeys: [openKeys.pop()].filter(item => item) as string[],
});
} else {
this.setState({ openKeys: [...openKeys] });
}
};
render(): React.ReactNode {
const {
collapsed,
fixSiderbar,
onCollapse,
theme,
siderWidth = 256,
isMobile,
layout,
logo,
title,
menuHeaderRender: renderLogoAndTitle,
onMenuHeaderClick,
} = this.props;
const { openKeys } = this.state;
// 如果收起,并且为顶部布局,openKeys 为 false 都不控制 openKeys
const defaultProps =
collapsed || layout !== 'sidemenu' || openKeys === false ? {} : { openKeys };
const siderClassName = classNames('ant-pro-sider-menu-sider', {
'fix-sider-bar': fixSiderbar,
light: theme === 'light',
});
return (
<Sider
collapsible
trigger={null}
collapsed={collapsed}
breakpoint="lg"
onCollapse={collapse => {
if (firstMount || !isMobile) {
if (onCollapse) {
onCollapse(collapse);
}
}
}}
width={siderWidth}
theme={theme}
className={siderClassName}
>
<div className="ant-pro-sider-menu-logo" onClick={onMenuHeaderClick} id="logo">
{defaultRenderLogoAndTitle(logo, title, renderLogoAndTitle)}
</div>
<BaseMenu
{...this.props}
mode="inline"
handleOpenChange={this.handleOpenChange}
onOpenChange={this.handleOpenChange}
style={{ padding: '16px 0', width: '100%' }}
{...defaultProps}
/>
</Sider>
);
}
}
import { MenuDataItem } from '@ant-design/pro-layout/es/typings';
import { urlToList } from '@ant-design/pro-layout/es/utils/pathTools';
import pathToRegexp from 'path-to-regexp';
import { BaseMenuProps } from './BaseMenu';
/**
* Recursively flatten the data
* [{path:string},{path:string}] => {path,path2}
* @param menus
*/
export const getFlatMenuKeys = (menuData: MenuDataItem[] = []): string[] => {
let keys: string[] = [];
menuData.forEach(item => {
if (!item) {
return;
}
keys.push(item.path);
if (item.children) {
keys = keys.concat(getFlatMenuKeys(item.children));
}
});
return keys;
};
export const getMenuMatches = (flatMenuKeys: string[] = [], path: string): string[] =>
flatMenuKeys.filter(item => item && pathToRegexp(item).test(path));
/**
* 获得菜单子节点
*/
export const getDefaultCollapsedSubMenus = (props: BaseMenuProps): string[] | false => {
const { location = { pathname: '/' }, flatMenuKeys, openKeys } = props;
if (openKeys === false) {
return false;
}
return urlToList(location.pathname)
.map(item => getMenuMatches(flatMenuKeys, item)[0])
.filter(item => item)
.reduce((acc, curr) => [...acc, curr], ['/']);
};
@import '~antd/es/style/themes/default.less';
@sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider-menu';
@nav-header-height: @layout-header-height;
.@{sider-menu-prefix-cls} {
&-logo {
position: relative;
height: @nav-header-height;
padding-left: (@menu-collapsed-width - 32px) / 2;
overflow: hidden;
line-height: @nav-header-height;
background: @layout-sider-background;
cursor: pointer;
transition: all 0.3s;
img {
display: inline-block;
height: 32px;
vertical-align: middle;
}
h1 {
display: inline-block;
margin: 0 0 0 12px;
color: white;
font-weight: 600;
font-size: 20px;
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
vertical-align: middle;
}
}
&-sider {
position: relative;
z-index: 10;
min-height: 100vh;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
&.fix-sider-bar {
position: fixed;
top: 0;
left: 0;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
.ant-menu-root {
height: ~'calc(100vh - @{nav-header-height})';
overflow-y: auto;
}
.ant-menu-inline {
border-right: 0;
.ant-menu-item,
.ant-menu-submenu-title {
width: 100%;
}
}
}
&.light {
background-color: white;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
.@{sider-menu-prefix-cls}-logo {
background: white;
box-shadow: 1px 1px 0 0 @border-color-split;
h1 {
color: @primary-color;
}
}
.ant-menu-light {
border-right-color: transparent;
}
}
}
&-icon {
width: 14px;
vertical-align: baseline;
}
.top-nav-menu li.ant-menu-item {
height: @nav-header-height;
line-height: @nav-header-height;
}
.drawer .drawer-content {
background: @layout-sider-background;
}
.ant-menu-inline-collapsed {
& > .ant-menu-item .sider-menu-item-img + span,
&
> .ant-menu-item-group
> .ant-menu-item-group-list
> .ant-menu-item
.sider-menu-item-img
+ span,
& > .ant-menu-submenu > .ant-menu-submenu-title .sider-menu-item-img + span {
display: inline-block;
max-width: 0;
opacity: 0;
}
}
.ant-menu-item .sider-menu-item-img + span,
.ant-menu-submenu-title .sider-menu-item-img + span {
opacity: 1;
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
}
.ant-drawer-body {
padding: 0;
}
}
import React from 'react';
import { Drawer } from 'antd';
import SiderMenu, { SiderMenuProps } from './SiderMenu';
import { getFlatMenuKeys } from './SiderMenuUtils';
const SiderMenuWrapper: React.FC<SiderMenuProps> = props => {
const { isMobile, menuData, collapsed, onCollapse } = props;
const flatMenuKeys = getFlatMenuKeys(menuData);
return isMobile ? (
<Drawer
visible={!collapsed}
placement="left"
className="ant-pro-sider-menu"
onClose={() => onCollapse && onCollapse(true)}
style={{
padding: 0,
height: '100vh',
}}
>
<SiderMenu {...props} flatMenuKeys={flatMenuKeys} collapsed={isMobile ? false : collapsed} />
</Drawer>
) : (
<SiderMenu className="ant-pro-sider-menu" {...props} flatMenuKeys={flatMenuKeys} />
);
};
SiderMenuWrapper.defaultProps = {
onCollapse: () => undefined,
};
export default SiderMenuWrapper;
@import '~antd/es/style/themes/default.less';
@top-nav-header-prefix-cls: ~'@{ant-prefix}-pro-top-nav-header';
.@{top-nav-header-prefix-cls} {
position: relative;
width: 100%;
height: @layout-header-height;
box-shadow: @box-shadow-base;
transition: background 0.3s, width 0.2s;
.ant-menu-submenu.ant-menu-submenu-horizontal {
height: 100%;
line-height: @layout-header-height;
.ant-menu-submenu-title {
height: 100%;
}
}
&.light {
background-color: @component-background;
h1 {
color: #002140;
}
}
&-main {
display: flex;
height: @layout-header-height;
padding-left: 24px;
&.wide {
max-width: 1200px;
margin: auto;
padding-left: 0;
}
.left {
display: flex;
flex: 1;
}
.right {
width: 324px;
}
}
&-logo {
position: relative;
width: 165px;
height: @layout-header-height;
overflow: hidden;
line-height: @layout-header-height;
transition: all 0.3s;
img {
display: inline-block;
height: 32px;
vertical-align: middle;
}
h1 {
display: inline-block;
margin: 0 0 0 12px;
color: @btn-primary-color;
font-weight: 400;
font-size: 16px;
vertical-align: top;
}
}
&-menu {
.ant-menu.ant-menu-horizontal {
height: @layout-header-height;
line-height: @layout-header-height;
border: none;
}
}
}
import './index.less';
import React, { Component } from 'react';
import { isBrowser } from '@ant-design/pro-layout/es/utils/utils';
import { SiderMenuProps, defaultRenderLogoAndTitle } from '../SiderMenu/SiderMenu';
import BaseMenu from '../SiderMenu/BaseMenu';
import { HeaderViewProps } from '../Header';
import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils';
export interface TopNavHeaderProps extends SiderMenuProps {
logo?: React.ReactNode;
onCollapse?: (collapse: boolean) => void;
rightContentRender?: HeaderViewProps['rightContentRender'];
}
interface TopNavHeaderState {
maxWidth?: number;
}
export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> {
static getDerivedStateFromProps(props: TopNavHeaderProps): TopNavHeaderState | null {
const { contentWidth } = props;
const innerWidth = isBrowser() ? window.innerWidth : 0;
return {
maxWidth: (contentWidth === 'Fixed' && innerWidth > 1200 ? 1200 : innerWidth) - 280 - 120,
};
}
state: TopNavHeaderState = {};
maim: HTMLDivElement | null = null;
render(): React.ReactNode {
const {
theme,
menuData,
onMenuHeaderClick,
contentWidth,
rightContentRender,
logo,
title,
menuHeaderRender,
} = this.props;
const { maxWidth } = this.state;
const flatMenuKeys = getFlatMenuKeys(menuData);
const baseClassName = 'ant-pro-top-nav-header';
return (
<div className={`${baseClassName} ${theme === 'light' ? 'light' : ''}`}>
<div
ref={ref => {
this.maim = ref;
}}
className={`${baseClassName}-main ${contentWidth === 'Fixed' ? 'wide' : ''}`}
>
<div className={`${baseClassName}-left`} onClick={onMenuHeaderClick}>
<div className={`${baseClassName}-logo`} key="logo" id="logo">
{defaultRenderLogoAndTitle(logo, title, menuHeaderRender)}
</div>
</div>
<div style={{ maxWidth, flex: 1 }} className={`${baseClassName}-menu`}>
<BaseMenu {...this.props} flatMenuKeys={flatMenuKeys} />
</div>
{rightContentRender &&
rightContentRender({
...this.props,
})}
</div>
</div>
);
}
}
import puppeteer from 'puppeteer';
import { BASE_URL, delay } from './utils';
// npm test .e2e.js for running all e2e or Login.e2e.js
describe('Login', () => {
it('should login with success', async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto(`${BASE_URL}/login`);
await page.type('#username', 'root');
await page.type('#password', 'pwd');
await delay(3000);
await page.click('button[type="submit"]');
await page.waitForSelector('.bg-primary.text-white.text-center.py-4', {
timeout: 2000,
});
await delay(3000);
await page.close();
browser.close();
});
});
const RouterConfig = require('../../config/config').default.routes;
const { uniq } = require('lodash');
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
function formatter(routes, parentPath = '') {
const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
let result = [];
routes.forEach(item => {
if (item.path) {
result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
}
if (item.routes) {
result = result.concat(
formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
);
}
});
return uniq(result.filter(item => !!item));
}
describe('Ant Design Pro E2E test', () => {
const testPage = path => async () => {
await page.goto(`${BASE_URL}${path}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0,
);
expect(haveFooter).toBeTruthy();
};
const routers = formatter(RouterConfig);
console.log('routers', routers);
routers.forEach(route => {
it(`test pages ${route}`, testPage(route));
});
});
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
describe('Homepage', () => {
it('topmenu should have footer', async () => {
const params = '/form/basic-form?navTheme=light&layout=topmenu';
await page.goto(`${BASE_URL}${params}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0,
);
expect(haveFooter).toBeTruthy();
});
});
export const BASE_URL = `http://localhost:${process.env.PORT || 8000}/data-platform`;
export function delay(time) {
return new Promise(function(resolve) {
setTimeout(resolve, time);
});
}
@import '~antd/es/style/themes/default.less';
@import '~nprogress/nprogress.css';
@import './mixins.less';
@import '~antd/es/collapse/style/index.less';
html,
body,
#root {
height: 100%;
}
.colorWeak {
filter: invert(80%);
}
.ant-layout {
min-height: 100vh;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
ol {
list-style: none;
}
@media (max-width: @screen-xs) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.pointer {
cursor: pointer;
}
.ant-pro-global-header {
background-color: #0168cf !important;
color: #fff !important;
}
.ant-pro-global-header-trigger:hover {
color: #007bff !important;
background-color: #0168cf !important;
}
import { Button, message, notification } from 'antd';
import React from 'react';
import { formatMessage } from 'umi-plugin-react/locale';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings;
// if pwa is true
if (pwa) {
// Notify user if offline now
window.addEventListener('sw.offline', () => {
message.warning(formatMessage({ id: 'app.pwa.offline' }));
});
// Pop up a prompt on the page asking the user if they want to use the latest version
window.addEventListener('sw.updated', (event: Event) => {
const e = event as CustomEvent;
const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
const worker = e.detail && e.detail.waiting;
if (worker) {
// Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = msgEvent => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(msgEvent.data);
}
};
worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
});
// Refresh current page to use the updated HTML and other assets after SW has skiped waiting
window.location.reload(true);
}
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.close(key);
reloadSW();
}}
>
{formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
</Button>
);
notification.open({
message: formatMessage({ id: 'app.pwa.serviceworker.updated' }),
description: formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }),
btn,
key,
onClose: async () => {},
});
});
} else if ('serviceWorker' in navigator) {
// eslint-disable-next-line compat/compat
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
return true;
})
.catch(() => {
console.log('serviceWorker unregister error');
});
}
:global(.ant-layout-sider-collapsed .ant-pro-sider-menu-logo) {
width: 64px;
}
/**
* Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
* You can view component api by:
* https://github.com/ant-design/ant-design-pro-layout
*/
import {
MenuDataItem,
BasicLayoutProps as ProLayoutProps,
Settings,
getMenuData,
} from '@ant-design/pro-layout';
import { HeaderViewProps } from '@ant-design/pro-layout/es/Header';
import { Layout, Menu, Icon, Badge } from 'antd';
import React, { useEffect } from 'react';
import Link from 'umi/link';
import { connect } from 'dva';
import { formatMessage } from 'umi-plugin-react/locale';
import classnames from 'classnames';
import ProLayout from '@/components/BasicLayout';
import Authorized from '@/utils/Authorized';
import SiderMenu from '@/components/SiderMenu';
import RightContent from '@/components/GlobalHeader/RightContent';
import { ConnectState, Dispatch } from '@/models/connect';
// import logo from '../assets/logo.svg';
import styles from '../components/GlobalHeader/index.less';
import { UmiType } from '@/utils/utils';
import HomeLayout from './HomeLayout';
import './Basic.less';
const { Header } = Layout;
export interface BasicLayoutProps extends ProLayoutProps {
breadcrumbNameMap: {
[path: string]: MenuDataItem;
};
settings: Settings;
isQuestion: boolean;
dispatch: Dispatch;
location: Location;
route: any;
}
export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
breadcrumbNameMap: {
[path: string]: MenuDataItem;
};
};
/**
* use Authorized check all menu item
*/
export const menuDataRender = (menuList: MenuDataItem[], isQuestion?: boolean): MenuDataItem[] =>
menuList.map(item => {
if (item.path && item.path === '/user') {
// eslint-disable-next-line no-param-reassign
item.isQuestion = isQuestion || false;
}
const localItem = {
...item,
children: item.children ? menuDataRender(item.children, isQuestion) : [],
};
return Authorized.check(item.authority, localItem, null) as MenuDataItem;
});
const footerRender: BasicLayoutProps['footerRender'] = () => (
<>
</>
);
const getSubMenu = (_children: MenuDataItem[], isQuestion: boolean, depth = '0') =>
_children &&
_children.map((route: any, i) => {
const key = `${depth}-${i}`;
// if (depth === '0') {
// return (
// <HeaderDropdown
// key={key}
// overlay={
// <Menu defaultSelectedKeys={['2']} style={{ lineHeight: '64px', float: 'left' }}>
// {getSubMenu(route.children, isQuestion, key)}
// </Menu>
// }
// >
// <span className={classnames(styles.action, styles.dropDown)}>
// <Icon type={route.icon || 'appstore'} className="mr-2" />
// <span>{route.name}</span>
// </span>
// </HeaderDropdown>
// );
// }
if (route.children !== undefined) {
let menuText = <span>{route.name}</span>;
if (route.path && route.path === '/user') {
menuText = (
<Badge dot={isQuestion}>
<span style={{ paddingRight: 8 }}>{route.name}</span>
</Badge>
);
}
return (
<Menu.SubMenu
key={route.path}
className=""
title={
<span>
<Icon type={route.icon || 'appstore'} className="mr-2" />
{menuText}
</span>
}
>
{getSubMenu(route.children, isQuestion, key)}
</Menu.SubMenu>
);
}
if (route.path && route.path === '/user/question') {
return (
<Menu.Item key={route.path}>
<Link to={route.path}>
<Icon type={route.icon || 'file'} className="mr-2" />
<Badge dot={isQuestion}>
<span style={{ paddingRight: 8 }}>{route.name}</span>
</Badge>
</Link>
</Menu.Item>
);
}
return route.path ? (
<Menu.Item key={route.path}>
<Link to={route.path}>
<Icon type={route.icon || 'file'} className="mr-2" />
<span>{route.name}</span>
</Link>
</Menu.Item>
) : null;
});
const BasicLayout: React.FC<BasicLayoutProps> = props => {
const {
dispatch,
children,
settings,
isQuestion,
location,
menu,
route = {
routes: [],
},
} = props;
/**
* constructor
*/
useEffect(() => {
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
dispatch({
type: 'global/isHaveQuestion',
});
dispatch({
type: 'settings/getSetting',
});
}
}, []);
/**
* init variables
*/
const handleMenuCollapse = (payload: boolean): void =>
dispatch &&
dispatch({
type: 'global/changeLayoutCollapsed',
payload,
});
const { publicPath } = window as (Window & UmiType);
const logo = (
<img
src={`${publicPath}images/logo_hnyc.png`}
className="mx-1"
alt=""
style={{ maxHeight: '2rem' }}
/>
);
if (location.pathname === '/home') {
const { routes = [] } = route;
const { menuData } = getMenuData(routes, menu, formatMessage, menuDataRender);
return (
<HomeLayout {...props}>
<Layout>
<Header className={classnames('bg-primary', styles.header)} style={{ padding: 0 }}>
{logo}
{/* {getSubMenu(menuData, isQuestion)} */}
<Menu
mode="horizontal"
theme="dark"
selectedKeys={[location.pathname]}
style={{ lineHeight: '64px', display: 'inline-block' }}
>
{getSubMenu(menuData, isQuestion)}
</Menu>
<RightContent {...props} />
</Header>
{children}
{/* {footerRender(props, null)} */}
</Layout>
</HomeLayout>
);
}
return (
<ProLayout
title=""
logo={logo}
menuHeaderRender={_logo => <Link to="/">{_logo}</Link>}
menuRender={(data: HeaderViewProps) => <SiderMenu {...data} />}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => {
if (menuItemProps.isUrl) {
return defaultDom;
}
if (menuItemProps.path === '/user/question') {
return (
<Link to={menuItemProps.path}>
<Badge dot={isQuestion} offset={[8, 0]}>
{defaultDom}
</Badge>
</Link>
);
}
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
breadcrumbRender={(routers = []) => [
{
path: '/',
breadcrumbName: formatMessage({
id: 'menu.home',
defaultMessage: 'Home',
}),
},
...routers,
]}
itemRender={($route, params, routes, paths) => {
const first = routes.indexOf($route) === 0;
return first ? (
<Link to={paths.join('/')}>{$route.breadcrumbName}</Link>
) : (
<span>{$route.breadcrumbName}</span>
);
}}
footerRender={footerRender}
menuDataRender={(menuData: MenuDataItem[]) => menuDataRender(menuData, isQuestion)}
formatMessage={formatMessage}
rightContentRender={rightProps => <RightContent {...rightProps} />}
{...props}
{...settings}
>
{children}
</ProLayout>
);
};
export default connect(({ global, settings }: ConnectState) => ({
collapsed: global.collapsed,
isQuestion: global.isQuestion,
settings,
}))(BasicLayout);
import React from 'react';
const Layout: React.FC = ({ children }) => <div>{children}</div>;
export default Layout;
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment