Commit 68960cc1 authored by 郝聪敏's avatar 郝聪敏

Initial commit

parents
Pipeline #725 failed with stages
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
/lambda/
/scripts
/config
.history
\ No newline at end of file
module.exports = {
parser: 'babel-eslint',
extends: [require.resolve('@umijs/fabric/dist/eslint')],
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
page: true,
},
rules: {
'max-len': ['error', { code: 200 }],
},
};
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
**/node_modules
# roadhog-api-doc ignore
/src/utils/request-temp.js
_roadhog-api-doc
# production
/dist
/.vscode
# misc
.DS_Store
npm-debug.log*
yarn-error.log
/coverage
.idea
yarn.lock
*bak
.vscode
# visual studio code
.history
*.log
functions/*
.temp/**
# umi
.umi
.umi-production
# screenshot
screenshot
.firebase
.eslintcache
build
**/*.svg
package.json
.umi
.umi-production
/dist
.dockerignore
.DS_Store
.eslintignore
*.png
*.toml
docker
.editorconfig
Dockerfile*
.gitignore
.prettierignore
LICENSE
.eslintcache
*.lock
yarn-error.log
.history
\ No newline at end of file
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.prettier,
};
const fabric = require('@umijs/fabric');
module.exports = {
...fabric.stylelint,
};
# 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 defaultSettings from './defaultSettings'; // https://umijs.org/config/
import slash from 'slash2';
import webpackPlugin from './plugin.config';
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 } = process.env;
const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site';
const plugins = [
[
'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: true,
},
// dynamicImport: {
// loadingComponent: './components/PageLoading/index',
// webpackChunkName: true,
// level: 3,
// },
pwa: 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'],
// },
},
],
[
'umi-plugin-pro-block',
{
moveMock: false,
moveService: false,
modifyRequest: true,
autoAddMenu: true,
},
],
];
export default {
plugins,
block: {
// 国内用户可以使用码云
// defaultGitUrl: 'https://gitee.com/ant-design/pro-blocks',
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: [
{
path: '/user',
component: '../layouts/UserLayout',
routes: [
{
name: 'login',
path: '/user/login',
component: './user/login',
},
],
},
{
path: '/',
component: '../layouts/SecurityLayout',
routes: [
{
path: '/',
component: '../layouts/BasicLayout',
authority: ['admin', 'user'],
routes: [
{
path: '/',
component: './Admin',
},
{
path: 'orderManage/pendingDeliveryOrder',
name: 'pendingDeliveryOrder',
component: './orderManage/pendingDeliveryOrder',
},
{
component: './404',
},
],
},
{
component: './404',
},
],
},
{
component: './404',
},
],
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
theme: {
'primary-color': primaryColor,
},
define: {
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, _, localName) => {
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 => a.replace(/([A-Z])/g, '-$1'))
.map(a => a.toLowerCase());
return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
}
return localName;
},
},
chainWebpack: webpackPlugin,
/*
proxy: {
'/server/api/': {
target: 'https://preview.pro.ant.design/',
changeOrigin: true,
pathRewrite: { '^/server': '' },
},
},
*/
};
export default {
navTheme: 'dark',
primaryColor: '#1890FF',
layout: 'sidemenu',
contentWidth: 'Fluid',
fixedHeader: false,
autoHideHeader: false,
fixSiderbar: false,
colorWeak: false,
menu: {
locale: true,
},
title: '商户管理后台',
pwa: false,
iconfontUrl: '',
};
const isProduction = process.env.NODE_ENV === 'production';
let envAPi = {
// api: '//backstms-qa.liangkebang.net',
api: '//yapi.quantgroups.com/mock/351',
};
let prodApi = {
api: '//backstms.q-gp.com',
};
let exportApi;
isProduction ? (exportApi = prodApi) : (exportApi = envAPi);
export default exportApi;
// 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) {
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 = moduleDirName; // handle tree shaking
if (packageName && packageName.match('^_')) {
// eslint-disable-next-line prefer-destructuring
packageName = packageName.match(/^_(@?[^@]+)/)[1];
}
return packageName;
}
export default config => {
// 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) {
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 => {
const packageName = getModulePackageName(module) || '';
if (packageName) {
return [
'bizcharts',
'gg-editor',
'g6',
'@antv',
'gg-editor-core',
'bizcharts-plugin-slider',
].includes(packageName);
}
return false;
},
name(module) {
const packageName = getModulePackageName(module);
if (packageName) {
if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
return 'viz'; // visualization package
}
}
return 'misc';
},
},
},
});
};
const getAntdSerials = color => {
const lightNum = 9;
const devide10 = 10; // 淡化(即less的tint)
const lightens = new Array(lightNum).fill(undefined).map((_, i) => {
return ThemeColorReplacer.varyColor.lighten(color, i / devide10);
});
const colorPalettes = generate(color);
const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',');
return lightens.concat(colorPalettes).concat(rgb);
};
// 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,
localStorage: null,
},
};
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
const getNotices = (req, res) => {
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': [
{
path: '/orderManage',
name: '订单管理',
icon: 'smile',
children: [
{
path: '/orderManage/pendingDeliveryOrder',
name: '待发货订单',
},
],
},
],
};
function getFakeCaptcha(req, res) {
return res.json('captcha-xxx');
} // 代码中会兼容本地 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, res) => {
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, res) => {
res.send({
status: 'ok',
currentAuthority: 'user',
});
},
'GET /api/500': (req, res) => {
res.status(500).send({
timestamp: 1513932555104,
status: 500,
error: 'error',
message: 'error',
path: '/base/category/list',
});
},
'GET /api/404': (req, res) => {
res.status(404).send({
timestamp: 1513932643431,
status: 404,
error: 'Not Found',
message: 'No message available',
path: '/base/category/list/2121212',
});
},
'GET /api/403': (req, res) => {
res.status(403).send({
timestamp: 1513932555104,
status: 403,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/401': (req, res) => {
res.status(401).send({
timestamp: 1513932555104,
status: 401,
error: 'Unauthorized',
message: 'Unauthorized',
path: '/base/category/list',
});
},
'GET /api/login/captcha': getFakeCaptcha,
'GET /api/kdsp/op/order/waiting-for-delivery': (req, res) => {
res.send({
code: 2000,
data: {
content: [{
orderNo: '123',
skuSource: '中联',
payTime: '2020-10-20',
receiverName: '郝聪敏',
receiverPhone: '13718672532',
receiveAddress: '北京市房山区',
status: '1'
}]
}
});
},
};
This diff is collapsed.
{
"name": "merchant-manage-ui",
"version": "1.0.0",
"private": true,
"description": "商户管理后台",
"scripts": {
"analyze": "cross-env ANALYZE=1 umi build",
"build": "umi build",
"deploy": "npm run site && npm run gh-pages",
"fetch:blocks": "pro fetch-blocks && npm run prettier",
"format-imports": "cross-env 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 ./src",
"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",
"start:no-ui": "cross-env UMI_UI=none umi dev",
"test": "umi test",
"test:all": "node ./tests/run-tests.js",
"test:component": "umi test ./src/components",
"ui": "umi ui"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-staged"
}
},
"lint-staged": {
"**/*.less": "stylelint --syntax less",
"**/*.{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/colors": "^3.1.0",
"@ant-design/pro-layout": "^4.10.0",
"@ant-design/pro-table": "^1.0.31",
"@antv/data-set": "^0.10.2",
"antd": "^3.23.6",
"classnames": "^2.2.6",
"dva": "^2.4.1",
"lodash": "^4.17.11",
"moment": "^2.24.0",
"omit.js": "^1.0.2",
"path-to-regexp": "^3.1.0",
"qs": "^6.9.0",
"react": "^16.8.6",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.8.6",
"react-helmet": "^5.2.1",
"redux": "^4.0.1",
"slash2": "^2.0.0",
"umi": "^2.13.0",
"umi-plugin-pro-block": "^1.3.4",
"umi-plugin-react": "^1.10.1",
"umi-request": "^1.2.7",
"webpack-theme-color-replacer": "^1.2.15"
},
"devDependencies": {
"@ant-design/pro-cli": "^1.0.13",
"@types/classnames": "^2.2.7",
"@types/express": "^4.17.0",
"@types/history": "^4.7.2",
"@types/jest": "^24.0.13",
"@types/lodash": "^4.14.144",
"@types/qs": "^6.5.3",
"@types/react": "^16.8.19",
"@types/react-dom": "^16.8.4",
"@types/react-helmet": "^5.0.13",
"@umijs/fabric": "^1.2.0",
"chalk": "^2.4.2",
"check-prettier": "^1.0.3",
"cross-env": "^6.0.0",
"cross-port-killer": "^1.1.1",
"enzyme": "^3.9.0",
"eslint": "5.16.0",
"express": "^4.17.1",
"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",
"stylelint": "^10.1.0",
"umi-plugin-ga": "^1.1.3",
"umi-plugin-pro": "^1.0.2",
"umi-types": "^0.5.0"
},
"engines": {
"node": ">=10.0.0"
},
"checkFiles": [
"src/**/*.js*",
"src/**/*.ts*",
"src/**/*.less",
"config/**/*.js*",
"scripts/**/*.js"
],
"author": "congmin.hao"
}
/* eslint-disable no-console */
export const dva = {
config: {
onError(e) {
e.preventDefault();
console.error(e.message);
},
},
};
<?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 { Result } from 'antd';
import check from './CheckPermissions';
const Authorized = ({
children,
authority,
noMatch = (
<Result
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
/>
),
}) => {
const childrenRender = typeof children === 'undefined' ? null : children;
const dom = check(authority, childrenRender, noMatch);
return <>{dom}</>;
};
export default Authorized;
import { Redirect, Route } from 'umi';
import React from 'react';
import Authorized from './Authorized';
const AuthorizedRoute = ({ component: Component, render, authority, redirectPath, ...rest }) => (
<Authorized
authority={authority}
noMatch={
<Route
{...rest}
render={() => (
<Redirect
to={{
pathname: redirectPath,
}}
/>
)}
/>
}
>
<Route {...rest} render={props => (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';
/**
* 通用权限检查方法
* Common check permissions method
* @param { 权限判定 | Permission judgment } authority
* @param { 你的权限 | Your permission description } currentAuthority
* @param { 通过的组件 | Passing components } target
* @param { 未通过的组件 | no pass components } Exception
*/
const checkPermissions = (authority, currentAuthority, target, Exception) => {
// 没有判定权限.默认查看所有
// 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 ok={target} error={Exception} promise={authority} />;
} // Function 处理
if (typeof authority === 'function') {
try {
const bool = authority(currentAuthority); // 函数执行后返回值是 Promise
if (bool instanceof Promise) {
return <PromiseRender 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(authority, target, Exception) {
return checkPermissions(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
export default class PromiseRender extends React.Component {
state = {
component: () => null,
};
componentDidMount() {
this.setRenderComponent(this.props);
}
shouldComponentUpdate = (nextProps, nextState) => {
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) {
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 => {
if (isComponentClass(target)) {
const Target = target;
return props => <Target {...props} />;
}
if (React.isValidElement(target)) {
return props => React.cloneElement(target, props);
}
return () => target;
};
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 => {
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 => {
if (isComponentClass(target)) {
const Target = target;
return props => <Target {...props} />;
}
if (React.isValidElement(target)) {
return props => 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, error) => {
/**
* conversion into a class
* 防止传入字符串时找不到staticContext造成报错
* String parameters can cause staticContext not found error
*/
let classError = false;
if (error) {
classError = () => error;
}
if (!authority) {
throw new Error('authority is required');
}
return function decideAuthority(target) {
const component = CheckPermissions(authority, target, classError || Exception403);
return checkIsInstantiation(component);
};
};
export default authorize;
import Authorized from './Authorized';
import Secured from './Secured';
import check from './CheckPermissions';
import renderAuthorize from './renderAuthorize';
Authorized.Secured = Secured;
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 = 'NULL';
/**
* use authority or getAuthority
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = Authorized => currentAuthority => {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (
Object.prototype.toString.call(currentAuthority) === '[object String]' ||
Array.isArray(currentAuthority)
) {
CURRENT = currentAuthority;
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default Authorized => renderAuthorize(Authorized);
import { Icon, Popover, Typography } from 'antd';
import React, { useRef } from 'react';
import { connect } from 'dva';
import { isAntDesignPro } from '@/utils/utils';
import styles from './index.less';
const firstUpperCase = pathString =>
pathString
.replace('.', '')
.split(/\/|-/)
.map(s => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()))
.filter(s => !!s)
.join(''); // when click block copy, send block url to ga
const onBlockCopy = label => {
if (!isAntDesignPro()) {
return;
}
const ga = window && window.ga;
if (ga) {
ga('send', 'event', {
eventCategory: 'block',
eventAction: 'copy',
eventLabel: label,
});
}
};
const BlockCodeView = ({ 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>
);
};
export default connect(({ routing }) => ({
location: routing.location,
}))(({ location }) => {
const url = location.pathname;
const divDom = useRef(null);
return (
<Popover
title="下载此页面到本地项目"
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>
);
});
.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 { Avatar, Icon, Menu, Spin } from 'antd';
import React from 'react';
import { connect } from 'dva';
import router from 'umi/router';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
class AvatarDropdown extends React.Component {
onMenuClick = event => {
const { key } = event;
if (key === 'logout') {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'login/logout',
});
}
return;
}
router.push(`/account/${key}`);
};
render() {
const {
currentUser = {
avatar: '',
name: '',
},
menu,
} = this.props;
const menuHeaderDropdown = (
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
{menu && (
<Menu.Item key="center">
<Icon type="user" />
个人中心
</Menu.Item>
)}
{menu && (
<Menu.Item key="settings">
<Icon type="setting" />
个人设置
</Menu.Item>
)}
{menu && <Menu.Divider />}
<Menu.Item key="logout">
<Icon type="logout" />
退出登录
</Menu.Item>
</Menu>
);
return currentUser && currentUser.name ? (
<HeaderDropdown overlay={menuHeaderDropdown}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} alt="avatar" />
<span className={styles.name}>{currentUser.name}</span>
</span>
</HeaderDropdown>
) : (
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
);
}
}
export default connect(({ user }) => ({
currentUser: user.currentUser,
}))(AvatarDropdown);
import React, { Component } from 'react';
import { Tag, message } from 'antd';
import { connect } from 'dva';
import groupBy from 'lodash/groupBy';
import moment from 'moment';
import NoticeIcon from '../NoticeIcon';
import styles from './index.less';
class GlobalHeaderRight extends Component {
componentDidMount() {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'global/fetchNotices',
});
}
}
changeReadState = clickedItem => {
const { id } = clickedItem;
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'global/changeNoticeReadState',
payload: id,
});
}
};
handleNoticeClear = (title, key) => {
const { dispatch } = this.props;
message.success(`${'清空了'} ${title}`);
if (dispatch) {
dispatch({
type: 'global/clearNotices',
payload: key,
});
}
};
getNoticeData = () => {
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).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 => {
const unreadMsg = {};
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);
}}
loading={fetchingNotices}
clearText="清空"
viewMoreText="查看更多"
onClear={this.handleNoticeClear}
onPopupVisibleChange={onNoticeVisibleChange}
onViewMore={() => message.info('Click on view more')}
clearClose
>
<NoticeIcon.Tab
tabKey="notification"
count={unreadMsg.notification}
list={noticeData.notification}
title="通知"
emptyText="你已查看所有通知"
showViewMore
/>
<NoticeIcon.Tab
tabKey="message"
count={unreadMsg.message}
list={noticeData.message}
title="消息"
emptyText="您已读完所有消息"
showViewMore
/>
<NoticeIcon.Tab
tabKey="event"
title="待办"
emptyText="你已完成所有待办"
count={unreadMsg.event}
list={noticeData.event}
showViewMore
/>
</NoticeIcon>
);
}
}
export default connect(({ user, global, loading }) => ({
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 Avatar from './AvatarDropdown';
import HeaderSearch from '../HeaderSearch';
import styles from './index.less';
const GlobalHeaderRight = 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="站内搜索"
defaultValue="umi ui"
dataSource={['搜索提示一', '搜索提示二', '搜索提示三']}
/>
<Tooltip title="使用文档">
<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 />
</div>
);
};
export default connect(({ settings }) => ({
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: 100%;
margin-left: auto;
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-pro-global-header) {
.dark {
.action {
color: @text-color;
> i {
color: @text-color;
}
&:hover {
color: rgba(255, 255, 255, 0.85);
> i {
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;
.account {
.avatar {
margin-right: 0;
}
}
}
}
import { Dropdown } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => (
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
);
export default HeaderDropdown;
@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 { AutoComplete, Icon, Input } from 'antd';
import React, { Component } from 'react';
import classNames from 'classnames';
import debounce from 'lodash/debounce';
import styles from './index.less';
export default class HeaderSearch extends Component {
static defaultProps = {
defaultActiveFirstOption: false,
onPressEnter: () => {},
onSearch: () => {},
onChange: () => {},
className: '',
placeholder: '',
dataSource: [],
defaultOpen: false,
onVisibleChange: () => {},
};
static getDerivedStateFromProps(props) {
if ('open' in props) {
return {
searchMode: props.open,
};
}
return null;
}
inputRef = null;
constructor(props) {
super(props);
this.state = {
searchMode: props.defaultOpen,
value: props.defaultValue,
};
this.debouncePressEnter = debounce(this.debouncePressEnter, 500, {
leading: true,
trailing: false,
});
}
onKeyDown = e => {
if (e.key === 'Enter') {
this.debouncePressEnter();
}
};
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,
});
};
debouncePressEnter = () => {
const { onPressEnter } = this.props;
const { value } = this.state;
onPressEnter(value || '');
};
render() {
const { className, defaultValue, 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;
}}
defaultValue={defaultValue}
aria-label={placeholder}
placeholder={placeholder}
onKeyDown={this.onKeyDown}
onBlur={this.leaveSearchMode}
/>
</AutoComplete>
</span>
);
}
}
@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 { Avatar, List } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './NoticeList.less';
const NoticeList = ({
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
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';
.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 { Badge, Icon, Spin, Tabs } from 'antd';
import React, { Component } from 'react';
import classNames from 'classnames';
import NoticeList from './NoticeList';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
const { TabPane } = Tabs;
export default class NoticeIcon extends Component {
static Tab = NoticeList;
static defaultProps = {
onItemClick: () => {},
onPopupVisibleChange: () => {},
onTabChange: () => {},
onClear: () => {},
onViewMore: () => {},
loading: false,
clearClose: false,
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
};
state = {
visible: false,
};
onItemClick = (item, tabProps) => {
const { onItemClick } = this.props;
if (onItemClick) {
onItemClick(item, tabProps);
}
};
onClear = (name, key) => {
const { onClear } = this.props;
if (onClear) {
onClear(name, key);
}
};
onTabChange = tabType => {
const { onTabChange } = this.props;
if (onTabChange) {
onTabChange(tabType);
}
};
onViewMore = (tabProps, event) => {
const { onViewMore } = this.props;
if (onViewMore) {
onViewMore(tabProps, event);
}
};
getNotificationBox() {
const { children, loading, clearText, viewMoreText } = this.props;
if (!children) {
return null;
}
const panes = React.Children.map(children, child => {
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 = msgCount > 0 ? `${title} (${msgCount})` : title;
return (
<TabPane tab={tabTitle} key={title}>
<NoticeList
clearText={clearText}
viewMoreText={viewMoreText}
data={list}
onClear={() => this.onClear(title, tabKey)}
onClick={item => this.onItemClick(item, child.props)}
onViewMore={event => 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 => {
const { onPopupVisibleChange } = this.props;
this.setState({
visible,
});
if (onPopupVisibleChange) {
onPopupVisibleChange(visible);
}
};
render() {
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 = {};
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';
.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 React from 'react';
import { Spin } from 'antd'; // loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
const PageLoading = () => (
<div
style={{
paddingTop: 100,
textAlign: 'center',
}}
>
<Spin size="large" />
</div>
);
export default PageLoading;
// 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) {
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);
const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',');
return lightens.concat(colorPalettes).concat(rgb);
},
changeColor(color) {
if (!color) {
return Promise.resolve();
}
const options = {
// new colors array, one-to-one corresponde with `matchColors`
newColors: this.getAntdSerials(color),
changeUrl(cssUrl) {
// while router is not `hash` mode, it needs absolute path
return `/${cssUrl}`;
},
};
return client.changer.changeColor(options, Promise);
},
};
const { uniq } = require('lodash');
const RouterConfig = require('../../config/config').default.routes;
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);
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();
});
});
@import '~antd/es/style/themes/default.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;
}
}
}
}
}
/**
* 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
*/
/* eslint-disable no-underscore-dangle, no-confusing-arrow */
import ProLayout from '@ant-design/pro-layout';
import React, { useState, useEffect } from 'react';
import Link from 'umi/link';
import { connect } from 'dva';
import { Result, Button, Layout, Menu, Icon } from 'antd';
import Authorized from '@/utils/Authorized';
import RightContent from '@/components/GlobalHeader/RightContent';
import { getAuthorityFromRouter } from '@/utils/utils';
import logo from '../assets/favicon.png';
import style from './BasicLayout.less';
const { Sider } = Layout;
const { SubMenu } = Menu;
const noMatch = (
<Result
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
extra={
<Button type="primary">
{window.__POWERED_BY_QIANKUN__ ? (
<a href={`${window.origin}/access/login`}>Go Login</a>
) : (
<Link to="/user/login">Go Login</Link>
)}
</Button>
}
/>
);
const BasicLayout = props => {
const {
dispatch,
children,
settings,
menuData = [],
location = {
pathname: '/',
},
} = props;
const [siderCollapsed, setSiderCollapsed] = useState(false);
/**
* constructor
*/
useEffect(() => {
if (dispatch) {
dispatch({
type: 'settings/getSetting',
});
dispatch({
type: 'menu/getMenuData',
});
}
}, []);
/**
* init variables
*/
const handleMenuCollapse = payload => {
if (dispatch) {
dispatch({
type: 'global/changeLayoutCollapsed',
payload,
});
}
}; // get children authority
const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/');
const _menu = param =>
param.map(v =>
v.children && v.children.length ? (
<SubMenu
key={v.path}
title={
<span>
<Icon type={v.icon} />
<span>{v.name}</span>
</span>
}
>
{_menu(v.children)}
</SubMenu>
) : (
<Menu.Item key={v.path}>
<Link to={v.path}>
<Icon type={v.icon} />
<span className="nav-text">{v.name}</span>
</Link>
</Menu.Item>
),
);
return window.__POWERED_BY_QIANKUN__ ? (
<div id="micro">
<Layout className={style.layout}>
<Sider
collapsible
collapsed={siderCollapsed}
onCollapse={() => setSiderCollapsed(!siderCollapsed)}
>
<Menu theme="dark" mode="inline" defaultSelectedKeys={[location.pathname]}>
{_menu(menuData)}
</Menu>
</Sider>
<Layout style={{ zIndex: 1 }}>
<Authorized authority={authorized.authority} noMatch={noMatch}>
{children}
</Authorized>
</Layout>
</Layout>
</div>
) : (
<ProLayout
logo={logo}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => {
if (menuItemProps.isUrl || menuItemProps.children) {
return defaultDom;
}
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
breadcrumbRender={(routers = []) => [
{
path: '/',
breadcrumbName: '首页',
},
...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>
);
}}
menuDataRender={() => menuData}
rightContentRender={rightProps => <RightContent {...rightProps} />}
pageTitleRender={() => ''}
{...props}
{...settings}
>
<Authorized authority={authorized.authority} noMatch={noMatch}>
{children}
</Authorized>
</ProLayout>
);
};
export default connect(({ global, settings, menu }) => ({
collapsed: global.collapsed,
settings,
menuData: menu.menuData,
}))(BasicLayout);
.layout {
height: 100%;
}
:global {
#micro {
width: 100%;
height: 100%;
.ant-layout-sider-trigger {
background: #002140;
}
.ant-pro-page-header-wrap-page-header-warp {
display: none;
}
.ant-pro-page-header-wrap-children-content {
margin: 15px;
}
.ant-layout {
min-height: 0 !important;
}
}
}
import React from 'react';
const Layout = ({ children }) => <div>{children}</div>;
export default Layout;
import React from 'react';
import { connect } from 'dva';
import { Redirect } from 'umi';
import PageLoading from '@/components/PageLoading';
import localStorage from '@/utils/localStorage';
class SecurityLayout extends React.Component {
state = {
isReady: false,
};
componentDidMount() {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
}
const delaySet = () => this.setState({ isReady: true });
setTimeout(() => {
delaySet();
}, 0);
}
/* eslint-disable no-underscore-dangle */
render() {
const { isReady } = this.state;
let isLogin = true;
// 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
const { children, loading } = this.props;
if (!localStorage.get('token')) {
isLogin = false;
}
// 切换子应用布局
if (window.__POWERED_BY_QIANKUN__) {
const container = document.querySelector('#container');
container.classList.add('micro-layout');
}
if ((!isLogin && loading) || !isReady) {
return <PageLoading />;
}
if (!isLogin) {
if (window.__POWERED_BY_QIANKUN__) {
window.location.href = `${window.origin}/access/login`;
return undefined;
}
return <Redirect to="/user/login"></Redirect>;
}
return children;
}
}
export default connect(({ user, loading }) => ({
currentUser: user.currentUser,
loading: loading.models.user,
}))(SecurityLayout);
import { getMenuData, getPageTitle } from '@ant-design/pro-layout';
import { Helmet } from 'react-helmet';
import React from 'react';
import { connect } from 'dva';
import logo from '../assets/logo.svg';
import styles from './UserLayout.less';
const UserLayout = props => {
const {
route = {
routes: [],
},
} = props;
const { routes = [] } = route;
const {
children,
location = {
pathname: '',
},
} = props;
const { breadcrumb } = getMenuData(routes);
const title = getPageTitle({
pathname: location.pathname,
breadcrumb,
...props,
});
return (
<>
<Helmet>
<title>{title}</title>
<meta name="description" content={title} />
</Helmet>
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.top}>
<div className={styles.header}>
<img alt="logo" className={styles.logo} src={logo} />
<span className={styles.title}>商户管理后台</span>
</div>
</div>
{children}
</div>
</div>
</>
);
};
export default connect(({ settings }) => ({ ...settings }))(UserLayout);
@import '~antd/es/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
flex: 1;
margin-top: 140px;
padding: 32px 0;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.content {
padding: 32px 0 24px;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
margin-bottom: 73px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
height: 44px;
margin-right: 16px;
vertical-align: top;
}
.title {
position: relative;
top: 2px;
color: @heading-color;
font-weight: 600;
font-size: 33px;
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
font-size: @font-size-base;
}
import { queryNotices } from '@/services/user';
const GlobalModel = {
namespace: 'global',
state: {
collapsed: false,
notices: [],
},
effects: {
*fetchNotices(_, { call, put, select }) {
const data = yield call(queryNotices);
yield put({
type: 'saveNotices',
payload: data,
});
const unreadCount = yield select(
state => state.global.notices.filter(item => !item.read).length,
);
yield put({
type: 'user/changeNotifyCount',
payload: {
totalCount: data.length,
unreadCount,
},
});
},
*clearNotices({ payload }, { put, select }) {
yield put({
type: 'saveClearedNotices',
payload,
});
const count = yield select(state => state.global.notices.length);
const unreadCount = yield select(
state => state.global.notices.filter(item => !item.read).length,
);
yield put({
type: 'user/changeNotifyCount',
payload: {
totalCount: count,
unreadCount,
},
});
},
*changeNoticeReadState({ payload }, { put, select }) {
const notices = yield select(state =>
state.global.notices.map(item => {
const notice = { ...item };
if (notice.id === payload) {
notice.read = true;
}
return notice;
}),
);
yield put({
type: 'saveNotices',
payload: notices,
});
yield put({
type: 'user/changeNotifyCount',
payload: {
totalCount: notices.length,
unreadCount: notices.filter(item => !item.read).length,
},
});
},
},
reducers: {
changeLayoutCollapsed(
state = {
notices: [],
collapsed: true,
},
{ payload },
) {
return { ...state, collapsed: payload };
},
saveNotices(state, { payload }) {
return {
collapsed: false,
...state,
notices: payload,
};
},
saveClearedNotices(
state = {
notices: [],
collapsed: true,
},
{ payload },
) {
return {
collapsed: false,
...state,
notices: state.notices.filter(item => item.type !== payload),
};
},
},
subscriptions: {
setup({ history }) {
// Subscribe history(url) change, trigger `load` action if pathname is `/`
history.listen(({ pathname, search }) => {
if (typeof window.ga !== 'undefined') {
window.ga('send', 'pageview', pathname + search);
}
});
},
},
};
export default GlobalModel;
import { routerRedux } from 'dva/router';
import { notification } from 'antd';
import { stringify } from 'querystring';
import { fakeAccountLogin, getFakeCaptcha } from '@/services/login';
import { setAuthority } from '@/utils/authority';
import { getPageQuery } from '@/utils/utils';
import localStorage from '@/utils/localStorage';
const Model = {
namespace: 'login',
state: {
status: undefined,
},
effects: {
*login({ payload }, { call, put }) {
const response = yield call(fakeAccountLogin, payload);
if (response.code === 2000) {
localStorage.set('token', response.data.accessToken);
localStorage.set('refreshtoken', response.data.refreshToken);
localStorage.set('atExpire', response.data.atExpire);
yield put({
type: 'changeLoginStatus',
payload: response,
});
yield put(routerRedux.replace('/'));
} else {
notification.error({ message: response.msg || '登录失败!' });
}
},
*getCaptcha({ payload }, { call }) {
yield call(getFakeCaptcha, payload);
},
*logout(_, { put }) {
const { redirect } = getPageQuery(); // redirect
if (window.location.pathname !== '/user/login' && !redirect) {
yield put(
routerRedux.replace({
pathname: '/user/login',
search: stringify({
redirect: window.location.href,
}),
}),
);
localStorage.clear();
window.location.reload();
}
},
},
reducers: {
changeLoginStatus(state, { payload }) {
setAuthority(payload.currentAuthority);
return { ...state, status: payload.status, type: payload.type };
},
},
};
export default Model;
import { getMenuData } from '@/services/menu';
const MenuModel = {
namespace: 'menu',
state: {
menuData: [
{
path: '/',
name: '主页',
icon: 'home',
key: '',
},
],
},
effects: {
*getMenuData({ payload }, { put, call }) {
const response = yield call(getMenuData, payload);
if (response.code === 2000) {
// 拿到数据后response.data后组装成新的数据复制给menuData
yield put({
type: 'save',
payload: response.data,
});
}
},
},
reducers: {
save(state, { payload }) {
const initializationData = tableData => {
const value = [];
const data = [...tableData];
data
.filter(item => item.node && item.node.uri !== '/systemManage')
.forEach(item => {
let obj = {};
obj = { ...item.node };
obj.path = obj.uri;
obj.icon = obj.picture;
obj.name = obj.name;
if (item.childNodes.length) {
obj.children = initializationData(item.childNodes);
} else {
obj.children = [];
}
value.push(obj);
});
return value;
};
const menuData = initializationData(payload);
menuData.push({
icon: "laptop",
id: "47291f3a-dfd8-4c4e-8fa8-5cf262ccf9a2",
name: "订单管理",
path: "/orderManage",
picture: "laptop",
type: {name: "资源组", value: 1},
uri: "/operation",
children: [{
icon: "reconciliation",
id: "169e18a2-0b65-465e-a090-f9b2edc442f3",
name: "待发货订单",
path: "/orderManage/pendingDeliveryOrder",
picture: "reconciliation",
type: {name: "菜单", value: 2},
uri: "/operation/operationCheck",
}],
});
return { ...state, menuData};
},
},
};
export default MenuModel;
import { message } from 'antd';
import defaultSettings from '../../config/defaultSettings';
import themeColorClient from '../components/SettingDrawer/themeColorClient';
const updateTheme = newPrimaryColor => {
if (newPrimaryColor) {
const timeOut = 0;
const hideMessage = message.loading('正在切换主题!', timeOut);
themeColorClient.changeColor(newPrimaryColor).finally(() => hideMessage());
}
};
const updateColorWeak = colorWeak => {
const root = document.getElementById('root');
if (root) {
root.className = colorWeak ? 'colorWeak' : '';
}
};
const SettingModel = {
namespace: 'settings',
state: defaultSettings,
reducers: {
getSetting(state = defaultSettings) {
const setting = {};
const urlParams = new URL(window.location.href);
Object.keys(state).forEach(key => {
if (urlParams.searchParams.has(key)) {
const value = urlParams.searchParams.get(key);
setting[key] = value === '1' ? true : value;
}
});
const { primaryColor, colorWeak } = setting;
if (primaryColor && state.primaryColor !== primaryColor) {
updateTheme(primaryColor);
}
updateColorWeak(!!colorWeak);
return { ...state, ...setting };
},
changeSetting(state = defaultSettings, { payload }) {
const urlParams = new URL(window.location.href);
Object.keys(defaultSettings).forEach(key => {
if (urlParams.searchParams.has(key)) {
urlParams.searchParams.delete(key);
}
});
Object.keys(payload).forEach(key => {
if (key === 'collapse') {
return;
}
let value = payload[key];
if (value === true) {
value = 1;
}
if (defaultSettings[key] !== value) {
urlParams.searchParams.set(key, value);
}
});
const { primaryColor, colorWeak, contentWidth } = payload;
if (primaryColor && state.primaryColor !== primaryColor) {
updateTheme(primaryColor);
}
if (state.contentWidth !== contentWidth && window.dispatchEvent) {
window.dispatchEvent(new Event('resize'));
}
updateColorWeak(!!colorWeak);
window.history.replaceState(null, 'setting', urlParams.href);
return { ...state, ...payload };
},
},
};
export default SettingModel;
import { queryCurrent, query as queryUsers } from '@/services/user';
const UserModel = {
namespace: 'user',
state: {
currentUser: {},
},
effects: {
*fetch(_, { call, put }) {
const response = yield call(queryUsers);
yield put({
type: 'save',
payload: response,
});
},
*fetchCurrent(_, { call, put }) {
const response = yield call(queryCurrent);
yield put({
type: 'saveCurrentUser',
payload: response,
});
},
},
reducers: {
saveCurrentUser(state, action) {
return { ...state, currentUser: action.payload || {} };
},
changeNotifyCount(
state = {
currentUser: {},
},
action,
) {
return {
...state,
currentUser: {
...state.currentUser,
notifyCount: action.payload.totalCount,
unreadCount: action.payload.unreadCount,
},
};
},
},
};
export default UserModel;
import { Button, Result } from 'antd';
import React from 'react';
import router from 'umi/router'; // 这里应该使用 antd 的 404 result 组件,
// 但是还没发布,先来个简单的。
const NoFoundPage = () => (
<Result
status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Button type="primary" onClick={() => router.push('/')}>
Back Home
</Button>
}
></Result>
);
export default NoFoundPage;
import React from 'react';
import { Card, Typography, Alert, Icon } from 'antd';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
export default () => (
<PageHeaderWrapper content=" 这个页面只有 admin 权限才能查看">
<Card>
<Alert
message="umi ui 现已发布,欢迎使用 npm run ui 启动体验。"
type="success"
showIcon
banner
style={{
margin: -12,
marginBottom: 48,
}}
/>
<Typography.Title
level={2}
style={{
textAlign: 'center',
}}
>
<Icon type="smile" theme="twoTone" /> Ant Design Pro{' '}
<Icon type="heart" theme="twoTone" twoToneColor="#eb2f96" /> You
</Typography.Title>
</Card>
<p
style={{
textAlign: 'center',
marginTop: 24,
}}
>
Want to add more pages? Please refer to{' '}
<a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
use block
</a>
</p>
</PageHeaderWrapper>
);
import React from 'react';
import Redirect from 'umi/redirect';
import { connect } from 'dva';
import pathToRegexp from 'path-to-regexp';
import Authorized from '@/utils/Authorized';
const getRouteAuthority = (path, routeData) => {
let authorities;
routeData.forEach(route => {
if (route.authority) {
authorities = route.authority;
} // match prefix
if (pathToRegexp(`${route.path}(.*)`).test(path)) {
// exact match
if (route.path === path) {
authorities = route.authority || authorities;
} // get children authority recursively
if (route.routes) {
authorities = getRouteAuthority(path, route.routes) || authorities;
}
}
});
return authorities;
};
const AuthComponent = ({
children,
route = {
routes: [],
},
location = {
pathname: '',
},
user,
}) => {
const { currentUser } = user;
const { routes = [] } = route;
const isLogin = currentUser && currentUser.name;
return (
<Authorized
authority={getRouteAuthority(location.pathname, routes) || ''}
noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
>
{children}
</Authorized>
);
};
export default connect(({ user }) => ({
user,
}))(AuthComponent);
import React from 'react';
import { Card, Typography, Alert } from 'antd';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
const CodePreview = ({ children }) => (
<pre
style={{
background: '#f2f4f5',
padding: '12px 20px',
margin: '12px 0',
}}
>
<code>
<Typography.Text copyable>{children}</Typography.Text>
</code>
</pre>
);
export default () => (
<PageHeaderWrapper>
<Card>
<Alert
message="umi ui 现已发布,欢迎使用 npm run ui 启动体验。"
type="success"
showIcon
banner
style={{
margin: -12,
marginBottom: 24,
}}
/>
<Typography.Text strong>
<a target="_blank" rel="noopener noreferrer" href="https://pro.ant.design/docs/block">
基于 block 开发,快速构建标准页面
</a>
</Typography.Text>
<CodePreview>npx umi block list</CodePreview>
<Typography.Text
strong
style={{
marginBottom: 12,
}}
>
<a
target="_blank"
rel="noopener noreferrer"
href="https://pro.ant.design/docs/available-script#npm-run-fetchblocks"
>
获取全部区块
</a>
</Typography.Text>
<CodePreview> npm run fetch:blocks</CodePreview>
</Card>
<p
style={{
textAlign: 'center',
marginTop: 24,
}}
>
Want to add more pages? Please refer to{' '}
<a href="https://pro.ant.design/docs/block-cn" target="_blank" rel="noopener noreferrer">
use block
</a>
</p>
</PageHeaderWrapper>
);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<title>商户管理后台</title>
<link rel="icon" href="/favicon.png" type="image/x-icon" />
</head>
<body>
<noscript>Out-of-the-box mid-stage front/design solution!</noscript>
<div id="root">
<style>
.page-loading-warp {
padding: 120px;
display: flex;
justify-content: center;
align-items: center;
}
.ant-spin {
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5;
list-style: none;
-webkit-font-feature-settings: 'tnum';
font-feature-settings: 'tnum';
position: absolute;
display: none;
color: #1890ff;
text-align: center;
vertical-align: middle;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-dot {
position: relative;
display: inline-block;
font-size: 20px;
width: 20px;
height: 20px;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
-ms-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
-ms-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: antSpinMove 1s infinite linear alternate;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: antRotate 1.2s infinite linear;
animation: antRotate 1.2s infinite linear;
}
.ant-spin-lg .ant-spin-dot {
font-size: 32px;
width: 32px;
height: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
</style>
<div class="page-loading-warp">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin"
><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
><i class="ant-spin-dot-item"></i><i class="ant-spin-dot-item"></i
></span>
</div>
</div>
</div>
</body>
</html>
import React, { useState, useEffect } from 'react';
import { Modal, Form, Select, Input, Icon, Card, notification } from 'antd';
import style from '../index.less';
import { updateExpress } from '../service';
const FormItem = Form.Item;
const { Option } = Select;
const LogisticsForm = props => {
const { modalVisible, onCancel, companys, skuList, onSubmit } = props;
const { getFieldDecorator, resetFields } = props.form;
const [result, setResult] = useState([{}]);
const formData = async (formDataList, fieldsValue, suffixes) => {
// 数据聚合
suffixes.forEach(suffixe => {
const formObj = {};
for(let key in fieldsValue) {
if (key.startsWith(suffixe)) {
formObj[key.split('-')[1]] = fieldsValue[key];
}
}
formDataList.push(formObj);
});
// 参数整合
const result = [];
formDataList.forEach(data => {
data.selectedGoods.forEach(orderSkuId => {
result.push({
orderSkuId: orderSkuId,
expressCompanyCode: data.selectedCompany.split('-')[0],
expressCompanyName: data.selectedCompany.split('-')[1],
delivery_no: data.orderNum,
})
})
})
// 校验重复商品
let orderSkuIds = [];
formDataList.forEach(data => {
orderSkuIds = [...orderSkuIds, ...data.selectedGoods];
});
if (orderSkuIds.length !== Array.from(new Set(orderSkuIds)).length) {
notification.open({ message: '商品选择重复!' });
return;
}
console.log(result);
await updateExpress(result);
onSubmit();
}
const handleSubmit = () => {
props.form.validateFields((err, fieldsValue) => {
let suffixes = [], formDataList = [];
if (err) return;
for(const key in fieldsValue) {
const suffixe = key.split('-')[0];
if(!suffixes.includes(suffixe)) suffixes.push(suffixe);
}
formData(formDataList, fieldsValue, suffixes);
});
}
const addForm = () => {
setResult([...result, {}])
}
const rmForm = () => {
if (result.length === 1) return;
setResult(result.slice(0, result.length - 1))
}
const reset = () => {
setResult([{}]);
}
return (
<Modal
destroyOnClose
title="物流操作确认"
visible={modalVisible}
onCancel={() => onCancel()}
onOk={handleSubmit}
afterClose={() => setResult([{}])}
>
<Form>
{result && result.length &&
result.map((v, i) => (
<Card key={i}>
<FormItem>
{getFieldDecorator(`${i}-selectedGoods`, {
rules: [
{
required: true,
message: '请选择该订单下的商品!',
},
],
})(
<Select
mode="multiple"
placeholder="请选择该订单下的商品"
>
{skuList.map(item => (
<Option value={item.orderSkuId} key={item.orderSkuId}>
{item.skuName + item.skuAttr}
</Option>
))}
</Select>,
)}
</FormItem>
<FormItem>
{getFieldDecorator(`${i}-selectedCompany`, {
rules: [
{
required: true,
message: '请选择物流公司!',
},
],
})(
<Select
placeholder="请选择物流公司"
>
{companys.map(item => (
<Option value={item.expressCompanyCode + '-' + item.expressCompanyName} key={item.expressCompanyCode}>
{item.expressCompanyName}
</Option>
))}
</Select>,
)}
</FormItem>
<FormItem>
{getFieldDecorator(`${i}-orderNum`, {
rules: [
{
required: true,
message: '请填写物流单号!',
},
],
})(<Input placeholder="请填写物流单号" />)}
</FormItem>
</Card>)
)}
<div className={style.logistics}>
<Icon className={style.logisticsIcon} onClick={addForm} type="plus-circle" />
<Icon className={style.logisticsIcon} onClick={rmForm} type="minus-circle" />
</div>
</Form>
</Modal>
);
};
export default Form.create()(LogisticsForm);
import React from 'react';
import { Modal, Form, Select, Input, Table } from 'antd';
const FormItem = Form.Item;
const { Option } = Select;
const LogisticsForm = props => {
const { modalVisible, onCancel, value } = props;
const columns = [
{
title: '商品名称',
dataIndex: 'skuName',
key: 'skuName',
},
{
title: '商品属性',
dataIndex: 'skuAttr',
key: 'skuAttr',
},
{
title: '商品件数(件)',
dataIndex: 'count',
key: 'count',
},
{
title: '物流公司',
dataIndex: 'expressCompanyName',
key: 'expressCompanyName',
},
{
title: '物流单号',
dataIndex: 'deliveryNo',
key: 'deliveryNo',
}
];
const style = {
width: '700px'
};
return (
<Modal
title="订单详情"
visible={modalVisible}
onCancel={() => onCancel()}
onOk={() => onCancel()}
bodyStyle={style}
width={700}
>
<Table columns={columns} dataSource={value} />
</Modal>
);
};
export default Form.create()(LogisticsForm);
import { Button, Divider, Dropdown, Icon, Menu, message, Table } from 'antd';
import React, { useState, useEffect, useRef } from 'react';
import { PageHeaderWrapper } from '@ant-design/pro-layout';
import ProTable from '@ant-design/pro-table';
import LogisticsForm from './components/LogisticsForm';
import OrderDetail from './components/OrderDetail';
import { queryToSend, queryExpress } from './service';
const TableList = () => {
const [companys, setCompanys] = useState([]);
const [LogisticsModalVisible, handleModalVisible] = useState(false);
const [detailModalVisible, handleDetailModalVisible] = useState(false);
const [skuList, setSkuList] = useState([]);
const actionRef = useRef();
const columns = [
{
title: '订单ID',
dataIndex: 'orderNo',
key: 'orderNo',
order: 6,
},
{
title: '商品渠道',
dataIndex: 'skuSource.desc',
key: 'skuSource.desc',
hideInSearch: true,
},
{
title: '交易时间',
dataIndex: 'payTime',
key: 'payTime',
valueType: 'dateTime',
hideInSearch: true,
},
{
title: '收货人手机',
dataIndex: 'receiverPhone',
key: 'receiverPhone',
order: 4,
},
{
title: '收货人姓名',
dataIndex: 'receiverName',
key: 'receiverName',
order: 3,
},
{
title: '收货地址',
dataIndex: 'receiveAddress',
key: 'receiveAddress',
search: false,
},
{
title: '物流状态',
dataIndex: 'logisticsStatus',
key: 'logisticsStatus',
filters: false,
valueEnum: {
'3': '全部',
'1': '未发货',
'2': '已发货',
},
order: 5,
},
{
title: '订单开始时间',
dataIndex: 'beginTime',
key: 'beginTime',
valueType: 'date',
hideInTable: true,
},
{
title: '订单结束时间',
dataIndex: 'endTime',
key: 'endTime',
valueType: 'date',
hideInTable: true,
},
{
title: '操作',
dataIndex: 'option',
key: 'option',
valueType: 'option',
render: (_, record) => (
<React.Fragment>
<a
onClick={() => {
setSkuList(record.skuList || []);
handleModalVisible(true);
}}
>
更新物流信息
</a>
<Divider type="vertical" />
<a
onClick={() => {
setSkuList(record.skuList || []);
handleDetailModalVisible(true);
}}>查看订单详情</a>
</React.Fragment>
),
},
];
const reload = () => {
handleModalVisible(false)
if (actionRef.current) {
actionRef.current.reload();
}
};
useEffect(() => {
const getCompanys = async () => {
const data = await queryExpress();
setCompanys(data);
};
getCompanys();
}, []);
return (
<PageHeaderWrapper>
<ProTable
actionRef={actionRef}
request={(params, sorter, filter) => queryToSend({ ...params })}
columns={columns}
search={{ collapsed: false, collapseRender: () => null }}
/>
<LogisticsForm onSubmit={reload} skuList={skuList} companys={companys} onCancel={() => handleModalVisible(false)} modalVisible={LogisticsModalVisible} />
<OrderDetail value={skuList} onCancel={() => handleDetailModalVisible(false)} modalVisible={detailModalVisible} />
</PageHeaderWrapper>
)
};
export default TableList;
@import '~antd/es/style/themes/default.less';
.tableList {
.tableListOperator {
margin-bottom: 16px;
button {
margin-right: 8px;
}
}
}
.tableListForm {
:global {
.ant-form-item {
display: flex;
margin-right: 0;
margin-bottom: 15px;
> .ant-form-item-label {
width: auto;
padding-right: 8px;
line-height: 32px;
}
.ant-form-item-control {
line-height: 32px;
}
}
.ant-form-item-control-wrapper {
flex: 1;
}
}
.submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
}
.button {
margin-right: 10px;
}
.buttonWrap {
margin: 10px 0 15px 0;
button {
margin-right: 10px;
}
}
.subContent {
height: 400px;
overflow: auto;
}
.wrapCont {
overflow: hidden;
}
.treeCont {
float: left;
width: 20%;
overflow: auto;
}
.tableCont {
float: right;
width: 78%;
}
.logistics {
width: 100%;
display: flex;
justify-content: center;
height: 30px;
align-items: center;
margin-top: 24px;
}
.logisticsIcon {
font-size: 25px;
&:first-child {
margin-right: 10px;
}
}
import * as api from './service';
const tableDate = [{
}];
const Model = {
namespace: 'pendingDeliveryOrder',
state: {
tableData: [],
typeList: [],
organizationList: [],
},
effects: {
*getSubjectList({ payload }, { call, put }) {
yield put({
type: 'saveData',
payload: {
tableData: tableDate,
},
});
// const response = yield call(api.subjectList, payload);
// if (response.code === 2000) {
// yield put({
// type: 'saveData',
// payload: {
// tableData: response.data,
// },
// });
// }
},
*subjectTypeList({ payload }, { call, put }) {
// const response = yield call(api.subjectTypeList, payload);
// if (response.code === 2000) {
// yield put({
// type: 'saveData',
// payload: {
// typeList: response.data,
// },
// });
// }
},
*organizationList({ payload }, { call, put }) {
// const response = yield call(api.organizationList, payload);
// if (response.code === 2000) {
// yield put({
// type: 'saveData',
// payload: {
// organizationList: response.data,
// },
// });
// }
},
},
reducers: {
saveData(state, action) {
const data = action.payload;
return { ...state, ...data };
},
},
};
export default Model;
import request from '@/utils/request';
// 待发货订单
export async function queryToSend(params) {
const statusMap = {
'全部': 3,
'未发货': 1,
'已发货': 2,
}
const transformedParam = {
...params,
logisticsStatus: statusMap[params.logisticsStatus] || 3,
pageNo: params.current,
pageSize: params.pageSize || 20,
}
const { data: { currentPage, dataList, totalCount } } = await request.post('/api/kdsp/op/mch-order/list', {
data: transformedParam
});
return {
current: currentPage,
data: dataList,
total: totalCount
}
}
// 快递公司
export async function queryExpress() {
const { data } = await request.get('/api/kdsp/op/express/list');
return data;
}
export async function updateExpress(params) {
return request.post('/api/kdsp/op/order/update-express-info', {
data: params
});
}
\ No newline at end of file
import { createContext } from 'react';
const LoginContext = createContext({});
export default LoginContext;
import { Button, Col, Form, Input, Row } from 'antd';
import React, { Component } from 'react';
import omit from 'omit.js';
import ItemMap from './map';
import LoginContext from './LoginContext';
import styles from './index.less';
const FormItem = Form.Item;
class WrapFormItem extends Component {
static defaultProps = {
getCaptchaButtonText: 'captcha',
getCaptchaSecondText: 'second',
};
interval = undefined;
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidMount() {
const { updateActive, name = '' } = this.props;
if (updateActive) {
updateActive(name);
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
onGetCaptcha = () => {
const { onGetCaptcha } = this.props;
const result = onGetCaptcha ? onGetCaptcha() : null;
if (result === false) {
return;
}
if (result instanceof Promise) {
result.then(this.runGetCaptchaCountDown);
} else {
this.runGetCaptchaCountDown();
}
};
getFormItemOptions = ({ onChange, defaultValue, customProps = {}, rules }) => {
const options = {
rules: rules || customProps.rules,
};
if (onChange) {
options.onChange = onChange;
}
if (defaultValue) {
options.initialValue = defaultValue;
}
return options;
};
runGetCaptchaCountDown = () => {
const { countDown } = this.props;
let count = countDown || 59;
this.setState({
count,
});
this.interval = window.setInterval(() => {
count -= 1;
this.setState({
count,
});
if (count === 0) {
clearInterval(this.interval);
}
}, 1000);
};
render() {
// 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props tabUtil
const { count } = this.state;
const {
onChange,
customProps,
defaultValue,
rules,
name,
getCaptchaButtonText,
getCaptchaSecondText,
updateActive,
type,
form,
tabUtil,
...restProps
} = this.props;
if (!name) {
return null;
}
if (!form) {
return null;
}
const { getFieldDecorator } = form; // get getFieldDecorator props
const options = this.getFormItemOptions(this.props);
const otherProps = restProps || {};
if (type === 'Captcha') {
const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']);
return (
<FormItem>
<Row gutter={8}>
<Col span={16}>
{getFieldDecorator(name, options)(<Input {...customProps} {...inputProps} />)}
</Col>
<Col span={8}>
<Button
disabled={!!count}
className={styles.getCaptcha}
size="large"
onClick={this.onGetCaptcha}
>
{count ? `${count} ${getCaptchaSecondText}` : getCaptchaButtonText}
</Button>
</Col>
</Row>
</FormItem>
);
}
return (
<FormItem>
{getFieldDecorator(name, options)(<Input {...customProps} {...otherProps} />)}
</FormItem>
);
}
}
const LoginItem = {};
Object.keys(ItemMap).forEach(key => {
const item = ItemMap[key];
LoginItem[key] = props => (
<LoginContext.Consumer>
{context => (
<WrapFormItem
customProps={item.props}
rules={item.rules}
{...props}
type={key}
{...context}
updateActive={context.updateActive}
/>
)}
</LoginContext.Consumer>
);
});
export default LoginItem;
import { Button, Form } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
const FormItem = Form.Item;
const LoginSubmit = ({ className, ...rest }) => {
const clsString = classNames(styles.submit, className);
return (
<FormItem>
<Button size="large" className={clsString} type="primary" htmlType="submit" {...rest} />
</FormItem>
);
};
export default LoginSubmit;
import React, { Component } from 'react';
import { Tabs } from 'antd';
import LoginContext from './LoginContext';
const { TabPane } = Tabs;
const generateId = (() => {
let i = 0;
return (prefix = '') => {
i += 1;
return `${prefix}${i}`;
};
})();
class LoginTab extends Component {
uniqueId = '';
constructor(props) {
super(props);
this.uniqueId = generateId('login-tab-');
}
componentDidMount() {
const { tabUtil } = this.props;
if (tabUtil) {
tabUtil.addTab(this.uniqueId);
}
}
render() {
const { children } = this.props;
return <TabPane {...this.props}>{children}</TabPane>;
}
}
const WrapContext = props => (
<LoginContext.Consumer>
{value => <LoginTab tabUtil={value.tabUtil} {...props} />}
</LoginContext.Consumer>
); // 标志位 用来判断是不是自定义组件
WrapContext.typeName = 'LoginTab';
export default WrapContext;
import { Form, Tabs } from 'antd';
import React, { Component } from 'react';
import classNames from 'classnames';
import LoginContext from './LoginContext';
import LoginItem from './LoginItem';
import LoginSubmit from './LoginSubmit';
import LoginTab from './LoginTab';
import styles from './index.less';
class Login extends Component {
static Tab = LoginTab;
static Submit = LoginSubmit;
static defaultProps = {
className: '',
defaultActiveKey: '',
onTabChange: () => {},
onSubmit: () => {},
};
constructor(props) {
super(props);
this.state = {
type: props.defaultActiveKey,
tabs: [],
active: {},
};
}
componentDidMount() {
const { form, onCreate } = this.props;
if (onCreate) {
onCreate(form);
}
}
onSwitch = type => {
this.setState(
{
type,
},
() => {
const { onTabChange } = this.props;
if (onTabChange) {
onTabChange(type);
}
},
);
};
getContext = () => {
const { form } = this.props;
const { tabs = [] } = this.state;
return {
tabUtil: {
addTab: id => {
this.setState({
tabs: [...tabs, id],
});
},
removeTab: id => {
this.setState({
tabs: tabs.filter(currentId => currentId !== id),
});
},
},
form: { ...form },
updateActive: activeItem => {
const { type = '', active = {} } = this.state;
if (active[type]) {
active[type].push(activeItem);
} else {
active[type] = [activeItem];
}
this.setState({
active,
});
},
};
};
handleSubmit = e => {
e.preventDefault();
const { active = {}, type = '' } = this.state;
const { form, onSubmit } = this.props;
const activeFields = active[type] || [];
if (form) {
form.validateFields(
activeFields,
{
force: true,
},
(err, values) => {
if (onSubmit) {
onSubmit(err, values);
}
},
);
}
};
render() {
const { className, children } = this.props;
const { type, tabs = [] } = this.state;
const TabChildren = [];
const otherChildren = [];
React.Children.forEach(children, child => {
if (!child) {
return;
}
if (child.type.typeName === 'LoginTab') {
TabChildren.push(child);
} else {
otherChildren.push(child);
}
});
return (
<LoginContext.Provider value={this.getContext()}>
<div className={classNames(className, styles.login)}>
<Form onSubmit={this.handleSubmit}>
{tabs.length ? (
<React.Fragment>
<Tabs
animated={false}
className={styles.tabs}
activeKey={type}
onChange={this.onSwitch}
>
{TabChildren}
</Tabs>
{otherChildren}
</React.Fragment>
) : (
children
)}
</Form>
</div>
</LoginContext.Provider>
);
}
}
Object.keys(LoginItem).forEach(item => {
Login[item] = LoginItem[item];
});
export default Form.create()(Login);
@import '~antd/es/style/themes/default.less';
.login {
:global {
.ant-tabs .ant-tabs-bar {
margin-bottom: 24px;
text-align: center;
border-bottom: 0;
}
.ant-form-item {
margin: 0 2px 24px;
}
}
.getCaptcha {
display: block;
width: 100%;
}
.icon {
margin-left: 16px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.other {
margin-top: 24px;
line-height: 22px;
text-align: left;
.register {
float: right;
}
}
.prefixIcon {
color: @disabled-color;
font-size: @font-size-base;
}
.submit {
width: 100%;
margin-top: 24px;
}
}
import { Icon } from 'antd';
import React from 'react';
import styles from './index.less';
export default {
UserName: {
props: {
size: 'large',
id: 'userName',
prefix: <Icon type="user" className={styles.prefixIcon} />,
placeholder: 'admin',
},
rules: [
{
required: true,
message: 'Please enter username!',
},
],
},
Password: {
props: {
size: 'large',
prefix: <Icon type="lock" className={styles.prefixIcon} />,
type: 'password',
id: 'password',
placeholder: '888888',
},
rules: [
{
required: true,
message: 'Please enter password!',
},
],
},
Mobile: {
props: {
size: 'large',
prefix: <Icon type="mobile" className={styles.prefixIcon} />,
placeholder: 'mobile number',
},
rules: [
{
required: true,
message: 'Please enter mobile number!',
},
{
pattern: /^1\d{10}$/,
message: 'Wrong mobile number format!',
},
],
},
Captcha: {
props: {
size: 'large',
prefix: <Icon type="mail" className={styles.prefixIcon} />,
placeholder: 'captcha',
},
rules: [
{
required: true,
message: 'Please enter Captcha!',
},
],
},
};
import { Alert } from 'antd';
import React, { Component } from 'react';
import { connect } from 'dva';
import LoginComponents from './components/Login';
import styles from './style.less';
const { UserName, Password, Submit } = LoginComponents;
@connect(({ login, loading }) => ({
userLogin: login,
submitting: loading.effects['login/login'],
}))
class Login extends Component {
loginForm = undefined;
handleSubmit = (err, values) => {
if (!err) {
const { dispatch } = this.props;
dispatch({
type: 'login/login',
payload: { ...values },
});
}
};
renderMessage = content => (
<Alert
style={{
marginBottom: 24,
}}
message={content}
type="error"
showIcon
/>
);
render() {
const { userLogin, submitting } = this.props;
const { status } = userLogin;
return (
<div className={styles.main}>
<LoginComponents
onSubmit={this.handleSubmit}
onCreate={form => {
this.loginForm = form;
}}
>
{status === 'error' &&
!submitting &&
this.renderMessage('账户或密码错误(admin/ant.design)')}
<UserName
name="username"
placeholder={`${'用户名'}`}
rules={[
{
required: true,
message: '请输入用户名!',
},
]}
/>
<Password
name="password"
placeholder={`${'密码'}`}
rules={[
{
required: true,
message: '请输入密码!',
},
]}
onPressEnter={e => {
e.preventDefault();
if (this.loginForm) {
this.loginForm.validateFields(this.handleSubmit);
}
}}
/>
<Submit loading={submitting}>登录</Submit>
</LoginComponents>
</div>
);
}
}
export default Login;
@import '~antd/es/style/themes/default.less';
.main {
width: 368px;
margin: 0 auto;
@media screen and (max-width: @screen-sm) {
width: 95%;
}
.icon {
margin-left: 16px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.other {
margin-top: 24px;
line-height: 22px;
text-align: left;
.register {
float: right;
}
}
:global {
.antd-pro-login-submit {
width: 100%;
margin-top: 24px;
}
}
}
import { stringify } from 'querystring';
import request from '@/utils/request';
export async function fakeAccountLogin(params) {
return request('/oauth/login', {
method: 'POST',
data: stringify(params),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
}
export async function getFakeCaptcha(mobile) {
return request(`/api/login/captcha?mobile=${mobile}`);
}
export async function getResFreshToen(token) {
return request(`/oauth/refreshtoken?refreshtoken=${token}`);
}
import request from '@/utils/request';
export async function getMenuData() {
return request.post('/user/getresources/APP001');
}
import request from '@/utils/request';
export async function queryCurrent() {
// 查询用户信息
return request.get('/oauth/currentuserinfo');
}
export async function homeInfo() {
// 查询主页信息
return request.post('/home/info');
}
import RenderAuthorize from '@/components/Authorized';
import { getAuthority } from './authority';
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable import/no-mutable-exports */
let Authorized = RenderAuthorize(getAuthority()); // Reload the rights component
const reloadAuthorized = () => {
Authorized = RenderAuthorize(getAuthority());
};
export { reloadAuthorized };
export default Authorized;
import { reloadAuthorized } from './Authorized'; // use localStorage to store the authority info, which might be sent from server in actual project.
export function getAuthority(str) {
const authorityString =
typeof str === 'undefined' && localStorage ? localStorage.getItem('antd-pro-authority') : str; // authorityString could be admin, "admin", ["admin"]
let authority;
try {
if (authorityString) {
authority = JSON.parse(authorityString);
}
} catch (e) {
authority = authorityString;
}
if (typeof authority === 'string') {
return [authority];
} // preview.pro.ant.design only do not use in your production.
// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
return ['admin'];
}
return authority;
}
export function setAuthority(authority) {
const proAuthority = typeof authority === 'string' ? [authority] : authority;
localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority)); // auto reload
reloadAuthorized();
}
import { getAuthority } from './authority';
describe('getAuthority should be strong', () => {
it('string', () => {
expect(getAuthority('admin')).toEqual(['admin']);
});
it('array with double quotes', () => {
expect(getAuthority('"admin"')).toEqual(['admin']);
});
it('array with single item', () => {
expect(getAuthority('["admin"]')).toEqual(['admin']);
});
it('array with multiple items', () => {
expect(getAuthority('["admin", "guest"]')).toEqual(['admin', 'guest']);
});
});
export default {
get(key) {
let result = localStorage.getItem(key);
try {
result = JSON.parse(result);
} catch {
console.log(); // eslint-disable-line no-console
}
return result;
},
set(key, value) {
const { toString } = Object.prototype;
if (toString.call(value) === '[object Array]' || toString.call(value) === '[object Object]') {
value = JSON.stringify(value); // eslint-disable-line no-param-reassign
}
return localStorage.setItem(key, value);
},
remove(key) {
return localStorage.removeItem(key);
},
clear() {
return localStorage.clear();
},
};
// An highlighted block
/**
* request 网络请求工具
* 更详细的 api 文档: https://github.com/umijs/umi-request
*/
/* eslint-disable no-underscore-dangle */
import { extend } from 'umi-request';
import { notification } from 'antd';
import localStorage from './localStorage';
import config from '../../config/env.config';
let isRefreshing = true;
let subscriber = [];
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};
const errorHandler = error => {
const { response } = error;
if (response && response.status) {
const errorText = codeMessage[response.status] || response.statusText;
const { status, url } = response;
notification.error({
message: `请求错误 ${status}: ${url}`,
description: errorText,
});
}
return response;
};
const request = extend({
// prefix: '/server',
prefix: `${config.api}`,
errorHandler,
// responseType: 'arrayBuffer',
// 默认错误处理
// credentials: 'include', // 默认请求是否带上cookie
});
const addSubscriber = callback => subscriber.push(callback);
const wait = async seconds => new Promise(resolve => setTimeout(resolve, seconds));
const onAccessTokenFetched = () => {
subscriber.forEach(callback => callback());
subscriber = [];
};
const refreshRequest = async (url, options) => {
const promise = new Promise(resolve => addSubscriber(() => resolve(request(url, options))));
if (isRefreshing) {
isRefreshing = false;
await wait(500);
const data = await refreshtoken();
if (data.code === 2000) {
localStorage.set('token', data.data.accessToken);
}
onAccessTokenFetched();
isRefreshing = true;
}
return promise;
};
/**
* 异常处理程序
*/
// request拦截器, 改变url 或 options.
request.interceptors.request.use(async (url, options) => {
const token = localStorage.get('token');
if (!token && !url.includes('/oauth/login')) {
window.location.href = `${window.origin}/user/login`;
}
const { headers } = options;
const header = {
...headers,
'Access-Token': token,
'Content-Type': headers['Content-Type'] ? headers['Content-Type'] : 'application/json',
};
return {
url,
options: { ...options, headers: header },
};
});
function refreshtoken() {
return request.get(`/oauth/refreshtoken?refreshtoken=${localStorage.get('refreshtoken')}`);
}
// response拦截器, 处理response
request.interceptors.response.use(async (response, options) => {
if (options.responseType === 'arrayBuffer') {
return response;
}
const data = await response.clone().json();
if (data.code === 4033) {
// token过期
const url = response.url.split(config.api)[1];
return refreshRequest(url, options);
}
if (data.code === 4011) {
const loginPath = window.__POWERED_BY_QIANKUN__
? `${window.origin}/access/login`
: `${window.origin}/user/login`;
notification.warning({
message: data.msg,
});
window.location.href = loginPath;
}
return response;
});
export default request;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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