提交 5cc79877 作者: 郁骅焌

Initial commit

上级
# 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 = {
extends: [require.resolve('@umijs/fabric/dist/eslint')],
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
page: true,
REACT_APP_ENV: true,
},
rules: {
'no-plusplus': 0,
'no-template-curly-in-string': 0,
'no-param-reassign': 0
}
};
# 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
package-lock.json
*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
CNAME
/build
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).
// https://umijs.org/config/
import { defineConfig } from 'umi';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
const { REACT_APP_ENV } = process.env;
export default defineConfig({
hash: true,
antd: {},
dva: {
hmr: true,
},
locale: {
// default zh-CN
default: 'zh-CN',
// default true, when it is true, will use `navigator.language` overwrite default
antd: true,
baseNavigator: true,
},
dynamicImport: {
loading: '@/components/PageLoading/index',
},
targets: {
ie: 11,
},
// umi routes: https://umijs.org/docs/routing
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: '/',
redirect: '/welcome',
},
{
path: '/welcome',
name: 'welcome',
icon: 'smile',
component: './Welcome',
},
{
path: '/admin',
name: 'admin',
icon: 'crown',
component: './Admin',
authority: ['admin'],
routes: [
{
path: '/admin/sub-page',
name: 'sub-page',
icon: 'smile',
component: './Welcome',
authority: ['admin'],
},
],
},
{
name: 'list.table-list',
icon: 'table',
path: '/list',
component: './ListTableList',
},
{
path: '/system',
name: 'system',
icon: 'setting',
routes: [
{
path: '/system/role',
name: 'role',
icon: 'SolutionOutlined',
component: './role',
},
],
},
{
path: '/demo',
name: 'demo',
icon: 'setting',
routes: [
{
path: '/demo/crud',
name: 'crud',
icon: 'SolutionOutlined',
component: './demo/crud',
},
],
},
{
component: './404',
},
],
},
{
component: './404',
},
],
},
{
component: './404',
},
],
// Theme for antd: https://ant.design/docs/react/customize-theme-cn
theme: {
// ...darkTheme,
'primary-color': defaultSettings.primaryColor,
},
ignoreMomentLocale: true,
proxy: proxy[REACT_APP_ENV || 'dev'],
manifest: {
basePath: '/',
},
});
export default {
navTheme: 'dark',
// 拂晓蓝
primaryColor: '#1890ff',
layout: 'sidemenu',
contentWidth: 'Fluid',
fixedHeader: false,
autoHideHeader: false,
fixSiderbar: false,
colorWeak: false,
menu: {
locale: true,
},
title: 'Sage Framework',
pwa: false,
iconfontUrl: '',
};
/**
* 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
* The agent cannot take effect in the production environment
* so there is no configuration of the production environment
* For details, please see
* https://pro.ant.design/docs/deploy
*/
export default {
dev: {
'/ebd/': {
target: 'http://47.103.50.109:9051',
changeOrigin: true,
pathRewrite: {
'^/ebd': '',
},
},
},
test: {
'/api/': {
target: 'https://preview.pro.ant.design',
changeOrigin: true,
pathRewrite: {
'^': '',
},
},
},
pre: {
'/api/': {
target: 'your pre url',
changeOrigin: true,
pathRewrite: {
'^': '',
},
},
},
};
module.exports = {
testURL: 'http://localhost:8000',
testEnvironment: './tests/PuppeteerEnvironment',
verbose: false,
globals: {
ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
localStorage: null,
},
};
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
// eslint-disable-next-line import/no-extraneous-dependencies
import { parse } from 'url';
// mock tableListDataSource
const genList = (current, pageSize) => {
const tableListDataSource = [];
for (let i = 0; i < pageSize; i += 1) {
const index = (current - 1) * 10 + i;
tableListDataSource.push({
key: index,
disabled: i % 6 === 0,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name: `TradeCode ${index}`,
owner: '曲丽丽',
desc: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
});
}
tableListDataSource.reverse();
return tableListDataSource;
};
let tableListDataSource = genList(1, 100);
function getRule(req, res, u) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
const params = parse(realUrl, true).query;
let dataSource = [...tableListDataSource].slice((current - 1) * pageSize, current * pageSize);
if (params.sorter) {
const s = params.sorter.split('_');
dataSource = dataSource.sort((prev, next) => {
if (s[1] === 'descend') {
return next[s[0]] - prev[s[0]];
}
return prev[s[0]] - next[s[0]];
});
}
if (params.status) {
const status = params.status.split(',');
let filterDataSource = [];
status.forEach(s => {
filterDataSource = filterDataSource.concat(
dataSource.filter(item => {
if (parseInt(`${item.status}`, 10) === parseInt(s.split('')[0], 10)) {
return true;
}
return false;
}),
);
});
dataSource = filterDataSource;
}
if (params.name) {
dataSource = dataSource.filter(data => data.name.includes(params.name || ''));
}
const result = {
data: dataSource,
total: tableListDataSource.length,
success: true,
pageSize,
current: parseInt(`${params.currentPage}`, 10) || 1,
};
return res.json(result);
}
function postRule(req, res, u, b) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const body = (b && b.body) || req.body;
const { method, name, desc, key } = body;
switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
tableListDataSource = tableListDataSource.filter(item => key.indexOf(item.key) === -1);
break;
case 'post':
(() => {
const i = Math.ceil(Math.random() * 10000);
const newRule = {
key: tableListDataSource.length,
href: 'https://ant.design',
avatar: [
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
][i % 2],
name,
owner: '曲丽丽',
desc,
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 2,
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
};
tableListDataSource.unshift(newRule);
return res.json(newRule);
})();
return;
case 'update':
(() => {
let newRule = {};
tableListDataSource = tableListDataSource.map(item => {
if (item.key === key) {
newRule = { ...item, desc, name };
return { ...item, desc, name };
}
return item;
});
return res.json(newRule);
})();
return;
default:
break;
}
const result = {
list: tableListDataSource,
pagination: {
total: tableListDataSource.length,
},
};
res.json(result);
}
export default {
'GET /api/rule': getRule,
'POST /api/rule': postRule,
};
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': {
'/form/advanced-form': {
authority: ['admin', 'user'],
},
},
};
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;
}
if (type === 'mobile') {
res.send({
status: 'ok',
type,
currentAuthority: 'admin',
});
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,
};
{
"name": "ant-design-pro",
"version": "1.0.0",
"private": true,
"description": "An out-of-box UI solution for enterprise applications",
"scripts": {
"analyze": "cross-env ANALYZE=1 umi build",
"build": "umi build",
"deploy": "npm run site && npm run gh-pages",
"dev": "npm run start:dev",
"fetch:blocks": "pro fetch-blocks --branch=umi@3 && npm run prettier",
"gh-pages": "cp CNAME ./dist/ && gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "umi g tmp",
"lint": "umi g tmp && 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": "prettier --check \"**/*\" --end-of-line auto",
"lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
"prettier": "prettier -c --write \"**/*\"",
"start": "umi dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none umi dev",
"start:no-mock": "cross-env MOCK=none umi dev",
"start:no-ui": "cross-env UMI_UI=none umi dev",
"start:pre": "cross-env REACT_APP_ENV=pre umi dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none umi dev",
"pretest": "node ./tests/beforeTest",
"test": "umi test",
"test:all": "node ./tests/run-tests.js",
"test:component": "umi test ./src/components",
"tsc": "tsc"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-staged"
}
},
"lint-staged": {
"**/*.less": "stylelint --syntax less",
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write"
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"dependencies": {
"@ant-design/icons": "^4.0.0",
"@ant-design/pro-layout": "^5.0.8",
"@ant-design/pro-table": "2.2.1",
"@umijs/hooks": "^1.9.2",
"antd": "^4.0.0",
"classnames": "^2.2.6",
"lodash": "^4.17.11",
"moment": "^2.24.0",
"omit.js": "^1.0.2",
"path-to-regexp": "2.4.0",
"qs": "^6.9.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-helmet-async": "^1.0.4",
"umi": "3.1.1",
"umi-request": "^1.0.8",
"use-merge-value": "^1.0.1"
},
"devDependencies": {
"@ant-design/pro-cli": "^1.0.18",
"@types/classnames": "^2.2.7",
"@types/express": "^4.17.0",
"@types/history": "^4.7.2",
"@types/jest": "^25.1.0",
"@types/lodash": "^4.14.144",
"@types/qs": "^6.5.3",
"@types/react": "^16.9.17",
"@types/react-dom": "^16.8.4",
"@types/react-helmet": "^5.0.13",
"@umijs/fabric": "^2.0.5",
"@umijs/plugin-blocks": "^2.0.5",
"@umijs/preset-ant-design-pro": "^1.2.0",
"@umijs/preset-react": "^1.4.8",
"@umijs/preset-ui": "^2.0.9",
"carlo": "^0.9.46",
"chalk": "^4.0.0",
"cross-env": "^7.0.0",
"cross-port-killer": "^1.1.1",
"detect-installer": "^1.0.1",
"enzyme": "^3.11.0",
"eslint": "^6.8.0",
"express": "^4.17.1",
"gh-pages": "^2.0.1",
"husky": "^4.0.7",
"jsdom-global": "^3.0.2",
"lint-staged": "^10.0.0",
"mockjs": "^1.0.1-beta3",
"prettier": "^2.0.1",
"pro-download": "1.0.1",
"puppeteer-core": "^3.0.0",
"stylelint": "^13.0.0"
},
"engines": {
"node": ">=10.0.0"
},
"checkFiles": [
"src/**/*.js*",
"src/**/*.ts*",
"src/**/*.less",
"config/**/*.js*",
"scripts/**/*.js"
]
}
preview.pro.ant.design
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="图层_1" width="512" height="512" x="0" y="0" enable-background="new 0 0 512 512" version="1.1" viewBox="0 0 512 512" xml:space="preserve"><path fill-rule="evenodd" d="M259.119,233.588c0-3.644,0.041-7.289-0.008-10.932 c-0.111-8.558-4.697-13.308-13.231-13.486c-6.658-0.139-13.326,0.12-19.98-0.096c-3.292-0.107-4.247,0.995-4.24,4.266 c0.094,44.794,0.101,89.589-0.008,134.383c-0.009,3.492,1.346,4.154,4.407,4.113c9.99-0.135,19.985-0.18,29.972,0.019 c3.52,0.069,4.561-1.136,4.534-4.575c-0.138-16.817-0.124-33.637-0.037-50.455c0.053-10.153-0.512-20.322,0.426-30.457 c1.339-14.5,10.473-23.758,24.802-25.619c4.382-0.569,8.652-0.143,12.92,0.505c4.367,0.663,6.274-1.139,6.266-5.396 c-0.015-7.494-0.048-14.987,0.015-22.48c0.029-3.358-1.322-5.272-4.784-5.789c-13.2-1.97-24.368,1.438-32.905,12.103 C263.914,223.876,262.08,228.86,259.119,233.588z" clip-rule="evenodd"/><g><defs><path id="SVGID_1_" d="M56.964,256.003c0-30.332,0.092-60.665-0.105-90.996c-0.028-4.252,1.185-5.176,5.265-5.141 c25.497,0.221,51.007-0.295,76.491,0.323c20.853,0.505,39.051,8.125,50.951,25.966c14.972,22.448,16.14,46.757,4.473,70.786 c-11.114,22.892-31.459,32.606-56.012,33.989c-12.13,0.683-24.326,0.327-36.489,0.176c-3.462-0.043-4.55,0.903-4.514,4.451 c0.174,17.164-0.026,34.333,0.161,51.496c0.04,3.676-0.859,4.918-4.695,4.82c-10.326-0.262-20.668-0.277-30.994,0.006 c-3.947,0.109-4.622-1.318-4.606-4.882C57.021,316.668,56.964,286.335,56.964,256.003z M115.378,191.878 c-4.832,0-9.666,0.105-14.493-0.042c-2.639-0.08-3.862,0.506-3.844,3.544c0.121,20.324,0.09,40.65,0.026,60.975 c-0.007,2.423,0.494,3.632,3.281,3.58c9.16-0.174,18.349,0.301,27.481-0.248c20.562-1.234,31.673-12.484,32.665-32.392 c0.974-19.536-9.987-32.905-29.141-35.031C126.09,191.681,115.378,191.978,115.378,191.878z"/></defs><use fill-rule="evenodd" clip-rule="evenodd" overflow="visible" xlink:href="#SVGID_1_"/><clipPath id="SVGID_2_"><use overflow="visible" xlink:href="#SVGID_1_"/></clipPath><path fill-rule="evenodd" d="M56.964,256.003c0,30.332,0.056,60.665-0.075,90.996 c-0.016,3.563,0.659,4.991,4.606,4.882c10.325-0.283,20.667-0.268,30.994-0.006c3.836,0.098,4.735-1.145,4.695-4.82 c-0.188-17.163,0.013-34.332-0.161-51.496c-0.036-3.548,1.052-4.494,4.514-4.451c12.163,0.151,24.359,0.507,36.489-0.176 c24.552-1.383,44.897-11.097,56.012-33.989c11.667-24.029,10.5-48.338-4.473-70.786c-11.9-17.841-30.098-25.461-50.951-25.966 c-25.484-0.618-50.995-0.103-76.491-0.323c-4.08-0.035-5.292,0.889-5.265,5.141C57.056,195.339,56.964,225.671,56.964,256.003z" clip-path="url(#SVGID_2_)" clip-rule="evenodd"/></g><g><defs><path id="SVGID_3_" d="M315.446,281.626c0.099-19.011,3.621-36.469,15.474-51.155c15.867-19.659,37.477-25.831,61.573-23.923 c38.108,3.018,61.858,28.28,63.712,66.526c0.692,14.277-0.41,28.252-5.881,41.65c-7.521,18.418-20.696,30.726-39.764,36.411 c-18.27,5.447-36.605,5.267-54.348-1.839c-25.895-10.371-37.139-31.363-40.178-57.816 C315.637,288.012,315.61,284.501,315.446,281.626z M417.02,280.467c-0.092-9.835-1.27-19.53-5.888-28.481 c-5.233-10.144-13.333-15.962-25.039-16.006c-11.688-0.044-20.367,5.439-25.118,15.79c-8.715,18.984-8.698,38.444-0.095,57.46 c4.719,10.431,13.509,15.761,25.142,15.739c11.655-0.021,19.883-5.677,25.127-15.869 C415.757,300.142,416.912,290.442,417.02,280.467z"/></defs><use fill-rule="evenodd" clip-rule="evenodd" overflow="visible" xlink:href="#SVGID_3_"/><clipPath id="SVGID_4_"><use overflow="visible" xlink:href="#SVGID_3_"/></clipPath><path fill-rule="evenodd" d="M315.446,281.626c0.164,2.875,0.19,6.386,0.589,9.854 c3.039,26.453,14.283,47.445,40.178,57.816c17.742,7.105,36.078,7.286,54.348,1.839c19.067-5.686,32.242-17.993,39.764-36.411 c5.471-13.398,6.573-27.373,5.881-41.65c-1.854-38.247-25.604-63.509-63.712-66.526c-24.097-1.908-45.706,4.264-61.573,23.923 C319.067,245.157,315.545,262.615,315.446,281.626z" clip-path="url(#SVGID_4_)" clip-rule="evenodd"/></g><path fill-rule="evenodd" d="M259.119,233.588c2.961-4.728,4.795-9.712,8.146-13.899 c8.537-10.664,19.705-14.072,32.905-12.103c3.462,0.517,4.813,2.431,4.784,5.789c-0.063,7.493-0.029,14.987-0.015,22.48 c0.009,4.258-1.898,6.06-6.266,5.396c-4.268-0.648-8.538-1.074-12.92-0.505c-14.329,1.861-23.463,11.119-24.802,25.619 c-0.938,10.135-0.373,20.304-0.426,30.457c-0.087,16.818-0.101,33.638,0.037,50.455c0.027,3.439-1.014,4.645-4.534,4.575 c-9.987-0.198-19.982-0.153-29.972-0.019c-3.061,0.041-4.416-0.621-4.407-4.113c0.109-44.794,0.103-89.589,0.008-134.383 c-0.007-3.271,0.948-4.373,4.24-4.266c6.654,0.216,13.322-0.042,19.98,0.096c8.535,0.178,13.12,4.928,13.231,13.486 C259.16,226.3,259.119,229.944,259.119,233.588z" clip-rule="evenodd"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200" version="1.1" viewBox="0 0 200 200"><title>Group 28 Copy 5</title><desc>Created with Sketch.</desc><defs><linearGradient id="linearGradient-1" x1="62.102%" x2="108.197%" y1="0%" y2="37.864%"><stop offset="0%" stop-color="#4285EB"/><stop offset="100%" stop-color="#2EC7FF"/></linearGradient><linearGradient id="linearGradient-2" x1="69.644%" x2="54.043%" y1="0%" y2="108.457%"><stop offset="0%" stop-color="#29CDFF"/><stop offset="37.86%" stop-color="#148EFF"/><stop offset="100%" stop-color="#0A60FF"/></linearGradient><linearGradient id="linearGradient-3" x1="69.691%" x2="16.723%" y1="-12.974%" y2="117.391%"><stop offset="0%" stop-color="#FA816E"/><stop offset="41.473%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient><linearGradient id="linearGradient-4" x1="68.128%" x2="30.44%" y1="-35.691%" y2="114.943%"><stop offset="0%" stop-color="#FA8E7D"/><stop offset="51.264%" stop-color="#F74A5C"/><stop offset="100%" stop-color="#F51D2C"/></linearGradient></defs><g id="Page-1" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><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 id="Shape" fill="url(#linearGradient-1)" 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"/><path id="Shape" fill="url(#linearGradient-2)" 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"/></g><path id="Shape" fill="url(#linearGradient-3)" 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"/></g><ellipse id="Combined-Shape" cx="100.519" cy="100.437" fill="url(#linearGradient-4)" rx="23.6" ry="23.581"/></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 React, { useEffect, useState, useImperativeHandle } from 'react'
import { SageTree } from '@/components/Common'
import { getMenu } from '@/services/menu'
// 遍历所有子节点数组改变结构
function loopTree(arr) {
arr.forEach(item => {
item.title = item.authName
item.key = item.authCode
if (item.hasSun) {
item.children = item.list.slice()
loopTree(item.children)
}
})
}
const MenuTree = (props, ref) => {
const [treeData, setTreeData] = useState([])
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
const queryMenu = async () => {
const res = await getMenu()
const { data } = res
const treeDataArr = data.slice()
loopTree(treeDataArr)
setTreeData(treeDataArr)
}
const onExpand = expandedkeys => {
// console.log('onExpand', expandedkeys); // if not set autoExpandParent to false, if children expanded, parent can not collapse.
// or, you can remove all expanded children keys.
setExpandedKeys(expandedkeys);
setAutoExpandParent(false);
};
const onCheck = checkedkeys => {
// console.log('onCheck', checkedkeys);
setCheckedKeys(checkedkeys);
};
const onSelect = (selectedkeys, info) => {
// console.log('onSelect', info);
setSelectedKeys(selectedkeys);
};
const getCheckedKeys = () => {
return checkedKeys
}
useEffect(() => {
queryMenu()
}, [])
// 暴露给外部的方法
useImperativeHandle(ref, () => ({
setCheckedKeys,
getCheckedKeys
}))
return (
<SageTree
checkable
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onCheck={onCheck}
checkedKeys={checkedKeys}
onSelect={onSelect}
selectedKeys={selectedKeys}
treeData={treeData}
/>
)
}
export default React.forwardRef(MenuTree)
import MenuTree from './MenuTree'
export {
MenuTree
}
import React from 'react'
import { Popconfirm, Divider } from 'antd'
/**
*
* @param {*} props
* actionList []
*/
const ActionSet = (props) => {
const { actionList: actionListProps } = props
const actionListState = actionListProps !== undefined ? actionListProps : []
return (
actionListState.map((item, index) => {
return (
<span key={item.key ? item.key : `action_key_${index}`}>
{
item.isConfirm ?
<Popconfirm
placement="topLeft"
title={item.confirmInfo}
onConfirm={item.method}
okText="确定"
cancelText="取消">
<a onClick={(e) => e.stopPropagation()}>{item.title}</a>
</Popconfirm> : <a onClick={item.method}>{item.title}</a>
}
{
index < actionListState.length - 1 ?
<Divider type="vertical" /> : null
}
</span>
)
})
)
}
export default ActionSet
import React from 'react'
import { Button } from 'antd'
import './style.less'
/**
*
* @param {success、warning、danger、primary} props
*/
const SageButton = (props) => {
const componentProps = {
...props
}
return (
<Button {...componentProps} className={`sage-button-${componentProps.type}`} />
)
}
export default SageButton
.sage-button-success {
color: #fff;
background-color: #13ce66;
border-color: #13ce66;
}
.sage-button-success:focus,.sage-button-success:hover {
background: #42d885;
border-color: #42d885;
color: #fff;
}
.sage-button-success:active {
outline: none !important;
}
.sage-button-warning {
color: #fff;
background-color: #ffba00;
border-color: #ffba00
}
.sage-button-warning:focus,.sage-button-warning:hover {
background: #ffc833;
border-color: #ffc833;
color: #fff
}
.sage-button-warning:active {
outline: none
}
import React, { useImperativeHandle } from 'react'
import {
Row,
Col,
Form,
Input,
Select,
InputNumber,
Switch,
Slider,
Radio,
Checkbox,
Rate,
TreeSelect,
Cascader,
DatePicker,
TimePicker,
AutoComplete,
Upload,
Button
} from 'antd'
import { UploadOutlined } from '@ant-design/icons'
import SimplePictureUpload from '../Upload/SimplePictureUpload'
import MultiplePictureUpload from '../Upload/MultiplePictureUpload'
const { TextArea } = Input
const { Option } = Select
const { MonthPicker, RangePicker } = DatePicker
const layout = {
labelCol: { span: 4 },
wrapperCol: { span: 20 },
};
const validateMessages = {
required: '${label}是必填项!'
};
// 展示属性
const TextField = (props) => {
return (
<span className="sage-form-text">{props.value}</span>
)
}
const normFile = e => {
console.log('Upload event:', e);
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
};
const FormComponentType = {
'text': TextField,
'input': Input,
'textarea': TextArea,
'inputnumber': InputNumber,
'autocomplete': AutoComplete,
'select': Select,
'switch': Switch,
'slider': Slider,
'radio': Radio,
'checkbox': Checkbox,
'rate': Rate,
'treeselect': TreeSelect,
'cascader': Cascader,
'datepicker': DatePicker,
'monthpicker': MonthPicker,
'rangepicker': RangePicker,
'timepicker': TimePicker,
'simplepictureupload': SimplePictureUpload,
'multiplepictureupload': MultiplePictureUpload,
'custom': 'Custom' // 自定义
}
/**
*
* @param {*} props
* 1、formFields 字段集
*/
const SageForm = (props, ref) => {
const {
formFields,
colNum = 1,
...formProps
} = props
const [form] = Form.useForm();
// 暴露外部方法
useImperativeHandle(ref, () => ({
submit: () => form.submit(),
validateFields: (nameList) => form.validateFields(nameList),
getFieldsValue: (nameList) => form.getFieldsValue(nameList),
setFieldsValue: (values) => form.setFieldsValue(values),
resetFields: (fields) => form.resetFields(fields)
}))
let formNode = null
formNode = formFields.map((item, index) => {
const FormComponent = FormComponentType[item.type]
if (item.type === 'custom') {
return (
<Col key={`col_key_${item.name}`} span={24 / colNum}>
{item.render}
</Col>
)
}
let formCompnentNode = null
const formItemProps = {
key: `form_key_${item.name}`,
label: item.label,
}
if (item.type !== 'text') {
formItemProps.name = item.name
formItemProps.rules = item.rules ? item.rules : []
}
if (item.type === 'switch') {
formItemProps.valuePropName = 'checked'
}
switch (item.type) {
case 'select':
formCompnentNode = (
<FormComponent {...item.props}>
{
item.options.map((item2, index2) => {
return (
<Option value={item2.value} key={item2.value}>{item2.text}</Option>
)
})
}
</FormComponent>
)
break;
case 'radio':
formCompnentNode = (
<FormComponent.Group {...item.props}>
{
item.options.map((item2, index2) => {
return (
<FormComponent value={item2.value} key={item2.value}>{item2.text}</FormComponent>
)
})
}
</FormComponent.Group>
)
break;
case 'checkbox':
formCompnentNode = <FormComponent.Group options={item.options} {...item.props} />
break;
case 'autocomplete':
if (item.children) {
formCompnentNode = <FormComponent {...item.props}>{item.children}</FormComponent>
} else {
formCompnentNode = <FormComponent {...item.props} />
}
break;
case 'simplepictureupload':
formCompnentNode = <FormComponent {...item} />
break;
case 'multiplepictureupload':
formCompnentNode = <FormComponent {...item} />
break;
default:
formCompnentNode = <FormComponent {...item.props} />
break;
}
return (
<Col key={`col_key_${item.name}`} span={24 / colNum}>
<Form.Item
{...formItemProps}
>
{formCompnentNode}
</Form.Item>
</Col>
)
})
return (
<Form
{...layout}
form={form}
// name="basic"
// initialValues={{ remember: true }}
validateMessages={validateMessages}
{...formProps}
>
<Row span={24}>
{formNode}
</Row>
</Form>
)
}
export default React.forwardRef(SageForm)
import React from 'react'
import './style.less'
const SageLayoutLR = (props) => {
const {left, right, leftWidth, rightWidth} = props
return (
<div className="sage-layout-lr">
<div className="sage-layout-left" style={leftWidth ? {"flex": `0 0 ${leftWidth}px`} : {}}>{left}</div>
<div className="sage-layout-right" style={rightWidth ? {"flex": `0 0 ${rightWidth}px`} : {}}>{right}</div>
</div>
)
}
export default SageLayoutLR
.sage-layout-lr {
display: flex;
background: #fff;
}
.sage-layout-left {
flex: 1;
}
.sage-layout-right {
flex: 1;
}
import { message } from 'antd'
const sageMessage = {
success: (text) => message.success(text)
}
export default sageMessage
import React, { useState, useImperativeHandle } from 'react'
import { Modal } from 'antd'
import './style.less'
const initState = {
visible: false,
maskClosable: false,
destroyOnClose: false,
title: null
}
const SageModal = (props, ref) => {
const {
visible: visibleProps,
maskClosable: maskClosableProps,
title: titleProps,
destroyOnClose: destroyOnCloseProps,
...modalProps
} = props
const [visible, setVisible] = useState(initState.visible)
const [maskClosable] = useState(initState.maskClosable)
const [title, setTitle] = useState(initState.title)
const [destroyOnClose] = useState(initState.destroyOnClose)
const visibleState = visibleProps !== undefined ? visibleProps : visible
const mastClosableState = maskClosableProps !== undefined ? maskClosableProps : maskClosable
const titleState = titleProps !== undefined ? titleProps : title
const destroyOnCloseState = destroyOnCloseProps !== undefined ? destroyOnCloseProps : destroyOnClose
const onCancel = () => {
setVisible(false)
}
// const onOk = () => {
// }
// 暴露外部方法
useImperativeHandle(ref, () => ({
setVisible,
setTitle
}))
return (
<div className="sage-modal">
<Modal
title={titleState}
visible={visibleState}
maskClosable={mastClosableState}
destroyOnClose={destroyOnCloseState}
onCancel={onCancel}
// onOk={onOk}
{...modalProps}
>
{props.children}
</Modal>
</div>
)
}
export default React.forwardRef(SageModal)
import React, { useEffect, useState, useImperativeHandle, useRef } from 'react'
import { ConfigProvider, Table, Card } from 'antd'
import { useFullscreen } from '@umijs/hooks'
import TableSearchForm from '@/components/Common/TableSearchForm'
import TableTool from '@/components/Common/TableTool'
import './style.less'
const initState = {
pageNum: 1,
pageSize: 10,
dataSource: [],
pagination: {
showQuickJumper: true
},
searchParams: {}
}
/**
*
* @param {} props
* 自定义props
* 1、request
* 2、hasNumber 是否有序列号
* 3、hasCheck 是否有checkbox
* 4、selectionType 选择框类型 checkbox(默认)、 radio
* 5、onClickRow
* 6、onDoubleClickRow
* 7、onMouseEnterRow
* 8、onMouseLeaveRow
* 9、onContextMenuRow
* 10、toolOptionConfig
* 表格工具['reload', 'hiddensearch', 'density', 'fullScreen']
*/
const SageTable = React.forwardRef((props, ref) => {
const rootRef = useRef(null)
const tableSearchFormRef = useRef()
const [selectedRowKeys, setSelectedRowKeys] = useState([])
const [selectedRows, setSelectedRows] = useState([])
const {
request,
hasNumber = false,
hasCheck = false,
selectionType = 'checkbox',
size: tableSizeProps,
columns: columnsProps,
dataSource: dataSourceProps,
pagination: paginationProps,
loading: loadingProps,
rowSelection: rowSelectionProps,
onClickRow: onClickRowProps,
onDoubleClickRow: onDoubleClickRowProps,
onMouseEnterRow: onMouseEnterRowProps,
onMouseLeaveRow: onMouseLeaveRowProps,
onContextMenuRow: onContextMenuRowProps,
// 搜索相关
searchFields: searchFieldsProps,
onSearchTable: onSearchTableProps,
onResetTable: onResetTableProps,
// 工具相关
toolBarRender: toolBarRenderProps,
toolOptionConfig: toolOptionConfigProps,
// 其余表格属性
...tableProps
} = props;
const { isFullscreen, setFull, exitFull } = useFullscreen({
dom: () => document.getElementsByClassName('ant-pro-page-header-wrap-children-content')[0]
})
const [tableSize, setTableSize] = useState('default')
const [loading, setLoading] = useState(false)
const [tableState, setTableState] = useState(initState)
// 获取表格数据
const queryTable = async (tableParams, status) => {
if (request) {
let queryParams = {
pageNum: tableState.pageNum,
pageSize: tableState.pageSize
}
// 是否有搜索条件
const hasSearchCondition = tableParams !== undefined
if (hasSearchCondition) {
queryParams = Object.assign(queryParams, {
...tableState.searchParams,
...tableParams
})
if (status === 'change') {
if (!tableParams.sortOrder) {
delete queryParams.sortOrder
}
if (!tableParams.sortField) {
delete queryParams.sortField
}
}
}
// 如果是重置则只保留分页和排序条件
if (status === 'reset') {
Object.keys(queryParams).forEach(item => {
if (!(item === 'pageNum' || item === 'pageSize' || item === 'sortField' || item === 'sortOrder')) {
delete queryParams[item]
}
})
}
setLoading(true)
const res = await request(queryParams)
setLoading(false)
if (res.isSuccess) {
const {
curPage: current,
pageCount: pageSize,
dataMaxCount: total,
} = res
const newSearchParams = Object.assign({}, queryParams)
if (newSearchParams.pageNum) {
delete newSearchParams.pageNum
}
if (queryParams.pageSize) {
delete newSearchParams.pageSize
}
setTableState({
pageNum: current,
pageSize,
dataSource: res.data.slice(),
pagination: {
...tableState.pagination,
current,
pageSize,
total
},
searchParams: hasSearchCondition ? { ...newSearchParams } : {}
})
}
}
}
// 改变表格数据
const onChangeTable = async (pagination, filters, sorter) => {
const tableParams = {
pageNum: pagination.current,
pageSize: pagination.pageSize
}
// 如果有排序则添加排序属性
if (Object.keys(sorter).length !== 0 && sorter.order) {
tableParams.sortField = sorter.column.sortField
tableParams.sortOrder = sorter.order === 'ascend' ? 'asc' : 'desc'
}
queryTable(tableParams, 'change')
}
// 刷新当前表格数据
const reloadTable = () => {
queryTable(tableState.searchParams)
}
// 获取选中keys
const getSelectedRowKeys = () => {
return selectedRowKeys
}
// 获取选中rows
const getSelectedRows = () => {
return selectedRows
}
// 工具方法
const tableToolProps = {
toolBarRender: toolBarRenderProps,
toolOptionConfig: toolOptionConfigProps,
// 刷新表格
onRefreshTable: () => {
queryTable(tableState.searchParams)
},
// 隐藏搜索条件
onHiddeSearch: () => {
if (tableSearchFormRef.current) {
tableSearchFormRef.current.hiddenSearch()
}
},
// 改变表格密度
onChangeSize: (size) => {
setTableSize(size)
},
// 全屏
setFull: () => setFull(),
// 退出全屏
exitFull: () => exitFull(),
}
useEffect(() => {
queryTable()
}, [])
const tableSizeState = tableSizeProps !== undefined ? tableSizeProps : tableSize
const dataSourceState = dataSourceProps !== undefined ? dataSourceProps.slice() : tableState.dataSource
const paginationState = paginationProps !== undefined ? paginationProps : tableState.pagination
const loadingState = loadingProps !== undefined ? loadingProps : loading
// 是否有序号
const columnsState = columnsProps !== undefined ? columnsProps.slice() : []
if (hasNumber) {
columnsState.unshift({
title: '序号',
render: (text, record, index) => `${(paginationState.current - 1) * paginationState.pageSize + index + 1}`,
align: 'center',
width: 60,
})
}
const tableSearchFormProps = {
searchFields: searchFieldsProps,
onSearchTable: onSearchTableProps,
onResetTable: onResetTableProps,
}
const lastTableState = {
...tableProps
}
const rowSelection = {
selectedRowKeys,
fixed: 'left',
type: selectionType,
onChange: (selectedrowkeys, selectedrows) => {
// console.log(`selectedRowKeys: ${selectedrowkeys}`, 'selectedRows: ', selectedrows);
setSelectedRowKeys(selectedrowkeys)
setSelectedRows(selectedrows)
}
};
if (hasCheck) {
lastTableState.rowSelection = {
...rowSelection,
...rowSelectionProps
}
}
// 暴露给外部的方法
useImperativeHandle(ref, () => ({
queryTable,
reloadTable,
getSelectedRowKeys,
setSelectedRowKeys,
getSelectedRows,
setSelectedRows,
getSearchFieldsValue: () => tableSearchFormRef.current.getFieldsValue()
}))
return (
<ConfigProvider
getPopupContainer={() => rootRef.current}
>
<div className="sage-pro-table" ref={rootRef}>
<Card
bordered={false}
style={{
height: '100%',
}}
bodyStyle={{
padding: 0,
}}
>
{
searchFieldsProps && searchFieldsProps.length !== 0 &&
<TableSearchForm
ref={tableSearchFormRef}
{...tableSearchFormProps}
/>
}
<TableTool
tableSize={tableSizeState}
isFullscreen={isFullscreen}
{...tableToolProps}
/>
<Table
size={tableSizeState}
columns={columnsState}
dataSource={dataSourceState}
pagination={paginationState}
loading={loadingState}
onChange={onChangeTable}
onRow={record => {
return {
// 点击行
onClick: event => {
event.stopPropagation()
if (onClickRowProps) {
onClickRowProps(record)
}
},
// 双击行
onDoubleClick: event => {
event.stopPropagation()
if (onDoubleClickRowProps) {
onDoubleClickRowProps(record)
}
},
// 鼠标右键
onContextMenu: event => {
event.stopPropagation()
if (onContextMenuRowProps) {
onContextMenuRowProps(record)
}
},
// 鼠标移入行
onMouseEnter: event => {
event.stopPropagation()
if (onMouseEnterRowProps) {
onMouseEnterRowProps(record)
}
},
// 鼠标移出行
onMouseLeave: event => {
event.stopPropagation()
if (onMouseLeaveRowProps) {
onMouseLeaveRowProps(record)
}
},
};
}}
{...lastTableState}
/>
</Card>
</div>
</ConfigProvider>
)
})
export default SageTable
import React, { useState, useImperativeHandle } from 'react';
import {
Row,
Col,
Form,
Input,
Select,
InputNumber,
Switch,
Slider,
Radio,
Checkbox,
Rate,
TreeSelect,
Cascader,
DatePicker,
TimePicker,
AutoComplete
} from 'antd'
import { SageButton } from '@/components/Common'
import { DownOutlined, UpOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import './style.less'
const { TextArea } = Input
const { Option } = Select
const { MonthPicker, RangePicker } = DatePicker
const FormComponentType = {
'input': Input,
'textarea': TextArea,
'inputnumber': InputNumber,
'autocomplete': AutoComplete,
'select': Select,
'switch': Switch,
'slider': Slider,
'radio': Radio,
'checkbox': Checkbox,
'rate': Rate,
'treeselect': TreeSelect,
'cascader': Cascader,
'datepicker': DatePicker,
'monthpicker': MonthPicker,
'rangepicker': RangePicker,
'timepicker': TimePicker,
'custom': 'Custom' // 自定义
}
const defaultExpandNum = 5
/**
* 1、expandNum 大于多少个可展开默认5
* 2、searchFields []
* name: 字段名
* label: 名称
* type: 类型
* props: {}
*/
const TableSearchForm = React.forwardRef((props, ref) => {
const [expand, setExpand] = useState(false)
const [hidden, setHidden] = useState(false)
const expandNum = props.expandNum ? props.expandNum : defaultExpandNum
const searchFields = props.searchFields ? props.searchFields : []
const [form] = Form.useForm();
const getFields = () => {
return searchFields.map((item, index) => {
const FormComponent = FormComponentType[item.type]
if (item.type === 'custom') {
return item.render
}
let formCompnentNode = null
const formItemProps = {
key: `form_key_${item.name}`,
label: item.label,
}
if (item.type !== 'text') {
formItemProps.name = item.name
formItemProps.rules = item.rules ? item.rules : []
}
if (item.type === 'switch') {
formItemProps.valuePropName = 'checked'
}
switch (item.type) {
case 'select':
formCompnentNode = (
<FormComponent {...item.props}>
{
item.options.map((item2, index2) => {
return (
<Option value={item2.value} key={item2.value}>{item2.text}</Option>
)
})
}
</FormComponent>
)
break;
case 'radio':
formCompnentNode = (
<FormComponent.Group {...item.props}>
{
item.options.map((item2, index2) => {
return (
<FormComponent value={item2.value} key={item2.value}>{item2.text}</FormComponent>
)
})
}
</FormComponent.Group>
)
break;
case 'checkbox':
formCompnentNode = <FormComponent.Group options={item.options} {...item.props} />
break;
case 'autocomplete':
if (item.children) {
formCompnentNode = <FormComponent {...item.props}>{item.children}</FormComponent>
} else {
formCompnentNode = <FormComponent {...item.props} />
}
break;
default:
formCompnentNode = <FormComponent {...item.props} />
break;
}
return (
<Col span={8} key={`search_form_key_${item.name}`} className={(index > expandNum - 1 && !expand) ? 'sage-hidden' : ''}>
<Form.Item
{...formItemProps}
>
{formCompnentNode}
</Form.Item>
</Col>
)
})
};
const onFinish = values => {
props.onSearchTable(values);
};
// 暴露外部方法
useImperativeHandle(ref, () => ({
hiddenSearch: () => {
setHidden(!hidden)
},
getFieldsValue: (nameList) => form.getFieldsValue(nameList),
setFieldsValue: (values) => form.setFieldsValue(values),
}))
return (
<Form
form={form}
// name="advanced_search"
className={`sage-table-search-form ${hidden ? 'sage-hidden' : ''}`}
onFinish={onFinish}
>
<Row gutter={24}>
{getFields()}
<Col span={(!expand && searchFields.length > expandNum ) ? 8 * (3 - expandNum % 3) : 8 * (3 - searchFields.length % 3)} style={{ textAlign: 'right' }}>
<SageButton type="success" htmlType="submit" icon={<SearchOutlined />}>
查询
</SageButton>
<SageButton
type="warning"
icon={<ReloadOutlined />}
style={{ margin: '0 8px' }}
onClick={() => {
form.resetFields()
props.onResetTable()
}}
>
重置
</SageButton>
{
searchFields.length > expandNum ?
<a
style={{ fontSize: 14 }}
onClick={() => {
setExpand(!expand);
}}
>
{expand ? <>收起 <UpOutlined /></> : <>展开 <DownOutlined /></>}
</a> : null
}
</Col>
</Row>
</Form>
);
});
export default TableSearchForm
.sage-table-search-form {
// margin-bottom: 12px;
padding: 12px;
padding-bottom: 0;
background: #fff;
}
.sage-table-search-form .ant-form-item {
display: flex;
margin-bottom: 12px;
}
.sage-table-search-form .ant-form-item-control-wrapper {
flex: 1;
}
import React from 'react'
import { Dropdown, Tooltip, Menu } from 'antd'
import { ColumnHeightOutlined } from '@ant-design/icons';
const DensityIcon = (props) => {
const { tableSize, onChangeSize } = props
return (
<Dropdown
// ref={ref}
overlay={
<Menu
selectedKeys={[tableSize]}
onClick={({ key }) => {
onChangeSize(key)
}}
style={{
width: 80,
}}
>
<Menu.Item key="default">默认</Menu.Item>
<Menu.Item key="middle">中等</Menu.Item>
<Menu.Item key="small">紧凑</Menu.Item>
</Menu>
}
trigger={['click']}
>
<Tooltip title="密度">
<ColumnHeightOutlined className="table-tool-icon" />
</Tooltip>
</Dropdown>
)
}
export default DensityIcon
import React from 'react';
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd';
import './style.less'
const FullScreenIcon = (props) => {
const {isFullscreen, setFull, exitFull} = props
return isFullscreen ? (
<Tooltip title="退出全屏">
<FullscreenExitOutlined onClick={exitFull} />
</Tooltip>
) : (
<Tooltip title="全屏">
<FullscreenOutlined onClick={setFull} />
</Tooltip>
);
};
export default FullScreenIcon;
.ant-pro-page-header-wrap-children-content:not(:root):-webkit-full-screen::backdrop {
min-height: 100vh;
overflow: auto;
background: #fff !important;
}
.ant-pro-page-header-wrap-children-content:not(:root):-ms-fullscreen::backdrop {
min-height: 100vh;
overflow: auto;
background: #fff !important;
}
.ant-pro-page-header-wrap-children-content:not(:root):fullscreen::backdrop {
min-height: 100vh;
overflow: auto;
background: #fff !important;
}
import React from 'react'
import { Tooltip } from 'antd'
import { FileSearchOutlined, FileSyncOutlined } from '@ant-design/icons';
import DensityIcon from './components/DensityIcon/index'
import FullscreenIcon from './components/FullscreenIcon/index'
import './style.less'
const defatultToolOption = ['reload', 'hiddensearch', 'density', 'fullScreen']
/**
*
* @param {} props
*/
const SageTableTool = (props) => {
const {
toolBarRender,
toolOptionConfig,
tableSize,
isFullscreen,
onRefreshTable,
onChangeSize,
onHiddeSearch,
setFull,
exitFull
} = props
// 控制工具按钮显示
let toolDom = null
let toolOption = []
if (toolOptionConfig !== null) {
if (toolOptionConfig === undefined) {
toolOption = defatultToolOption
} else {
toolOption = toolOptionConfig.slice()
}
}
toolDom = toolOption.map(item => {
let eachDom = null
switch (item) {
case 'reload':
eachDom = (
<span className="sage-table-toolbar-item" key={`sage-table-toolbar-item-${item}`}>
<Tooltip title="刷新表格">
<FileSyncOutlined className="table-tool-icon" onClick={onRefreshTable} />
</Tooltip>
</span>
)
break;
case 'hiddensearch':
eachDom = (
<span className="sage-table-toolbar-item" key={`sage-table-toolbar-item-${item}`}>
<Tooltip title="搜索栏">
<FileSearchOutlined className="table-tool-icon" onClick={onHiddeSearch} />
</Tooltip>
</span>
)
break;
case 'density':
eachDom = (
<span className="sage-table-toolbar-item" key={`sage-table-toolbar-item-${item}`}>
<DensityIcon tableSize={tableSize} onChangeSize={onChangeSize} />
</span>
)
break;
case 'fullScreen':
eachDom = (
<span className="sage-table-toolbar-item" key={`sage-table-toolbar-item-${item}`}>
<FullscreenIcon isFullscreen={isFullscreen} setFull={setFull} exitFull={exitFull} />
</span>
)
break;
default:
break;
}
return eachDom
})
return (
<div className="sage-table-toolbar">
<div className="sage-table-toolbar-button">
{toolBarRender()}
</div>
<div className="sage-table-toolbar-option">
{toolDom}
</div>
</div>
)
}
export default SageTableTool
.sage-table-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
height: 52px;
padding: 0 12px;
line-height: 52px;
background: #fff;
}
.sage-table-toolbar-option {
margin-right: 16px;
}
.sage-table-toolbar-item {
display: inline-block;
margin-left: 16px;
.anticon {
vertical-align: middle;
&:hover {
cursor: pointer;
}
}
}
.table-tool-icon {
font-size: 18px;
}
import React, { useState, useEffect } from 'react'
import { Tree } from 'antd'
const SageTree = (props) => {
const {
request,
treeData: treeDataProps,
...treeProps
} = props
const [treeData, setTreeData] = useState([])
const queryTree = async () => {
if (request) {
const res = await request()
}
}
useEffect(() => {
queryTree()
}, [])
const treeDataState = treeDataProps !== undefined ? treeDataProps : treeData
return(
<Tree
treeData={treeDataState}
{...treeProps}
/>
)
}
export default SageTree
import React, { useState, useEffect } from 'react'
import { Upload, Modal, message } from 'antd'
import { PlusOutlined } from '@ant-design/icons'
import './style.less'
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
const MutilplePictureUpload = (props) => {
const {
name: field,
children: childrenProps,
maxNum = 3,
props: {
action = '/ebd/sys/file/upload',
listType = 'picture-card',
accept = '.jpg,.jpeg,.png,.gif,.bmp',
name = 'file',
fileList: fileListProps,
uploadSuccess,
...otherUploadProps
}
} = props
const [previewVisible, setPreviewVisible] = useState(false)
const [previewImage, setPreviewImage] = useState('')
const [fileList, setFileList] = useState([])
useEffect(() => {
setFileList(fileListProps || [])
}, [fileListProps])
// 上传之前的操作
const beforeUpload = file => {
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
message.warn('图片大小不能大于10MB!')
}
return isLt10M
}
// 上传
const handleChange = ({ file, fileList: _fileList }) => {
if (file.status === 'done') {
if (uploadSuccess) {
const imageList = []
_fileList.forEach(item => {
imageList.push(item.response?.data.id || item.id)
})
uploadSuccess(field, imageList)
}
}
setFileList(_fileList)
}
// 删除
const hanldeRemove = (file) => {
const newFileList = fileList.slice()
fileList.forEach((item, index) => {
if (item.uid === file.uid) {
newFileList.splice(index, 1)
}
})
// setFileList(newFileList)
if (uploadSuccess) {
const imageList = []
newFileList.forEach(item => {
imageList.push(item.response?.data.id || item.id)
})
uploadSuccess(field, imageList)
}
}
// 预览
const handlePreview = async file => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setPreviewImage(file.url || file.preview)
setPreviewVisible(true)
};
// 关闭预览
const handleCancel = () => {
setPreviewVisible(false)
}
const uploadButton = childrenProps || (
<div>
<PlusOutlined />
<div className="ant-upload-text">上传图片</div>
</div>
);
return (
<div className="clearfix">
<Upload
fileList={fileList}
action={action}
listType={listType}
accept={accept}
name={name}
onPreview={handlePreview}
beforeUpload={beforeUpload}
onChange={handleChange}
onRemove={hanldeRemove}
{...otherUploadProps}
>
{fileList.length >= maxNum ? null : uploadButton}
</Upload>
<Modal
visible={previewVisible}
title="图片预览"
footer={null}
onCancel={handleCancel}
>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
)
}
export default MutilplePictureUpload
.ant-upload-select-picture-card i {
color: #999;
font-size: 32px;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
import React, { useState, useEffect } from 'react'
import { Button, Upload, message } from 'antd'
import { UploadOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons'
import './style.less'
function getBase64(img, callback) {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result));
reader.readAsDataURL(img);
}
const SimplePictureUpload = (props) => {
const {
name: field,
children: childrenProps,
props: {
action = '/ebd/sys/file/upload',
listType = 'picture-card',
accept = '.jpg,.jpeg,.png,.gif,.bmp',
name = 'file',
showUploadList = false,
className = "avatar-uploader",
uploadSuccess,
previewImage: previewImageProps,
...otherUploadProps
}
} = props
const [previewImage, setPreviewImage] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
setPreviewImage(previewImageProps)
}, [previewImageProps])
// 上传之前的操作
const beforeUpload = file => {
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
message.warn('图片大小不能大于10MB!')
}
return isLt10M
}
// 上传
const handleChange = (info) => {
if (info.file.status === 'uploading') {
setLoading(true)
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, imageUrl => {
setPreviewImage(imageUrl)
setLoading(false)
});
if (uploadSuccess) {
uploadSuccess(field, info.file.response.data.id)
}
}
}
const uploadButton = childrenProps || (
<div>
{loading ? <LoadingOutlined /> : <PlusOutlined />}
<div className="ant-upload-text">上传</div>
</div>
);
return (
<Upload
action={action}
listType={listType}
accept={accept}
name={name}
showUploadList={showUploadList}
className={className}
beforeUpload={beforeUpload}
onChange={handleChange}
{...otherUploadProps}
>
{previewImage ? <img src={previewImage} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
</Upload>
)
}
export default SimplePictureUpload
.avatar-uploader > .ant-upload {
width: 128px;
height: 128px;
}
import SageButton from './Button'
import SageTable from './Table'
import ActionSet from './ActionSet'
import SageModal from './Modal'
import SageForm from './Form'
import SageTree from './Tree'
import sageMessage from './Message'
import SageLayoutLR from './Layout/LayoutLR'
export {
SageLayoutLR,
SageButton,
SageTable,
ActionSet,
SageModal,
SageForm,
SageTree,
sageMessage
}
import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
import { Avatar, Menu, Spin } from 'antd';
import React from 'react';
import { history, connect } from 'umi';
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;
}
history.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">
<UserOutlined />
个人中心
</Menu.Item>
)}
{menu && (
<Menu.Item key="settings">
<SettingOutlined />
个人设置
</Menu.Item>
)}
{menu && <Menu.Divider />}
<Menu.Item key="logout">
<LogoutOutlined />
退出登录
</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>
) : (
<span className={`${styles.action} ${styles.account}`}>
<Spin
size="small"
style={{
marginLeft: 8,
marginRight: 8,
}}
/>
</span>
);
}
}
export default connect(({ user }) => ({
currentUser: user.currentUser,
}))(AvatarDropdown);
import React, { Component } from 'react';
import { connect } from 'umi';
import { Tag, message } from 'antd';
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 || notices.length === 0 || !Array.isArray(notices)) {
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 { Tooltip, Tag } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import React from 'react';
import { connect } from 'umi';
import Avatar from './AvatarDropdown';
import HeaderSearch from '../HeaderSearch';
import SelectLang from '../SelectLang';
import styles from './index.less';
const ENVTagColor = {
dev: 'orange',
test: 'green',
pre: '#87d068',
};
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"
options={[
{
label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>,
value: 'umi ui',
},
{
label: <a href="next.ant.design">Ant Design</a>,
value: 'Ant Design',
},
{
label: <a href="https://protable.ant.design/">Pro Table</a>,
value: 'Pro Table',
},
{
label: <a href="https://prolayout.ant.design/">Pro Layout</a>,
value: 'Pro Layout',
},
]}
// onSearch={value => {
// //console.log('input', value);
// }}
/> */}
{
// <Tooltip title="使用文档">
// <a
// target="_blank"
// href="https://pro.ant.design/docs/getting-started"
// rel="noopener noreferrer"
// className={styles.action}
// >
// <QuestionCircleOutlined />
// </a>
// </Tooltip>
}
<Avatar />
{REACT_APP_ENV && (
<span>
<Tag color={ENVTagColor[REACT_APP_ENV]}>{REACT_APP_ENV}</Tag>
</span>
)}
{
// <SelectLang className={styles.action} />
}
</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);
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
min-width: 160px;
}
}
.right {
display: flex;
float: right;
height: @layout-header-height;
margin-left: auto;
overflow: hidden;
.action {
display: flex;
align-items: center;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all 0.3s;
> span {
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 {
.action {
color: rgba(255, 255, 255, 0.85);
> span {
color: rgba(255, 255, 255, 0.85);
}
&:hover,
&:global(.opened) {
background: @primary-color;
}
}
}
:global(.ant-pro-global-header) {
.dark {
.action {
color: @text-color;
> span {
color: @text-color;
}
&:hover {
color: rgba(255, 255, 255, 0.85);
> span {
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;
}
.right {
position: absolute;
top: 0;
right: 12px;
.account {
.avatar {
margin-right: 0;
}
}
.search {
display: none;
}
}
}
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: @popover-bg;
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 { SearchOutlined } from '@ant-design/icons';
import { AutoComplete, Input } from 'antd';
import useMergeValue from 'use-merge-value';
import React, { useRef } from 'react';
import classNames from 'classnames';
import styles from './index.less';
const HeaderSearch = props => {
const {
className,
defaultValue,
onVisibleChange,
placeholder,
open,
defaultOpen,
...restProps
} = props;
const inputRef = useRef(null);
const [value, setValue] = useMergeValue(defaultValue, {
value: props.value,
onChange: props.onChange,
});
const [searchMode, setSearchMode] = useMergeValue(defaultOpen || false, {
value: props.open,
onChange: onVisibleChange,
});
const inputClass = classNames(styles.input, {
[styles.show]: searchMode,
});
return (
<div
className={classNames(className, styles.headerSearch)}
onClick={() => {
setSearchMode(true);
if (searchMode && inputRef.current) {
inputRef.current.focus();
}
}}
onTransitionEnd={({ propertyName }) => {
if (propertyName === 'width' && !searchMode) {
if (onVisibleChange) {
onVisibleChange(searchMode);
}
}
}}
>
<SearchOutlined
key="Icon"
style={{
cursor: 'pointer',
}}
/>
<AutoComplete
key="AutoComplete"
className={inputClass}
value={value}
style={{
height: 28,
marginTop: -6,
}}
options={restProps.options}
onChange={setValue}
>
<Input
ref={inputRef}
defaultValue={defaultValue}
aria-label={placeholder}
placeholder={placeholder}
onKeyDown={e => {
if (e.key === 'Enter') {
if (restProps.onSearch) {
restProps.onSearch(value);
}
}
}}
onBlur={() => {
setSearchMode(false);
}}
/>
</AutoComplete>
</div>
);
};
export default HeaderSearch;
@import '~antd/es/style/themes/default.less';
.headerSearch {
.input {
width: 0;
min-width: 0;
overflow: hidden;
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 || 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: @component-background;
}
.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;
&:only-child {
width: 100%;
}
&:not(:only-child):last-child {
border-left: 1px solid @border-color-split;
}
}
}
import { BellOutlined } from '@ant-design/icons';
import { Badge, Spin, Tabs } from 'antd';
import useMergeValue from 'use-merge-value';
import React from 'react';
import classNames from 'classnames';
import NoticeList from './NoticeList';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
const { TabPane } = Tabs;
const NoticeIcon = props => {
const getNotificationBox = () => {
const {
children,
loading,
onClear,
onTabChange,
onItemClick,
onViewMore,
clearText,
viewMoreText,
} = props;
if (!children) {
return null;
}
const panes = [];
React.Children.forEach(children, child => {
if (!child) {
return;
}
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;
panes.push(
<TabPane tab={tabTitle} key={tabKey}>
<NoticeList
clearText={clearText}
viewMoreText={viewMoreText}
data={list}
onClear={() => onClear && onClear(title, tabKey)}
onClick={item => onItemClick && onItemClick(item, child.props)}
onViewMore={event => onViewMore && onViewMore(child.props, event)}
showClear={showClear}
showViewMore={showViewMore}
title={title}
{...child.props}
/>
</TabPane>,
);
});
return (
<Spin spinning={loading} delay={300}>
<Tabs className={styles.tabs} onChange={onTabChange}>
{panes}
</Tabs>
</Spin>
);
};
const { className, count, bell } = props;
const [visible, setVisible] = useMergeValue(false, {
value: props.popupVisible,
onChange: props.onPopupVisibleChange,
});
const noticeButtonClass = classNames(className, styles.noticeButton);
const notificationBox = getNotificationBox();
const NoticeBellIcon = bell || <BellOutlined 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;
}
return (
<HeaderDropdown
placement="bottomRight"
overlay={notificationBox}
overlayClassName={styles.popover}
trigger={['click']}
visible={visible}
onVisibleChange={setVisible}
>
{trigger}
</HeaderDropdown>
);
};
NoticeIcon.defaultProps = {
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
};
NoticeIcon.Tab = NoticeList;
export default NoticeIcon;
@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 { PageLoading } from '@ant-design/pro-layout'; // loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
export default PageLoading;
import { GlobalOutlined } from '@ant-design/icons';
import { Menu } from 'antd';
import { getLocale, setLocale } from 'umi';
import React from 'react';
import classNames from 'classnames';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
const SelectLang = props => {
const { className } = props;
const selectedLang = getLocale();
const changeLang = ({ key }) => setLocale(key);
const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
const languageLabels = {
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
'en-US': 'English',
'pt-BR': 'Português',
};
const languageIcons = {
'zh-CN': '🇨🇳',
'zh-TW': '🇭🇰',
'en-US': '🇺🇸',
'pt-BR': '🇧🇷',
};
const langMenu = (
<Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}>
{locales.map(locale => (
<Menu.Item key={locale}>
<span role="img" aria-label={languageLabels[locale]}>
{languageIcons[locale]}
</span>{' '}
{languageLabels[locale]}
</Menu.Item>
))}
</Menu>
);
return (
<HeaderDropdown overlay={langMenu} placement="bottomRight">
<span className={classNames(styles.dropDown, className)}>
<GlobalOutlined title="语言" />
</span>
</HeaderDropdown>
);
};
export default SelectLang;
@import '~antd/es/style/themes/default.less';
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
min-width: 160px;
}
}
.dropDown {
line-height: @layout-header-height;
vertical-align: top;
cursor: pointer;
> span {
font-size: 16px !important;
transform: none !important;
svg {
position: relative;
top: -1px;
}
}
}
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));
}
beforeEach(async () => {
await page.goto(`${BASE_URL}`);
await page.evaluate(() => {
localStorage.setItem('antd-pro-authority', '["admin"]');
});
});
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));
});
it('topmenu should have footer', async () => {
const params = '?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 { Button, message, notification } from 'antd';
import React from 'react';
import { formatMessage } from 'umi';
import defaultSettings from '../config/defaultSettings';
const { pwa } = defaultSettings; // if pwa is true
if (pwa) {
// Notify user if offline now
window.addEventListener('sw.offline', () => {
message.warning(
formatMessage({
id: 'app.pwa.offline',
}),
);
}); // Pop up a prompt on the page asking the user if they want to use the latest version
window.addEventListener('sw.updated', event => {
const e = event;
const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
const worker = e.detail && e.detail.waiting;
if (!worker) {
return true;
} // Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = msgEvent => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(msgEvent.data);
}
};
worker.postMessage(
{
type: 'skip-waiting',
},
[channel.port2],
);
}); // Refresh current page to use the updated HTML and other assets after SW has skiped waiting
window.location.reload(true);
return true;
};
const key = `open${Date.now()}`;
const btn = (
<Button
type="primary"
onClick={() => {
notification.close(key);
reloadSW();
}}
>
{formatMessage({
id: 'app.pwa.serviceworker.updated.ok',
})}
</Button>
);
notification.open({
message: formatMessage({
id: 'app.pwa.serviceworker.updated',
}),
description: formatMessage({
id: 'app.pwa.serviceworker.updated.hint',
}),
btn,
key,
onClose: async () => {},
});
});
} else if ('serviceWorker' in navigator) {
// unregister service worker
const { serviceWorker } = navigator;
if (serviceWorker.getRegistrations) {
serviceWorker.getRegistrations().then(sws => {
sws.forEach(sw => {
sw.unregister();
});
});
}
serviceWorker.getRegistration().then(sw => {
if (sw) sw.unregister();
}); // remove all caches
if (window.caches && window.caches.keys) {
caches.keys().then(keys => {
keys.forEach(key => {
caches.delete(key);
});
});
}
}
@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;
}
// 覆盖antd相关样式
.ant-page-header-heading {
display: none !important;
}
.ant-pro-page-header-wrap-children-content {
margin: 12px 12px 0 !important;
}
// 公共样式
.sage-hidden {
display: none !important;
}
// 新增组件样式
@media (max-width: @screen-xs) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}
// 兼容IE11
@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
body .ant-design-pro > .ant-layout {
min-height: 100vh;
}
}
/**
* Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
* You can view component api by:
* https://github.com/ant-design/ant-design-pro-layout
*/
import ProLayout, { DefaultFooter } from '@ant-design/pro-layout';
import React, { useEffect } from 'react';
import { Link, useIntl, connect } from 'umi';
import { GithubOutlined } from '@ant-design/icons';
import { Result, Button } from 'antd';
import Authorized from '@/utils/Authorized';
import RightContent from '@/components/GlobalHeader/RightContent';
import { getAuthorityFromRouter } from '@/utils/utils';
import logo from '../assets/logo.svg';
const noMatch = (
<Result
status={403}
title="403"
subTitle="Sorry, you are not authorized to access this page."
extra={
<Button type="primary">
<Link to="/user/login">Go Login</Link>
</Button>
}
/>
);
/**
* use Authorized check all menu item
*/
const menuDataRender = menuList =>
menuList.map(item => {
const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
return Authorized.check(item.authority, localItem, null);
});
const defaultFooterDom = (
<DefaultFooter
copyright="2019 蚂蚁金服体验技术部出品"
links={[
{
key: 'Ant Design Pro',
title: 'Ant Design Pro',
href: 'https://pro.ant.design',
blankTarget: true,
},
{
key: 'github',
title: <GithubOutlined />,
href: 'https://github.com/ant-design/ant-design-pro',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
/>
);
const BasicLayout = props => {
const {
dispatch,
children,
settings,
location = {
pathname: '/',
},
} = props;
/**
* constructor
*/
useEffect(() => {
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
}
}, []);
/**
* init variables
*/
const handleMenuCollapse = payload => {
if (dispatch) {
dispatch({
type: 'global/changeLayoutCollapsed',
payload,
});
}
}; // get children authority
const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || {
authority: undefined,
};
const { formatMessage } = useIntl();
return (
<ProLayout
logo={logo}
formatMessage={formatMessage}
menuHeaderRender={(logoDom, titleDom) => (
<Link to="/">
{logoDom}
{titleDom}
</Link>
)}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => {
if (menuItemProps.isUrl || menuItemProps.children || !menuItemProps.path) {
return defaultDom;
}
return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}}
breadcrumbRender={(routers = []) => [
{
path: '/',
breadcrumbName: formatMessage({
id: 'menu.home',
}),
},
...routers,
]}
itemRender={(route, params, routes, paths) => {
const first = routes.indexOf(route) === 0;
return first ? (
<Link to={paths.join('/')}>{route.breadcrumbName}</Link>
) : (
<span>{route.breadcrumbName}</span>
);
}}
footerRender={() => defaultFooterDom}
menuDataRender={menuDataRender}
rightContentRender={() => <RightContent />}
{...props}
{...settings}
>
<Authorized authority={authorized.authority} noMatch={noMatch}>
{children}
</Authorized>
</ProLayout>
);
};
export default connect(({ global, settings }) => ({
collapsed: global.collapsed,
settings,
}))(BasicLayout);
import React from 'react';
const Layout = ({ children }) => <>{children}</>;
export default Layout;
import React from 'react';
import { PageLoading } from '@ant-design/pro-layout';
import { Redirect, connect } from 'umi';
import { stringify } from 'querystring';
class SecurityLayout extends React.Component {
state = {
isReady: false,
};
componentDidMount() {
this.setState({
isReady: true,
});
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
}
}
render() {
const { isReady } = this.state;
const { children, loading, currentUser } = this.props; // You can replace it to your authentication rule (such as check token exists)
// 你可以把它替换成你自己的登录认证规则(比如判断 token 是否存在)
const isLogin = currentUser && currentUser.userid;
const queryString = stringify({
redirect: window.location.href,
});
if ((!isLogin && loading) || !isReady) {
return <PageLoading />;
}
if (!isLogin && window.location.pathname !== '/user/login') {
return <Redirect to={`/user/login?${queryString}`} />;
}
return children;
}
}
export default connect(({ user, loading }) => ({
currentUser: user.currentUser,
loading: loading.models.user,
}))(SecurityLayout);
import { DefaultFooter, getMenuData, getPageTitle } from '@ant-design/pro-layout';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import { Link, useIntl, connect } from 'umi';
import React from 'react';
import SelectLang from '@/components/SelectLang';
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 { formatMessage } = useIntl();
const { breadcrumb } = getMenuData(routes);
const title = getPageTitle({
pathname: location.pathname,
formatMessage,
breadcrumb,
...props,
});
return (
<HelmetProvider>
<Helmet>
<title>{title}</title>
<meta name="description" content={title} />
</Helmet>
<div className={styles.container}>
<div className={styles.lang}>
<SelectLang />
</div>
<div className={styles.content}>
<div className={styles.top}>
<div className={styles.header}>
<Link to="/">
<img alt="logo" className={styles.logo} src={logo} />
<span className={styles.title}>Ant Design</span>
</Link>
</div>
<div className={styles.desc}>Ant Design 是西湖区最具影响力的 Web 设计规范</div>
</div>
{children}
</div>
<DefaultFooter />
</div>
</HelmetProvider>
);
};
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;
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;
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 component from './en-US/component';
import globalHeader from './en-US/globalHeader';
import menu from './en-US/menu';
import pwa from './en-US/pwa';
import settingDrawer from './en-US/settingDrawer';
import settings from './en-US/settings';
export default {
'navBar.lang': 'Languages',
'layout.user.link.help': 'Help',
'layout.user.link.privacy': 'Privacy',
'layout.user.link.terms': 'Terms',
'app.preview.down.block': 'Download this page to your local project',
'app.welcome.link.fetch-blocks': 'Get all block',
'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
};
export default {
'component.tagSelect.expand': 'Expand',
'component.tagSelect.collapse': 'Collapse',
'component.tagSelect.all': 'All',
};
export default {
'component.globalHeader.search': 'Search',
'component.globalHeader.search.example1': 'Search example 1',
'component.globalHeader.search.example2': 'Search example 2',
'component.globalHeader.search.example3': 'Search example 3',
'component.globalHeader.help': 'Help',
'component.globalHeader.notification': 'Notification',
'component.globalHeader.notification.empty': 'You have viewed all notifications.',
'component.globalHeader.message': 'Message',
'component.globalHeader.message.empty': 'You have viewed all messsages.',
'component.globalHeader.event': 'Event',
'component.globalHeader.event.empty': 'You have viewed all events.',
'component.noticeIcon.clear': 'Clear',
'component.noticeIcon.cleared': 'Cleared',
'component.noticeIcon.empty': 'No notifications',
'component.noticeIcon.view-more': 'View more',
};
export default {
'menu.welcome': 'Welcome',
'menu.more-blocks': 'More Blocks',
'menu.home': 'Home',
'menu.admin': 'Admin',
'menu.admin.sub-page': 'Sub-Page',
'menu.login': 'Login',
'menu.register': 'Register',
'menu.register.result': 'Register Result',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'Analysis',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Workplace',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'Form',
'menu.form.basic-form': 'Basic Form',
'menu.form.step-form': 'Step Form',
'menu.form.step-form.info': 'Step Form(write transfer information)',
'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
'menu.form.step-form.result': 'Step Form(finished)',
'menu.form.advanced-form': 'Advanced Form',
'menu.list': 'List',
'menu.list.table-list': 'Search Table',
'menu.list.basic-list': 'Basic List',
'menu.list.card-list': 'Card List',
'menu.list.search-list': 'Search List',
'menu.list.search-list.articles': 'Search List(articles)',
'menu.list.search-list.projects': 'Search List(projects)',
'menu.list.search-list.applications': 'Search List(applications)',
'menu.profile': 'Profile',
'menu.profile.basic': 'Basic Profile',
'menu.profile.advanced': 'Advanced Profile',
'menu.result': 'Result',
'menu.result.success': 'Success',
'menu.result.fail': 'Fail',
'menu.exception': 'Exception',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Trigger',
'menu.account': 'Account',
'menu.account.center': 'Account Center',
'menu.account.settings': 'Account Settings',
'menu.account.trigger': 'Trigger Error',
'menu.account.logout': 'Logout',
'menu.editor': 'Graphic Editor',
'menu.editor.flow': 'Flow Editor',
'menu.editor.mind': 'Mind Editor',
'menu.editor.koni': 'Koni Editor',
};
export default {
'app.pwa.offline': 'You are offline now',
'app.pwa.serviceworker.updated': 'New content is available',
'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page',
'app.pwa.serviceworker.updated.ok': 'Refresh',
};
export default {
'app.setting.pagestyle': 'Page style setting',
'app.setting.pagestyle.dark': 'Dark style',
'app.setting.pagestyle.light': 'Light style',
'app.setting.content-width': 'Content Width',
'app.setting.content-width.fixed': 'Fixed',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.themecolor': 'Theme Color',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Navigation Mode',
'app.setting.sidemenu': 'Side Menu Layout',
'app.setting.topmenu': 'Top Menu Layout',
'app.setting.fixedheader': 'Fixed Header',
'app.setting.fixedsidebar': 'Fixed Sidebar',
'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
'app.setting.hideheader': 'Hidden Header when scrolling',
'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
'app.setting.othersettings': 'Other Settings',
'app.setting.weakmode': 'Weak Mode',
'app.setting.copy': 'Copy Setting',
'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js',
'app.setting.production.hint':
'Setting panel shows in development environment only, please manually modify',
};
export default {
'app.settings.menuMap.basic': 'Basic Settings',
'app.settings.menuMap.security': 'Security Settings',
'app.settings.menuMap.binding': 'Account Binding',
'app.settings.menuMap.notification': 'New Message Notification',
'app.settings.basic.avatar': 'Avatar',
'app.settings.basic.change-avatar': 'Change avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Please input your email!',
'app.settings.basic.nickname': 'Nickname',
'app.settings.basic.nickname-message': 'Please input your Nickname!',
'app.settings.basic.profile': 'Personal profile',
'app.settings.basic.profile-message': 'Please input your personal profile!',
'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
'app.settings.basic.country': 'Country/Region',
'app.settings.basic.country-message': 'Please input your country!',
'app.settings.basic.geographic': 'Province or city',
'app.settings.basic.geographic-message': 'Please input your geographic info!',
'app.settings.basic.address': 'Street Address',
'app.settings.basic.address-message': 'Please input your address!',
'app.settings.basic.phone': 'Phone Number',
'app.settings.basic.phone-message': 'Please input your phone!',
'app.settings.basic.update': 'Update Information',
'app.settings.security.strong': 'Strong',
'app.settings.security.medium': 'Medium',
'app.settings.security.weak': 'Weak',
'app.settings.security.password': 'Account Password',
'app.settings.security.password-description': 'Current password strength',
'app.settings.security.phone': 'Security Phone',
'app.settings.security.phone-description': 'Bound phone',
'app.settings.security.question': 'Security Question',
'app.settings.security.question-description':
'The security question is not set, and the security policy can effectively protect the account security',
'app.settings.security.email': 'Backup Email',
'app.settings.security.email-description': 'Bound Email',
'app.settings.security.mfa': 'MFA Device',
'app.settings.security.mfa-description':
'Unbound MFA device, after binding, can be confirmed twice',
'app.settings.security.modify': 'Modify',
'app.settings.security.set': 'Set',
'app.settings.security.bind': 'Bind',
'app.settings.binding.taobao': 'Binding Taobao',
'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
'app.settings.binding.alipay': 'Binding Alipay',
'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
'app.settings.binding.dingding': 'Binding DingTalk',
'app.settings.binding.dingding-description': 'Currently unbound DingTalk account',
'app.settings.binding.bind': 'Bind',
'app.settings.notification.password': 'Account Password',
'app.settings.notification.password-description':
'Messages from other users will be notified in the form of a station letter',
'app.settings.notification.messages': 'System Messages',
'app.settings.notification.messages-description':
'System messages will be notified in the form of a station letter',
'app.settings.notification.todo': 'To-do Notification',
'app.settings.notification.todo-description':
'The to-do list will be notified in the form of a letter from the station',
'app.settings.open': 'Open',
'app.settings.close': 'Close',
};
import component from './pt-BR/component';
import globalHeader from './pt-BR/globalHeader';
import menu from './pt-BR/menu';
import pwa from './pt-BR/pwa';
import settingDrawer from './pt-BR/settingDrawer';
import settings from './pt-BR/settings';
export default {
'navBar.lang': 'Idiomas',
'layout.user.link.help': 'ajuda',
'layout.user.link.privacy': 'política de privacidade',
'layout.user.link.terms': 'termos de serviços',
'app.preview.down.block': 'Download this page to your local project',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
};
export default {
'component.tagSelect.expand': 'Expandir',
'component.tagSelect.collapse': 'Diminuir',
'component.tagSelect.all': 'Todas',
};
export default {
'component.globalHeader.search': 'Busca',
'component.globalHeader.search.example1': 'Exemplo de busca 1',
'component.globalHeader.search.example2': 'Exemplo de busca 2',
'component.globalHeader.search.example3': 'Exemplo de busca 3',
'component.globalHeader.help': 'Ajuda',
'component.globalHeader.notification': 'Notificação',
'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.',
'component.globalHeader.message': 'Mensagem',
'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.',
'component.globalHeader.event': 'Evento',
'component.globalHeader.event.empty': 'Você visualizou todos os eventos.',
'component.noticeIcon.clear': 'Limpar',
'component.noticeIcon.cleared': 'Limpo',
'component.noticeIcon.empty': 'Sem notificações',
'component.noticeIcon.loaded': 'Carregado',
'component.noticeIcon.view-more': 'Veja mais',
};
export default {
'menu.welcome': 'Welcome',
'menu.more-blocks': 'More Blocks',
'menu.home': 'Início',
'menu.login': 'Login',
'menu.admin': 'Admin',
'menu.admin.sub-page': 'Sub-Page',
'menu.register': 'Registro',
'menu.register.result': 'Resultado de registro',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'Análise',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Ambiente de Trabalho',
'menu.exception.403': '403',
'menu.exception.404': '404',
'menu.exception.500': '500',
'menu.form': 'Formulário',
'menu.form.basic-form': 'Formulário Básico',
'menu.form.step-form': 'Formulário Assistido',
'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)',
'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)',
'menu.form.step-form.result': 'Formulário Assistido(finalizado)',
'menu.form.advanced-form': 'Formulário Avançado',
'menu.list': 'Lista',
'menu.list.table-list': 'Tabela de Busca',
'menu.list.basic-list': 'Lista Básica',
'menu.list.card-list': 'Lista de Card',
'menu.list.search-list': 'Lista de Busca',
'menu.list.search-list.articles': 'Lista de Busca(artigos)',
'menu.list.search-list.projects': 'Lista de Busca(projetos)',
'menu.list.search-list.applications': 'Lista de Busca(aplicações)',
'menu.profile': 'Perfil',
'menu.profile.basic': 'Perfil Básico',
'menu.profile.advanced': 'Perfil Avançado',
'menu.result': 'Resultado',
'menu.result.success': 'Sucesso',
'menu.result.fail': 'Falha',
'menu.exception': 'Exceção',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Disparar',
'menu.account': 'Conta',
'menu.account.center': 'Central da Conta',
'menu.account.settings': 'Configurar Conta',
'menu.account.trigger': 'Disparar Erro',
'menu.account.logout': 'Sair',
'menu.editor': 'Graphic Editor',
'menu.editor.flow': 'Flow Editor',
'menu.editor.mind': 'Mind Editor',
'menu.editor.koni': 'Koni Editor',
};
export default {
'app.pwa.offline': 'Você está offline agora',
'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível',
'app.pwa.serviceworker.updated.hint':
'Por favor, pressione o botão "Atualizar" para recarregar a página atual',
'app.pwa.serviceworker.updated.ok': 'Atualizar',
};
export default {
'app.setting.pagestyle': 'Configuração de estilo da página',
'app.setting.pagestyle.dark': 'Dark style',
'app.setting.pagestyle.light': 'Light style',
'app.setting.content-width': 'Largura do conteúdo',
'app.setting.content-width.fixed': 'Fixo',
'app.setting.content-width.fluid': 'Fluido',
'app.setting.themecolor': 'Cor do Tema',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Modo de Navegação',
'app.setting.sidemenu': 'Layout do Menu Lateral',
'app.setting.topmenu': 'Layout do Menu Superior',
'app.setting.fixedheader': 'Cabeçalho fixo',
'app.setting.fixedsidebar': 'Barra lateral fixa',
'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral',
'app.setting.hideheader': 'Esconder o cabeçalho quando rolar',
'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado',
'app.setting.othersettings': 'Outras configurações',
'app.setting.weakmode': 'Weak Mode',
'app.setting.copy': 'Copiar Configuração',
'app.setting.copyinfo':
'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js',
'app.setting.production.hint':
'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o',
};
export default {
'app.settings.menuMap.basic': 'Configurações Básicas',
'app.settings.menuMap.security': 'Configurações de Segurança',
'app.settings.menuMap.binding': 'Vinculação de Conta',
'app.settings.menuMap.notification': 'Mensagens de Notificação',
'app.settings.basic.avatar': 'Avatar',
'app.settings.basic.change-avatar': 'Alterar avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Por favor insira seu email!',
'app.settings.basic.nickname': 'Nome de usuário',
'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!',
'app.settings.basic.profile': 'Perfil pessoal',
'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!',
'app.settings.basic.profile-placeholder': 'Breve introdução sua',
'app.settings.basic.country': 'País/Região',
'app.settings.basic.country-message': 'Por favor insira país!',
'app.settings.basic.geographic': 'Província, estado ou cidade',
'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!',
'app.settings.basic.address': 'Endereço',
'app.settings.basic.address-message': 'Por favor insira seu endereço!',
'app.settings.basic.phone': 'Número de telefone',
'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!',
'app.settings.basic.update': 'Atualizar Informações',
'app.settings.security.strong': 'Forte',
'app.settings.security.medium': 'Média',
'app.settings.security.weak': 'Fraca',
'app.settings.security.password': 'Senha da Conta',
'app.settings.security.password-description': 'Força da senha',
'app.settings.security.phone': 'Telefone de Seguraça',
'app.settings.security.phone-description': 'Telefone vinculado',
'app.settings.security.question': 'Pergunta de Segurança',
'app.settings.security.question-description':
'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta',
'app.settings.security.email': 'Email de Backup',
'app.settings.security.email-description': 'Email vinculado',
'app.settings.security.mfa': 'Dispositivo MFA',
'app.settings.security.mfa-description':
'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes',
'app.settings.security.modify': 'Modificar',
'app.settings.security.set': 'Atribuir',
'app.settings.security.bind': 'Vincular',
'app.settings.binding.taobao': 'Vincular Taobao',
'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao',
'app.settings.binding.alipay': 'Vincular Alipay',
'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay',
'app.settings.binding.dingding': 'Vincular DingTalk',
'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk',
'app.settings.binding.bind': 'Vincular',
'app.settings.notification.password': 'Senha da Conta',
'app.settings.notification.password-description':
'Mensagens de outros usuários serão notificadas na forma de uma estação de letra',
'app.settings.notification.messages': 'Mensagens de Sistema',
'app.settings.notification.messages-description':
'Mensagens de sistema serão notificadas na forma de uma estação de letra',
'app.settings.notification.todo': 'Notificação de To-do',
'app.settings.notification.todo-description':
'A lista de to-do será notificada na forma de uma estação de letra',
'app.settings.open': 'Aberto',
'app.settings.close': 'Fechado',
};
import component from './zh-CN/component';
import globalHeader from './zh-CN/globalHeader';
import menu from './zh-CN/menu';
import pwa from './zh-CN/pwa';
import settingDrawer from './zh-CN/settingDrawer';
import settings from './zh-CN/settings';
export default {
'navBar.lang': '语言',
'layout.user.link.help': '帮助',
'layout.user.link.privacy': '隐私',
'layout.user.link.terms': '条款',
'app.preview.down.block': '下载此页面到本地项目',
'app.welcome.link.fetch-blocks': '获取全部区块',
'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面',
...globalHeader,
...menu,
...settingDrawer,
...settings,
...pwa,
...component,
};
export default {
'component.tagSelect.expand': '展开',
'component.tagSelect.collapse': '收起',
'component.tagSelect.all': '全部',
};
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论