diff --git a/package.json b/package.json
index 1761a83212a4750b79563fa4ef665166f8169262..786691b1ed28802f9407ee6ef3096f8d3b4ed09d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nemean-cli",
- "version": "0.1.5",
+ "version": "0.2.7",
"main": "src/index.js",
"bin": "bin/npmrc.js",
"license": "ISC",
diff --git a/src/index.js b/src/index.js
index 0c24a26f1ed785d4a0c5c4498faf54b7116c2583..97f008b7febfee56df9b158509c7932e4dfeddae 100644
--- a/src/index.js
+++ b/src/index.js
@@ -24,7 +24,7 @@ class Factory {
switch (this.type) {
case "web":
break;
- case "mobile":
+ case "pro":
break;
default:
Factory.log("未知类型", "red");
@@ -34,6 +34,7 @@ class Factory {
filter: src => {
return (
!(src.indexOf("package.json") >= 0) &&
+ !(src.indexOf("package-lock.json") >= 0) &&
!(src.indexOf("eslintrc") >= 0) &&
!(src.indexOf("README.md") >= 0) &&
!(src.indexOf("gitignore") >= 0)
@@ -56,7 +57,7 @@ class Factory {
case "web":
temPackage = fs.readJSONSync(`${this.CUR_POJ_PATH}package.json`);
break;
- case "mobile":
+ case "pro":
temPackage = fs.readJSONSync(`${this.CUR_POJ_PATH}/package.json`);
break;
default:
@@ -111,11 +112,6 @@ class Factory {
}
});
- if (curPackage.dependencies && curPackage.dependencies["nemean-cli"]) {
- Factory.log("删除nemean-cli包");
- delete curPackage.dependencies["nemean-cli"];
- }
-
// scripts
// 暂时不考虑覆盖问题
curPackage.scripts = temPackage.scripts;
@@ -125,6 +121,15 @@ class Factory {
// pre commit
curPackage["pre-commit"] = temPackage["pre-commit"];
+ Factory.log("============================ \n", "yellow");
+ Factory.log(JSON.stringify(curPackage.dependencies), "yellow");
+ Factory.log("============================ \n", "yellow");
+
+ if (curPackage.dependencies && curPackage.dependencies["nemean-cli"]) {
+ Factory.log("删除nemean-cli包");
+ delete curPackage.dependencies["nemean-cli"];
+ }
+
fs.writeJSONSync(`${PRO_PATH}/package.json`, curPackage, {
spaces: 2
});
@@ -132,19 +137,15 @@ class Factory {
// 后续工作
callBack() {
- if (this.SYS_TYPE && this.SYS_TYPE.toLowerCase().indexOf("window") > 0) {
- // windows
- } else {
- Factory.log("开始安装依赖 \n");
- shell.exec("cnpm i --registry=http://101.132.127.199:7001");
- }
+ Factory.log("开始卸载nemean-cli依赖 \n");
+ shell.exec("npm uninstall nemean-cli --save");
}
start() {
try {
this.copyFile(); // copy核心文件
this.trimPackage(); // 处理依赖
- // this.callBack(); // 执行后续
+ this.callBack(); // 执行后续
} catch (e) {
Factory.log("构建失败" + e, "red");
}
@@ -160,8 +161,8 @@ module.exports = function begin() {
message: "请选择项目类型:",
pageSize: 2,
choices: [
- { name: "Ant Design Pro", value: "web" }
- // { name: "Mobile端项目", value: "mobile" },
+ { name: "app", value: "web" },
+ { name: "Ant design pro", value: "pro" }
]
}
];
diff --git a/template/pro/.dockerignore b/template/pro/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..8e90ca6d5026b261dd5e7af54bbe65eddda0a94b
--- /dev/null
+++ b/template/pro/.dockerignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+**/node_modules
+/src/utils/request-temp.js
+
+# production
+/.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/mock
+.temp/**
+
+# umi
+.umi
+.umi-production
+
+# screenshot
+screenshot
+.firebase
\ No newline at end of file
diff --git a/template/pro/.editorconfig b/template/pro/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..7e3649acc2c165b62750e2ca02b80f8ee0da6c4d
--- /dev/null
+++ b/template/pro/.editorconfig
@@ -0,0 +1,16 @@
+# 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
diff --git a/template/pro/.eslintignore b/template/pro/.eslintignore
new file mode 100644
index 0000000000000000000000000000000000000000..16116a2b4b25634df526f83eace6e02c0cda1b20
--- /dev/null
+++ b/template/pro/.eslintignore
@@ -0,0 +1,4 @@
+/lambda/
+/scripts
+/config
+.history
\ No newline at end of file
diff --git a/template/pro/.eslintrc.js b/template/pro/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ca3d282ef1ee3d551c675896645c6fd7ae0b680
--- /dev/null
+++ b/template/pro/.eslintrc.js
@@ -0,0 +1,7 @@
+module.exports = {
+ extends: [require.resolve('@umijs/fabric/dist/eslint')],
+ globals: {
+ ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true,
+ page: true,
+ },
+};
diff --git a/template/pro/.github/ISSUE_TEMPLATE/bug_report.md b/template/pro/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000000000000000000000000000000000000..eb90f979c42c37cc09256dee730f390e9abc0dc6
--- /dev/null
+++ b/template/pro/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,24 @@
+---
+name: '报告Bug 🐛'
+about: 报告 Ant Design Pro 的 bug
+title: '[BUG]'
+labels: bug
+assignees: ''
+---
+
+**bug 描述** [详细地描述 bug,让大家都能理解]
+
+**复现步骤** [清晰描述复现步骤,让别人也能看到问题]
+
+**期望结果** [描述你原本期望看到的结果]
+
+**复现代码** [提供可复现的代码,仓库,或线上示例]
+
+**版本信息:**
+
+- Ant Design Pro 版本: [e.g. 4.0.0]
+- umi 版本
+- 浏览器环境
+- 开发环境 [e.g. mac OS]
+
+**其他信息** [如截图等其他信息可以贴在这里]
diff --git a/template/pro/.github/ISSUE_TEMPLATE/feature_request.md b/template/pro/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000000000000000000000000000000000000..bdc3858238f3a5f8305698f2390a4649f393faf4
--- /dev/null
+++ b/template/pro/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,13 @@
+---
+name: '功能需求 ✨'
+about: 对 Ant Design Pro 的需求或建议
+title: '[需求]'
+labels: feature
+assignees: ''
+---
+
+**需求描述** [详细地描述需求,让大家都能理解]
+
+**解决方案** [如果你有解决方案,在这里清晰地阐述]
+
+**其他信息** [如截图等其他信息可以贴在这里]
diff --git a/template/pro/.github/ISSUE_TEMPLATE/question.md b/template/pro/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 0000000000000000000000000000000000000000..3ad4067295f7e2034d039a4a3f2eb92c6a78ac65
--- /dev/null
+++ b/template/pro/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,13 @@
+---
+name: '疑问或需要帮助 ❓'
+about: 对 Ant Design Pro 使用的疑问或需要帮助
+title: '[问题]'
+labels: question
+assignees: ''
+---
+
+**问题描述** [详细地描述问题,让大家都能理解]
+
+**示例代码** [如果有必要,展示代码,线上示例,或仓库]
+
+**其他信息** [如截图等其他信息可以贴在这里]
diff --git a/template/pro/.gitignore.template b/template/pro/.gitignore.template
new file mode 100644
index 0000000000000000000000000000000000000000..7fd9f581cd9eb0afc2650747267d81bd20d1902d
--- /dev/null
+++ b/template/pro/.gitignore.template
@@ -0,0 +1,40 @@
+# 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
diff --git a/template/pro/.gitpod.yml b/template/pro/.gitpod.yml
new file mode 100644
index 0000000000000000000000000000000000000000..32d8e645edf6a93521b3553fb97ce0539a6ed692
--- /dev/null
+++ b/template/pro/.gitpod.yml
@@ -0,0 +1,6 @@
+ports:
+ - port: 8000
+ onOpen: open-preview
+tasks:
+ - init: npm install
+ command: npm start
diff --git a/template/pro/.prettierignore b/template/pro/.prettierignore
new file mode 100644
index 0000000000000000000000000000000000000000..4fa82fc90008418f78708af1b08ba1005b1c9df2
--- /dev/null
+++ b/template/pro/.prettierignore
@@ -0,0 +1,20 @@
+**/*.svg
+package.json
+.umi
+.umi-production
+/dist
+.dockerignore
+.DS_Store
+.eslintignore
+*.png
+*.toml
+docker
+.editorconfig
+Dockerfile*
+.gitignore
+.prettierignore
+LICENSE
+.eslintcache
+*.lock
+yarn-error.log
+.history
\ No newline at end of file
diff --git a/template/pro/.prettierrc.js b/template/pro/.prettierrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..7b597d78917c7e33a81bb3e83c6067f6a9a970e6
--- /dev/null
+++ b/template/pro/.prettierrc.js
@@ -0,0 +1,5 @@
+const fabric = require('@umijs/fabric');
+
+module.exports = {
+ ...fabric.prettier,
+};
diff --git a/template/pro/.stylelintrc.js b/template/pro/.stylelintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..c2030787de72c5cffc44099fbdbae55c32afd482
--- /dev/null
+++ b/template/pro/.stylelintrc.js
@@ -0,0 +1,5 @@
+const fabric = require('@umijs/fabric');
+
+module.exports = {
+ ...fabric.stylelint,
+};
diff --git a/template/pro/CNAME b/template/pro/CNAME
new file mode 100644
index 0000000000000000000000000000000000000000..30c2d4d3624a8970342c7c6514217648aaed82d4
--- /dev/null
+++ b/template/pro/CNAME
@@ -0,0 +1 @@
+preview.pro.ant.design
\ No newline at end of file
diff --git a/template/pro/CODE_OF_CONDUCT.md b/template/pro/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000000000000000000000000000000000..2b4571cbff565760f8b127e347ad658461e08297
--- /dev/null
+++ b/template/pro/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+- Using welcoming and inclusive language
+- Being respectful of differing viewpoints and experiences
+- Gracefully accepting constructive criticism
+- Focusing on what is best for the community
+- Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+- The use of sexualized language or imagery and unwelcome sexual attention or advances
+- Trolling, insulting/derogatory comments, and personal or political attacks
+- Public or private harassment
+- Publishing others' private information, such as a physical or electronic address, without explicit permission
+- Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at afc163@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/template/pro/Dockerfile b/template/pro/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..35380f482e83ff636346b0881cc9499c09af43ed
--- /dev/null
+++ b/template/pro/Dockerfile
@@ -0,0 +1,14 @@
+FROM circleci/node:latest-browsers
+
+WORKDIR /usr/src/app/
+USER root
+COPY package.json ./
+RUN yarn
+
+COPY ./ ./
+
+RUN npm run test:all
+
+RUN npm run fetch:blocks
+
+CMD ["npm", "run", "build"]
diff --git a/template/pro/Dockerfile.dev b/template/pro/Dockerfile.dev
new file mode 100644
index 0000000000000000000000000000000000000000..b2c3d258c0a2ba220648786880a24966c1d2b6b8
--- /dev/null
+++ b/template/pro/Dockerfile.dev
@@ -0,0 +1,12 @@
+FROM node:latest
+
+WORKDIR /usr/src/app/
+
+COPY package.json ./
+RUN npm install --silent --no-cache --registry=https://registry.npm.taobao.org
+
+COPY ./ ./
+
+RUN npm run fetch:blocks
+
+CMD ["npm", "run", "start"]
diff --git a/template/pro/Dockerfile.hub b/template/pro/Dockerfile.hub
new file mode 100644
index 0000000000000000000000000000000000000000..58bbcbc7873c9cc16f63004e68716c119f17eb89
--- /dev/null
+++ b/template/pro/Dockerfile.hub
@@ -0,0 +1,27 @@
+FROM circleci/node:latest-browsers as builder
+
+WORKDIR /usr/src/app/
+USER root
+COPY package.json ./
+RUN yarn
+
+COPY ./ ./
+
+RUN npm run test:all
+
+RUN npm run fetch:blocks
+
+RUN npm run build
+
+
+FROM nginx
+
+WORKDIR /usr/share/nginx/html/
+
+COPY ./docker/nginx.conf /etc/nginx/conf.d/default.conf
+
+COPY --from=builder /usr/src/app/dist /usr/share/nginx/html/
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]
\ No newline at end of file
diff --git a/template/pro/LICENSE b/template/pro/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..c17ba2fb848af3fca998074297d00f38536d5d13
--- /dev/null
+++ b/template/pro/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Alipay.inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/template/pro/README.fr-FR.md b/template/pro/README.fr-FR.md
new file mode 100644
index 0000000000000000000000000000000000000000..5e2e7444d5be8b56fa1a5c9f71fd827ea5ce911a
--- /dev/null
+++ b/template/pro/README.fr-FR.md
@@ -0,0 +1,124 @@
+[English](./README.md) | [简体中文](./README.zh-CN.md) | [Русский](./README.ru-RU.md) | [Türkçe](./README.tr-TR.md) | [日本語](./README.ja-JP.md) | Français
+
+
Ant Design Pro
+
+
+
+Une solution UI prête à l'emploi pour des applications d'entreprise en tant que modèle React.
+
+[](http://umijs.org/) [](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) [](https://david-dm.org/ant-design/ant-design-pro) [](https://david-dm.org/ant-design/ant-design-pro?type=dev) [](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://app.netlify.com/sites/ant-design-pro/deploys) 
+
+
+
+
+
+- Aperçu: http://preview.pro.ant.design
+- Page d'accueil: http://pro.ant.design
+- Documentation: http://pro.ant.design/docs/getting-started
+- ChangeLog: http://pro.ant.design/docs/changelog
+- FAQ: http://pro.ant.design/docs/faq
+- Site mirroir en Chine: http://ant-design-pro.gitee.io
+
+## 2.0 Sorti maintenant! 🎉🎉🎉
+
+[Annoncement de Ant Design Pro 2.0.0](https://medium.com/ant-design/beautiful-and-powerful-ant-design-pro-2-0-release-51358da5af95)
+
+## Recrutement pour la traduction :loudspeaker:
+
+Nous avons besoin de votre aide: https://github.com/ant-design/ant-design-pro/issues/120
+
+## Fonctionnalités
+
+- :gem: **Design soigné**: Suit [la spécification Ant Design](http://ant.design/)
+- :triangular_ruler: **Modèles communs**: Modèles typiques d'application d'entreprise
+- :rocket: **Développement dernier cri**: Infrastructure de développement de React/umi/dva/antd la plus récente
+- :iphone: **Design adapté**: Conçu pour des tailles d'écran variables
+- :art: **Thématisation**: Thème personnalisable avec configuration simple
+- :globe_with_meridians: **International**: Solution i18n intégrée
+- :gear: **Meilleures pratiques**: Flux de travail solide pour rendre votre code sain
+- :1234: **Développement simulé**: Solution de développement simulée facile à utiliser
+- :white_check_mark: **Tests UI**: Volez en toute sécurité avec les tests unitaires et e2e
+
+## Modèles
+
+```
+- Tableau de bord
+ - Analytique
+ - Moniteur
+ - Espace de travail
+- Formulaire
+ - Formulaire de base
+ - Formulaire par étape
+ - Formulaire avancé
+- Liste
+ - Tableau standard
+ - Liste standard
+ - Liste de cartes
+ - Liste de recherche (Projet/Applications/Article)
+- Profil
+ - Profil simple
+ - Profil avancé
+- Compte
+ - Centre du compte
+ - Paramètres du compte
+- Résultat
+ - Succès
+ - Échec
+- Exception
+ - 403
+ - 404
+ - 500
+- Utilisateur
+ - Connexion
+ - S'inscrire
+ - Résultat de l'inscription
+```
+
+## Utilisation
+
+### Utiliser bash
+
+```bash
+$ yarn create umi # or npm create umi
+
+# Choose ant-design-pro:
+ Select the boilerplate type (Use arrow keys)
+❯ ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
+ app - Create project with a simple boilerplate, support typescript.
+ block - Create a umi block.
+ library - Create a library with umi.
+ plugin - Create a umi plugin.
+
+$ npm install
+$ npm start # visit http://localhost:8000
+```
+
+### Utilisation de Gitpod
+
+Ouvrez le projet avec Gitpod (environnement de développement gratuit pour GitHub) et commencez à coder immédiatement.
+
+[](https://gitpod.io/#https://github.com/ant-design/ant-design-pro)
+
+Plus d'instructions dans la [documentation](http://pro.ant.design/docs/getting-started).
+
+## Support des navigateurs
+
+Navigateurs modernes et IE11.
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)Opera |
+| --- | --- | --- | --- | --- |
+| IE11, Edge | deux dernières versions | deux dernières versions | deux dernières versions | deux dernières versions |
+
+## Contribution
+
+Toute forme de contribution est la bienvenue, voici quelques exemples de façons dont vous pouvez contribuer à ce projet:
+
+- Utiliser Ant Design Pro dans votre travail quotidien.
+- Soumettre des [issues](http://github.com/ant-design/ant-design-pro/issues) pour reporter les bugs ou poser des questions.
+- Proposer des [pull requests](http://github.com/ant-design/ant-design-pro/pulls) pour améliorer notre code.
+
+
diff --git a/template/pro/README.ja-JP.md b/template/pro/README.ja-JP.md
new file mode 100644
index 0000000000000000000000000000000000000000..995692de174d547221f51dad3b4c41d9d9da494c
--- /dev/null
+++ b/template/pro/README.ja-JP.md
@@ -0,0 +1,126 @@
+[English](./README.md) | [简体中文](./README.zh-CN.md) | [Русский](./README.ru-RU.md) | [Türkçe](./README.tr-TR.md) | 日本語 | [Français](./README.fr-FR.md)
+
+Ant Design Pro
+
+
+
+独創的な業務システムの UI を解決するための React ボイラープレート。
+
+[](http://umijs.org/) [](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) [](https://david-dm.org/ant-design/ant-design-pro) [](https://david-dm.org/ant-design/ant-design-pro?type=dev) [](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://app.netlify.com/sites/ant-design-pro/deploys) 
+
+
+
+
+
+- プレビュー: http://preview.pro.ant.design
+- ホームページ: http://pro.ant.design
+- ドキュメント: http://pro.ant.design/docs/getting-started
+- 変更ログ: http://pro.ant.design/docs/changelog
+- FAQ: http://pro.ant.design/docs/faq
+- 中国のミラーサイト: http://ant-design-pro.gitee.io
+
+## 2.0 がリリースされました 🎉🎉🎉
+
+[Announcing Ant Design Pro 2.0.0](https://medium.com/ant-design/beautiful-and-powerful-ant-design-pro-2-0-release-51358da5af95)
+
+## 翻訳の募集 :loudspeaker:
+
+私たちはあなたの助けを必要としています。: https://github.com/ant-design/ant-design-pro/issues/120
+
+## 特徴
+
+- :gem: **きちんとしたデザイン**: [Ant Design specification](http://ant.design/) に従ってくださ い。
+- :triangular_ruler: **共通のテンプレート**: 業務システム用のテンプレート
+- :rocket: **現状のアート開発**: `React/umi/dva/antd` の最新開発スタック
+- :iphone: **レスポンシブ**: さまざまな画面サイズ用の設計
+- :art: **テーマ**: シンプルな設定でカスタマイズ可能なテーマ
+- :globe_with_meridians: **国際化**: 国際化の解決策を内蔵
+- :gear: **ベストプラクティス**: コードを美しくするための正しいワークフロー
+- :1234: **モック開発**: 使いやすいモック開発
+- :white_check_mark: **UI テスト**: ユニットテストと e2e テスト
+
+## テンプレート
+
+```
+- ダッシュボード
+ - アナリティクス
+ - モニター
+ - ワークスペース
+- 形
+ - 基本フォーム
+ - ステップフォーム
+ - 高度なフォーム
+ - リスト
+ - スタンダードテーブル
+ - スタンダードリスト
+ - カードリスト
+ - 検索リスト(プロジェクト/アプリケーション/記事)
+ - プロフィール
+ - 簡単なプロフィール
+ - 高度なプロファイル
+ - アカウント
+ - アカウントセンター
+ - アカウント設定
+ - 結果
+ - 成功
+ - 失敗
+ - 例外
+ - 403
+ - 404
+ - 500
+ - ユーザー
+ - ログイン
+ - 登録
+ - 登録結果
+```
+
+## 使用法
+
+### bash を使う方法
+
+```bash
+$ yarn create umi # or npm create umi
+
+# Choose ant-design-pro:
+ Select the boilerplate type (Use arrow keys)
+❯ ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
+ app - Create project with a simple boilerplate, support typescript.
+ block - Create a umi block.
+ library - Create a library with umi.
+ plugin - Create a umi plugin.
+
+$ npm install
+$ npm start # http://localhost:8000 を開く
+```
+
+### Gitpod を使う方法
+
+Gitpod(GitHub 用の無料オンライン開発環境)でプロジェクトを開き、すぐにコーディングを開始できます。
+
+[](https://gitpod.io/#https://github.com/ant-design/ant-design-pro)
+
+その他の指示は [ドキュメント](http://pro.ant.design/docs/getting-started) を確認してください。
+
+## サポートするブラウザー
+
+モダンなブラウザと IE11。
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)Opera |
+| --- | --- | --- | --- | --- |
+| IE11, Edge | 最新版から 2 バージョン | 最新版から 2 バージョン | 最新版から 2 バージョン | 最新版から 2 バージョン |
+
+## 貢献する
+
+どんな種類の貢献でも大歓迎です。あなたがこのプロジェクトに貢献できる方法のいくつかの例はここにあります。:
+
+- 毎日の仕事に Ant Design Pro を使用すること。
+- 報告すること。 [issues](http://github.com/ant-design/ant-design-pro/issues) にバグ報告や質問をしてください。
+- 更新すること。 改善を、[pull requests](http://github.com/ant-design/ant-design-pro/pulls) で送ってください。
+
+[](https://david-dm.org/ant-design/ant-design-pro?type=dev)
+
+
diff --git a/template/pro/README.md b/template/pro/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..395030b5e5244da5b55e5396dd79c5f591efede2
--- /dev/null
+++ b/template/pro/README.md
@@ -0,0 +1,126 @@
+English | [简体中文](./README.zh-CN.md) | [Русский](./README.ru-RU.md) | [Türkçe](./README.tr-TR.md) | [日本語](./README.ja-JP.md) | [Français](./README.fr-FR.md)
+
+Ant Design Pro
+
+
+
+An out-of-box UI solution for enterprise applications as a React boilerplate.
+
+[](http://umijs.org/) [](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) [](https://david-dm.org/ant-design/ant-design-pro) [](https://david-dm.org/ant-design/ant-design-pro?type=dev) [](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://app.netlify.com/sites/ant-design-pro/deploys) 
+
+
+
+
+
+- Preview: http://preview.pro.ant.design
+- Home Page: http://pro.ant.design
+- Documentation: http://pro.ant.design/docs/getting-started
+- ChangeLog: http://pro.ant.design/docs/changelog
+- FAQ: http://pro.ant.design/docs/faq
+- Mirror Site in China: http://ant-design-pro.gitee.io
+
+## 4.0 Released Now! 🎉🎉🎉
+
+[Announcing Ant Design Pro 4.0.0](https://medium.com/ant-design/ant-design-pro-v4-is-here-6f23098ae9d9)
+
+## Translation Recruitment :loudspeaker:
+
+We need your help: https://github.com/ant-design/ant-design-pro/issues/120
+
+## Features
+
+- :bulb: **TypeScript**: A language for application-scale JavaScript
+- :scroll: **Blocks**: Build page with block template
+- :gem: **Neat Design**: Follow [Ant Design specification](http://ant.design/)
+- :triangular_ruler: **Common Templates**: Typical templates for enterprise applications
+- :rocket: **State of The Art Development**: Newest development stack of React/umi/dva/antd
+- :iphone: **Responsive**: Designed for variable screen sizes
+- :art: **Theming**: Customizable theme with simple config
+- :globe_with_meridians: **International**: Built-in i18n solution
+- :gear: **Best Practices**: Solid workflow to make your code healthy
+- :1234: **Mock development**: Easy to use mock development solution
+- :white_check_mark: **UI Test**: Fly safely with unit and e2e tests
+
+## Templates
+
+```
+- Dashboard
+ - Analytic
+ - Monitor
+ - Workspace
+- Form
+ - Basic Form
+ - Step Form
+ - Advanced From
+- List
+ - Standard Table
+ - Standard List
+ - Card List
+ - Search List (Project/Applications/Article)
+- Profile
+ - Simple Profile
+ - Advanced Profile
+- Account
+ - Account Center
+ - Account Settings
+- Result
+ - Success
+ - Failed
+- Exception
+ - 403
+ - 404
+ - 500
+- User
+ - Login
+ - Register
+ - Register Result
+```
+
+## Usage
+
+### Use bash
+
+```bash
+$ yarn create umi # or npm create umi
+
+# Choose ant-design-pro:
+ Select the boilerplate type (Use arrow keys)
+❯ ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
+ app - Create project with a simple boilerplate, support typescript.
+ block - Create a umi block.
+ library - Create a library with umi.
+ plugin - Create a umi plugin.
+
+$ npm install
+$ npm start # visit http://localhost:8000
+```
+
+### Use Gitpod
+
+Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
+
+[](https://gitpod.io/#https://github.com/ant-design/ant-design-pro)
+
+More instructions at [documentation](http://pro.ant.design/docs/getting-started).
+
+## Browsers support
+
+Modern browsers and IE11.
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)Opera |
+| --- | --- | --- | --- | --- |
+| IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+
+## Contributing
+
+Any type of contribution is welcome, here are some examples of how you may contribute to this project:
+
+- Use Ant Design Pro in your daily work.
+- Submit [issues](http://github.com/ant-design/ant-design-pro/issues) to report bugs or ask questions.
+- Propose [pull requests](http://github.com/ant-design/ant-design-pro/pulls) to improve our code.
+
+
diff --git a/template/pro/README.pt-BR.md b/template/pro/README.pt-BR.md
new file mode 100644
index 0000000000000000000000000000000000000000..5e3c691c63e68325b4fa47cff5328c772fb01287
--- /dev/null
+++ b/template/pro/README.pt-BR.md
@@ -0,0 +1,126 @@
+English | [简体中文](./README.zh-CN.md) | [Русский](./README.ru-RU.md) | [Türkçe](./README.tr-TR.md) | [日本語](./README.ja-JP.md) | [Français](./README.fr-FR.md) | [Português](./README.fr-FR.md)
+
+Ant Design Pro
+
+
+
+Uma solução de UI pronta para aplicações corporativos na forma de um boilerplate React.
+
+[](http://umijs.org/) [](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) [](https://david-dm.org/ant-design/ant-design-pro) [](https://david-dm.org/ant-design/ant-design-pro?type=dev) [](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://app.netlify.com/sites/ant-design-pro/deploys) 
+
+
+
+
+
+- Prévia: http://preview.pro.ant.design
+- Página Inicial: http://pro.ant.design
+- Documentação: http://pro.ant.design/docs/getting-started
+- Mudanças: http://pro.ant.design/docs/changelog
+- FAQ: http://pro.ant.design/docs/faq
+- Site Alternativo na China: http://ant-design-pro.gitee.io
+
+## 4.0 Lançado! 🎉🎉🎉
+
+[Anúncio do Ant Design Pro 4.0.0](https://medium.com/ant-design/ant-design-pro-v4-is-here-6f23098ae9d9)
+
+## Recrutamento para tradução :loudspeaker:
+
+Precisamos da sua ajuda: https://github.com/ant-design/ant-design-pro/issues/120
+
+## Recursos
+
+- :bulb: **TypeScript**: Uma linguaguem para escalar aplicações JavaScript
+- :scroll: **Blocks**: Crie páginas com block template
+- :gem: **Design Elegante**: Segue as [especificações do Ant Design](http://ant.design/)
+- :triangular_ruler: **Modelos Comuns**: Modelos comuns para apliações empresariais
+- :rocket: **Estado da Arte do Desenvolvimento**: Stack de desenvolvimento mais recente do React/umi/dva/antd
+- :iphone: **Responsivo**: Projetado para tamanhos de telas variados
+- :art: **Personalização**: Customizável através de uma simples configuração
+- :globe_with_meridians: **Internacionalização**: Incluso i18n por padrão
+- :gear: **Melhores Práticas**: Fluxo de trabalho sólido para manter seu código saudável
+- :1234: **Desenvolvimento de Mock**: Fácil solução para desenvolvimento de mocks
+- :white_check_mark: **Testes de UI**: Voe tranquilamente com testes unitários e testes e2e
+
+## Modelos
+
+```
+- Painel de Controle
+ - Gráficos
+ - Monitoramento
+ - Areás de Trabalho
+- Formulários
+ - Formulários Básicos
+ - Formulário com Etapas
+ - Formulários Avançados
+- Listas
+ - Tabela Padrão
+ - Lista Padrão
+ - Lista com Cards
+ - Lista com Busca (Projeto/Aplicações/Artigos)
+- Perfís
+ - Perfil Simples
+ - Perfil Avançado
+- Conta
+ - Detalhes da Conta
+ - Configurações da Conta
+- Resultados
+ - Secesso
+ - Falha
+- Exceções
+ - 403
+ - 404
+ - 500
+- Usuário
+ - Login
+ - Cadastro
+ - Resultado do Cadastro
+```
+
+## Uso
+
+### Use o bash
+
+```bash
+$ yarn create umi # ou npm create umi
+
+# Escolha ant-design-pro:
+ Selecione o tipo do boilerplate (Use as teclas de seta)
+❯ ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
+ app - Create project with a simple boilerplate, support typescript.
+ block - Create a umi block.
+ library - Create a library with umi.
+ plugin - Create a umi plugin.
+
+$ npm install
+$ npm start # visit http://localhost:8000
+```
+
+### Use Gitpod
+
+Abra o projeto no Gitpod (ambiente gratuito de desenvolvimento online para o GitHub) e comece a codificar imediatamente.
+
+[](https://gitpod.io/#https://github.com/ant-design/ant-design-pro)
+
+Mais instruções na [documentação](http://pro.ant.design/docs/getting-started).
+
+## Suporte a navegadores
+
+Navegadores modernos e IE11.
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)Opera |
+| --- | --- | --- | --- | --- |
+| IE11, Edge | últimas 2 versões | últimas 2 versões | últimas 2 versões | últimas 2 versões |
+
+## Contribuindo
+
+Qualquer tipo de contribuição é bem-vinda, aqui estão alguns exemplos de como você pode contribuir com esse projeto:
+
+- Use Ant Design Pro no seu trabalho diário.
+- Submeta [issues](http://github.com/ant-design/ant-design-pro/issues) para reportar bugs ou tirar dúvidas.
+- Proponha [pull requests](http://github.com/ant-design/ant-design-pro/pulls) para melhorar nosso código.
+
+
diff --git a/template/pro/README.ru-RU.md b/template/pro/README.ru-RU.md
new file mode 100644
index 0000000000000000000000000000000000000000..359fc29fa930dc6adc146c1b831e749df82199d9
--- /dev/null
+++ b/template/pro/README.ru-RU.md
@@ -0,0 +1,112 @@
+[English](./README.md) | [简体中文](./README.zh-CN.md) | Русский | [Türkçe](./README.tr-TR.md) | [日本語](./README.ja-JP.md) | [Français](./README.fr-FR.md)
+
+Ant Design Pro
+
+
+
+UI-решение "из коробки" для корпоративных приложений как React boilerplate
+
+[](http://umijs.org/) [](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) [](https://david-dm.org/ant-design/ant-design-pro) [](https://david-dm.org/ant-design/ant-design-pro?type=dev) [](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://app.netlify.com/sites/ant-design-pro/deploys) 
+
+
+
+
+
+- Демо: http://preview.pro.ant.design
+- Домашняя страница: http://pro.ant.design
+- Документация: http://pro.ant.design/docs/getting-started
+- История изменений: http://pro.ant.design/docs/changelog
+- FAQ: http://pro.ant.design/docs/faq
+- Китайское зеркало сайта: http://ant-design-pro.gitee.io
+
+## Поиск переводчиков :loudspeaker:
+
+Нам нужна ваша помощь: https://github.com/ant-design/ant-design-pro/issues/120
+
+## Возможности
+
+- :gem: **Аккуратный дизайн**: Посмотрите [спецификацию Ant Design](http://ant.design/)
+- :triangular_ruler: **Общие шаблоны**: Стандартные шаблоны для корпоративных приложений
+- :rocket: **Разработка, как искусство**: Новейший стек технологий React/umi/dva/antd
+- :iphone: **Отзывчивая верстка**: Создан для экранов разных размеров
+- :art: **Темизация**: Возможность изменения темы с помощью конфигурации
+- :globe_with_meridians: **Мультиязычность**: Встроенное i18n решение
+- :gear: **Лучшие практики**: Надежные процессы для хорошего кода
+- :1234: **Разработка по шаблону**: Простое в использовании решение для разработки
+- :white_check_mark: **UI тесты**: Разрабатывайте безопасно с юнит и e2e тестами
+
+## Шаблоны
+
+```
+- Dashboard
+ - Analytic
+ - Monitor
+ - Workspace
+- Form
+ - Basic Form
+ - Step Form
+ - Advanced From
+- List
+ - Standard Table
+ - Standard List
+ - Card List
+ - Search List (Project/Applications/Article)
+- Profile
+ - Simple Profile
+ - Advanced Profile
+- Account
+ - Account Center
+ - Account Settings
+- Result
+ - Success
+ - Failed
+- Exception
+ - 403
+ - 404
+ - 500
+- User
+ - Login
+ - Register
+ - Register Result
+```
+
+## Использование
+
+```bash
+$ yarn create umi # or npm create umi
+
+# Choose ant-design-pro:
+ Select the boilerplate type (Use arrow keys)
+❯ ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
+ app - Create project with a simple boilerplate, support typescript.
+ block - Create a umi block.
+ library - Create a library with umi.
+ plugin - Create a umi plugin.
+
+$ npm install
+$ npm start # visit http://localhost:8000
+```
+
+Больше информации в [документации](http://pro.ant.design/docs/getting-started).
+
+## Совместимость
+
+Современные браузеры и IE11.
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)Opera |
+| --- | --- | --- | --- | --- |
+| IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+
+## Распространение
+
+Любые варианты распространения приветствуются! Вот несколько примеров того, как вы можете помочь распространению проекта:
+
+- Использовать Ant Design Pro в ежедневной работе.
+- Создавать [задачи](http://github.com/ant-design/ant-design-pro/issues) заводить баги или отвечать на вопросы.
+- Делать [pull-реквесты](http://github.com/ant-design/ant-design-pro/pulls) для совершенствования нашего кода.
+
+
diff --git a/template/pro/README.tr-TR.md b/template/pro/README.tr-TR.md
new file mode 100644
index 0000000000000000000000000000000000000000..f071b9ebd65d71706566afce015b07588b18f9a8
--- /dev/null
+++ b/template/pro/README.tr-TR.md
@@ -0,0 +1,116 @@
+[English](./README.md) | [简体中文](./README.zh-CN.md) | [Русский](./README.ru-RU.md) | Türkçe | [日本語](./README.ja-JP.md) | [Français](./README.fr-FR.md)
+
+Ant Design Pro
+
+
+
+React ile kurumsal uygulamalar için taslak olarak geliştirilmiş kullanıma hazır bir UI çözümü.
+
+[](https://circleci.com/gh/ant-design/ant-design-pro/) [](https://ci.appveyor.com/project/afc163/ant-design-pro/branch/master) [](https://david-dm.org/ant-design/ant-design-pro) [](https://david-dm.org/ant-design/ant-design-pro?type=dev) [](https://gitter.im/ant-design/pro-english?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)(🇺🇸) [](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)(🇨🇳) [](https://app.netlify.com/sites/ant-design-pro/deploys) 
+
+
+
+
+
+- Önizleme: http://preview.pro.ant.design
+- Anasayfa: http://pro.ant.design
+- Dokümantasyon: http://pro.ant.design/docs/getting-started
+- ChangeLog: http://pro.ant.design/docs/changelog
+- SSS: http://pro.ant.design/docs/faq
+- Çinde barındırılan site: http://ant-design-pro.gitee.io
+
+## 2.0 Versiyonu Şimdi Yayında! 🎉🎉🎉
+
+[Announcing Ant Design Pro 2.0.0](https://medium.com/ant-design/beautiful-and-powerful-ant-design-pro-2-0-release-51358da5af95)
+
+## Çeviri Desteği :loudspeaker:
+
+Çeviriler için yardımınıza ihtiyacımız var: https://github.com/ant-design/ant-design-pro/issues/120
+
+## Özellikler
+
+- :gem: **Zarif Tasarım**: Buradan [Ant Design özellikleri](http://ant.design/)
+- :triangular_ruler: **Ortak Şablonlar**: Kurumsal uygulamalar için şablonlar
+- :rocket: **Sanatsal gelişim durumu**: Newest development stack of React/umi/dva/antd
+- :iphone: **Responsive**: Değişken ekran boyutları için tasarlanmıştır
+- :art: **Tema Kullanımı**: Basit ayarlar ile özelleştirilebilir tema
+- :globe_with_meridians: **Uluslararası**: Built-in i18n solution
+- :gear: **Best Practices**: İyi kod için sağlam iş akışı
+- :1234: **Mock Geliştirme**: Model(Mock) geliştirmeler için kolay çözüm
+- :white_check_mark: **UI Testi**: Unit ve e2e testleri ile güvenli sürdürülebilirlik
+
+## Şablonlar
+
+```
+- Dashboard
+ - Analitik
+ - Monitör
+ - Çalışma alanı
+- Form
+ - Basit Form
+ - Step Form
+ - Gelişmiş Form
+- List
+ - Standard Tablo
+ - Standard Liste
+ - Kart Liste
+ - Arama Listesi (Project/Applications/Article)
+- Profil
+ - Basit Profil
+ - Gelişmiş Profil
+- Hesap
+ - Hesap Yönetimi
+ - Hesap Ayarları
+- Sonuç
+ - Başarılı
+ - Hatalı
+- Hatalar
+ - 403
+ - 404
+ - 500
+- Kullanıcı
+ - Giriş
+ - Kayıt
+ - Kayıt Sonucu
+```
+
+## Kullanım
+
+```bash
+$ yarn create umi # or npm create umi
+
+# Choose ant-design-pro:
+ Select the boilerplate type (Use arrow keys)
+❯ ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
+ app - Create project with a simple boilerplate, support typescript.
+ block - Create a umi block.
+ library - Create a library with umi.
+ plugin - Create a umi plugin.
+
+$ npm install
+$ npm start # visit http://localhost:8000
+```
+
+Daha fazla talimat için [dokümantasyon](http://pro.ant.design/docs/getting-started) sayfasına göz atın.
+
+## Tarayıcı desteği
+
+Modern internet tarayıcıları ve IE11.
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)Opera |
+| --- | --- | --- | --- | --- |
+| IE11, Edge | son 2 versiyon | son 2 versiyon | son 2 versiyon | son 2 versiyon |
+
+## Destek
+
+Her türlü desteğinize açığız, bu projeye nasıl katkıda bulunabileceğinize dair bazı örnekler:
+
+- Günlük işinizde Ant Design Pro kullanın.
+- Hataları bildirmek veya soru sormak için [issues](http://github.com/ant-design/ant-design-pro/issues) gönderin.
+- kodumuzu geliştirmek için [pull requests](http://github.com/ant-design/ant-design-pro/pulls) gönderin.
+
+
diff --git a/template/pro/README.zh-CN.md b/template/pro/README.zh-CN.md
new file mode 100644
index 0000000000000000000000000000000000000000..4fcd27c1a32a7e4f5d03065b2a1c02047ede33b5
--- /dev/null
+++ b/template/pro/README.zh-CN.md
@@ -0,0 +1,114 @@
+[English](./README.md) | 简体中文 | [Русский](./README.ru-RU.md) | [Türkçe](./README.tr-TR.md) | [日本語](./README.ja-JP.md) | [Français](./README.fr-FR.md)
+
+Ant Design Pro
+
+
+
+开箱即用的中台前端/设计解决方案。
+
+[](http://umijs.org/) [](https://dev.azure.com/ant-design/ant-design-pro/_build/latest?definitionId=1?branchName=master) [](https://david-dm.org/ant-design/ant-design-pro) [](https://david-dm.org/ant-design/ant-design-pro?type=dev) [](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://app.netlify.com/sites/ant-design-pro/deploys) 
+
+
+
+
+
+- 预览:http://preview.pro.ant.design
+- 首页:http://pro.ant.design/index-cn
+- 使用文档:http://pro.ant.design/docs/getting-started-cn
+- 更新日志: http://pro.ant.design/docs/changelog-cn
+- 常见问题:http://pro.ant.design/docs/faq-cn
+- 国内镜像:http://ant-design-pro.gitee.io
+
+## 现在我们发布了 4.0! 🎉🎉🎉
+
+[Announcing Ant Design Pro 4.0.0](https://zhuanlan.zhihu.com/p/67498559)
+
+## 特性
+
+- :bulb: **TypeScript**: 应用程序级 JavaScript 的语言
+- :scroll: **区块**: 通过区块模板快速构建页面
+- :gem: **优雅美观**:基于 Ant Design 体系精心设计
+- :triangular_ruler: **常见设计模式**:提炼自中后台应用的典型页面和场景
+- :rocket: **最新技术栈**:使用 React/umi/dva/antd 等前端前沿技术开发
+- :iphone: **响应式**:针对不同屏幕大小设计
+- :art: **主题**:可配置的主题满足多样化的品牌诉求
+- :globe_with_meridians: **国际化**:内建业界通用的国际化方案
+- :gear: **最佳实践**:良好的工程实践助您持续产出高质量代码
+- :1234: **Mock 数据**:实用的本地数据调试方案
+- :white_check_mark: **UI 测试**:自动化测试保障前端产品质量
+
+## 模板
+
+```
+- Dashboard
+ - 分析页
+ - 监控页
+ - 工作台
+- 表单页
+ - 基础表单页
+ - 分步表单页
+ - 高级表单页
+- 列表页
+ - 查询表格
+ - 标准列表
+ - 卡片列表
+ - 搜索列表(项目/应用/文章)
+- 详情页
+ - 基础详情页
+ - 高级详情页
+- 用户
+ - 用户中心页
+ - 用户设置页
+- 结果
+ - 成功页
+ - 失败页
+- 异常
+ - 403 无权限
+ - 404 找不到
+ - 500 服务器出错
+- 帐户
+ - 登录
+ - 注册
+ - 注册成功
+```
+
+## 使用
+
+```bash
+$ yarn create umi # or npm create umi
+
+# Choose ant-design-pro:
+ Select the boilerplate type (Use arrow keys)
+❯ ant-design-pro - Create project with an layout-only ant-design-pro boilerplate, use together with umi block.
+ app - Create project with a simple boilerplate, support typescript.
+ block - Create a umi block.
+ library - Create a library with umi.
+ plugin - Create a umi plugin.
+
+$ npm install
+$ npm start # visit http://localhost:8000
+```
+
+更多信息请参考 [使用文档](http://pro.ant.design/docs/getting-started)。
+
+## 支持环境
+
+现代浏览器及 IE11。
+
+| [ ](http://godban.github.io/browsers-support-badges/)IE / Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari | [ ](http://godban.github.io/browsers-support-badges/)Opera |
+| --- | --- | --- | --- | --- |
+| IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
+
+## 参与贡献
+
+我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :smiley::
+
+- 在你的公司或个人项目中使用 Ant Design Pro。
+- 通过 [Issue](http://github.com/ant-design/ant-design-pro/issues) 报告 bug 或进行咨询。
+- 提交 [Pull Request](http://github.com/ant-design/ant-design-pro/pulls) 改进 Pro 的代码。
+
+
diff --git a/template/pro/azure-pipelines.yml b/template/pro/azure-pipelines.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2df685a5dd6c44538cf54537cd2ff99ffa7b99ba
--- /dev/null
+++ b/template/pro/azure-pipelines.yml
@@ -0,0 +1,78 @@
+# Node.js
+# Build a general Node.js project with npm.
+# Add steps that analyze code, save build artifacts, deploy, and more:
+# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
+name: ant design pro
+
+trigger:
+ - master
+
+jobs:
+ - job: lintAndBuild
+
+ pool:
+ vmImage: 'Ubuntu-16.04'
+
+ steps:
+ - checkout: self
+ clean: false
+ - script: yarn install
+ displayName: install
+ - script: npm run lint
+ displayName: lint
+ - script: npm run build
+ env:
+ PROGRESS: none
+ displayName: build
+
+ - job: test
+ pool:
+ vmImage: 'Ubuntu-16.04'
+
+ container:
+ image: circleci/node:latest-browsers
+ options: '-u root'
+
+ steps:
+ - script: yarn install
+ displayName: install
+ - script: npm run test:all
+ env:
+ PROGRESS: none
+ displayName: test
+
+ - job: Windows
+ pool:
+ vmImage: 'vs2017-win2016'
+ steps:
+ - task: NodeTool@0
+ inputs:
+ versionSpec: '11.x'
+ - script: yarn install
+ displayName: install
+ - script: npm run lint
+ displayName: lint
+ - script: npm run test:all
+ env:
+ PROGRESS: none
+ displayName: test
+ - script: npm run build
+ env:
+ PROGRESS: none
+ displayName: build
+
+ - job: MacOS
+ pool:
+ vmImage: 'macOS-10.13'
+ steps:
+ - task: NodeTool@0
+ inputs:
+ versionSpec: '11.x'
+ - script: yarn install
+ displayName: install
+ - script: npm run lint
+ displayName: lint
+ - script: npm run
+ env:
+ PROGRESS: none
+ displayName: build
diff --git a/template/pro/config/config.ts b/template/pro/config/config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..43c2006c841a30ed94bc1517fae19be186459e71
--- /dev/null
+++ b/template/pro/config/config.ts
@@ -0,0 +1,191 @@
+import { IConfig, IPlugin } from 'umi-types';
+import defaultSettings from './defaultSettings'; // https://umijs.org/config/
+
+import slash from 'slash2';
+import webpackPlugin from './plugin.config';
+const { pwa, primaryColor } = defaultSettings;
+
+// preview.pro.ant.design only do not use in your production ;
+// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION } = process.env;
+const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site';
+const plugins: IPlugin[] = [
+ [
+ 'umi-plugin-react',
+ {
+ antd: true,
+ dva: {
+ hmr: true,
+ },
+ locale: {
+ // default false
+ enable: true,
+ // default zh-CN
+ default: 'zh-CN',
+ // default true, when it is true, will use `navigator.language` overwrite default
+ baseNavigator: true,
+ },
+ // dynamicImport: {
+ // loadingComponent: './components/PageLoading/index',
+ // webpackChunkName: true,
+ // level: 3,
+ // },
+ pwa: pwa
+ ? {
+ workboxPluginMode: 'InjectManifest',
+ workboxOptions: {
+ importWorkboxFrom: 'local',
+ },
+ }
+ : false,
+ // default close dll, because issue https://github.com/ant-design/ant-design-pro/issues/4665
+ // dll features https://webpack.js.org/plugins/dll-plugin/
+ // dll: {
+ // include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch'],
+ // exclude: ['@babel/runtime', 'netlify-lambda'],
+ // },
+ },
+ ],
+ [
+ 'umi-plugin-pro-block',
+ {
+ moveMock: false,
+ moveService: false,
+ modifyRequest: true,
+ autoAddMenu: true,
+ },
+ ],
+]; // 针对 preview.pro.ant.design 的 GA 统计代码
+
+if (isAntDesignProPreview) {
+ plugins.push([
+ 'umi-plugin-ga',
+ {
+ code: 'UA-72788897-6',
+ },
+ ]);
+ plugins.push([
+ 'umi-plugin-pro',
+ {
+ serverUrl: 'https://ant-design-pro.netlify.com',
+ },
+ ]);
+}
+
+export default {
+ plugins,
+ block: {
+ // 国内用户可以使用码云
+ // defaultGitUrl: 'https://gitee.com/ant-design/pro-blocks',
+ defaultGitUrl: 'https://github.com/ant-design/pro-blocks',
+ },
+ hash: true,
+ targets: {
+ ie: 11,
+ },
+ devtool: isAntDesignProPreview ? 'source-map' : false,
+ // umi routes: https://umijs.org/zh/guide/router.html
+ routes: [
+ {
+ path: '/user',
+ component: '../layouts/UserLayout',
+ routes: [
+ {
+ name: 'login',
+ path: '/user/login',
+ component: './user/login',
+ },
+ ],
+ },
+ {
+ path: '/',
+ component: '../layouts/SecurityLayout',
+ routes: [
+ {
+ path: '/',
+ component: '../layouts/BasicLayout',
+ authority: ['admin', 'user'],
+ routes: [
+ {
+ path: '/',
+ redirect: '/welcome',
+ },
+ {
+ path: '/welcome',
+ name: 'welcome',
+ icon: 'smile',
+ component: './Welcome',
+ },
+ {
+ component: './404',
+ },
+ ],
+ },
+ {
+ component: './404',
+ },
+ ],
+ },
+
+ {
+ component: './404',
+ },
+ ],
+ // Theme for antd: https://ant.design/docs/react/customize-theme-cn
+ theme: {
+ 'primary-color': primaryColor,
+ },
+ define: {
+ ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION:
+ ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION || '', // preview.pro.ant.design only do not use in your production ; preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+ },
+ ignoreMomentLocale: true,
+ lessLoaderOptions: {
+ javascriptEnabled: true,
+ },
+ disableRedirectHoist: true,
+ cssLoaderOptions: {
+ modules: true,
+ getLocalIdent: (
+ context: {
+ resourcePath: string;
+ },
+ _: string,
+ localName: string,
+ ) => {
+ if (
+ context.resourcePath.includes('node_modules') ||
+ context.resourcePath.includes('ant.design.pro.less') ||
+ context.resourcePath.includes('global.less')
+ ) {
+ return localName;
+ }
+
+ const match = context.resourcePath.match(/src(.*)/);
+
+ if (match && match[1]) {
+ const antdProPath = match[1].replace('.less', '');
+ const arr = slash(antdProPath)
+ .split('/')
+ .map((a: string) => a.replace(/([A-Z])/g, '-$1'))
+ .map((a: string) => a.toLowerCase());
+ return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
+ }
+
+ return localName;
+ },
+ },
+ manifest: {
+ basePath: '/',
+ },
+ chainWebpack: webpackPlugin,
+ /*
+ proxy: {
+ '/server/api/': {
+ target: 'https://preview.pro.ant.design/',
+ changeOrigin: true,
+ pathRewrite: { '^/server': '' },
+ },
+ },
+ */
+} as IConfig;
diff --git a/template/pro/config/defaultSettings.ts b/template/pro/config/defaultSettings.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4d860940edaaa21862fd4a258bdee4cea83a6210
--- /dev/null
+++ b/template/pro/config/defaultSettings.ts
@@ -0,0 +1,60 @@
+import { MenuTheme } from 'antd/es/menu/MenuContext';
+
+export type ContentWidth = 'Fluid' | 'Fixed';
+
+export interface DefaultSettings {
+ /**
+ * theme for nav menu
+ */
+ navTheme: MenuTheme;
+ /**
+ * primary color of ant design
+ */
+ primaryColor: string;
+ /**
+ * nav menu position: `sidemenu` or `topmenu`
+ */
+ layout: 'sidemenu' | 'topmenu';
+ /**
+ * layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
+ */
+ contentWidth: ContentWidth;
+ /**
+ * sticky header
+ */
+ fixedHeader: boolean;
+ /**
+ * auto hide header
+ */
+ autoHideHeader: boolean;
+ /**
+ * sticky siderbar
+ */
+ fixSiderbar: boolean;
+ menu: { locale: boolean };
+ title: string;
+ pwa: boolean;
+ // Your custom iconfont Symbol script Url
+ // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
+ // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理
+ // Usage: https://github.com/ant-design/ant-design-pro/pull/3517
+ iconfontUrl: string;
+ colorWeak: boolean;
+}
+
+export default {
+ navTheme: 'dark',
+ primaryColor: '#1890FF',
+ layout: 'sidemenu',
+ contentWidth: 'Fluid',
+ fixedHeader: false,
+ autoHideHeader: false,
+ fixSiderbar: false,
+ colorWeak: false,
+ menu: {
+ locale: true,
+ },
+ title: 'Ant Design Pro',
+ pwa: false,
+ iconfontUrl: '',
+} as DefaultSettings;
diff --git a/template/pro/config/plugin.config.ts b/template/pro/config/plugin.config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..92a614039a128b4dc93fb8613d2c897addabda69
--- /dev/null
+++ b/template/pro/config/plugin.config.ts
@@ -0,0 +1,104 @@
+// Change theme plugin
+// eslint-disable-next-line eslint-comments/abdeils - enable - pair;
+/* eslint-disable import/no-extraneous-dependencies */
+import ThemeColorReplacer from 'webpack-theme-color-replacer';
+import generate from '@ant-design/colors/lib/generate';
+import path from 'path';
+
+function getModulePackageName(module: { context: string }) {
+ if (!module.context) return null;
+
+ const nodeModulesPath = path.join(__dirname, '../node_modules/');
+ if (module.context.substring(0, nodeModulesPath.length) !== nodeModulesPath) {
+ return null;
+ }
+
+ const moduleRelativePath = module.context.substring(nodeModulesPath.length);
+ const [moduleDirName] = moduleRelativePath.split(path.sep);
+ let packageName: string | null = moduleDirName;
+ // handle tree shaking
+ if (packageName && packageName.match('^_')) {
+ // eslint-disable-next-line prefer-destructuring
+ packageName = packageName.match(/^_(@?[^@]+)/)![1];
+ }
+ return packageName;
+}
+
+export default (config: any) => {
+ // preview.pro.ant.design only do not use in your production;
+ if (
+ process.env.ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site' ||
+ process.env.NODE_ENV !== 'production'
+ ) {
+ config.plugin('webpack-theme-color-replacer').use(ThemeColorReplacer, [
+ {
+ fileName: 'css/theme-colors-[contenthash:8].css',
+ matchColors: getAntdSerials('#1890ff'), // 主色系列
+ // 改变样式选择器,解决样式覆盖问题
+ changeSelector(selector: string): string {
+ switch (selector) {
+ case '.ant-calendar-today .ant-calendar-date':
+ return ':not(.ant-calendar-selected-date)' + selector;
+ case '.ant-btn:focus,.ant-btn:hover':
+ return '.ant-btn:focus:not(.ant-btn-primary),.ant-btn:hover:not(.ant-btn-primary)';
+ case '.ant-btn.active,.ant-btn:active':
+ return '.ant-btn.active:not(.ant-btn-primary),.ant-btn:active:not(.ant-btn-primary)';
+ default:
+ return selector;
+ }
+ },
+ // isJsUgly: true,
+ },
+ ]);
+ }
+
+ // optimize chunks
+ config.optimization
+ // share the same chunks across different modules
+ .runtimeChunk(false)
+ .splitChunks({
+ chunks: 'async',
+ name: 'vendors',
+ maxInitialRequests: Infinity,
+ minSize: 0,
+ cacheGroups: {
+ vendors: {
+ test: (module: { context: string }) => {
+ const packageName = getModulePackageName(module) || '';
+ if (packageName) {
+ return [
+ 'bizcharts',
+ 'gg-editor',
+ 'g6',
+ '@antv',
+ 'gg-editor-core',
+ 'bizcharts-plugin-slider',
+ ].includes(packageName);
+ }
+ return false;
+ },
+ name(module: { context: string }) {
+ const packageName = getModulePackageName(module);
+ if (packageName) {
+ if (['bizcharts', '@antv_data-set'].indexOf(packageName) >= 0) {
+ return 'viz'; // visualization package
+ }
+ }
+ return 'misc';
+ },
+ },
+ },
+ });
+};
+
+const getAntdSerials = (color: string) => {
+ const lightNum = 9;
+ const devide10 = 10;
+ // 淡化(即less的tint)
+ const lightens = new Array(lightNum).fill(undefined).map((_, i: number) => {
+ return ThemeColorReplacer.varyColor.lighten(color, i / devide10);
+ });
+ const colorPalettes = generate(color);
+ const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',');
+ return lightens.concat(colorPalettes).concat(rgb);
+};
diff --git a/template/pro/docker/docker-compose.dev.yml b/template/pro/docker/docker-compose.dev.yml
new file mode 100644
index 0000000000000000000000000000000000000000..38ab39b66b587c15b7195a71dadebdb29e7423a9
--- /dev/null
+++ b/template/pro/docker/docker-compose.dev.yml
@@ -0,0 +1,14 @@
+version: '3.5'
+
+services:
+ ant-design-pro_dev:
+ ports:
+ - 8000:8000
+ build:
+ context: ../
+ dockerfile: Dockerfile.dev
+ container_name: 'ant-design-pro_dev'
+ volumes:
+ - ../src:/usr/src/app/src
+ - ../config:/usr/src/app/config
+ - ../mock:/usr/src/app/mock
diff --git a/template/pro/docker/docker-compose.yml b/template/pro/docker/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..13e12db8ba687fce09dd6f93556b24959fceb41e
--- /dev/null
+++ b/template/pro/docker/docker-compose.yml
@@ -0,0 +1,21 @@
+version: '3.5'
+
+services:
+ ant-design-pro_build:
+ build: ../
+ container_name: 'ant-design-pro_build'
+ volumes:
+ - dist:/usr/src/app/dist
+
+ ant-design-pro_web:
+ image: nginx
+ ports:
+ - 80:80
+ container_name: 'ant-design-pro_web'
+ restart: unless-stopped
+ volumes:
+ - dist:/usr/share/nginx/html:ro
+ - ./nginx.conf:/etc/nginx/conf.d/default.conf
+
+volumes:
+ dist:
diff --git a/template/pro/docker/nginx.conf b/template/pro/docker/nginx.conf
new file mode 100644
index 0000000000000000000000000000000000000000..9d0418c4e76419dd686cea46bfb4158ad123230a
--- /dev/null
+++ b/template/pro/docker/nginx.conf
@@ -0,0 +1,21 @@
+server {
+ listen 80;
+ # gzip config
+ gzip on;
+ gzip_min_length 1k;
+ gzip_comp_level 9;
+ gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
+ gzip_vary on;
+ gzip_disable "MSIE [1-6]\.";
+
+ root /usr/share/nginx/html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+ location /api {
+ proxy_pass https://ant-design-pro.netlify.com;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+}
diff --git a/template/pro/jest-puppeteer.config.js b/template/pro/jest-puppeteer.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..21b41e4aaf354b1081863e45321f229f7ffe9416
--- /dev/null
+++ b/template/pro/jest-puppeteer.config.js
@@ -0,0 +1,12 @@
+// ps https://github.com/GoogleChrome/puppeteer/issues/3120
+module.exports = {
+ launch: {
+ args: [
+ '--disable-gpu',
+ '--disable-dev-shm-usage',
+ '--no-first-run',
+ '--no-zygote',
+ '--no-sandbox',
+ ],
+ },
+};
diff --git a/template/pro/jest.config.js b/template/pro/jest.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..502fe338e6554278c3d1148fbae26901438eb444
--- /dev/null
+++ b/template/pro/jest.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ testURL: 'http://localhost:8000',
+ preset: 'jest-puppeteer',
+ globals: {
+ ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false,
+ },
+};
diff --git a/template/pro/jsconfig.json b/template/pro/jsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..f87334d482029d82509ddb6c0dab5ab62236cd15
--- /dev/null
+++ b/template/pro/jsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/template/pro/lambda/api.js b/template/pro/lambda/api.js
new file mode 100644
index 0000000000000000000000000000000000000000..105f0b04009dbda90109cd8b2cdea82c0870e2e1
--- /dev/null
+++ b/template/pro/lambda/api.js
@@ -0,0 +1,26 @@
+// [START functions import]
+const express = require('express');
+const serverLess = require('serverless-http');
+
+const matchMock = require('./mock/matchMock');
+
+const app = express();
+
+app.all('*', (req, res, next) => {
+ res.header('Access-Control-Allow-Origin', '*');
+ res.header(
+ 'Access-Control-Allow-Headers',
+ 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild',
+ );
+ res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
+
+ if (req.method == 'OPTIONS') {
+ res.send(200);
+ } else {
+ next();
+ }
+});
+
+app.use(matchMock);
+
+exports.handler = serverLess(app);
diff --git a/template/pro/lambda/mock/index.js b/template/pro/lambda/mock/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..f412ec37d3bc39d9eced1cd3d7769f1cf208328d
--- /dev/null
+++ b/template/pro/lambda/mock/index.js
@@ -0,0 +1,4322 @@
+(function(global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined'
+ ? (module.exports = factory(require('mockjs'), require('moment')))
+ : typeof define === 'function' && define.amd
+ ? define(['mockjs', 'moment'], factory)
+ : ((global = global || self), (global.mock = factory(global.mockjs, global.moment)));
+})(this, function(mockjs, moment) {
+ 'use strict';
+
+ mockjs = mockjs && mockjs.hasOwnProperty('default') ? mockjs['default'] : mockjs;
+ moment = moment && moment.hasOwnProperty('default') ? moment['default'] : moment;
+
+ function _defineProperty(obj, key, value) {
+ if (key in obj) {
+ Object.defineProperty(obj, key, {
+ value: value,
+ enumerable: true,
+ configurable: true,
+ writable: true,
+ });
+ } else {
+ obj[key] = value;
+ }
+
+ return obj;
+ }
+
+ function _objectSpread(target) {
+ for (var i = 1; i < arguments.length; i++) {
+ var source = arguments[i] != null ? arguments[i] : {};
+ var ownKeys = Object.keys(source);
+
+ if (typeof Object.getOwnPropertySymbols === 'function') {
+ ownKeys = ownKeys.concat(
+ Object.getOwnPropertySymbols(source).filter(function(sym) {
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
+ }),
+ );
+ }
+
+ ownKeys.forEach(function(key) {
+ _defineProperty(target, key, source[key]);
+ });
+ }
+
+ return target;
+ }
+
+ const titles = [
+ 'Alipay',
+ 'Angular',
+ 'Ant Design',
+ 'Ant Design Pro',
+ 'Bootstrap',
+ 'React',
+ 'Vue',
+ 'Webpack',
+ ];
+ const avatars = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
+ 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
+ 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
+ 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
+ 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
+ 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
+ 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
+ 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png',
+ ];
+ const avatars2 = [
+ 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png',
+ ];
+ const covers = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
+ ];
+ const desc = [
+ '那是一种内在的东西, 他们到达不了,也无法触及的',
+ '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ '生命就像一盒巧克力,结果往往出人意料',
+ '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ '那时候我只会想自己想要什么,从不想自己拥有什么',
+ ];
+ const user = [
+ '付小小',
+ '曲丽丽',
+ '林东东',
+ '周星星',
+ '吴加好',
+ '朱偏右',
+ '鱼酱',
+ '乐哥',
+ '谭小仪',
+ '仲尼',
+ ];
+
+ function fakeList(count) {
+ const list = [];
+
+ for (let i = 0; i < count; i += 1) {
+ list.push({
+ id: `fake-list-${i}`,
+ owner: user[i % 10],
+ title: titles[i % 8],
+ avatar: avatars[i % 8],
+ cover: parseInt(i / 4, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
+ status: ['active', 'exception', 'normal'][i % 3],
+ percent: Math.ceil(Math.random() * 50) + 50,
+ logo: avatars[i % 8],
+ href: 'https://ant.design',
+ updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
+ createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i),
+ subDescription: desc[i % 5],
+ description:
+ '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
+ activeUser: Math.ceil(Math.random() * 100000) + 100000,
+ newUser: Math.ceil(Math.random() * 1000) + 1000,
+ star: Math.ceil(Math.random() * 100) + 100,
+ like: Math.ceil(Math.random() * 100) + 100,
+ message: Math.ceil(Math.random() * 10) + 10,
+ content:
+ '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
+ members: [
+ {
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
+ name: '曲丽丽',
+ id: 'member1',
+ },
+ {
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
+ name: '王昭君',
+ id: 'member2',
+ },
+ {
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
+ name: '董娜娜',
+ id: 'member3',
+ },
+ ],
+ });
+ }
+
+ return list;
+ }
+
+ let sourceData;
+
+ function getFakeList(req, res) {
+ const params = req.query;
+ const count = params.count * 1 || 20;
+ const result = fakeList(count);
+ sourceData = result;
+ return res.json(result);
+ }
+
+ function postFakeList(req, res) {
+ const {
+ /* url = '', */
+ body,
+ } = req; // const params = getUrlParams(url);
+
+ const { method, id } = body; // const count = (params.count * 1) || 20;
+
+ let result = sourceData;
+
+ switch (method) {
+ case 'delete':
+ result = result.filter(item => item.id !== id);
+ break;
+
+ case 'update':
+ result.forEach((item, i) => {
+ if (item.id === id) {
+ result[i] = Object.assign(item, body);
+ }
+ });
+ break;
+
+ case 'post':
+ result.unshift({
+ body,
+ id: `fake-list-${result.length}`,
+ createdAt: new Date().getTime(),
+ });
+ break;
+
+ default:
+ break;
+ }
+
+ return res.json(result);
+ }
+
+ const getNotice = [
+ {
+ id: 'xxx1',
+ title: titles[0],
+ logo: avatars[0],
+ description: '那是一种内在的东西,他们到达不了,也无法触及的',
+ updatedAt: new Date(),
+ member: '科学搬砖组',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx2',
+ title: titles[1],
+ logo: avatars[1],
+ description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ updatedAt: new Date('2017-07-24'),
+ member: '全组都是吴彦祖',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx3',
+ title: titles[2],
+ logo: avatars[2],
+ description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ updatedAt: new Date(),
+ member: '中二少女团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx4',
+ title: titles[3],
+ logo: avatars[3],
+ description: '那时候我只会想自己想要什么,从不想自己拥有什么',
+ updatedAt: new Date('2017-07-23'),
+ member: '程序员日常',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx5',
+ title: titles[4],
+ logo: avatars[4],
+ description: '凛冬将至',
+ updatedAt: new Date('2017-07-23'),
+ member: '高逼格设计天团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx6',
+ title: titles[5],
+ logo: avatars[5],
+ description: '生命就像一盒巧克力,结果往往出人意料',
+ updatedAt: new Date('2017-07-23'),
+ member: '骗你来学计算机',
+ href: '',
+ memberLink: '',
+ },
+ ];
+ const getActivities = [
+ {
+ id: 'trend-1',
+ updatedAt: new Date(),
+ user: {
+ name: '曲丽丽',
+ avatar: avatars2[0],
+ },
+ group: {
+ name: '高逼格设计天团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-2',
+ updatedAt: new Date(),
+ user: {
+ name: '付小小',
+ avatar: avatars2[1],
+ },
+ group: {
+ name: '高逼格设计天团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-3',
+ updatedAt: new Date(),
+ user: {
+ name: '林东东',
+ avatar: avatars2[2],
+ },
+ group: {
+ name: '中二少女团',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '六月迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ {
+ id: 'trend-4',
+ updatedAt: new Date(),
+ user: {
+ name: '周星星',
+ avatar: avatars2[4],
+ },
+ project: {
+ name: '5 月日常迭代',
+ link: 'http://github.com/',
+ },
+ template: '将 @{project} 更新至已发布状态',
+ },
+ {
+ id: 'trend-5',
+ updatedAt: new Date(),
+ user: {
+ name: '朱偏右',
+ avatar: avatars2[3],
+ },
+ project: {
+ name: '工程效能',
+ link: 'http://github.com/',
+ },
+ comment: {
+ name: '留言',
+ link: 'http://github.com/',
+ },
+ template: '在 @{project} 发布了 @{comment}',
+ },
+ {
+ id: 'trend-6',
+ updatedAt: new Date(),
+ user: {
+ name: '乐哥',
+ avatar: avatars2[5],
+ },
+ group: {
+ name: '程序员日常',
+ link: 'http://github.com/',
+ },
+ project: {
+ name: '品牌迭代',
+ link: 'http://github.com/',
+ },
+ template: '在 @{group} 新建项目 @{project}',
+ },
+ ];
+
+ function getFakeCaptcha(req, res) {
+ return res.json('captcha-xxx');
+ }
+
+ var api = {
+ 'GET /api/project/notice': getNotice,
+ 'GET /api/activities': getActivities,
+ 'POST /api/forms': (req, res) => {
+ res.send({
+ message: 'Ok',
+ });
+ },
+ 'GET /api/tags': mockjs.mock({
+ 'list|100': [
+ {
+ name: '@city',
+ 'value|1-100': 150,
+ 'type|0-2': 1,
+ },
+ ],
+ }),
+ 'GET /api/fake_list': getFakeList,
+ 'POST /api/fake_list': postFakeList,
+ 'GET /api/captcha': getFakeCaptcha,
+ };
+
+ const visitData = [];
+ const beginDay = new Date().getTime();
+ const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
+
+ for (let i = 0; i < fakeY.length; i += 1) {
+ visitData.push({
+ x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY[i],
+ });
+ }
+
+ const visitData2 = [];
+ const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
+
+ for (let i = 0; i < fakeY2.length; i += 1) {
+ visitData2.push({
+ x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY2[i],
+ });
+ }
+
+ const salesData = [];
+
+ for (let i = 0; i < 12; i += 1) {
+ salesData.push({
+ x: `${i + 1}月`,
+ y: Math.floor(Math.random() * 1000) + 200,
+ });
+ }
+
+ const searchData = [];
+
+ for (let i = 0; i < 50; i += 1) {
+ searchData.push({
+ index: i + 1,
+ keyword: `搜索关键词-${i}`,
+ count: Math.floor(Math.random() * 1000),
+ range: Math.floor(Math.random() * 100),
+ status: Math.floor((Math.random() * 10) % 2),
+ });
+ }
+
+ const salesTypeData = [
+ {
+ x: '家用电器',
+ y: 4544,
+ },
+ {
+ x: '食用酒水',
+ y: 3321,
+ },
+ {
+ x: '个护健康',
+ y: 3113,
+ },
+ {
+ x: '服饰箱包',
+ y: 2341,
+ },
+ {
+ x: '母婴产品',
+ y: 1231,
+ },
+ {
+ x: '其他',
+ y: 1231,
+ },
+ ];
+ const salesTypeDataOnline = [
+ {
+ x: '家用电器',
+ y: 244,
+ },
+ {
+ x: '食用酒水',
+ y: 321,
+ },
+ {
+ x: '个护健康',
+ y: 311,
+ },
+ {
+ x: '服饰箱包',
+ y: 41,
+ },
+ {
+ x: '母婴产品',
+ y: 121,
+ },
+ {
+ x: '其他',
+ y: 111,
+ },
+ ];
+ const salesTypeDataOffline = [
+ {
+ x: '家用电器',
+ y: 99,
+ },
+ {
+ x: '食用酒水',
+ y: 188,
+ },
+ {
+ x: '个护健康',
+ y: 344,
+ },
+ {
+ x: '服饰箱包',
+ y: 255,
+ },
+ {
+ x: '其他',
+ y: 65,
+ },
+ ];
+ const offlineData = [];
+
+ for (let i = 0; i < 10; i += 1) {
+ offlineData.push({
+ name: `Stores ${i}`,
+ cvr: Math.ceil(Math.random() * 9) / 10,
+ });
+ }
+
+ const offlineChartData = [];
+
+ for (let i = 0; i < 20; i += 1) {
+ offlineChartData.push({
+ x: new Date().getTime() + 1000 * 60 * 30 * i,
+ y1: Math.floor(Math.random() * 100) + 10,
+ y2: Math.floor(Math.random() * 100) + 10,
+ });
+ }
+
+ const radarOriginData = [
+ {
+ name: '个人',
+ ref: 10,
+ koubei: 8,
+ output: 4,
+ contribute: 5,
+ hot: 7,
+ },
+ {
+ name: '团队',
+ ref: 3,
+ koubei: 9,
+ output: 6,
+ contribute: 3,
+ hot: 1,
+ },
+ {
+ name: '部门',
+ ref: 4,
+ koubei: 1,
+ output: 6,
+ contribute: 5,
+ hot: 7,
+ },
+ ];
+ const radarData = [];
+ const radarTitleMap = {
+ ref: '引用',
+ koubei: '口碑',
+ output: '产量',
+ contribute: '贡献',
+ hot: '热度',
+ };
+ radarOriginData.forEach(item => {
+ Object.keys(item).forEach(key => {
+ if (key !== 'name') {
+ radarData.push({
+ name: item.name,
+ label: radarTitleMap[key],
+ value: item[key],
+ });
+ }
+ });
+ });
+ const getFakeChartData = {
+ visitData,
+ visitData2,
+ salesData,
+ searchData,
+ offlineData,
+ offlineChartData,
+ salesTypeData,
+ salesTypeDataOnline,
+ salesTypeDataOffline,
+ radarData,
+ };
+ var chart = {
+ 'GET /api/fake_chart_data': getFakeChartData,
+ };
+
+ var city = {
+ '110000': [
+ {
+ province: '北京市',
+ name: '市辖区',
+ id: '110100',
+ },
+ ],
+ '120000': [
+ {
+ province: '天津市',
+ name: '市辖区',
+ id: '120100',
+ },
+ ],
+ '130000': [
+ {
+ province: '河北省',
+ name: '石家庄市',
+ id: '130100',
+ },
+ {
+ province: '河北省',
+ name: '唐山市',
+ id: '130200',
+ },
+ {
+ province: '河北省',
+ name: '秦皇岛市',
+ id: '130300',
+ },
+ {
+ province: '河北省',
+ name: '邯郸市',
+ id: '130400',
+ },
+ {
+ province: '河北省',
+ name: '邢台市',
+ id: '130500',
+ },
+ {
+ province: '河北省',
+ name: '保定市',
+ id: '130600',
+ },
+ {
+ province: '河北省',
+ name: '张家口市',
+ id: '130700',
+ },
+ {
+ province: '河北省',
+ name: '承德市',
+ id: '130800',
+ },
+ {
+ province: '河北省',
+ name: '沧州市',
+ id: '130900',
+ },
+ {
+ province: '河北省',
+ name: '廊坊市',
+ id: '131000',
+ },
+ {
+ province: '河北省',
+ name: '衡水市',
+ id: '131100',
+ },
+ {
+ province: '河北省',
+ name: '省直辖县级行政区划',
+ id: '139000',
+ },
+ ],
+ '140000': [
+ {
+ province: '山西省',
+ name: '太原市',
+ id: '140100',
+ },
+ {
+ province: '山西省',
+ name: '大同市',
+ id: '140200',
+ },
+ {
+ province: '山西省',
+ name: '阳泉市',
+ id: '140300',
+ },
+ {
+ province: '山西省',
+ name: '长治市',
+ id: '140400',
+ },
+ {
+ province: '山西省',
+ name: '晋城市',
+ id: '140500',
+ },
+ {
+ province: '山西省',
+ name: '朔州市',
+ id: '140600',
+ },
+ {
+ province: '山西省',
+ name: '晋中市',
+ id: '140700',
+ },
+ {
+ province: '山西省',
+ name: '运城市',
+ id: '140800',
+ },
+ {
+ province: '山西省',
+ name: '忻州市',
+ id: '140900',
+ },
+ {
+ province: '山西省',
+ name: '临汾市',
+ id: '141000',
+ },
+ {
+ province: '山西省',
+ name: '吕梁市',
+ id: '141100',
+ },
+ ],
+ '150000': [
+ {
+ province: '内蒙古自治区',
+ name: '呼和浩特市',
+ id: '150100',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '包头市',
+ id: '150200',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '乌海市',
+ id: '150300',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '赤峰市',
+ id: '150400',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '通辽市',
+ id: '150500',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '鄂尔多斯市',
+ id: '150600',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '呼伦贝尔市',
+ id: '150700',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '巴彦淖尔市',
+ id: '150800',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '乌兰察布市',
+ id: '150900',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '兴安盟',
+ id: '152200',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '锡林郭勒盟',
+ id: '152500',
+ },
+ {
+ province: '内蒙古自治区',
+ name: '阿拉善盟',
+ id: '152900',
+ },
+ ],
+ '210000': [
+ {
+ province: '辽宁省',
+ name: '沈阳市',
+ id: '210100',
+ },
+ {
+ province: '辽宁省',
+ name: '大连市',
+ id: '210200',
+ },
+ {
+ province: '辽宁省',
+ name: '鞍山市',
+ id: '210300',
+ },
+ {
+ province: '辽宁省',
+ name: '抚顺市',
+ id: '210400',
+ },
+ {
+ province: '辽宁省',
+ name: '本溪市',
+ id: '210500',
+ },
+ {
+ province: '辽宁省',
+ name: '丹东市',
+ id: '210600',
+ },
+ {
+ province: '辽宁省',
+ name: '锦州市',
+ id: '210700',
+ },
+ {
+ province: '辽宁省',
+ name: '营口市',
+ id: '210800',
+ },
+ {
+ province: '辽宁省',
+ name: '阜新市',
+ id: '210900',
+ },
+ {
+ province: '辽宁省',
+ name: '辽阳市',
+ id: '211000',
+ },
+ {
+ province: '辽宁省',
+ name: '盘锦市',
+ id: '211100',
+ },
+ {
+ province: '辽宁省',
+ name: '铁岭市',
+ id: '211200',
+ },
+ {
+ province: '辽宁省',
+ name: '朝阳市',
+ id: '211300',
+ },
+ {
+ province: '辽宁省',
+ name: '葫芦岛市',
+ id: '211400',
+ },
+ ],
+ '220000': [
+ {
+ province: '吉林省',
+ name: '长春市',
+ id: '220100',
+ },
+ {
+ province: '吉林省',
+ name: '吉林市',
+ id: '220200',
+ },
+ {
+ province: '吉林省',
+ name: '四平市',
+ id: '220300',
+ },
+ {
+ province: '吉林省',
+ name: '辽源市',
+ id: '220400',
+ },
+ {
+ province: '吉林省',
+ name: '通化市',
+ id: '220500',
+ },
+ {
+ province: '吉林省',
+ name: '白山市',
+ id: '220600',
+ },
+ {
+ province: '吉林省',
+ name: '松原市',
+ id: '220700',
+ },
+ {
+ province: '吉林省',
+ name: '白城市',
+ id: '220800',
+ },
+ {
+ province: '吉林省',
+ name: '延边朝鲜族自治州',
+ id: '222400',
+ },
+ ],
+ '230000': [
+ {
+ province: '黑龙江省',
+ name: '哈尔滨市',
+ id: '230100',
+ },
+ {
+ province: '黑龙江省',
+ name: '齐齐哈尔市',
+ id: '230200',
+ },
+ {
+ province: '黑龙江省',
+ name: '鸡西市',
+ id: '230300',
+ },
+ {
+ province: '黑龙江省',
+ name: '鹤岗市',
+ id: '230400',
+ },
+ {
+ province: '黑龙江省',
+ name: '双鸭山市',
+ id: '230500',
+ },
+ {
+ province: '黑龙江省',
+ name: '大庆市',
+ id: '230600',
+ },
+ {
+ province: '黑龙江省',
+ name: '伊春市',
+ id: '230700',
+ },
+ {
+ province: '黑龙江省',
+ name: '佳木斯市',
+ id: '230800',
+ },
+ {
+ province: '黑龙江省',
+ name: '七台河市',
+ id: '230900',
+ },
+ {
+ province: '黑龙江省',
+ name: '牡丹江市',
+ id: '231000',
+ },
+ {
+ province: '黑龙江省',
+ name: '黑河市',
+ id: '231100',
+ },
+ {
+ province: '黑龙江省',
+ name: '绥化市',
+ id: '231200',
+ },
+ {
+ province: '黑龙江省',
+ name: '大兴安岭地区',
+ id: '232700',
+ },
+ ],
+ '310000': [
+ {
+ province: '上海市',
+ name: '市辖区',
+ id: '310100',
+ },
+ ],
+ '320000': [
+ {
+ province: '江苏省',
+ name: '南京市',
+ id: '320100',
+ },
+ {
+ province: '江苏省',
+ name: '无锡市',
+ id: '320200',
+ },
+ {
+ province: '江苏省',
+ name: '徐州市',
+ id: '320300',
+ },
+ {
+ province: '江苏省',
+ name: '常州市',
+ id: '320400',
+ },
+ {
+ province: '江苏省',
+ name: '苏州市',
+ id: '320500',
+ },
+ {
+ province: '江苏省',
+ name: '南通市',
+ id: '320600',
+ },
+ {
+ province: '江苏省',
+ name: '连云港市',
+ id: '320700',
+ },
+ {
+ province: '江苏省',
+ name: '淮安市',
+ id: '320800',
+ },
+ {
+ province: '江苏省',
+ name: '盐城市',
+ id: '320900',
+ },
+ {
+ province: '江苏省',
+ name: '扬州市',
+ id: '321000',
+ },
+ {
+ province: '江苏省',
+ name: '镇江市',
+ id: '321100',
+ },
+ {
+ province: '江苏省',
+ name: '泰州市',
+ id: '321200',
+ },
+ {
+ province: '江苏省',
+ name: '宿迁市',
+ id: '321300',
+ },
+ ],
+ '330000': [
+ {
+ province: '浙江省',
+ name: '杭州市',
+ id: '330100',
+ },
+ {
+ province: '浙江省',
+ name: '宁波市',
+ id: '330200',
+ },
+ {
+ province: '浙江省',
+ name: '温州市',
+ id: '330300',
+ },
+ {
+ province: '浙江省',
+ name: '嘉兴市',
+ id: '330400',
+ },
+ {
+ province: '浙江省',
+ name: '湖州市',
+ id: '330500',
+ },
+ {
+ province: '浙江省',
+ name: '绍兴市',
+ id: '330600',
+ },
+ {
+ province: '浙江省',
+ name: '金华市',
+ id: '330700',
+ },
+ {
+ province: '浙江省',
+ name: '衢州市',
+ id: '330800',
+ },
+ {
+ province: '浙江省',
+ name: '舟山市',
+ id: '330900',
+ },
+ {
+ province: '浙江省',
+ name: '台州市',
+ id: '331000',
+ },
+ {
+ province: '浙江省',
+ name: '丽水市',
+ id: '331100',
+ },
+ ],
+ '340000': [
+ {
+ province: '安徽省',
+ name: '合肥市',
+ id: '340100',
+ },
+ {
+ province: '安徽省',
+ name: '芜湖市',
+ id: '340200',
+ },
+ {
+ province: '安徽省',
+ name: '蚌埠市',
+ id: '340300',
+ },
+ {
+ province: '安徽省',
+ name: '淮南市',
+ id: '340400',
+ },
+ {
+ province: '安徽省',
+ name: '马鞍山市',
+ id: '340500',
+ },
+ {
+ province: '安徽省',
+ name: '淮北市',
+ id: '340600',
+ },
+ {
+ province: '安徽省',
+ name: '铜陵市',
+ id: '340700',
+ },
+ {
+ province: '安徽省',
+ name: '安庆市',
+ id: '340800',
+ },
+ {
+ province: '安徽省',
+ name: '黄山市',
+ id: '341000',
+ },
+ {
+ province: '安徽省',
+ name: '滁州市',
+ id: '341100',
+ },
+ {
+ province: '安徽省',
+ name: '阜阳市',
+ id: '341200',
+ },
+ {
+ province: '安徽省',
+ name: '宿州市',
+ id: '341300',
+ },
+ {
+ province: '安徽省',
+ name: '六安市',
+ id: '341500',
+ },
+ {
+ province: '安徽省',
+ name: '亳州市',
+ id: '341600',
+ },
+ {
+ province: '安徽省',
+ name: '池州市',
+ id: '341700',
+ },
+ {
+ province: '安徽省',
+ name: '宣城市',
+ id: '341800',
+ },
+ ],
+ '350000': [
+ {
+ province: '福建省',
+ name: '福州市',
+ id: '350100',
+ },
+ {
+ province: '福建省',
+ name: '厦门市',
+ id: '350200',
+ },
+ {
+ province: '福建省',
+ name: '莆田市',
+ id: '350300',
+ },
+ {
+ province: '福建省',
+ name: '三明市',
+ id: '350400',
+ },
+ {
+ province: '福建省',
+ name: '泉州市',
+ id: '350500',
+ },
+ {
+ province: '福建省',
+ name: '漳州市',
+ id: '350600',
+ },
+ {
+ province: '福建省',
+ name: '南平市',
+ id: '350700',
+ },
+ {
+ province: '福建省',
+ name: '龙岩市',
+ id: '350800',
+ },
+ {
+ province: '福建省',
+ name: '宁德市',
+ id: '350900',
+ },
+ ],
+ '360000': [
+ {
+ province: '江西省',
+ name: '南昌市',
+ id: '360100',
+ },
+ {
+ province: '江西省',
+ name: '景德镇市',
+ id: '360200',
+ },
+ {
+ province: '江西省',
+ name: '萍乡市',
+ id: '360300',
+ },
+ {
+ province: '江西省',
+ name: '九江市',
+ id: '360400',
+ },
+ {
+ province: '江西省',
+ name: '新余市',
+ id: '360500',
+ },
+ {
+ province: '江西省',
+ name: '鹰潭市',
+ id: '360600',
+ },
+ {
+ province: '江西省',
+ name: '赣州市',
+ id: '360700',
+ },
+ {
+ province: '江西省',
+ name: '吉安市',
+ id: '360800',
+ },
+ {
+ province: '江西省',
+ name: '宜春市',
+ id: '360900',
+ },
+ {
+ province: '江西省',
+ name: '抚州市',
+ id: '361000',
+ },
+ {
+ province: '江西省',
+ name: '上饶市',
+ id: '361100',
+ },
+ ],
+ '370000': [
+ {
+ province: '山东省',
+ name: '济南市',
+ id: '370100',
+ },
+ {
+ province: '山东省',
+ name: '青岛市',
+ id: '370200',
+ },
+ {
+ province: '山东省',
+ name: '淄博市',
+ id: '370300',
+ },
+ {
+ province: '山东省',
+ name: '枣庄市',
+ id: '370400',
+ },
+ {
+ province: '山东省',
+ name: '东营市',
+ id: '370500',
+ },
+ {
+ province: '山东省',
+ name: '烟台市',
+ id: '370600',
+ },
+ {
+ province: '山东省',
+ name: '潍坊市',
+ id: '370700',
+ },
+ {
+ province: '山东省',
+ name: '济宁市',
+ id: '370800',
+ },
+ {
+ province: '山东省',
+ name: '泰安市',
+ id: '370900',
+ },
+ {
+ province: '山东省',
+ name: '威海市',
+ id: '371000',
+ },
+ {
+ province: '山东省',
+ name: '日照市',
+ id: '371100',
+ },
+ {
+ province: '山东省',
+ name: '莱芜市',
+ id: '371200',
+ },
+ {
+ province: '山东省',
+ name: '临沂市',
+ id: '371300',
+ },
+ {
+ province: '山东省',
+ name: '德州市',
+ id: '371400',
+ },
+ {
+ province: '山东省',
+ name: '聊城市',
+ id: '371500',
+ },
+ {
+ province: '山东省',
+ name: '滨州市',
+ id: '371600',
+ },
+ {
+ province: '山东省',
+ name: '菏泽市',
+ id: '371700',
+ },
+ ],
+ '410000': [
+ {
+ province: '河南省',
+ name: '郑州市',
+ id: '410100',
+ },
+ {
+ province: '河南省',
+ name: '开封市',
+ id: '410200',
+ },
+ {
+ province: '河南省',
+ name: '洛阳市',
+ id: '410300',
+ },
+ {
+ province: '河南省',
+ name: '平顶山市',
+ id: '410400',
+ },
+ {
+ province: '河南省',
+ name: '安阳市',
+ id: '410500',
+ },
+ {
+ province: '河南省',
+ name: '鹤壁市',
+ id: '410600',
+ },
+ {
+ province: '河南省',
+ name: '新乡市',
+ id: '410700',
+ },
+ {
+ province: '河南省',
+ name: '焦作市',
+ id: '410800',
+ },
+ {
+ province: '河南省',
+ name: '濮阳市',
+ id: '410900',
+ },
+ {
+ province: '河南省',
+ name: '许昌市',
+ id: '411000',
+ },
+ {
+ province: '河南省',
+ name: '漯河市',
+ id: '411100',
+ },
+ {
+ province: '河南省',
+ name: '三门峡市',
+ id: '411200',
+ },
+ {
+ province: '河南省',
+ name: '南阳市',
+ id: '411300',
+ },
+ {
+ province: '河南省',
+ name: '商丘市',
+ id: '411400',
+ },
+ {
+ province: '河南省',
+ name: '信阳市',
+ id: '411500',
+ },
+ {
+ province: '河南省',
+ name: '周口市',
+ id: '411600',
+ },
+ {
+ province: '河南省',
+ name: '驻马店市',
+ id: '411700',
+ },
+ {
+ province: '河南省',
+ name: '省直辖县级行政区划',
+ id: '419000',
+ },
+ ],
+ '420000': [
+ {
+ province: '湖北省',
+ name: '武汉市',
+ id: '420100',
+ },
+ {
+ province: '湖北省',
+ name: '黄石市',
+ id: '420200',
+ },
+ {
+ province: '湖北省',
+ name: '十堰市',
+ id: '420300',
+ },
+ {
+ province: '湖北省',
+ name: '宜昌市',
+ id: '420500',
+ },
+ {
+ province: '湖北省',
+ name: '襄阳市',
+ id: '420600',
+ },
+ {
+ province: '湖北省',
+ name: '鄂州市',
+ id: '420700',
+ },
+ {
+ province: '湖北省',
+ name: '荆门市',
+ id: '420800',
+ },
+ {
+ province: '湖北省',
+ name: '孝感市',
+ id: '420900',
+ },
+ {
+ province: '湖北省',
+ name: '荆州市',
+ id: '421000',
+ },
+ {
+ province: '湖北省',
+ name: '黄冈市',
+ id: '421100',
+ },
+ {
+ province: '湖北省',
+ name: '咸宁市',
+ id: '421200',
+ },
+ {
+ province: '湖北省',
+ name: '随州市',
+ id: '421300',
+ },
+ {
+ province: '湖北省',
+ name: '恩施土家族苗族自治州',
+ id: '422800',
+ },
+ {
+ province: '湖北省',
+ name: '省直辖县级行政区划',
+ id: '429000',
+ },
+ ],
+ '430000': [
+ {
+ province: '湖南省',
+ name: '长沙市',
+ id: '430100',
+ },
+ {
+ province: '湖南省',
+ name: '株洲市',
+ id: '430200',
+ },
+ {
+ province: '湖南省',
+ name: '湘潭市',
+ id: '430300',
+ },
+ {
+ province: '湖南省',
+ name: '衡阳市',
+ id: '430400',
+ },
+ {
+ province: '湖南省',
+ name: '邵阳市',
+ id: '430500',
+ },
+ {
+ province: '湖南省',
+ name: '岳阳市',
+ id: '430600',
+ },
+ {
+ province: '湖南省',
+ name: '常德市',
+ id: '430700',
+ },
+ {
+ province: '湖南省',
+ name: '张家界市',
+ id: '430800',
+ },
+ {
+ province: '湖南省',
+ name: '益阳市',
+ id: '430900',
+ },
+ {
+ province: '湖南省',
+ name: '郴州市',
+ id: '431000',
+ },
+ {
+ province: '湖南省',
+ name: '永州市',
+ id: '431100',
+ },
+ {
+ province: '湖南省',
+ name: '怀化市',
+ id: '431200',
+ },
+ {
+ province: '湖南省',
+ name: '娄底市',
+ id: '431300',
+ },
+ {
+ province: '湖南省',
+ name: '湘西土家族苗族自治州',
+ id: '433100',
+ },
+ ],
+ '440000': [
+ {
+ province: '广东省',
+ name: '广州市',
+ id: '440100',
+ },
+ {
+ province: '广东省',
+ name: '韶关市',
+ id: '440200',
+ },
+ {
+ province: '广东省',
+ name: '深圳市',
+ id: '440300',
+ },
+ {
+ province: '广东省',
+ name: '珠海市',
+ id: '440400',
+ },
+ {
+ province: '广东省',
+ name: '汕头市',
+ id: '440500',
+ },
+ {
+ province: '广东省',
+ name: '佛山市',
+ id: '440600',
+ },
+ {
+ province: '广东省',
+ name: '江门市',
+ id: '440700',
+ },
+ {
+ province: '广东省',
+ name: '湛江市',
+ id: '440800',
+ },
+ {
+ province: '广东省',
+ name: '茂名市',
+ id: '440900',
+ },
+ {
+ province: '广东省',
+ name: '肇庆市',
+ id: '441200',
+ },
+ {
+ province: '广东省',
+ name: '惠州市',
+ id: '441300',
+ },
+ {
+ province: '广东省',
+ name: '梅州市',
+ id: '441400',
+ },
+ {
+ province: '广东省',
+ name: '汕尾市',
+ id: '441500',
+ },
+ {
+ province: '广东省',
+ name: '河源市',
+ id: '441600',
+ },
+ {
+ province: '广东省',
+ name: '阳江市',
+ id: '441700',
+ },
+ {
+ province: '广东省',
+ name: '清远市',
+ id: '441800',
+ },
+ {
+ province: '广东省',
+ name: '东莞市',
+ id: '441900',
+ },
+ {
+ province: '广东省',
+ name: '中山市',
+ id: '442000',
+ },
+ {
+ province: '广东省',
+ name: '潮州市',
+ id: '445100',
+ },
+ {
+ province: '广东省',
+ name: '揭阳市',
+ id: '445200',
+ },
+ {
+ province: '广东省',
+ name: '云浮市',
+ id: '445300',
+ },
+ ],
+ '450000': [
+ {
+ province: '广西壮族自治区',
+ name: '南宁市',
+ id: '450100',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '柳州市',
+ id: '450200',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '桂林市',
+ id: '450300',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '梧州市',
+ id: '450400',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '北海市',
+ id: '450500',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '防城港市',
+ id: '450600',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '钦州市',
+ id: '450700',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '贵港市',
+ id: '450800',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '玉林市',
+ id: '450900',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '百色市',
+ id: '451000',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '贺州市',
+ id: '451100',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '河池市',
+ id: '451200',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '来宾市',
+ id: '451300',
+ },
+ {
+ province: '广西壮族自治区',
+ name: '崇左市',
+ id: '451400',
+ },
+ ],
+ '460000': [
+ {
+ province: '海南省',
+ name: '海口市',
+ id: '460100',
+ },
+ {
+ province: '海南省',
+ name: '三亚市',
+ id: '460200',
+ },
+ {
+ province: '海南省',
+ name: '三沙市',
+ id: '460300',
+ },
+ {
+ province: '海南省',
+ name: '儋州市',
+ id: '460400',
+ },
+ {
+ province: '海南省',
+ name: '省直辖县级行政区划',
+ id: '469000',
+ },
+ ],
+ '500000': [
+ {
+ province: '重庆市',
+ name: '市辖区',
+ id: '500100',
+ },
+ {
+ province: '重庆市',
+ name: '县',
+ id: '500200',
+ },
+ ],
+ '510000': [
+ {
+ province: '四川省',
+ name: '成都市',
+ id: '510100',
+ },
+ {
+ province: '四川省',
+ name: '自贡市',
+ id: '510300',
+ },
+ {
+ province: '四川省',
+ name: '攀枝花市',
+ id: '510400',
+ },
+ {
+ province: '四川省',
+ name: '泸州市',
+ id: '510500',
+ },
+ {
+ province: '四川省',
+ name: '德阳市',
+ id: '510600',
+ },
+ {
+ province: '四川省',
+ name: '绵阳市',
+ id: '510700',
+ },
+ {
+ province: '四川省',
+ name: '广元市',
+ id: '510800',
+ },
+ {
+ province: '四川省',
+ name: '遂宁市',
+ id: '510900',
+ },
+ {
+ province: '四川省',
+ name: '内江市',
+ id: '511000',
+ },
+ {
+ province: '四川省',
+ name: '乐山市',
+ id: '511100',
+ },
+ {
+ province: '四川省',
+ name: '南充市',
+ id: '511300',
+ },
+ {
+ province: '四川省',
+ name: '眉山市',
+ id: '511400',
+ },
+ {
+ province: '四川省',
+ name: '宜宾市',
+ id: '511500',
+ },
+ {
+ province: '四川省',
+ name: '广安市',
+ id: '511600',
+ },
+ {
+ province: '四川省',
+ name: '达州市',
+ id: '511700',
+ },
+ {
+ province: '四川省',
+ name: '雅安市',
+ id: '511800',
+ },
+ {
+ province: '四川省',
+ name: '巴中市',
+ id: '511900',
+ },
+ {
+ province: '四川省',
+ name: '资阳市',
+ id: '512000',
+ },
+ {
+ province: '四川省',
+ name: '阿坝藏族羌族自治州',
+ id: '513200',
+ },
+ {
+ province: '四川省',
+ name: '甘孜藏族自治州',
+ id: '513300',
+ },
+ {
+ province: '四川省',
+ name: '凉山彝族自治州',
+ id: '513400',
+ },
+ ],
+ '520000': [
+ {
+ province: '贵州省',
+ name: '贵阳市',
+ id: '520100',
+ },
+ {
+ province: '贵州省',
+ name: '六盘水市',
+ id: '520200',
+ },
+ {
+ province: '贵州省',
+ name: '遵义市',
+ id: '520300',
+ },
+ {
+ province: '贵州省',
+ name: '安顺市',
+ id: '520400',
+ },
+ {
+ province: '贵州省',
+ name: '毕节市',
+ id: '520500',
+ },
+ {
+ province: '贵州省',
+ name: '铜仁市',
+ id: '520600',
+ },
+ {
+ province: '贵州省',
+ name: '黔西南布依族苗族自治州',
+ id: '522300',
+ },
+ {
+ province: '贵州省',
+ name: '黔东南苗族侗族自治州',
+ id: '522600',
+ },
+ {
+ province: '贵州省',
+ name: '黔南布依族苗族自治州',
+ id: '522700',
+ },
+ ],
+ '530000': [
+ {
+ province: '云南省',
+ name: '昆明市',
+ id: '530100',
+ },
+ {
+ province: '云南省',
+ name: '曲靖市',
+ id: '530300',
+ },
+ {
+ province: '云南省',
+ name: '玉溪市',
+ id: '530400',
+ },
+ {
+ province: '云南省',
+ name: '保山市',
+ id: '530500',
+ },
+ {
+ province: '云南省',
+ name: '昭通市',
+ id: '530600',
+ },
+ {
+ province: '云南省',
+ name: '丽江市',
+ id: '530700',
+ },
+ {
+ province: '云南省',
+ name: '普洱市',
+ id: '530800',
+ },
+ {
+ province: '云南省',
+ name: '临沧市',
+ id: '530900',
+ },
+ {
+ province: '云南省',
+ name: '楚雄彝族自治州',
+ id: '532300',
+ },
+ {
+ province: '云南省',
+ name: '红河哈尼族彝族自治州',
+ id: '532500',
+ },
+ {
+ province: '云南省',
+ name: '文山壮族苗族自治州',
+ id: '532600',
+ },
+ {
+ province: '云南省',
+ name: '西双版纳傣族自治州',
+ id: '532800',
+ },
+ {
+ province: '云南省',
+ name: '大理白族自治州',
+ id: '532900',
+ },
+ {
+ province: '云南省',
+ name: '德宏傣族景颇族自治州',
+ id: '533100',
+ },
+ {
+ province: '云南省',
+ name: '怒江傈僳族自治州',
+ id: '533300',
+ },
+ {
+ province: '云南省',
+ name: '迪庆藏族自治州',
+ id: '533400',
+ },
+ ],
+ '540000': [
+ {
+ province: '西藏自治区',
+ name: '拉萨市',
+ id: '540100',
+ },
+ {
+ province: '西藏自治区',
+ name: '日喀则市',
+ id: '540200',
+ },
+ {
+ province: '西藏自治区',
+ name: '昌都市',
+ id: '540300',
+ },
+ {
+ province: '西藏自治区',
+ name: '林芝市',
+ id: '540400',
+ },
+ {
+ province: '西藏自治区',
+ name: '山南市',
+ id: '540500',
+ },
+ {
+ province: '西藏自治区',
+ name: '那曲地区',
+ id: '542400',
+ },
+ {
+ province: '西藏自治区',
+ name: '阿里地区',
+ id: '542500',
+ },
+ ],
+ '610000': [
+ {
+ province: '陕西省',
+ name: '西安市',
+ id: '610100',
+ },
+ {
+ province: '陕西省',
+ name: '铜川市',
+ id: '610200',
+ },
+ {
+ province: '陕西省',
+ name: '宝鸡市',
+ id: '610300',
+ },
+ {
+ province: '陕西省',
+ name: '咸阳市',
+ id: '610400',
+ },
+ {
+ province: '陕西省',
+ name: '渭南市',
+ id: '610500',
+ },
+ {
+ province: '陕西省',
+ name: '延安市',
+ id: '610600',
+ },
+ {
+ province: '陕西省',
+ name: '汉中市',
+ id: '610700',
+ },
+ {
+ province: '陕西省',
+ name: '榆林市',
+ id: '610800',
+ },
+ {
+ province: '陕西省',
+ name: '安康市',
+ id: '610900',
+ },
+ {
+ province: '陕西省',
+ name: '商洛市',
+ id: '611000',
+ },
+ ],
+ '620000': [
+ {
+ province: '甘肃省',
+ name: '兰州市',
+ id: '620100',
+ },
+ {
+ province: '甘肃省',
+ name: '嘉峪关市',
+ id: '620200',
+ },
+ {
+ province: '甘肃省',
+ name: '金昌市',
+ id: '620300',
+ },
+ {
+ province: '甘肃省',
+ name: '白银市',
+ id: '620400',
+ },
+ {
+ province: '甘肃省',
+ name: '天水市',
+ id: '620500',
+ },
+ {
+ province: '甘肃省',
+ name: '武威市',
+ id: '620600',
+ },
+ {
+ province: '甘肃省',
+ name: '张掖市',
+ id: '620700',
+ },
+ {
+ province: '甘肃省',
+ name: '平凉市',
+ id: '620800',
+ },
+ {
+ province: '甘肃省',
+ name: '酒泉市',
+ id: '620900',
+ },
+ {
+ province: '甘肃省',
+ name: '庆阳市',
+ id: '621000',
+ },
+ {
+ province: '甘肃省',
+ name: '定西市',
+ id: '621100',
+ },
+ {
+ province: '甘肃省',
+ name: '陇南市',
+ id: '621200',
+ },
+ {
+ province: '甘肃省',
+ name: '临夏回族自治州',
+ id: '622900',
+ },
+ {
+ province: '甘肃省',
+ name: '甘南藏族自治州',
+ id: '623000',
+ },
+ ],
+ '630000': [
+ {
+ province: '青海省',
+ name: '西宁市',
+ id: '630100',
+ },
+ {
+ province: '青海省',
+ name: '海东市',
+ id: '630200',
+ },
+ {
+ province: '青海省',
+ name: '海北藏族自治州',
+ id: '632200',
+ },
+ {
+ province: '青海省',
+ name: '黄南藏族自治州',
+ id: '632300',
+ },
+ {
+ province: '青海省',
+ name: '海南藏族自治州',
+ id: '632500',
+ },
+ {
+ province: '青海省',
+ name: '果洛藏族自治州',
+ id: '632600',
+ },
+ {
+ province: '青海省',
+ name: '玉树藏族自治州',
+ id: '632700',
+ },
+ {
+ province: '青海省',
+ name: '海西蒙古族藏族自治州',
+ id: '632800',
+ },
+ ],
+ '640000': [
+ {
+ province: '宁夏回族自治区',
+ name: '银川市',
+ id: '640100',
+ },
+ {
+ province: '宁夏回族自治区',
+ name: '石嘴山市',
+ id: '640200',
+ },
+ {
+ province: '宁夏回族自治区',
+ name: '吴忠市',
+ id: '640300',
+ },
+ {
+ province: '宁夏回族自治区',
+ name: '固原市',
+ id: '640400',
+ },
+ {
+ province: '宁夏回族自治区',
+ name: '中卫市',
+ id: '640500',
+ },
+ ],
+ '650000': [
+ {
+ province: '新疆维吾尔自治区',
+ name: '乌鲁木齐市',
+ id: '650100',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '克拉玛依市',
+ id: '650200',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '吐鲁番市',
+ id: '650400',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '哈密市',
+ id: '650500',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '昌吉回族自治州',
+ id: '652300',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '博尔塔拉蒙古自治州',
+ id: '652700',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '巴音郭楞蒙古自治州',
+ id: '652800',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '阿克苏地区',
+ id: '652900',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '克孜勒苏柯尔克孜自治州',
+ id: '653000',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '喀什地区',
+ id: '653100',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '和田地区',
+ id: '653200',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '伊犁哈萨克自治州',
+ id: '654000',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '塔城地区',
+ id: '654200',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '阿勒泰地区',
+ id: '654300',
+ },
+ {
+ province: '新疆维吾尔自治区',
+ name: '自治区直辖县级行政区划',
+ id: '659000',
+ },
+ ],
+ };
+
+ var province = [
+ {
+ name: '北京市',
+ id: '110000',
+ },
+ {
+ name: '天津市',
+ id: '120000',
+ },
+ {
+ name: '河北省',
+ id: '130000',
+ },
+ {
+ name: '山西省',
+ id: '140000',
+ },
+ {
+ name: '内蒙古自治区',
+ id: '150000',
+ },
+ {
+ name: '辽宁省',
+ id: '210000',
+ },
+ {
+ name: '吉林省',
+ id: '220000',
+ },
+ {
+ name: '黑龙江省',
+ id: '230000',
+ },
+ {
+ name: '上海市',
+ id: '310000',
+ },
+ {
+ name: '江苏省',
+ id: '320000',
+ },
+ {
+ name: '浙江省',
+ id: '330000',
+ },
+ {
+ name: '安徽省',
+ id: '340000',
+ },
+ {
+ name: '福建省',
+ id: '350000',
+ },
+ {
+ name: '江西省',
+ id: '360000',
+ },
+ {
+ name: '山东省',
+ id: '370000',
+ },
+ {
+ name: '河南省',
+ id: '410000',
+ },
+ {
+ name: '湖北省',
+ id: '420000',
+ },
+ {
+ name: '湖南省',
+ id: '430000',
+ },
+ {
+ name: '广东省',
+ id: '440000',
+ },
+ {
+ name: '广西壮族自治区',
+ id: '450000',
+ },
+ {
+ name: '海南省',
+ id: '460000',
+ },
+ {
+ name: '重庆市',
+ id: '500000',
+ },
+ {
+ name: '四川省',
+ id: '510000',
+ },
+ {
+ name: '贵州省',
+ id: '520000',
+ },
+ {
+ name: '云南省',
+ id: '530000',
+ },
+ {
+ name: '西藏自治区',
+ id: '540000',
+ },
+ {
+ name: '陕西省',
+ id: '610000',
+ },
+ {
+ name: '甘肃省',
+ id: '620000',
+ },
+ {
+ name: '青海省',
+ id: '630000',
+ },
+ {
+ name: '宁夏回族自治区',
+ id: '640000',
+ },
+ {
+ name: '新疆维吾尔自治区',
+ id: '650000',
+ },
+ {
+ name: '台湾省',
+ id: '710000',
+ },
+ {
+ name: '香港特别行政区',
+ id: '810000',
+ },
+ {
+ name: '澳门特别行政区',
+ id: '820000',
+ },
+ ];
+ function getProvince(req, res) {
+ return res.json(province);
+ }
+
+ function getCity(req, res) {
+ return res.json(city[req.params.province]);
+ }
+
+ var geographic = {
+ 'GET /api/geographic/province': getProvince,
+ 'GET /api/geographic/city/:province': getCity,
+ };
+
+ 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',
+ },
+ ]);
+
+ var notices = {
+ 'GET /api/notices': getNotices,
+ };
+
+ const basicGoods = [
+ {
+ id: '1234561',
+ name: '矿泉水 550ml',
+ barcode: '12421432143214321',
+ price: '2.00',
+ num: '1',
+ amount: '2.00',
+ },
+ {
+ id: '1234562',
+ name: '凉茶 300ml',
+ barcode: '12421432143214322',
+ price: '3.00',
+ num: '2',
+ amount: '6.00',
+ },
+ {
+ id: '1234563',
+ name: '好吃的薯片',
+ barcode: '12421432143214323',
+ price: '7.00',
+ num: '4',
+ amount: '28.00',
+ },
+ {
+ id: '1234564',
+ name: '特别好吃的蛋卷',
+ barcode: '12421432143214324',
+ price: '8.50',
+ num: '3',
+ amount: '25.50',
+ },
+ ];
+ const basicProgress = [
+ {
+ key: '1',
+ time: '2017-10-01 14:10',
+ rate: '联系客户',
+ status: 'processing',
+ operator: '取货员 ID1234',
+ cost: '5mins',
+ },
+ {
+ key: '2',
+ time: '2017-10-01 14:05',
+ rate: '取货员出发',
+ status: 'success',
+ operator: '取货员 ID1234',
+ cost: '1h',
+ },
+ {
+ key: '3',
+ time: '2017-10-01 13:05',
+ rate: '取货员接单',
+ status: 'success',
+ operator: '取货员 ID1234',
+ cost: '5mins',
+ },
+ {
+ key: '4',
+ time: '2017-10-01 13:00',
+ rate: '申请审批通过',
+ status: 'success',
+ operator: '系统',
+ cost: '1h',
+ },
+ {
+ key: '5',
+ time: '2017-10-01 12:00',
+ rate: '发起退货申请',
+ status: 'success',
+ operator: '用户',
+ cost: '5mins',
+ },
+ ];
+ const advancedOperation1 = [
+ {
+ key: 'op1',
+ type: '订购关系生效',
+ name: '曲丽丽',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+ {
+ key: 'op2',
+ type: '财务复审',
+ name: '付小小',
+ status: 'reject',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '不通过原因',
+ },
+ {
+ key: 'op3',
+ type: '部门初审',
+ name: '周毛毛',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+ {
+ key: 'op4',
+ type: '提交订单',
+ name: '林东东',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '很棒',
+ },
+ {
+ key: 'op5',
+ type: '创建订单',
+ name: '汗牙牙',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+ ];
+ const advancedOperation2 = [
+ {
+ key: 'op1',
+ type: '订购关系生效',
+ name: '曲丽丽',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+ ];
+ const advancedOperation3 = [
+ {
+ key: 'op1',
+ type: '创建订单',
+ name: '汗牙牙',
+ status: 'agree',
+ updatedAt: '2017-10-03 19:23:12',
+ memo: '-',
+ },
+ ];
+ const getProfileAdvancedData = {
+ advancedOperation1,
+ advancedOperation2,
+ advancedOperation3,
+ };
+ const { Random } = mockjs;
+ var profile = {
+ 'GET /api/profile/advanced': getProfileAdvancedData,
+ 'GET /api/profile/basic': (req, res) => {
+ const { id } = req.query;
+ const application = {
+ id,
+ status: '已取货',
+ orderNo: Random.id(),
+ childOrderNo: Random.id(),
+ };
+ const userInfo = {
+ name: Random.cname(),
+ tel: '18100000000',
+ delivery: '菜鸟物流',
+ addr: '浙江省杭州市西湖区万塘路18号',
+ remark: '备注',
+ };
+ res.json({
+ userInfo,
+ application,
+ basicGoods,
+ basicProgress,
+ });
+ },
+ };
+
+ /*! https://mths.be/punycode v1.4.1 by @mathias */
+
+ /** Highest positive signed 32-bit float value */
+ var maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1
+
+ /** Bootstring parameters */
+
+ var base = 36;
+ var tMin = 1;
+ var tMax = 26;
+ var skew = 38;
+ var damp = 700;
+ var initialBias = 72;
+ var initialN = 128; // 0x80
+
+ var delimiter = '-'; // '\x2D'
+ var regexNonASCII = /[^\x20-\x7E]/; // unprintable ASCII chars + non-ASCII chars
+
+ var regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators
+
+ /** Error messages */
+
+ var errors = {
+ overflow: 'Overflow: input needs wider integers to process',
+ 'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
+ 'invalid-input': 'Invalid input',
+ };
+ /** Convenience shortcuts */
+
+ var baseMinusTMin = base - tMin;
+ var floor = Math.floor;
+ var stringFromCharCode = String.fromCharCode;
+ /*--------------------------------------------------------------------------*/
+
+ /**
+ * A generic error utility function.
+ * @private
+ * @param {String} type The error type.
+ * @returns {Error} Throws a `RangeError` with the applicable error message.
+ */
+
+ function error(type) {
+ throw new RangeError(errors[type]);
+ }
+ /**
+ * A generic `Array#map` utility function.
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} callback The function that gets called for every array
+ * item.
+ * @returns {Array} A new array of values returned by the callback function.
+ */
+
+ function map(array, fn) {
+ var length = array.length;
+ var result = [];
+
+ while (length--) {
+ result[length] = fn(array[length]);
+ }
+
+ return result;
+ }
+ /**
+ * A simple `Array#map`-like wrapper to work with domain name strings or email
+ * addresses.
+ * @private
+ * @param {String} domain The domain name or email address.
+ * @param {Function} callback The function that gets called for every
+ * character.
+ * @returns {Array} A new string of characters returned by the callback
+ * function.
+ */
+
+ function mapDomain(string, fn) {
+ var parts = string.split('@');
+ var result = '';
+
+ if (parts.length > 1) {
+ // In email addresses, only the domain name should be punycoded. Leave
+ // the local part (i.e. everything up to `@`) intact.
+ result = parts[0] + '@';
+ string = parts[1];
+ } // Avoid `split(regex)` for IE8 compatibility. See #17.
+
+ string = string.replace(regexSeparators, '\x2E');
+ var labels = string.split('.');
+ var encoded = map(labels, fn).join('.');
+ return result + encoded;
+ }
+ /**
+ * Creates an array containing the numeric code points of each Unicode
+ * character in the string. While JavaScript uses UCS-2 internally,
+ * this function will convert a pair of surrogate halves (each of which
+ * UCS-2 exposes as separate characters) into a single code point,
+ * matching UTF-16.
+ * @see `punycode.ucs2.encode`
+ * @see
+ * @memberOf punycode.ucs2
+ * @name decode
+ * @param {String} string The Unicode input string (UCS-2).
+ * @returns {Array} The new array of code points.
+ */
+
+ function ucs2decode(string) {
+ var output = [],
+ counter = 0,
+ length = string.length,
+ value,
+ extra;
+
+ while (counter < length) {
+ value = string.charCodeAt(counter++);
+
+ if (value >= 0xd800 && value <= 0xdbff && counter < length) {
+ // high surrogate, and there is a next character
+ extra = string.charCodeAt(counter++);
+
+ if ((extra & 0xfc00) == 0xdc00) {
+ // low surrogate
+ output.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
+ } else {
+ // unmatched surrogate; only append this code unit, in case the next
+ // code unit is the high surrogate of a surrogate pair
+ output.push(value);
+ counter--;
+ }
+ } else {
+ output.push(value);
+ }
+ }
+
+ return output;
+ }
+ /**
+ * Converts a digit/integer into a basic code point.
+ * @see `basicToDigit()`
+ * @private
+ * @param {Number} digit The numeric value of a basic code point.
+ * @returns {Number} The basic code point whose value (when used for
+ * representing integers) is `digit`, which needs to be in the range
+ * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
+ * used; else, the lowercase form is used. The behavior is undefined
+ * if `flag` is non-zero and `digit` has no uppercase form.
+ */
+
+ function digitToBasic(digit, flag) {
+ // 0..25 map to ASCII a..z or A..Z
+ // 26..35 map to ASCII 0..9
+ return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
+ }
+ /**
+ * Bias adaptation function as per section 3.4 of RFC 3492.
+ * https://tools.ietf.org/html/rfc3492#section-3.4
+ * @private
+ */
+
+ function adapt(delta, numPoints, firstTime) {
+ var k = 0;
+ delta = firstTime ? floor(delta / damp) : delta >> 1;
+ delta += floor(delta / numPoints);
+
+ for (
+ ;
+ /* no initialization */
+ delta > (baseMinusTMin * tMax) >> 1;
+ k += base
+ ) {
+ delta = floor(delta / baseMinusTMin);
+ }
+
+ return floor(k + ((baseMinusTMin + 1) * delta) / (delta + skew));
+ }
+ /**
+ * Converts a string of Unicode symbols (e.g. a domain name label) to a
+ * Punycode string of ASCII-only symbols.
+ * @memberOf punycode
+ * @param {String} input The string of Unicode symbols.
+ * @returns {String} The resulting Punycode string of ASCII-only symbols.
+ */
+
+ function encode(input) {
+ var n,
+ delta,
+ handledCPCount,
+ basicLength,
+ bias,
+ j,
+ m,
+ q,
+ k,
+ t,
+ currentValue,
+ output = [],
+ /** `inputLength` will hold the number of code points in `input`. */
+ inputLength,
+ /** Cached calculation results */
+ handledCPCountPlusOne,
+ baseMinusT,
+ qMinusT; // Convert the input in UCS-2 to Unicode
+
+ input = ucs2decode(input); // Cache the length
+
+ inputLength = input.length; // Initialize the state
+
+ n = initialN;
+ delta = 0;
+ bias = initialBias; // Handle the basic code points
+
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+
+ if (currentValue < 0x80) {
+ output.push(stringFromCharCode(currentValue));
+ }
+ }
+
+ handledCPCount = basicLength = output.length; // `handledCPCount` is the number of code points that have been handled;
+ // `basicLength` is the number of basic code points.
+ // Finish the basic string - if it is not empty - with a delimiter
+
+ if (basicLength) {
+ output.push(delimiter);
+ } // Main encoding loop:
+
+ while (handledCPCount < inputLength) {
+ // All non-basic code points < n have been handled already. Find the next
+ // larger one:
+ for (m = maxInt, j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+
+ if (currentValue >= n && currentValue < m) {
+ m = currentValue;
+ }
+ } // Increase `delta` enough to advance the decoder's state to ,
+ // but guard against overflow
+
+ handledCPCountPlusOne = handledCPCount + 1;
+
+ if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
+ error('overflow');
+ }
+
+ delta += (m - n) * handledCPCountPlusOne;
+ n = m;
+
+ for (j = 0; j < inputLength; ++j) {
+ currentValue = input[j];
+
+ if (currentValue < n && ++delta > maxInt) {
+ error('overflow');
+ }
+
+ if (currentValue == n) {
+ // Represent delta as a generalized variable-length integer
+ for (
+ q = delta, k = base;
+ ;
+ /* no condition */
+ k += base
+ ) {
+ t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
+
+ if (q < t) {
+ break;
+ }
+
+ qMinusT = q - t;
+ baseMinusT = base - t;
+ output.push(stringFromCharCode(digitToBasic(t + (qMinusT % baseMinusT), 0)));
+ q = floor(qMinusT / baseMinusT);
+ }
+
+ output.push(stringFromCharCode(digitToBasic(q, 0)));
+ bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength);
+ delta = 0;
+ ++handledCPCount;
+ }
+ }
+
+ ++delta;
+ ++n;
+ }
+
+ return output.join('');
+ }
+ /**
+ * Converts a Unicode string representing a domain name or an email address to
+ * Punycode. Only the non-ASCII parts of the domain name will be converted,
+ * i.e. it doesn't matter if you call it with a domain that's already in
+ * ASCII.
+ * @memberOf punycode
+ * @param {String} input The domain name or email address to convert, as a
+ * Unicode string.
+ * @returns {String} The Punycode representation of the given domain name or
+ * email address.
+ */
+
+ function toASCII(input) {
+ return mapDomain(input, function(string) {
+ return regexNonASCII.test(string) ? 'xn--' + encode(string) : string;
+ });
+ }
+
+ // shim for using process in browser
+
+ if (typeof global.setTimeout === 'function');
+
+ if (typeof global.clearTimeout === 'function');
+
+ var performance = global.performance || {};
+
+ var performanceNow =
+ performance.now ||
+ performance.mozNow ||
+ performance.msNow ||
+ performance.oNow ||
+ performance.webkitNow ||
+ function() {
+ return new Date().getTime();
+ }; // generate timestamp or delta
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ function isNull(arg) {
+ return arg === null;
+ }
+ function isNullOrUndefined(arg) {
+ return arg == null;
+ }
+ function isString(arg) {
+ return typeof arg === 'string';
+ }
+ function isObject(arg) {
+ return typeof arg === 'object' && arg !== null;
+ }
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a
+ // copy of this software and associated documentation files (the
+ // "Software"), to deal in the Software without restriction, including
+ // without limitation the rights to use, copy, modify, merge, publish,
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
+ // persons to whom the Software is furnished to do so, subject to the
+ // following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included
+ // in all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
+ // If obj.hasOwnProperty has been overridden, then calling
+ // obj.hasOwnProperty(prop) will break.
+ // See: https://github.com/joyent/node/issues/1707
+ function hasOwnProperty(obj, prop) {
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+ }
+
+ var isArray =
+ Array.isArray ||
+ function(xs) {
+ return Object.prototype.toString.call(xs) === '[object Array]';
+ };
+
+ function stringifyPrimitive(v) {
+ switch (typeof v) {
+ case 'string':
+ return v;
+
+ case 'boolean':
+ return v ? 'true' : 'false';
+
+ case 'number':
+ return isFinite(v) ? v : '';
+
+ default:
+ return '';
+ }
+ }
+
+ function stringify(obj, sep, eq, name) {
+ sep = sep || '&';
+ eq = eq || '=';
+
+ if (obj === null) {
+ obj = undefined;
+ }
+
+ if (typeof obj === 'object') {
+ return map$1(objectKeys(obj), function(k) {
+ var ks = encodeURIComponent(stringifyPrimitive(k)) + eq;
+
+ if (isArray(obj[k])) {
+ return map$1(obj[k], function(v) {
+ return ks + encodeURIComponent(stringifyPrimitive(v));
+ }).join(sep);
+ } else {
+ return ks + encodeURIComponent(stringifyPrimitive(obj[k]));
+ }
+ }).join(sep);
+ }
+
+ if (!name) return '';
+ return (
+ encodeURIComponent(stringifyPrimitive(name)) +
+ eq +
+ encodeURIComponent(stringifyPrimitive(obj))
+ );
+ }
+
+ function map$1(xs, f) {
+ if (xs.map) return xs.map(f);
+ var res = [];
+
+ for (var i = 0; i < xs.length; i++) {
+ res.push(f(xs[i], i));
+ }
+
+ return res;
+ }
+
+ var objectKeys =
+ Object.keys ||
+ function(obj) {
+ var res = [];
+
+ for (var key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) res.push(key);
+ }
+
+ return res;
+ };
+
+ function parse(qs, sep, eq, options) {
+ sep = sep || '&';
+ eq = eq || '=';
+ var obj = {};
+
+ if (typeof qs !== 'string' || qs.length === 0) {
+ return obj;
+ }
+
+ var regexp = /\+/g;
+ qs = qs.split(sep);
+ var maxKeys = 1000;
+
+ if (options && typeof options.maxKeys === 'number') {
+ maxKeys = options.maxKeys;
+ }
+
+ var len = qs.length; // maxKeys <= 0 means that we should not limit keys count
+
+ if (maxKeys > 0 && len > maxKeys) {
+ len = maxKeys;
+ }
+
+ for (var i = 0; i < len; ++i) {
+ var x = qs[i].replace(regexp, '%20'),
+ idx = x.indexOf(eq),
+ kstr,
+ vstr,
+ k,
+ v;
+
+ if (idx >= 0) {
+ kstr = x.substr(0, idx);
+ vstr = x.substr(idx + 1);
+ } else {
+ kstr = x;
+ vstr = '';
+ }
+
+ k = decodeURIComponent(kstr);
+ v = decodeURIComponent(vstr);
+
+ if (!hasOwnProperty(obj, k)) {
+ obj[k] = v;
+ } else if (isArray(obj[k])) {
+ obj[k].push(v);
+ } else {
+ obj[k] = [obj[k], v];
+ }
+ }
+
+ return obj;
+ }
+
+ // Copyright Joyent, Inc. and other Node contributors.
+ function Url() {
+ this.protocol = null;
+ this.slashes = null;
+ this.auth = null;
+ this.host = null;
+ this.port = null;
+ this.hostname = null;
+ this.hash = null;
+ this.search = null;
+ this.query = null;
+ this.pathname = null;
+ this.path = null;
+ this.href = null;
+ } // Reference: RFC 3986, RFC 1808, RFC 2396
+ // define these here so at least they only have to be
+ // compiled once on the first module load.
+
+ var protocolPattern = /^([a-z0-9.+-]+:)/i,
+ portPattern = /:[0-9]*$/,
+ // Special case for a simple path URL
+ simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,
+ // RFC 2396: characters reserved for delimiting URLs.
+ // We actually just auto-escape these.
+ delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'],
+ // RFC 2396: characters not allowed for various reasons.
+ unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims),
+ // Allowed by RFCs, but cause of XSS attacks. Always escape these.
+ autoEscape = ["'"].concat(unwise),
+ // Characters that are never ever allowed in a hostname.
+ // Note that any invalid chars are also handled, but these
+ // are the ones that are *expected* to be seen, so we fast-path
+ // them.
+ nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape),
+ hostEndingChars = ['/', '?', '#'],
+ hostnameMaxLen = 255,
+ hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/,
+ hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/,
+ // protocols that can allow "unsafe" and "unwise" chars.
+ unsafeProtocol = {
+ javascript: true,
+ 'javascript:': true,
+ },
+ // protocols that never have a hostname.
+ hostlessProtocol = {
+ javascript: true,
+ 'javascript:': true,
+ },
+ // protocols that always contain a // bit.
+ slashedProtocol = {
+ http: true,
+ https: true,
+ ftp: true,
+ gopher: true,
+ file: true,
+ 'http:': true,
+ 'https:': true,
+ 'ftp:': true,
+ 'gopher:': true,
+ 'file:': true,
+ };
+
+ function urlParse(url, parseQueryString, slashesDenoteHost) {
+ if (url && isObject(url) && url instanceof Url) return url;
+ var u = new Url();
+ u.parse(url, parseQueryString, slashesDenoteHost);
+ return u;
+ }
+
+ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
+ return parse$1(this, url, parseQueryString, slashesDenoteHost);
+ };
+
+ function parse$1(self, url, parseQueryString, slashesDenoteHost) {
+ if (!isString(url)) {
+ throw new TypeError("Parameter 'url' must be a string, not " + typeof url);
+ } // Copy chrome, IE, opera backslash-handling behavior.
+ // Back slashes before the query string get converted to forward slashes
+ // See: https://code.google.com/p/chromium/issues/detail?id=25916
+
+ var queryIndex = url.indexOf('?'),
+ splitter = queryIndex !== -1 && queryIndex < url.indexOf('#') ? '?' : '#',
+ uSplit = url.split(splitter),
+ slashRegex = /\\/g;
+ uSplit[0] = uSplit[0].replace(slashRegex, '/');
+ url = uSplit.join(splitter);
+ var rest = url; // trim before proceeding.
+ // This is to support parse stuff like " http://foo.com \n"
+
+ rest = rest.trim();
+
+ if (!slashesDenoteHost && url.split('#').length === 1) {
+ // Try fast path regexp
+ var simplePath = simplePathPattern.exec(rest);
+
+ if (simplePath) {
+ self.path = rest;
+ self.href = rest;
+ self.pathname = simplePath[1];
+
+ if (simplePath[2]) {
+ self.search = simplePath[2];
+
+ if (parseQueryString) {
+ self.query = parse(self.search.substr(1));
+ } else {
+ self.query = self.search.substr(1);
+ }
+ } else if (parseQueryString) {
+ self.search = '';
+ self.query = {};
+ }
+
+ return self;
+ }
+ }
+
+ var proto = protocolPattern.exec(rest);
+
+ if (proto) {
+ proto = proto[0];
+ var lowerProto = proto.toLowerCase();
+ self.protocol = lowerProto;
+ rest = rest.substr(proto.length);
+ } // figure out if it's got a host
+ // user@server is *always* interpreted as a hostname, and url
+ // resolution will treat //foo/bar as host=foo,path=bar because that's
+ // how the browser resolves relative URLs.
+
+ if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) {
+ var slashes = rest.substr(0, 2) === '//';
+
+ if (slashes && !(proto && hostlessProtocol[proto])) {
+ rest = rest.substr(2);
+ self.slashes = true;
+ }
+ }
+
+ var i, hec, l, p;
+
+ if (!hostlessProtocol[proto] && (slashes || (proto && !slashedProtocol[proto]))) {
+ // there's a hostname.
+ // the first instance of /, ?, ;, or # ends the host.
+ //
+ // If there is an @ in the hostname, then non-host chars *are* allowed
+ // to the left of the last @ sign, unless some host-ending character
+ // comes *before* the @-sign.
+ // URLs are obnoxious.
+ //
+ // ex:
+ // http://a@b@c/ => user:a@b host:c
+ // http://a@b?@c => user:a host:c path:/?@c
+ // v0.12 TODO(isaacs): This is not quite how Chrome does things.
+ // Review our test case against browsers more comprehensively.
+ // find the first instance of any hostEndingChars
+ var hostEnd = -1;
+
+ for (i = 0; i < hostEndingChars.length; i++) {
+ hec = rest.indexOf(hostEndingChars[i]);
+ if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) hostEnd = hec;
+ } // at this point, either we have an explicit point where the
+ // auth portion cannot go past, or the last @ char is the decider.
+
+ var auth, atSign;
+
+ if (hostEnd === -1) {
+ // atSign can be anywhere.
+ atSign = rest.lastIndexOf('@');
+ } else {
+ // atSign must be in auth portion.
+ // http://a@b/c@d => host:b auth:a path:/c@d
+ atSign = rest.lastIndexOf('@', hostEnd);
+ } // Now we have a portion which is definitely the auth.
+ // Pull that off.
+
+ if (atSign !== -1) {
+ auth = rest.slice(0, atSign);
+ rest = rest.slice(atSign + 1);
+ self.auth = decodeURIComponent(auth);
+ } // the host is the remaining to the left of the first non-host char
+
+ hostEnd = -1;
+
+ for (i = 0; i < nonHostChars.length; i++) {
+ hec = rest.indexOf(nonHostChars[i]);
+ if (hec !== -1 && (hostEnd === -1 || hec < hostEnd)) hostEnd = hec;
+ } // if we still have not hit it, then the entire thing is a host.
+
+ if (hostEnd === -1) hostEnd = rest.length;
+ self.host = rest.slice(0, hostEnd);
+ rest = rest.slice(hostEnd); // pull out port.
+
+ parseHost(self); // we've indicated that there is a hostname,
+ // so even if it's empty, it has to be present.
+
+ self.hostname = self.hostname || ''; // if hostname begins with [ and ends with ]
+ // assume that it's an IPv6 address.
+
+ var ipv6Hostname =
+ self.hostname[0] === '[' && self.hostname[self.hostname.length - 1] === ']'; // validate a little.
+
+ if (!ipv6Hostname) {
+ var hostparts = self.hostname.split(/\./);
+
+ for (i = 0, l = hostparts.length; i < l; i++) {
+ var part = hostparts[i];
+ if (!part) continue;
+
+ if (!part.match(hostnamePartPattern)) {
+ var newpart = '';
+
+ for (var j = 0, k = part.length; j < k; j++) {
+ if (part.charCodeAt(j) > 127) {
+ // we replace non-ASCII char with a temporary placeholder
+ // we need this to make sure size of hostname is not
+ // broken by replacing non-ASCII by nothing
+ newpart += 'x';
+ } else {
+ newpart += part[j];
+ }
+ } // we test again with ASCII char only
+
+ if (!newpart.match(hostnamePartPattern)) {
+ var validParts = hostparts.slice(0, i);
+ var notHost = hostparts.slice(i + 1);
+ var bit = part.match(hostnamePartStart);
+
+ if (bit) {
+ validParts.push(bit[1]);
+ notHost.unshift(bit[2]);
+ }
+
+ if (notHost.length) {
+ rest = '/' + notHost.join('.') + rest;
+ }
+
+ self.hostname = validParts.join('.');
+ break;
+ }
+ }
+ }
+ }
+
+ if (self.hostname.length > hostnameMaxLen) {
+ self.hostname = '';
+ } else {
+ // hostnames are always lower case.
+ self.hostname = self.hostname.toLowerCase();
+ }
+
+ if (!ipv6Hostname) {
+ // IDNA Support: Returns a punycoded representation of "domain".
+ // It only converts parts of the domain name that
+ // have non-ASCII characters, i.e. it doesn't matter if
+ // you call it with a domain that already is ASCII-only.
+ self.hostname = toASCII(self.hostname);
+ }
+
+ p = self.port ? ':' + self.port : '';
+ var h = self.hostname || '';
+ self.host = h + p;
+ self.href += self.host; // strip [ and ] from the hostname
+ // the host field still retains them, though
+
+ if (ipv6Hostname) {
+ self.hostname = self.hostname.substr(1, self.hostname.length - 2);
+
+ if (rest[0] !== '/') {
+ rest = '/' + rest;
+ }
+ }
+ } // now rest is set to the post-host stuff.
+ // chop off any delim chars.
+
+ if (!unsafeProtocol[lowerProto]) {
+ // First, make 100% sure that any "autoEscape" chars get
+ // escaped, even if encodeURIComponent doesn't think they
+ // need to be.
+ for (i = 0, l = autoEscape.length; i < l; i++) {
+ var ae = autoEscape[i];
+ if (rest.indexOf(ae) === -1) continue;
+ var esc = encodeURIComponent(ae);
+
+ if (esc === ae) {
+ esc = escape(ae);
+ }
+
+ rest = rest.split(ae).join(esc);
+ }
+ } // chop off from the tail first.
+
+ var hash = rest.indexOf('#');
+
+ if (hash !== -1) {
+ // got a fragment string.
+ self.hash = rest.substr(hash);
+ rest = rest.slice(0, hash);
+ }
+
+ var qm = rest.indexOf('?');
+
+ if (qm !== -1) {
+ self.search = rest.substr(qm);
+ self.query = rest.substr(qm + 1);
+
+ if (parseQueryString) {
+ self.query = parse(self.query);
+ }
+
+ rest = rest.slice(0, qm);
+ } else if (parseQueryString) {
+ // no query string, but parseQueryString still requested
+ self.search = '';
+ self.query = {};
+ }
+
+ if (rest) self.pathname = rest;
+
+ if (slashedProtocol[lowerProto] && self.hostname && !self.pathname) {
+ self.pathname = '/';
+ } //to support http.request
+
+ if (self.pathname || self.search) {
+ p = self.pathname || '';
+ var s = self.search || '';
+ self.path = p + s;
+ } // finally, reconstruct the href based on what has been validated.
+
+ self.href = format(self);
+ return self;
+ } // format a parsed object into a url string
+
+ function format(self) {
+ var auth = self.auth || '';
+
+ if (auth) {
+ auth = encodeURIComponent(auth);
+ auth = auth.replace(/%3A/i, ':');
+ auth += '@';
+ }
+
+ var protocol = self.protocol || '',
+ pathname = self.pathname || '',
+ hash = self.hash || '',
+ host = false,
+ query = '';
+
+ if (self.host) {
+ host = auth + self.host;
+ } else if (self.hostname) {
+ host = auth + (self.hostname.indexOf(':') === -1 ? self.hostname : '[' + this.hostname + ']');
+
+ if (self.port) {
+ host += ':' + self.port;
+ }
+ }
+
+ if (self.query && isObject(self.query) && Object.keys(self.query).length) {
+ query = stringify(self.query);
+ }
+
+ var search = self.search || (query && '?' + query) || '';
+ if (protocol && protocol.substr(-1) !== ':') protocol += ':'; // only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
+ // unless they had them to begin with.
+
+ if (self.slashes || ((!protocol || slashedProtocol[protocol]) && host !== false)) {
+ host = '//' + (host || '');
+ if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname;
+ } else if (!host) {
+ host = '';
+ }
+
+ if (hash && hash.charAt(0) !== '#') hash = '#' + hash;
+ if (search && search.charAt(0) !== '?') search = '?' + search;
+ pathname = pathname.replace(/[?#]/g, function(match) {
+ return encodeURIComponent(match);
+ });
+ search = search.replace('#', '%23');
+ return protocol + host + pathname + search + hash;
+ }
+
+ Url.prototype.format = function() {
+ return format(this);
+ };
+
+ Url.prototype.resolve = function(relative) {
+ return this.resolveObject(urlParse(relative, false, true)).format();
+ };
+
+ Url.prototype.resolveObject = function(relative) {
+ if (isString(relative)) {
+ var rel = new Url();
+ rel.parse(relative, false, true);
+ relative = rel;
+ }
+
+ var result = new Url();
+ var tkeys = Object.keys(this);
+
+ for (var tk = 0; tk < tkeys.length; tk++) {
+ var tkey = tkeys[tk];
+ result[tkey] = this[tkey];
+ } // hash is always overridden, no matter what.
+ // even href="" will remove it.
+
+ result.hash = relative.hash; // if the relative url is empty, then there's nothing left to do here.
+
+ if (relative.href === '') {
+ result.href = result.format();
+ return result;
+ } // hrefs like //foo/bar always cut to the protocol.
+
+ if (relative.slashes && !relative.protocol) {
+ // take everything except the protocol from relative
+ var rkeys = Object.keys(relative);
+
+ for (var rk = 0; rk < rkeys.length; rk++) {
+ var rkey = rkeys[rk];
+ if (rkey !== 'protocol') result[rkey] = relative[rkey];
+ } //urlParse appends trailing / to urls like http://www.example.com
+
+ if (slashedProtocol[result.protocol] && result.hostname && !result.pathname) {
+ result.path = result.pathname = '/';
+ }
+
+ result.href = result.format();
+ return result;
+ }
+
+ var relPath;
+
+ if (relative.protocol && relative.protocol !== result.protocol) {
+ // if it's a known url protocol, then changing
+ // the protocol does weird things
+ // first, if it's not file:, then we MUST have a host,
+ // and if there was a path
+ // to begin with, then we MUST have a path.
+ // if it is file:, then the host is dropped,
+ // because that's known to be hostless.
+ // anything else is assumed to be absolute.
+ if (!slashedProtocol[relative.protocol]) {
+ var keys = Object.keys(relative);
+
+ for (var v = 0; v < keys.length; v++) {
+ var k = keys[v];
+ result[k] = relative[k];
+ }
+
+ result.href = result.format();
+ return result;
+ }
+
+ result.protocol = relative.protocol;
+
+ if (!relative.host && !hostlessProtocol[relative.protocol]) {
+ relPath = (relative.pathname || '').split('/');
+
+ while (relPath.length && !(relative.host = relPath.shift()));
+
+ if (!relative.host) relative.host = '';
+ if (!relative.hostname) relative.hostname = '';
+ if (relPath[0] !== '') relPath.unshift('');
+ if (relPath.length < 2) relPath.unshift('');
+ result.pathname = relPath.join('/');
+ } else {
+ result.pathname = relative.pathname;
+ }
+
+ result.search = relative.search;
+ result.query = relative.query;
+ result.host = relative.host || '';
+ result.auth = relative.auth;
+ result.hostname = relative.hostname || relative.host;
+ result.port = relative.port; // to support http.request
+
+ if (result.pathname || result.search) {
+ var p = result.pathname || '';
+ var s = result.search || '';
+ result.path = p + s;
+ }
+
+ result.slashes = result.slashes || relative.slashes;
+ result.href = result.format();
+ return result;
+ }
+
+ var isSourceAbs = result.pathname && result.pathname.charAt(0) === '/',
+ isRelAbs = relative.host || (relative.pathname && relative.pathname.charAt(0) === '/'),
+ mustEndAbs = isRelAbs || isSourceAbs || (result.host && relative.pathname),
+ removeAllDots = mustEndAbs,
+ srcPath = (result.pathname && result.pathname.split('/')) || [],
+ psychotic = result.protocol && !slashedProtocol[result.protocol];
+ relPath = (relative.pathname && relative.pathname.split('/')) || []; // if the url is a non-slashed url, then relative
+ // links like ../.. should be able
+ // to crawl up to the hostname, as well. This is strange.
+ // result.protocol has already been set by now.
+ // Later on, put the first path part into the host field.
+
+ if (psychotic) {
+ result.hostname = '';
+ result.port = null;
+
+ if (result.host) {
+ if (srcPath[0] === '') srcPath[0] = result.host;
+ else srcPath.unshift(result.host);
+ }
+
+ result.host = '';
+
+ if (relative.protocol) {
+ relative.hostname = null;
+ relative.port = null;
+
+ if (relative.host) {
+ if (relPath[0] === '') relPath[0] = relative.host;
+ else relPath.unshift(relative.host);
+ }
+
+ relative.host = null;
+ }
+
+ mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
+ }
+
+ var authInHost;
+
+ if (isRelAbs) {
+ // it's absolute.
+ result.host = relative.host || relative.host === '' ? relative.host : result.host;
+ result.hostname =
+ relative.hostname || relative.hostname === '' ? relative.hostname : result.hostname;
+ result.search = relative.search;
+ result.query = relative.query;
+ srcPath = relPath; // fall through to the dot-handling below.
+ } else if (relPath.length) {
+ // it's relative
+ // throw away the existing file, and take the new path instead.
+ if (!srcPath) srcPath = [];
+ srcPath.pop();
+ srcPath = srcPath.concat(relPath);
+ result.search = relative.search;
+ result.query = relative.query;
+ } else if (!isNullOrUndefined(relative.search)) {
+ // just pull out the search.
+ // like href='?foo'.
+ // Put this after the other two cases because it simplifies the booleans
+ if (psychotic) {
+ result.hostname = result.host = srcPath.shift(); //occationaly the auth can get stuck only in host
+ //this especially happens in cases like
+ //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
+
+ authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false;
+
+ if (authInHost) {
+ result.auth = authInHost.shift();
+ result.host = result.hostname = authInHost.shift();
+ }
+ }
+
+ result.search = relative.search;
+ result.query = relative.query; //to support http.request
+
+ if (!isNull(result.pathname) || !isNull(result.search)) {
+ result.path =
+ (result.pathname ? result.pathname : '') + (result.search ? result.search : '');
+ }
+
+ result.href = result.format();
+ return result;
+ }
+
+ if (!srcPath.length) {
+ // no path at all. easy.
+ // we've already handled the other stuff above.
+ result.pathname = null; //to support http.request
+
+ if (result.search) {
+ result.path = '/' + result.search;
+ } else {
+ result.path = null;
+ }
+
+ result.href = result.format();
+ return result;
+ } // if a url ENDs in . or .., then it must get a trailing slash.
+ // however, if it ends in anything else non-slashy,
+ // then it must NOT get a trailing slash.
+
+ var last = srcPath.slice(-1)[0];
+ var hasTrailingSlash =
+ ((result.host || relative.host || srcPath.length > 1) && (last === '.' || last === '..')) ||
+ last === ''; // strip single dots, resolve double dots to parent dir
+ // if the path tries to go above the root, `up` ends up > 0
+
+ var up = 0;
+
+ for (var i = srcPath.length; i >= 0; i--) {
+ last = srcPath[i];
+
+ if (last === '.') {
+ srcPath.splice(i, 1);
+ } else if (last === '..') {
+ srcPath.splice(i, 1);
+ up++;
+ } else if (up) {
+ srcPath.splice(i, 1);
+ up--;
+ }
+ } // if the path is allowed to go above the root, restore leading ..s
+
+ if (!mustEndAbs && !removeAllDots) {
+ for (; up--; up) {
+ srcPath.unshift('..');
+ }
+ }
+
+ if (mustEndAbs && srcPath[0] !== '' && (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
+ srcPath.unshift('');
+ }
+
+ if (hasTrailingSlash && srcPath.join('/').substr(-1) !== '/') {
+ srcPath.push('');
+ }
+
+ var isAbsolute = srcPath[0] === '' || (srcPath[0] && srcPath[0].charAt(0) === '/'); // put the host back
+
+ if (psychotic) {
+ result.hostname = result.host = isAbsolute ? '' : srcPath.length ? srcPath.shift() : ''; //occationaly the auth can get stuck only in host
+ //this especially happens in cases like
+ //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
+
+ authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false;
+
+ if (authInHost) {
+ result.auth = authInHost.shift();
+ result.host = result.hostname = authInHost.shift();
+ }
+ }
+
+ mustEndAbs = mustEndAbs || (result.host && srcPath.length);
+
+ if (mustEndAbs && !isAbsolute) {
+ srcPath.unshift('');
+ }
+
+ if (!srcPath.length) {
+ result.pathname = null;
+ result.path = null;
+ } else {
+ result.pathname = srcPath.join('/');
+ } //to support request.http
+
+ if (!isNull(result.pathname) || !isNull(result.search)) {
+ result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : '');
+ }
+
+ result.auth = relative.auth || result.auth;
+ result.slashes = result.slashes || relative.slashes;
+ result.href = result.format();
+ return result;
+ };
+
+ Url.prototype.parseHost = function() {
+ return parseHost(this);
+ };
+
+ function parseHost(self) {
+ var host = self.host;
+ var port = portPattern.exec(host);
+
+ if (port) {
+ port = port[0];
+
+ if (port !== ':') {
+ self.port = port.substr(1);
+ }
+
+ host = host.substr(0, host.length - port.length);
+ }
+
+ if (host) self.hostname = host;
+ }
+
+ let tableListDataSource = [];
+
+ for (let i = 0; i < 46; i += 1) {
+ tableListDataSource.push({
+ key: i,
+ 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 ${i}`,
+ title: `一个任务名称 ${i}`,
+ owner: '曲丽丽',
+ desc: '这是一段描述',
+ callNo: Math.floor(Math.random() * 1000),
+ status: Math.floor(Math.random() * 10) % 4,
+ updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
+ createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
+ progress: Math.ceil(Math.random() * 100),
+ });
+ }
+
+ function getRule(req, res, u) {
+ let url = u;
+
+ if (!url || Object.prototype.toString.call(url) !== '[object String]') {
+ url = req.url; // eslint-disable-line
+ }
+
+ const params = urlParse(url, true).query;
+ let dataSource = tableListDataSource;
+
+ 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(data => parseInt(data.status, 10) === parseInt(s[0], 10)),
+ );
+ });
+ dataSource = filterDataSource;
+ }
+
+ if (params.name) {
+ dataSource = dataSource.filter(data => data.name.indexOf(params.name) > -1);
+ }
+
+ let pageSize = 10;
+
+ if (params.pageSize) {
+ pageSize = params.pageSize * 1;
+ }
+
+ const result = {
+ list: dataSource,
+ pagination: {
+ total: dataSource.length,
+ pageSize,
+ current: parseInt(params.currentPage, 10) || 1,
+ },
+ };
+ return res.json(result);
+ }
+
+ function postRule(req, res, u, b) {
+ let url = u;
+
+ if (!url || Object.prototype.toString.call(url) !== '[object String]') {
+ url = req.url; // eslint-disable-line
+ }
+
+ 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);
+ tableListDataSource.unshift({
+ key: i,
+ 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 ${i}`,
+ title: `一个任务名称 ${i}`,
+ 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),
+ });
+ break;
+
+ case 'update':
+ tableListDataSource = tableListDataSource.map(item => {
+ if (item.key === key) {
+ Object.assign(item, {
+ desc,
+ name,
+ });
+ return item;
+ }
+
+ return item;
+ });
+ break;
+
+ default:
+ break;
+ }
+
+ return getRule(req, res, u);
+ }
+
+ var rule = {
+ 'GET /api/rule': getRule,
+ 'POST /api/rule': postRule,
+ };
+
+ // 代码中会兼容本地 service mock 以及部署站点的静态数据
+ var user$1 = {
+ // 支持值为 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: '海纳百川',
+ },
+ ],
+ notice: [
+ {
+ id: 'xxx1',
+ title: titles[0],
+ logo: avatars[0],
+ description: '那是一种内在的东西,他们到达不了,也无法触及的',
+ updatedAt: new Date(),
+ member: '科学搬砖组',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx2',
+ title: titles[1],
+ logo: avatars[1],
+ description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ updatedAt: new Date('2017-07-24'),
+ member: '全组都是吴彦祖',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx3',
+ title: titles[2],
+ logo: avatars[2],
+ description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ updatedAt: new Date(),
+ member: '中二少女团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx4',
+ title: titles[3],
+ logo: avatars[3],
+ description: '那时候我只会想自己想要什么,从不想自己拥有什么',
+ updatedAt: new Date('2017-07-23'),
+ member: '程序员日常',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx5',
+ title: titles[4],
+ logo: avatars[4],
+ description: '凛冬将至',
+ updatedAt: new Date('2017-07-23'),
+ member: '高逼格设计天团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx6',
+ title: titles[5],
+ logo: avatars[5],
+ description: '生命就像一盒巧克力,结果往往出人意料',
+ updatedAt: new Date('2017-07-23'),
+ member: '骗你来学计算机',
+ href: '',
+ memberLink: '',
+ },
+ ],
+ notifyCount: 12,
+ unreadCount: 11,
+ country: 'China',
+ geographic: {
+ province: {
+ label: '浙江省',
+ key: '330000',
+ },
+ city: {
+ label: '杭州市',
+ key: '330100',
+ },
+ },
+ address: '西湖区工专路 77 号',
+ phone: '0752-268888888',
+ },
+ // GET POST 可省略
+ 'GET /api/users': [
+ {
+ key: '1',
+ name: 'John Brown',
+ age: 32,
+ address: 'New York No. 1 Lake Park',
+ },
+ {
+ key: '2',
+ name: 'Jim Green',
+ age: 42,
+ address: 'London No. 1 Lake Park',
+ },
+ {
+ key: '3',
+ name: 'Joe Black',
+ age: 32,
+ address: 'Sidney No. 1 Lake Park',
+ },
+ ],
+ 'POST /api/login/account': (req, res) => {
+ const { password, userName, type } = req.body;
+
+ if (password === 'ant.design' && userName === 'admin') {
+ res.send({
+ status: 'ok',
+ type,
+ currentAuthority: 'admin',
+ });
+ return;
+ }
+
+ if (password === 'ant.design' && userName === 'user') {
+ res.send({
+ status: 'ok',
+ type,
+ currentAuthority: 'user',
+ });
+ return;
+ }
+
+ res.send({
+ status: 'error',
+ type,
+ currentAuthority: 'guest',
+ });
+ },
+ 'POST /api/register': (req, res) => {
+ res.send({
+ status: 'ok',
+ currentAuthority: 'user',
+ });
+ },
+ 'GET /api/500': (req, res) => {
+ res.status(500).send({
+ timestamp: 1513932555104,
+ status: 500,
+ error: 'error',
+ message: 'error',
+ path: '/base/category/list',
+ });
+ },
+ 'GET /api/404': (req, res) => {
+ res.status(404).send({
+ timestamp: 1513932643431,
+ status: 404,
+ error: 'Not Found',
+ message: 'No message available',
+ path: '/base/category/list/2121212',
+ });
+ },
+ 'GET /api/403': (req, res) => {
+ res.status(403).send({
+ timestamp: 1513932555104,
+ status: 403,
+ error: 'Unauthorized',
+ message: 'Unauthorized',
+ path: '/base/category/list',
+ });
+ },
+ 'GET /api/401': (req, res) => {
+ res.status(401).send({
+ timestamp: 1513932555104,
+ status: 401,
+ error: 'Unauthorized',
+ message: 'Unauthorized',
+ path: '/base/category/list',
+ });
+ },
+ };
+
+ const data = _objectSpread({}, api, chart, geographic, notices, profile, rule, user$1);
+
+ return data;
+});
diff --git a/template/pro/lambda/mock/matchMock.js b/template/pro/lambda/mock/matchMock.js
new file mode 100644
index 0000000000000000000000000000000000000000..d74236126428dd543e48b76cbd9a1b3be1eec21a
--- /dev/null
+++ b/template/pro/lambda/mock/matchMock.js
@@ -0,0 +1,116 @@
+const pathToRegexp = require('path-to-regexp');
+const bodyParser = require('body-parser');
+
+const mockFile = require('./index');
+
+const BODY_PARSED_METHODS = ['post', 'put', 'patch'];
+
+const debug = console.log;
+function parseKey(key) {
+ let method = 'get';
+ let path = key;
+ if (key.indexOf(' ') > -1) {
+ const spliced = key.split(' ');
+ method = spliced[0].toLowerCase();
+ path = spliced[1]; // eslint-disable-line
+ }
+ const routerBasePath = `${path}`;
+ return {
+ method,
+ path: routerBasePath,
+ };
+}
+
+function createHandler(method, path, handler) {
+ return (req, res, next) => {
+ function sendData() {
+ if (typeof handler === 'function') {
+ handler(req, res, next);
+ } else {
+ res.json(handler);
+ }
+ }
+ if (BODY_PARSED_METHODS.includes(method)) {
+ bodyParser.json({ limit: '5mb', strict: false })(req, res, () => {
+ bodyParser.urlencoded({ limit: '5mb', extended: true })(req, res, () => {
+ sendData();
+ });
+ });
+ } else {
+ sendData();
+ }
+ };
+}
+
+function normalizeConfig(config) {
+ return Object.keys(config).reduce((memo, key) => {
+ const handler = config[key];
+ const { method, path } = parseKey(key);
+ const keys = [];
+ const re = pathToRegexp(path, keys);
+ memo.push({
+ method,
+ path,
+ re,
+ keys,
+ handler: createHandler(method, path, handler),
+ });
+ return memo;
+ }, []);
+}
+
+const mockData = normalizeConfig(mockFile);
+
+function matchMock(req) {
+ const { path: exceptPath } = req;
+ const exceptMethod = req.method.toLowerCase();
+ function decodeParam(val) {
+ if (typeof val !== 'string' || val.length === 0) {
+ return val;
+ }
+
+ try {
+ return decodeURIComponent(val);
+ } catch (err) {
+ if (err instanceof URIError) {
+ err.message = `Failed to decode param ' ${val} '`;
+ err.statusCode = 400;
+ err.status = 400;
+ }
+
+ throw err;
+ }
+ }
+ // eslint-disable-next-line no-restricted-syntax
+ for (const mock of mockData) {
+ const { method, re, keys } = mock;
+ if (method === exceptMethod) {
+ const match = re.exec(req.path);
+ if (match) {
+ const params = {};
+
+ for (let i = 1; i < match.length; i += 1) {
+ const key = keys[i - 1];
+ const prop = key.name;
+ const val = decodeParam(match[i]);
+
+ if (val !== undefined || !hasOwnProperty.call(params, prop)) {
+ params[prop] = val;
+ }
+ }
+ req.params = params;
+ return mock;
+ }
+ }
+ }
+
+ return mockData.filter(({ method, re }) => method === exceptMethod && re.test(exceptPath))[0];
+}
+module.exports = (req, res, next) => {
+ const match = matchMock(req);
+ if (match) {
+ debug(`mock matched: [${match.method}] ${match.path}`);
+ return match.handler(req, res, next);
+ }
+ return next();
+};
diff --git a/template/pro/mock/notices.ts b/template/pro/mock/notices.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b9e3bf29487d895fec60267f5cba32a783efe135
--- /dev/null
+++ b/template/pro/mock/notices.ts
@@ -0,0 +1,105 @@
+import { Request, Response } from 'express';
+
+const getNotices = (req: Request, res: Response) => {
+ res.json([
+ {
+ id: '000000001',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+ title: '你收到了 14 份新周报',
+ datetime: '2017-08-09',
+ type: 'notification',
+ },
+ {
+ id: '000000002',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
+ title: '你推荐的 曲妮妮 已通过第三轮面试',
+ datetime: '2017-08-08',
+ type: 'notification',
+ },
+ {
+ id: '000000003',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
+ title: '这种模板可以区分多种通知类型',
+ datetime: '2017-08-07',
+ read: true,
+ type: 'notification',
+ },
+ {
+ id: '000000004',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
+ title: '左侧图标用于区分不同的类型',
+ datetime: '2017-08-07',
+ type: 'notification',
+ },
+ {
+ id: '000000005',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+ title: '内容不要超过两行字,超出时自动截断',
+ datetime: '2017-08-07',
+ type: 'notification',
+ },
+ {
+ id: '000000006',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+ title: '曲丽丽 评论了你',
+ description: '描述信息描述信息描述信息',
+ datetime: '2017-08-07',
+ type: 'message',
+ clickClose: true,
+ },
+ {
+ id: '000000007',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+ title: '朱偏右 回复了你',
+ description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+ datetime: '2017-08-07',
+ type: 'message',
+ clickClose: true,
+ },
+ {
+ id: '000000008',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+ title: '标题',
+ description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+ datetime: '2017-08-07',
+ type: 'message',
+ clickClose: true,
+ },
+ {
+ id: '000000009',
+ title: '任务名称',
+ description: '任务需要在 2017-01-12 20:00 前启动',
+ extra: '未开始',
+ status: 'todo',
+ type: 'event',
+ },
+ {
+ id: '000000010',
+ title: '第三方紧急代码变更',
+ description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+ extra: '马上到期',
+ status: 'urgent',
+ type: 'event',
+ },
+ {
+ id: '000000011',
+ title: '信息安全考试',
+ description: '指派竹尔于 2017-01-09 前完成更新并发布',
+ extra: '已耗时 8 天',
+ status: 'doing',
+ type: 'event',
+ },
+ {
+ id: '000000012',
+ title: 'ABCD 版本发布',
+ description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+ extra: '进行中',
+ status: 'processing',
+ type: 'event',
+ },
+ ]);
+};
+
+export default {
+ 'GET /api/notices': getNotices,
+};
diff --git a/template/pro/mock/route.ts b/template/pro/mock/route.ts
new file mode 100644
index 0000000000000000000000000000000000000000..418d10f1ab333d66dfed05220d8da9a11110a9e8
--- /dev/null
+++ b/template/pro/mock/route.ts
@@ -0,0 +1,5 @@
+export default {
+ '/api/auth_routes': {
+ '/form/advanced-form': { authority: ['admin', 'user'] },
+ },
+};
diff --git a/template/pro/mock/user.ts b/template/pro/mock/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..80cbd911e73344929647f97c57da1317dbfd49e3
--- /dev/null
+++ b/template/pro/mock/user.ts
@@ -0,0 +1,145 @@
+import { Request, Response } from 'express';
+
+function getFakeCaptcha(req: Request, res: Response) {
+ 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: Request, res: Response) => {
+ const { password, userName, type } = req.body;
+ if (password === 'ant.design' && userName === 'admin') {
+ res.send({
+ status: 'ok',
+ type,
+ currentAuthority: 'admin',
+ });
+ return;
+ }
+ if (password === 'ant.design' && userName === 'user') {
+ res.send({
+ status: 'ok',
+ type,
+ currentAuthority: 'user',
+ });
+ return;
+ }
+ res.send({
+ status: 'error',
+ type,
+ currentAuthority: 'guest',
+ });
+ },
+ 'POST /api/register': (req: Request, res: Response) => {
+ res.send({ status: 'ok', currentAuthority: 'user' });
+ },
+ 'GET /api/500': (req: Request, res: Response) => {
+ res.status(500).send({
+ timestamp: 1513932555104,
+ status: 500,
+ error: 'error',
+ message: 'error',
+ path: '/base/category/list',
+ });
+ },
+ 'GET /api/404': (req: Request, res: Response) => {
+ res.status(404).send({
+ timestamp: 1513932643431,
+ status: 404,
+ error: 'Not Found',
+ message: 'No message available',
+ path: '/base/category/list/2121212',
+ });
+ },
+ 'GET /api/403': (req: Request, res: Response) => {
+ res.status(403).send({
+ timestamp: 1513932555104,
+ status: 403,
+ error: 'Unauthorized',
+ message: 'Unauthorized',
+ path: '/base/category/list',
+ });
+ },
+ 'GET /api/401': (req: Request, res: Response) => {
+ res.status(401).send({
+ timestamp: 1513932555104,
+ status: 401,
+ error: 'Unauthorized',
+ message: 'Unauthorized',
+ path: '/base/category/list',
+ });
+ },
+
+ 'GET /api/login/captcha': getFakeCaptcha,
+};
diff --git a/template/pro/netlify.toml b/template/pro/netlify.toml
new file mode 100644
index 0000000000000000000000000000000000000000..9d3438c4aff06f5a644d10167b5c7133f5175e45
--- /dev/null
+++ b/template/pro/netlify.toml
@@ -0,0 +1,16 @@
+[build]
+ functions = "./functions"
+
+[[redirects]]
+ from = "/api/*"
+ to = "/.netlify/functions/api/:splat"
+ status = 200
+ force = true
+ [redirects.headers]
+ X-From = "Netlify"
+ X-Api-Key = "some-api-key-string"
+
+[[redirects]]
+ from = "/*"
+ to = "/index.html"
+ status = 200
\ No newline at end of file
diff --git a/template/pro/package.json b/template/pro/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..c0f56b46de29d423278ded6dae6e342582aee279
--- /dev/null
+++ b/template/pro/package.json
@@ -0,0 +1,161 @@
+{
+ "name": "ant-design-pro",
+ "version": "4.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",
+ "ui": "umi ui",
+ "deploy": "cross-env ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION=site npm run site && npm run gh-pages",
+ "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./",
+ "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build",
+ "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up",
+ "docker:build": "docker-compose -f ./docker/docker-compose.dev.yml build",
+ "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up",
+ "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro",
+ "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro",
+ "fetch:blocks": "pro fetch-blocks && npm run prettier",
+ "format-imports": "cross-env import-sort --write '**/*.{js,jsx,ts,tsx}'",
+ "functions:build": "netlify-lambda build ./lambda",
+ "functions:run": "cross-env NODE_ENV=dev netlify-lambda serve ./lambda",
+ "gh-pages": "cp CNAME ./dist/ && gh-pages -d dist",
+ "i18n-remove": "pro i18n-remove --locale=zh-CN --write",
+ "lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
+ "lint-staged": "lint-staged",
+ "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
+ "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style",
+ "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
+ "lint:prettier": "check-prettier lint",
+ "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less",
+ "prettier": "prettier -c --write \"**/*\"",
+ "site": "npm run fetch:blocks && npm run build && npm run functions:build",
+ "start": "umi dev",
+ "start:no-mock": "cross-env MOCK=none umi dev",
+ "test": "umi test",
+ "test:all": "node ./tests/run-tests.js",
+ "test:component": "umi test ./src/components"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "npm run lint-staged"
+ }
+ },
+ "lint-staged": {
+ "**/*.less": "stylelint --syntax less",
+ "**/*.{js,jsx,tsx,ts,less,md,json}": [
+ "prettier --write",
+ "git add"
+ ],
+ "**/*.{js,jsx}": "npm run lint-staged:js",
+ "**/*.{js,ts,tsx}": "npm run lint-staged:js"
+ },
+ "browserslist": [
+ "> 1%",
+ "last 2 versions",
+ "not ie <= 10"
+ ],
+ "dependencies": {
+ "@ant-design/colors": "^3.1.0",
+ "@ant-design/pro-layout": "^4.5.16",
+ "@antv/data-set": "^0.10.2",
+ "antd": "^3.23.6",
+ "classnames": "^2.2.6",
+ "dva": "^2.4.1",
+ "lodash": "^4.17.11",
+ "moment": "^2.24.0",
+ "omit.js": "^1.0.2",
+ "path-to-regexp": "^3.1.0",
+ "qs": "^6.9.0",
+ "react": "^16.8.6",
+ "react-copy-to-clipboard": "^5.0.1",
+ "react-document-title": "^2.0.3",
+ "react-dom": "^16.8.6",
+ "redux": "^4.0.1",
+ "slash2": "^2.0.0",
+ "umi": "^2.9.6",
+ "umi-plugin-pro-block": "^1.3.4",
+ "umi-plugin-react": "^1.10.1",
+ "umi-request": "^1.2.7",
+ "webpack-theme-color-replacer": "^1.2.15"
+ },
+ "devDependencies": {
+ "@ant-design/pro-cli": "^1.0.13",
+ "@types/classnames": "^2.2.7",
+ "@types/express": "^4.17.0",
+ "@types/history": "^4.7.2",
+ "@types/jest": "^24.0.13",
+ "@types/lodash": "^4.14.144",
+ "@types/qs": "^6.5.3",
+ "@types/react": "^16.8.19",
+ "@types/react-document-title": "^2.0.3",
+ "@types/react-dom": "^16.8.4",
+ "@umijs/fabric": "^1.2.0",
+ "chalk": "^2.4.2",
+ "check-prettier": "^1.0.3",
+ "cross-env": "^6.0.0",
+ "cross-port-killer": "^1.1.1",
+ "enzyme": "^3.9.0",
+ "eslint": "5.16.0",
+ "express": "^4.17.1",
+ "gh-pages": "^2.0.1",
+ "husky": "^3.0.0",
+ "import-sort-cli": "^6.0.0",
+ "import-sort-parser-babylon": "^6.0.0",
+ "import-sort-parser-typescript": "^6.0.0",
+ "import-sort-style-module": "^6.0.0",
+ "jest-puppeteer": "^4.2.0",
+ "lint-staged": "^9.0.0",
+ "mockjs": "^1.0.1-beta3",
+ "netlify-lambda": "^1.4.13",
+ "node-fetch": "^2.6.0",
+ "prettier": "^1.17.1",
+ "pro-download": "1.0.1",
+ "serverless-http": "^2.0.2",
+ "stylelint": "^10.1.0",
+ "umi-plugin-ga": "^1.1.3",
+ "umi-plugin-pro": "^1.0.2",
+ "umi-types": "^0.5.0"
+ },
+ "optionalDependencies": {
+ "puppeteer": "^1.17.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "checkFiles": [
+ "src/**/*.js*",
+ "src/**/*.ts*",
+ "src/**/*.less",
+ "config/**/*.js*",
+ "scripts/**/*.js"
+ ],
+ "create-umi": {
+ "ignoreScript": [
+ "docker*",
+ "functions*",
+ "site",
+ "generateMock"
+ ],
+ "ignoreDependencies": [
+ "netlify*",
+ "serverless"
+ ],
+ "ignore": [
+ ".dockerignore",
+ ".git",
+ ".gitpod.yml",
+ "CODE_OF_CONDUCT.md",
+ "Dockerfile",
+ "Dockerfile.*",
+ "lambda",
+ "LICENSE",
+ "netlify.toml",
+ "README.*.md",
+ "azure-pipelines.yml",
+ "docker",
+ "CNAME",
+ "create-umi"
+ ]
+ }
+}
diff --git a/template/pro/public/favicon.png b/template/pro/public/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..ece59ce54690c0e1c1e6984ec9dd815645ab4cb8
Binary files /dev/null and b/template/pro/public/favicon.png differ
diff --git a/template/pro/public/icons/icon-128x128.png b/template/pro/public/icons/icon-128x128.png
new file mode 100644
index 0000000000000000000000000000000000000000..48d0e2339a60a637b94319c65e8654289b4f4b6c
Binary files /dev/null and b/template/pro/public/icons/icon-128x128.png differ
diff --git a/template/pro/public/icons/icon-192x192.png b/template/pro/public/icons/icon-192x192.png
new file mode 100644
index 0000000000000000000000000000000000000000..938e9b53f6850d97c693b3e3107968fbced5050c
Binary files /dev/null and b/template/pro/public/icons/icon-192x192.png differ
diff --git a/template/pro/public/icons/icon-512x512.png b/template/pro/public/icons/icon-512x512.png
new file mode 100644
index 0000000000000000000000000000000000000000..21fc108f00294d66855a6c10af022b9d6d0f23b9
Binary files /dev/null and b/template/pro/public/icons/icon-512x512.png differ
diff --git a/template/pro/src/assets/logo.svg b/template/pro/src/assets/logo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e9f8c2a9d78f344a4b020d3b4e972c393b0a1079
--- /dev/null
+++ b/template/pro/src/assets/logo.svg
@@ -0,0 +1,43 @@
+
+
+
+ Group 28 Copy 5
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/template/pro/src/components/Authorized/Authorized.tsx b/template/pro/src/components/Authorized/Authorized.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b5eff807d971975d5352ae39ee0477fb61b2ac24
--- /dev/null
+++ b/template/pro/src/components/Authorized/Authorized.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import check, { IAuthorityType } from './CheckPermissions';
+
+import AuthorizedRoute from './AuthorizedRoute';
+import Secured from './Secured';
+
+interface AuthorizedProps {
+ authority: IAuthorityType;
+ noMatch?: React.ReactNode;
+}
+
+type IAuthorizedType = React.FunctionComponent & {
+ Secured: typeof Secured;
+ check: typeof check;
+ AuthorizedRoute: typeof AuthorizedRoute;
+};
+
+const Authorized: React.FunctionComponent = ({
+ children,
+ authority,
+ noMatch = null,
+}) => {
+ const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
+ const dom = check(authority, childrenRender, noMatch);
+ return <>{dom}>;
+};
+
+export default Authorized as IAuthorizedType;
diff --git a/template/pro/src/components/Authorized/AuthorizedRoute.tsx b/template/pro/src/components/Authorized/AuthorizedRoute.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7743eaeea67b126f9f8c5d6c8f023b7ad5a75b02
--- /dev/null
+++ b/template/pro/src/components/Authorized/AuthorizedRoute.tsx
@@ -0,0 +1,33 @@
+import { Redirect, Route } from 'umi';
+
+import React from 'react';
+import Authorized from './Authorized';
+import { IAuthorityType } from './CheckPermissions';
+
+interface AuthorizedRoutePops {
+ currentAuthority: string;
+ component: React.ComponentClass;
+ render: (props: any) => React.ReactNode;
+ redirectPath: string;
+ authority: IAuthorityType;
+}
+
+const AuthorizedRoute: React.SFC = ({
+ component: Component,
+ render,
+ authority,
+ redirectPath,
+ ...rest
+}) => (
+ } />}
+ >
+ (Component ? : render(props))}
+ />
+
+);
+
+export default AuthorizedRoute;
diff --git a/template/pro/src/components/Authorized/CheckPermissions.tsx b/template/pro/src/components/Authorized/CheckPermissions.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..caa15a3873ff97f07c8313282fc6d00b4ca2d5bb
--- /dev/null
+++ b/template/pro/src/components/Authorized/CheckPermissions.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { CURRENT } from './renderAuthorize';
+// eslint-disable-next-line import/no-cycle
+import PromiseRender from './PromiseRender';
+
+export type IAuthorityType =
+ | undefined
+ | string
+ | string[]
+ | Promise
+ | ((currentAuthority: string | string[]) => IAuthorityType);
+
+/**
+ * 通用权限检查方法
+ * Common check permissions method
+ * @param { 权限判定 | Permission judgment } authority
+ * @param { 你的权限 | Your permission description } currentAuthority
+ * @param { 通过的组件 | Passing components } target
+ * @param { 未通过的组件 | no pass components } Exception
+ */
+const checkPermissions = (
+ authority: IAuthorityType,
+ currentAuthority: string | string[],
+ target: T,
+ Exception: K,
+): T | K | React.ReactNode => {
+ // 没有判定权限.默认查看所有
+ // Retirement authority, return target;
+ if (!authority) {
+ return target;
+ }
+ // 数组处理
+ if (Array.isArray(authority)) {
+ if (Array.isArray(currentAuthority)) {
+ if (currentAuthority.some(item => authority.includes(item))) {
+ return target;
+ }
+ } else if (authority.includes(currentAuthority)) {
+ return target;
+ }
+ return Exception;
+ }
+ // string 处理
+ if (typeof authority === 'string') {
+ if (Array.isArray(currentAuthority)) {
+ if (currentAuthority.some(item => authority === item)) {
+ return target;
+ }
+ } else if (authority === currentAuthority) {
+ return target;
+ }
+ return Exception;
+ }
+ // Promise 处理
+ if (authority instanceof Promise) {
+ return ok={target} error={Exception} promise={authority} />;
+ }
+ // Function 处理
+ if (typeof authority === 'function') {
+ try {
+ const bool = authority(currentAuthority);
+ // 函数执行后返回值是 Promise
+ if (bool instanceof Promise) {
+ return 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: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
+ return checkPermissions(authority, CURRENT, target, Exception);
+}
+
+export default check;
diff --git a/template/pro/src/components/Authorized/PromiseRender.tsx b/template/pro/src/components/Authorized/PromiseRender.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..10b8fc9f931e196ab5856bc7186b5b0223ffa102
--- /dev/null
+++ b/template/pro/src/components/Authorized/PromiseRender.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { Spin } from 'antd';
+import isEqual from 'lodash/isEqual';
+import { isComponentClass } from './Secured';
+// eslint-disable-next-line import/no-cycle
+
+interface PromiseRenderProps {
+ ok: T;
+ error: K;
+ promise: Promise;
+}
+
+interface PromiseRenderState {
+ component: React.ComponentClass | React.FunctionComponent;
+}
+
+export default class PromiseRender extends React.Component<
+ PromiseRenderProps,
+ PromiseRenderState
+> {
+ state: PromiseRenderState = {
+ component: () => null,
+ };
+
+ componentDidMount() {
+ this.setRenderComponent(this.props);
+ }
+
+ shouldComponentUpdate = (nextProps: PromiseRenderProps, nextState: PromiseRenderState) => {
+ const { component } = this.state;
+ if (!isEqual(nextProps, this.props)) {
+ this.setRenderComponent(nextProps);
+ }
+ if (nextState.component !== component) return true;
+ return false;
+ };
+
+ // set render Component : ok or error
+ setRenderComponent(props: PromiseRenderProps) {
+ const ok = this.checkIsInstantiation(props.ok);
+ const error = this.checkIsInstantiation(props.error);
+ props.promise
+ .then(() => {
+ this.setState({
+ component: ok,
+ });
+ return true;
+ })
+ .catch(() => {
+ this.setState({
+ component: error,
+ });
+ });
+ }
+
+ // Determine whether the incoming component has been instantiated
+ // AuthorizedRoute is already instantiated
+ // Authorized render is already instantiated, children is no instantiated
+ // Secured is not instantiated
+ checkIsInstantiation = (
+ target: React.ReactNode | React.ComponentClass,
+ ): React.FunctionComponent => {
+ if (isComponentClass(target)) {
+ const Target = target as React.ComponentClass;
+ return (props: any) => ;
+ }
+ if (React.isValidElement(target)) {
+ return (props: any) => React.cloneElement(target, props);
+ }
+ return () => target as (React.ReactNode & null);
+ };
+
+ render() {
+ const { component: Component } = this.state;
+ const { ok, error, promise, ...rest } = this.props;
+
+ return Component ? (
+
+ ) : (
+
+
+
+ );
+ }
+}
diff --git a/template/pro/src/components/Authorized/Secured.tsx b/template/pro/src/components/Authorized/Secured.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0bdbbe4e66788cfd3f0466bbbb5cbe14f4d6524a
--- /dev/null
+++ b/template/pro/src/components/Authorized/Secured.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import CheckPermissions from './CheckPermissions';
+
+/**
+ * 默认不能访问任何页面
+ * default is "NULL"
+ */
+const Exception403 = () => 403;
+
+export const isComponentClass = (component: React.ComponentClass | React.ReactNode): boolean => {
+ if (!component) return false;
+ const proto = Object.getPrototypeOf(component);
+ if (proto === React.Component || proto === Function.prototype) return true;
+ return isComponentClass(proto);
+};
+
+// Determine whether the incoming component has been instantiated
+// AuthorizedRoute is already instantiated
+// Authorized render is already instantiated, children is no instantiated
+// Secured is not instantiated
+const checkIsInstantiation = (target: React.ComponentClass | React.ReactNode) => {
+ if (isComponentClass(target)) {
+ const Target = target as React.ComponentClass;
+ return (props: any) => ;
+ }
+ if (React.isValidElement(target)) {
+ return (props: any) => React.cloneElement(target, props);
+ }
+ return () => target;
+};
+
+/**
+ * 用于判断是否拥有权限访问此 view 权限
+ * authority 支持传入 string, () => boolean | Promise
+ * e.g. 'user' 只有 user 用户能访问
+ * e.g. 'user,admin' user 和 admin 都能访问
+ * e.g. ()=>boolean 返回true能访问,返回false不能访问
+ * e.g. Promise then 能访问 catch不能访问
+ * e.g. authority support incoming string, () => boolean | Promise
+ * e.g. 'user' only user user can access
+ * e.g. 'user, admin' user and admin can access
+ * e.g. () => boolean true to be able to visit, return false can not be accessed
+ * e.g. Promise then can not access the visit to catch
+ * @param {string | function | Promise} authority
+ * @param {ReactNode} error 非必需参数
+ */
+const authorize = (authority: string, error?: React.ReactNode) => {
+ /**
+ * conversion into a class
+ * 防止传入字符串时找不到staticContext造成报错
+ * String parameters can cause staticContext not found error
+ */
+ let classError: boolean | React.FunctionComponent = false;
+ if (error) {
+ classError = (() => error) as React.FunctionComponent;
+ }
+ if (!authority) {
+ throw new Error('authority is required');
+ }
+ return function decideAuthority(target: React.ComponentClass | React.ReactNode) {
+ const component = CheckPermissions(authority, target, classError || Exception403);
+ return checkIsInstantiation(component);
+ };
+};
+
+export default authorize;
diff --git a/template/pro/src/components/Authorized/index.tsx b/template/pro/src/components/Authorized/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9bed79676a921622a50e415b8fd5e9fec5e384a1
--- /dev/null
+++ b/template/pro/src/components/Authorized/index.tsx
@@ -0,0 +1,13 @@
+import Authorized from './Authorized';
+import AuthorizedRoute from './AuthorizedRoute';
+import Secured from './Secured';
+import check from './CheckPermissions';
+import renderAuthorize from './renderAuthorize';
+
+Authorized.Secured = Secured;
+Authorized.AuthorizedRoute = AuthorizedRoute;
+Authorized.check = check;
+
+const RenderAuthorize = renderAuthorize(Authorized);
+
+export default RenderAuthorize;
diff --git a/template/pro/src/components/Authorized/renderAuthorize.ts b/template/pro/src/components/Authorized/renderAuthorize.ts
new file mode 100644
index 0000000000000000000000000000000000000000..df008750168e2473f16c1cae5bc518b628186fc7
--- /dev/null
+++ b/template/pro/src/components/Authorized/renderAuthorize.ts
@@ -0,0 +1,30 @@
+/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable import/no-mutable-exports */
+let CURRENT: string | string[] = 'NULL';
+
+type CurrentAuthorityType = string | string[] | (() => typeof CURRENT);
+/**
+ * use authority or getAuthority
+ * @param {string|()=>String} currentAuthority
+ */
+const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => (
+ currentAuthority: CurrentAuthorityType,
+): T => {
+ if (currentAuthority) {
+ if (typeof currentAuthority === 'function') {
+ CURRENT = currentAuthority();
+ }
+ if (
+ Object.prototype.toString.call(currentAuthority) === '[object String]' ||
+ Array.isArray(currentAuthority)
+ ) {
+ CURRENT = currentAuthority as string[];
+ }
+ } else {
+ CURRENT = 'NULL';
+ }
+ return Authorized;
+};
+
+export { CURRENT };
+export default (Authorized: T) => renderAuthorize(Authorized);
diff --git a/template/pro/src/components/CopyBlock/index.less b/template/pro/src/components/CopyBlock/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..83d899a141fc4a71f6e5b26e762202dbbf8869b4
--- /dev/null
+++ b/template/pro/src/components/CopyBlock/index.less
@@ -0,0 +1,29 @@
+.copy-block {
+ position: fixed;
+ right: 80px;
+ bottom: 40px;
+ z-index: 99;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ font-size: 20px;
+ background: #fff;
+ border-radius: 40px;
+ box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14),
+ 0 1px 10px 0 rgba(0, 0, 0, 0.12);
+ cursor: pointer;
+}
+
+.copy-block-view {
+ position: relative;
+ .copy-block-code {
+ display: inline-block;
+ margin: 0 0.2em;
+ padding: 0.2em 0.4em 0.1em;
+ font-size: 85%;
+ border-radius: 3px;
+ }
+}
diff --git a/template/pro/src/components/CopyBlock/index.tsx b/template/pro/src/components/CopyBlock/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ccdda639c9ca1d54813f973ac309e8bd52a858ed
--- /dev/null
+++ b/template/pro/src/components/CopyBlock/index.tsx
@@ -0,0 +1,80 @@
+import { Icon, Popover, Typography } from 'antd';
+import React, { useRef } from 'react';
+
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import { connect } from 'dva';
+import { isAntDesignPro } from '@/utils/utils';
+import styles from './index.less';
+
+const firstUpperCase = (pathString: string): string =>
+ pathString
+ .replace('.', '')
+ .split(/\/|-/)
+ .map((s): string => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()))
+ .filter((s): boolean => !!s)
+ .join('');
+
+// when click block copy, send block url to ga
+const onBlockCopy = (label: string) => {
+ if (!isAntDesignPro()) {
+ return;
+ }
+
+ const ga = window && window.ga;
+ if (ga) {
+ ga('send', 'event', {
+ eventCategory: 'block',
+ eventAction: 'copy',
+ eventLabel: label,
+ });
+ }
+};
+
+const BlockCodeView: React.SFC<{
+ url: string;
+}> = ({ url }) => {
+ const blockUrl = `npx umi block add ${firstUpperCase(url)} --path=${url}`;
+ return (
+
+
onBlockCopy(url),
+ }}
+ style={{
+ display: 'flex',
+ }}
+ >
+
+ {blockUrl}
+
+
+
+ );
+};
+
+interface RoutingType {
+ location: {
+ pathname: string;
+ };
+}
+
+export default connect(({ routing }: { routing: RoutingType }) => ({
+ location: routing.location,
+}))(({ location }: RoutingType) => {
+ const url = location.pathname;
+ const divDom = useRef(null);
+ return (
+ }
+ placement="topLeft"
+ content={ }
+ trigger="click"
+ getPopupContainer={dom => (divDom.current ? divDom.current : dom)}
+ >
+
+
+
+
+ );
+});
diff --git a/template/pro/src/components/GlobalHeader/AvatarDropdown.tsx b/template/pro/src/components/GlobalHeader/AvatarDropdown.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9a7f0218283b156e9d894a891d23183333b6a182
--- /dev/null
+++ b/template/pro/src/components/GlobalHeader/AvatarDropdown.tsx
@@ -0,0 +1,75 @@
+import { Avatar, Icon, Menu, Spin } from 'antd';
+import { ClickParam } from 'antd/es/menu';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+import React from 'react';
+import { connect } from 'dva';
+import router from 'umi/router';
+
+import { ConnectProps, ConnectState } from '@/models/connect';
+import { CurrentUser } from '@/models/user';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+export interface GlobalHeaderRightProps extends ConnectProps {
+ currentUser?: CurrentUser;
+ menu?: boolean;
+}
+
+class AvatarDropdown extends React.Component {
+ onMenuClick = (event: ClickParam) => {
+ const { key } = event;
+
+ if (key === 'logout') {
+ const { dispatch } = this.props;
+ if (dispatch) {
+ dispatch({
+ type: 'login/logout',
+ });
+ }
+
+ return;
+ }
+ router.push(`/account/${key}`);
+ };
+
+ render(): React.ReactNode {
+ const { currentUser = { avatar: '', name: '' }, menu } = this.props;
+
+ const menuHeaderDropdown = (
+
+ {menu && (
+
+
+
+
+ )}
+ {menu && (
+
+
+
+
+ )}
+ {menu && }
+
+
+
+
+
+
+ );
+
+ return currentUser && currentUser.name ? (
+
+
+
+ {currentUser.name}
+
+
+ ) : (
+
+ );
+ }
+}
+export default connect(({ user }: ConnectState) => ({
+ currentUser: user.currentUser,
+}))(AvatarDropdown);
diff --git a/template/pro/src/components/GlobalHeader/NoticeIconView.tsx b/template/pro/src/components/GlobalHeader/NoticeIconView.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0f019060fbe43f40267caf18426013a88c408446
--- /dev/null
+++ b/template/pro/src/components/GlobalHeader/NoticeIconView.tsx
@@ -0,0 +1,154 @@
+import React, { Component } from 'react';
+import { Tag, message } from 'antd';
+import { connect } from 'dva';
+import { formatMessage } from 'umi-plugin-react/locale';
+import groupBy from 'lodash/groupBy';
+import moment from 'moment';
+
+import { NoticeItem } from '@/models/global';
+import NoticeIcon from '../NoticeIcon';
+import { CurrentUser } from '@/models/user';
+import { ConnectProps, ConnectState } from '@/models/connect';
+import styles from './index.less';
+
+export interface GlobalHeaderRightProps extends ConnectProps {
+ notices?: NoticeItem[];
+ currentUser?: CurrentUser;
+ fetchingNotices?: boolean;
+ onNoticeVisibleChange?: (visible: boolean) => void;
+ onNoticeClear?: (tabName?: string) => void;
+}
+
+class GlobalHeaderRight extends Component {
+ componentDidMount() {
+ const { dispatch } = this.props;
+ if (dispatch) {
+ dispatch({
+ type: 'global/fetchNotices',
+ });
+ }
+ }
+
+ changeReadState = (clickedItem: NoticeItem): void => {
+ const { id } = clickedItem;
+ const { dispatch } = this.props;
+ if (dispatch) {
+ dispatch({
+ type: 'global/changeNoticeReadState',
+ payload: id,
+ });
+ }
+ };
+
+ handleNoticeClear = (title: string, key: string) => {
+ const { dispatch } = this.props;
+ message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`);
+ if (dispatch) {
+ dispatch({
+ type: 'global/clearNotices',
+ payload: key,
+ });
+ }
+ };
+
+ getNoticeData = (): { [key: string]: NoticeItem[] } => {
+ const { notices = [] } = this.props;
+ if (notices.length === 0) {
+ return {};
+ }
+ const newNotices = notices.map(notice => {
+ const newNotice = { ...notice };
+ if (newNotice.datetime) {
+ newNotice.datetime = moment(notice.datetime as string).fromNow();
+ }
+ if (newNotice.id) {
+ newNotice.key = newNotice.id;
+ }
+ if (newNotice.extra && newNotice.status) {
+ const color = {
+ todo: '',
+ processing: 'blue',
+ urgent: 'red',
+ doing: 'gold',
+ }[newNotice.status];
+ newNotice.extra = (
+
+ {newNotice.extra}
+
+ );
+ }
+ return newNotice;
+ });
+ return groupBy(newNotices, 'type');
+ };
+
+ getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
+ const unreadMsg: { [key: string]: number } = {};
+ Object.keys(noticeData).forEach(key => {
+ const value = noticeData[key];
+ if (!unreadMsg[key]) {
+ unreadMsg[key] = 0;
+ }
+ if (Array.isArray(value)) {
+ unreadMsg[key] = value.filter(item => !item.read).length;
+ }
+ });
+ return unreadMsg;
+ };
+
+ render() {
+ const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props;
+ const noticeData = this.getNoticeData();
+ const unreadMsg = this.getUnreadData(noticeData);
+
+ return (
+ {
+ this.changeReadState(item as NoticeItem);
+ }}
+ loading={fetchingNotices}
+ clearText={formatMessage({ id: 'component.noticeIcon.clear' })}
+ viewMoreText={formatMessage({ id: 'component.noticeIcon.view-more' })}
+ onClear={this.handleNoticeClear}
+ onPopupVisibleChange={onNoticeVisibleChange}
+ onViewMore={() => message.info('Click on view more')}
+ clearClose
+ >
+
+
+
+
+ );
+ }
+}
+
+export default connect(({ user, global, loading }: ConnectState) => ({
+ currentUser: user.currentUser,
+ collapsed: global.collapsed,
+ fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
+ fetchingNotices: loading.effects['global/fetchNotices'],
+ notices: global.notices,
+}))(GlobalHeaderRight);
diff --git a/template/pro/src/components/GlobalHeader/RightContent.tsx b/template/pro/src/components/GlobalHeader/RightContent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..333d1b543d079d05cdc44d4787e2bc779273dca2
--- /dev/null
+++ b/template/pro/src/components/GlobalHeader/RightContent.tsx
@@ -0,0 +1,75 @@
+import { Icon, Tooltip } from 'antd';
+import React from 'react';
+import { connect } from 'dva';
+import { formatMessage } from 'umi-plugin-react/locale';
+import { ConnectProps, ConnectState } from '@/models/connect';
+
+import Avatar from './AvatarDropdown';
+import HeaderSearch from '../HeaderSearch';
+import SelectLang from '../SelectLang';
+import styles from './index.less';
+
+export type SiderTheme = 'light' | 'dark';
+export interface GlobalHeaderRightProps extends ConnectProps {
+ theme?: SiderTheme;
+ layout: 'sidemenu' | 'topmenu';
+}
+
+const GlobalHeaderRight: React.SFC = props => {
+ const { theme, layout } = props;
+ let className = styles.right;
+
+ if (theme === 'dark' && layout === 'topmenu') {
+ className = `${styles.right} ${styles.dark}`;
+ }
+
+ return (
+
+
{
+ console.log('input', value);
+ }}
+ onPressEnter={value => {
+ console.log('enter', value);
+ }}
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default connect(({ settings }: ConnectState) => ({
+ theme: settings.navTheme,
+ layout: settings.layout,
+}))(GlobalHeaderRight);
diff --git a/template/pro/src/components/GlobalHeader/index.less b/template/pro/src/components/GlobalHeader/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..590a14fadbcc044e4f95723da7ce13133ba4ae92
--- /dev/null
+++ b/template/pro/src/components/GlobalHeader/index.less
@@ -0,0 +1,134 @@
+@import '~antd/es/style/themes/default.less';
+
+@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
+
+.logo {
+ display: inline-block;
+ height: @layout-header-height;
+ padding: 0 0 0 24px;
+ font-size: 20px;
+ line-height: @layout-header-height;
+ vertical-align: top;
+ cursor: pointer;
+ img {
+ display: inline-block;
+ vertical-align: middle;
+ }
+}
+
+.menu {
+ :global(.anticon) {
+ margin-right: 8px;
+ }
+ :global(.ant-dropdown-menu-item) {
+ min-width: 160px;
+ }
+}
+
+.trigger {
+ height: @layout-header-height;
+ padding: ~'calc((@{layout-header-height} - 20px) / 2)' 24px;
+ font-size: 20px;
+ cursor: pointer;
+ transition: all 0.3s, padding 0s;
+ &:hover {
+ background: @pro-header-hover-bg;
+ }
+}
+
+.right {
+ float: right;
+ height: 100%;
+ margin-left: auto;
+ overflow: hidden;
+ .action {
+ display: inline-block;
+ height: 100%;
+ padding: 0 12px;
+ cursor: pointer;
+ transition: all 0.3s;
+ > i {
+ color: @text-color;
+ vertical-align: middle;
+ }
+ &:hover {
+ background: @pro-header-hover-bg;
+ }
+ &:global(.opened) {
+ background: @pro-header-hover-bg;
+ }
+ }
+ .search {
+ padding: 0 12px;
+ &:hover {
+ background: transparent;
+ }
+ }
+ .account {
+ .avatar {
+ margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
+ margin-right: 8px;
+ color: @primary-color;
+ vertical-align: top;
+ background: rgba(255, 255, 255, 0.85);
+ }
+ }
+}
+
+.dark {
+ height: @layout-header-height;
+ .action {
+ color: rgba(255, 255, 255, 0.85);
+ > i {
+ color: rgba(255, 255, 255, 0.85);
+ }
+ &:hover,
+ &:global(.opened) {
+ background: @primary-color;
+ }
+ }
+}
+
+:global(.ant-pro-global-header) {
+ .dark {
+ .action {
+ color: @text-color;
+ > i {
+ color: @text-color;
+ }
+ &:hover {
+ color: rgba(255, 255, 255, 0.85);
+ > i {
+ color: rgba(255, 255, 255, 0.85);
+ }
+ }
+ }
+ }
+}
+
+@media only screen and (max-width: @screen-md) {
+ :global(.ant-divider-vertical) {
+ vertical-align: unset;
+ }
+ .name {
+ display: none;
+ }
+ i.trigger {
+ padding: 22px 12px;
+ }
+ .logo {
+ position: relative;
+ padding-right: 12px;
+ padding-left: 12px;
+ }
+ .right {
+ position: absolute;
+ top: 0;
+ right: 12px;
+ .account {
+ .avatar {
+ margin-right: 0;
+ }
+ }
+ }
+}
diff --git a/template/pro/src/components/HeaderDropdown/index.less b/template/pro/src/components/HeaderDropdown/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..c2b98585db108abd4be9b8a2fec373e926bf26ca
--- /dev/null
+++ b/template/pro/src/components/HeaderDropdown/index.less
@@ -0,0 +1,16 @@
+@import '~antd/es/style/themes/default.less';
+
+.container > * {
+ background-color: #fff;
+ border-radius: 4px;
+ box-shadow: @shadow-1-down;
+}
+
+@media screen and (max-width: @screen-xs) {
+ .container {
+ width: 100% !important;
+ }
+ .container > * {
+ border-radius: 0 !important;
+ }
+}
diff --git a/template/pro/src/components/HeaderDropdown/index.tsx b/template/pro/src/components/HeaderDropdown/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f668a2b6615ff431c6940def97455f428ac227e8
--- /dev/null
+++ b/template/pro/src/components/HeaderDropdown/index.tsx
@@ -0,0 +1,19 @@
+import { DropDownProps } from 'antd/es/dropdown';
+import { Dropdown } from 'antd';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+declare type OverlayFunc = () => React.ReactNode;
+
+export interface HeaderDropdownProps extends DropDownProps {
+ overlayClassName?: string;
+ overlay: React.ReactNode | OverlayFunc;
+ placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
+}
+
+const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => (
+
+);
+
+export default HeaderDropdown;
diff --git a/template/pro/src/components/HeaderSearch/index.less b/template/pro/src/components/HeaderSearch/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..8f40cc7f5a742d90bf1f2844f836c172089c1fad
--- /dev/null
+++ b/template/pro/src/components/HeaderSearch/index.less
@@ -0,0 +1,32 @@
+@import '~antd/es/style/themes/default.less';
+
+.headerSearch {
+ :global(.anticon-search) {
+ font-size: 16px;
+ cursor: pointer;
+ }
+ .input {
+ width: 0;
+ background: transparent;
+ border-radius: 0;
+ transition: width 0.3s, margin-left 0.3s;
+ :global(.ant-select-selection) {
+ background: transparent;
+ }
+ input {
+ padding-right: 0;
+ padding-left: 0;
+ border: 0;
+ box-shadow: none !important;
+ }
+ &,
+ &:hover,
+ &:focus {
+ border-bottom: 1px solid @border-color-base;
+ }
+ &.show {
+ width: 210px;
+ margin-left: 8px;
+ }
+ }
+}
diff --git a/template/pro/src/components/HeaderSearch/index.tsx b/template/pro/src/components/HeaderSearch/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..85f8a387f38148f3923b943d3e1652f92e5028c6
--- /dev/null
+++ b/template/pro/src/components/HeaderSearch/index.tsx
@@ -0,0 +1,147 @@
+import { AutoComplete, Icon, Input } from 'antd';
+import { AutoCompleteProps, DataSourceItemType } from 'antd/es/auto-complete';
+import React, { Component } from 'react';
+
+import classNames from 'classnames';
+import debounce from 'lodash/debounce';
+import styles from './index.less';
+
+export interface HeaderSearchProps {
+ onPressEnter: (value: string) => void;
+ onSearch: (value: string) => void;
+ onChange: (value: string) => void;
+ onVisibleChange: (b: boolean) => void;
+ className: string;
+ placeholder: string;
+ defaultActiveFirstOption: boolean;
+ dataSource: DataSourceItemType[];
+ defaultOpen: boolean;
+ open?: boolean;
+ defaultValue?: string;
+}
+
+interface HeaderSearchState {
+ value?: string;
+ searchMode: boolean;
+}
+
+export default class HeaderSearch extends Component {
+ static defaultProps = {
+ defaultActiveFirstOption: false,
+ onPressEnter: () => {},
+ onSearch: () => {},
+ onChange: () => {},
+ className: '',
+ placeholder: '',
+ dataSource: [],
+ defaultOpen: false,
+ onVisibleChange: () => {},
+ };
+
+ static getDerivedStateFromProps(props: HeaderSearchProps) {
+ if ('open' in props) {
+ return {
+ searchMode: props.open,
+ };
+ }
+ return null;
+ }
+
+ private inputRef: Input | null = null;
+
+ constructor(props: HeaderSearchProps) {
+ super(props);
+ this.state = {
+ searchMode: props.defaultOpen,
+ value: props.defaultValue,
+ };
+ this.debouncePressEnter = debounce(this.debouncePressEnter, 500, {
+ leading: true,
+ trailing: false,
+ });
+ }
+
+ onKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ this.debouncePressEnter();
+ }
+ };
+
+ onChange: AutoCompleteProps['onChange'] = value => {
+ if (typeof value === 'string') {
+ const { onSearch, onChange } = this.props;
+ this.setState({ value });
+ if (onSearch) {
+ onSearch(value);
+ }
+ if (onChange) {
+ onChange(value);
+ }
+ }
+ };
+
+ enterSearchMode = () => {
+ const { onVisibleChange } = this.props;
+ onVisibleChange(true);
+ this.setState({ searchMode: true }, () => {
+ const { searchMode } = this.state;
+ if (searchMode && this.inputRef) {
+ this.inputRef.focus();
+ }
+ });
+ };
+
+ leaveSearchMode = () => {
+ this.setState({
+ searchMode: false,
+ });
+ };
+
+ debouncePressEnter = () => {
+ const { onPressEnter } = this.props;
+ const { value } = this.state;
+ onPressEnter(value || '');
+ };
+
+ render() {
+ const { className, defaultValue, placeholder, open, ...restProps } = this.props;
+ const { searchMode, value } = this.state;
+ delete restProps.defaultOpen; // for rc-select not affected
+ const inputClass = classNames(styles.input, {
+ [styles.show]: searchMode,
+ });
+
+ return (
+ {
+ if (propertyName === 'width' && !searchMode) {
+ const { onVisibleChange } = this.props;
+ onVisibleChange(searchMode);
+ }
+ }}
+ >
+
+
+ {
+ this.inputRef = node;
+ }}
+ defaultValue={defaultValue}
+ aria-label={placeholder}
+ placeholder={placeholder}
+ onKeyDown={this.onKeyDown}
+ onBlur={this.leaveSearchMode}
+ />
+
+
+ );
+ }
+}
diff --git a/template/pro/src/components/NoticeIcon/NoticeList.less b/template/pro/src/components/NoticeIcon/NoticeList.less
new file mode 100755
index 0000000000000000000000000000000000000000..ce07d712b8a239711c435fcaa18faecc2808e356
--- /dev/null
+++ b/template/pro/src/components/NoticeIcon/NoticeList.less
@@ -0,0 +1,105 @@
+@import '~antd/es/style/themes/default.less';
+
+.list {
+ max-height: 400px;
+ overflow: auto;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ .item {
+ padding-right: 24px;
+ padding-left: 24px;
+ overflow: hidden;
+ cursor: pointer;
+ transition: all 0.3s;
+
+ .meta {
+ width: 100%;
+ }
+
+ .avatar {
+ margin-top: 4px;
+ background: #fff;
+ }
+ .iconElement {
+ font-size: 32px;
+ }
+
+ &.read {
+ opacity: 0.4;
+ }
+ &:last-child {
+ border-bottom: 0;
+ }
+ &:hover {
+ background: @primary-1;
+ }
+ .title {
+ margin-bottom: 8px;
+ font-weight: normal;
+ }
+ .description {
+ font-size: 12px;
+ line-height: @line-height-base;
+ }
+ .datetime {
+ margin-top: 4px;
+ font-size: 12px;
+ line-height: @line-height-base;
+ }
+ .extra {
+ float: right;
+ margin-top: -1.5px;
+ margin-right: 0;
+ color: @text-color-secondary;
+ font-weight: normal;
+ }
+ }
+ .loadMore {
+ padding: 8px 0;
+ color: @primary-6;
+ text-align: center;
+ cursor: pointer;
+ &.loadedAll {
+ color: rgba(0, 0, 0, 0.25);
+ cursor: unset;
+ }
+ }
+}
+
+.notFound {
+ padding: 73px 0 88px;
+ color: @text-color-secondary;
+ text-align: center;
+ img {
+ display: inline-block;
+ height: 76px;
+ margin-bottom: 16px;
+ }
+}
+
+.bottomBar {
+ height: 46px;
+ color: @text-color;
+ line-height: 46px;
+ text-align: center;
+ border-top: 1px solid @border-color-split;
+ border-radius: 0 0 @border-radius-base @border-radius-base;
+ transition: all 0.3s;
+ div {
+ display: inline-block;
+ width: 50%;
+ cursor: pointer;
+ transition: all 0.3s;
+ user-select: none;
+ &:hover {
+ color: @heading-color;
+ }
+ &:only-child {
+ width: 100%;
+ }
+ &:not(:only-child):last-child {
+ border-left: 1px solid @border-color-split;
+ }
+ }
+}
diff --git a/template/pro/src/components/NoticeIcon/NoticeList.tsx b/template/pro/src/components/NoticeIcon/NoticeList.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f056b5b4ce8c79b00621ead715178c497bc98712
--- /dev/null
+++ b/template/pro/src/components/NoticeIcon/NoticeList.tsx
@@ -0,0 +1,114 @@
+import { Avatar, List } from 'antd';
+
+import React from 'react';
+import classNames from 'classnames';
+import { NoticeIconData } from './index';
+import styles from './NoticeList.less';
+
+export interface NoticeIconTabProps {
+ count?: number;
+ name?: string;
+ showClear?: boolean;
+ showViewMore?: boolean;
+ style?: React.CSSProperties;
+ title: string;
+ tabKey: string;
+ data?: NoticeIconData[];
+ onClick?: (item: NoticeIconData) => void;
+ onClear?: () => void;
+ emptyText?: string;
+ clearText?: string;
+ viewMoreText?: string;
+ list: NoticeIconData[];
+ onViewMore?: (e: any) => void;
+}
+const NoticeList: React.SFC = ({
+ data = [],
+ onClick,
+ onClear,
+ title,
+ onViewMore,
+ emptyText,
+ showClear = true,
+ clearText,
+ viewMoreText,
+ showViewMore = false,
+}) => {
+ if (data.length === 0) {
+ return (
+
+
+
{emptyText}
+
+ );
+ }
+ return (
+
+
+ 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' ? (
+
+ ) : (
+ {item.avatar}
+ )
+ ) : null;
+
+ return (
+ onClick && onClick(item)}
+ >
+
+ {item.title}
+ {item.extra}
+
+ }
+ description={
+
+
{item.description}
+
{item.datetime}
+
+ }
+ />
+
+ );
+ }}
+ />
+
+ {showClear ? (
+
+ {clearText} {title}
+
+ ) : null}
+ {showViewMore ? (
+
{
+ if (onViewMore) {
+ onViewMore(e);
+ }
+ }}
+ >
+ {viewMoreText}
+
+ ) : null}
+
+
+ );
+};
+
+export default NoticeList;
diff --git a/template/pro/src/components/NoticeIcon/index.less b/template/pro/src/components/NoticeIcon/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..650ccd2ddd94e7525004840f3acba01c718e502d
--- /dev/null
+++ b/template/pro/src/components/NoticeIcon/index.less
@@ -0,0 +1,31 @@
+@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;
+ }
+ }
+}
diff --git a/template/pro/src/components/NoticeIcon/index.tsx b/template/pro/src/components/NoticeIcon/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bb3caa4e05e441996a499fc7dd5063c2e27c488c
--- /dev/null
+++ b/template/pro/src/components/NoticeIcon/index.tsx
@@ -0,0 +1,175 @@
+import { Badge, Icon, Spin, Tabs } from 'antd';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import NoticeList, { NoticeIconTabProps } from './NoticeList';
+
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+const { TabPane } = Tabs;
+
+export interface NoticeIconData {
+ avatar?: string | React.ReactNode;
+ title?: React.ReactNode;
+ description?: React.ReactNode;
+ datetime?: React.ReactNode;
+ extra?: React.ReactNode;
+ style?: React.CSSProperties;
+ key?: string | number;
+ read?: boolean;
+}
+
+export interface NoticeIconProps {
+ count?: number;
+ bell?: React.ReactNode;
+ className?: string;
+ loading?: boolean;
+ onClear?: (tabName: string, tabKey: string) => void;
+ onItemClick?: (item: NoticeIconData, tabProps: NoticeIconTabProps) => void;
+ onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void;
+ onTabChange?: (tabTile: string) => void;
+ style?: React.CSSProperties;
+ onPopupVisibleChange?: (visible: boolean) => void;
+ popupVisible?: boolean;
+ clearText?: string;
+ viewMoreText?: string;
+ clearClose?: boolean;
+ children: React.ReactElement[];
+}
+
+export default class NoticeIcon extends Component {
+ public static Tab: typeof NoticeList = NoticeList;
+
+ static defaultProps = {
+ onItemClick: (): void => {},
+ onPopupVisibleChange: (): void => {},
+ onTabChange: (): void => {},
+ onClear: (): void => {},
+ onViewMore: (): void => {},
+ loading: false,
+ clearClose: false,
+ emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
+ };
+
+ state = {
+ visible: false,
+ };
+
+ onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps): void => {
+ const { onItemClick } = this.props;
+ if (onItemClick) {
+ onItemClick(item, tabProps);
+ }
+ };
+
+ onClear = (name: string, key: string): void => {
+ const { onClear } = this.props;
+ if (onClear) {
+ onClear(name, key);
+ }
+ };
+
+ onTabChange = (tabType: string): void => {
+ const { onTabChange } = this.props;
+ if (onTabChange) {
+ onTabChange(tabType);
+ }
+ };
+
+ onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent): void => {
+ const { onViewMore } = this.props;
+ if (onViewMore) {
+ onViewMore(tabProps, event);
+ }
+ };
+
+ getNotificationBox(): React.ReactNode {
+ const { children, loading, clearText, viewMoreText } = this.props;
+ if (!children) {
+ return null;
+ }
+ const panes = React.Children.map(
+ children,
+ (child: React.ReactElement): React.ReactNode => {
+ if (!child) {
+ return null;
+ }
+ const { list, title, count, tabKey, showClear, showViewMore } = child.props;
+ const len = list && list.length ? list.length : 0;
+ const msgCount = count || count === 0 ? count : len;
+ const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title;
+ return (
+
+ this.onClear(title, tabKey)}
+ onClick={(item): void => this.onItemClick(item, child.props)}
+ onViewMore={(event): void => this.onViewMore(child.props, event)}
+ showClear={showClear}
+ showViewMore={showViewMore}
+ title={title}
+ {...child.props}
+ />
+
+ );
+ },
+ );
+ return (
+ <>
+
+
+ {panes}
+
+
+ >
+ );
+ }
+
+ handleVisibleChange = (visible: boolean): void => {
+ const { onPopupVisibleChange } = this.props;
+ this.setState({ visible });
+ if (onPopupVisibleChange) {
+ onPopupVisibleChange(visible);
+ }
+ };
+
+ render(): React.ReactNode {
+ const { className, count, popupVisible, bell } = this.props;
+ const { visible } = this.state;
+ const noticeButtonClass = classNames(className, styles.noticeButton);
+ const notificationBox = this.getNotificationBox();
+ const NoticeBellIcon = bell || ;
+ const trigger = (
+
+
+ {NoticeBellIcon}
+
+
+ );
+ if (!notificationBox) {
+ return trigger;
+ }
+ const popoverProps: {
+ visible?: boolean;
+ } = {};
+ if ('popupVisible' in this.props) {
+ popoverProps.visible = popupVisible;
+ }
+
+ return (
+
+ {trigger}
+
+ );
+ }
+}
diff --git a/template/pro/src/components/PageLoading/index.tsx b/template/pro/src/components/PageLoading/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0fa952cc1810543a29b469fc9e3424b632c01e5a
--- /dev/null
+++ b/template/pro/src/components/PageLoading/index.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { Spin } from 'antd';
+
+// loading components from code split
+// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
+const PageLoading: React.FC = () => (
+
+
+
+);
+export default PageLoading;
diff --git a/template/pro/src/components/SelectLang/index.less b/template/pro/src/components/SelectLang/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..7cb057ed2524fd00a0802523fc72da24d3efbd55
--- /dev/null
+++ b/template/pro/src/components/SelectLang/index.less
@@ -0,0 +1,24 @@
+@import '~antd/es/style/themes/default.less';
+
+.menu {
+ :global(.anticon) {
+ margin-right: 8px;
+ }
+ :global(.ant-dropdown-menu-item) {
+ min-width: 160px;
+ }
+}
+
+.dropDown {
+ line-height: @layout-header-height;
+ vertical-align: top;
+ cursor: pointer;
+ > i {
+ font-size: 16px !important;
+ transform: none !important;
+ svg {
+ position: relative;
+ top: -1px;
+ }
+ }
+}
diff --git a/template/pro/src/components/SelectLang/index.tsx b/template/pro/src/components/SelectLang/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dc168a533d269557fe94d5515b8e4d5f9ffce7ef
--- /dev/null
+++ b/template/pro/src/components/SelectLang/index.tsx
@@ -0,0 +1,51 @@
+import { Icon, Menu } from 'antd';
+import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale';
+
+import { ClickParam } from 'antd/es/menu';
+import React from 'react';
+import classNames from 'classnames';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+interface SelectLangProps {
+ className?: string;
+}
+const SelectLang: React.FC = props => {
+ const { className } = props;
+ const selectedLang = getLocale();
+ const changeLang = ({ key }: ClickParam): void => setLocale(key, false);
+ 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 = (
+
+ {locales.map(locale => (
+
+
+ {languageIcons[locale]}
+ {' '}
+ {languageLabels[locale]}
+
+ ))}
+
+ );
+ return (
+
+
+
+
+
+ );
+};
+
+export default SelectLang;
diff --git a/template/pro/src/components/SettingDrawer/themeColorClient.ts b/template/pro/src/components/SettingDrawer/themeColorClient.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19bcb055d8ca3ec5a3b347b909d7d703c5591bde
--- /dev/null
+++ b/template/pro/src/components/SettingDrawer/themeColorClient.ts
@@ -0,0 +1,31 @@
+// eslint-disable-next-line eslint-comments/disable-enable-pair
+/* eslint-disable import/no-extraneous-dependencies */
+import client from 'webpack-theme-color-replacer/client';
+import generate from '@ant-design/colors/lib/generate';
+
+export default {
+ getAntdSerials(color: string): string[] {
+ const lightCount = 9;
+ const divide = 10;
+ // 淡化(即less的tint)
+ let lightens = new Array(lightCount).fill(0);
+ lightens = lightens.map((_, i) => client.varyColor.lighten(color, i / divide));
+ const colorPalettes = generate(color);
+ const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',');
+ return lightens.concat(colorPalettes).concat(rgb);
+ },
+ changeColor(color?: string): Promise {
+ if (!color) {
+ return Promise.resolve();
+ }
+ const options = {
+ // new colors array, one-to-one corresponde with `matchColors`
+ newColors: this.getAntdSerials(color),
+ changeUrl(cssUrl: string): string {
+ // while router is not `hash` mode, it needs absolute path
+ return `/${cssUrl}`;
+ },
+ };
+ return client.changer.changeColor(options, Promise);
+ },
+};
diff --git a/template/pro/src/e2e/__mocks__/antd-pro-merge-less.js b/template/pro/src/e2e/__mocks__/antd-pro-merge-less.js
new file mode 100644
index 0000000000000000000000000000000000000000..f237ddf58ed4ffa78778ce7f9339b647c177a932
--- /dev/null
+++ b/template/pro/src/e2e/__mocks__/antd-pro-merge-less.js
@@ -0,0 +1 @@
+export default undefined;
diff --git a/template/pro/src/e2e/baseLayout.e2e.js b/template/pro/src/e2e/baseLayout.e2e.js
new file mode 100644
index 0000000000000000000000000000000000000000..5d25f5c4fd215dcff9de5461863c76ee40220aae
--- /dev/null
+++ b/template/pro/src/e2e/baseLayout.e2e.js
@@ -0,0 +1,39 @@
+const { uniq } = require('lodash');
+const RouterConfig = require('../../config/config').default.routes;
+
+const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
+
+function formatter(routes, parentPath = '') {
+ const fixedParentPath = parentPath.replace(/\/{1,}/g, '/');
+ let result = [];
+ routes.forEach(item => {
+ if (item.path) {
+ result.push(`${fixedParentPath}/${item.path}`.replace(/\/{1,}/g, '/'));
+ }
+ if (item.routes) {
+ result = result.concat(
+ formatter(item.routes, item.path ? `${fixedParentPath}/${item.path}` : parentPath),
+ );
+ }
+ });
+ return uniq(result.filter(item => !!item));
+}
+
+describe('Ant Design Pro E2E test', () => {
+ const testPage = path => async () => {
+ await page.goto(`${BASE_URL}${path}`);
+ await page.waitForSelector('footer', {
+ timeout: 2000,
+ });
+ const haveFooter = await page.evaluate(
+ () => document.getElementsByTagName('footer').length > 0,
+ );
+ expect(haveFooter).toBeTruthy();
+ };
+
+ const routers = formatter(RouterConfig);
+ console.log('routers', routers);
+ routers.forEach(route => {
+ it(`test pages ${route}`, testPage(route));
+ });
+});
diff --git a/template/pro/src/e2e/topMenu.e2e.js b/template/pro/src/e2e/topMenu.e2e.js
new file mode 100644
index 0000000000000000000000000000000000000000..7c5f8556e81e441cbabe990071c64303eb820d98
--- /dev/null
+++ b/template/pro/src/e2e/topMenu.e2e.js
@@ -0,0 +1,15 @@
+const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
+
+describe('Homepage', () => {
+ it('topmenu should have footer', async () => {
+ const params = '/form/basic-form?navTheme=light&layout=topmenu';
+ await page.goto(`${BASE_URL}${params}`);
+ await page.waitForSelector('footer', {
+ timeout: 2000,
+ });
+ const haveFooter = await page.evaluate(
+ () => document.getElementsByTagName('footer').length > 0,
+ );
+ expect(haveFooter).toBeTruthy();
+ });
+});
diff --git a/template/pro/src/global.less b/template/pro/src/global.less
new file mode 100644
index 0000000000000000000000000000000000000000..b4237f7e6208bb12c2ab1cf22c47a950d031f9b3
--- /dev/null
+++ b/template/pro/src/global.less
@@ -0,0 +1,47 @@
+@import '~antd/es/style/themes/default.less';
+
+html,
+body,
+#root {
+ height: 100%;
+}
+
+.colorWeak {
+ filter: invert(80%);
+}
+
+.ant-layout {
+ min-height: 100vh;
+}
+
+canvas {
+ display: block;
+}
+
+body {
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+ul,
+ol {
+ list-style: none;
+}
+
+@media (max-width: @screen-xs) {
+ .ant-table {
+ width: 100%;
+ overflow-x: auto;
+ &-thead > tr,
+ &-tbody > tr {
+ > th,
+ > td {
+ white-space: pre;
+ > span {
+ display: block;
+ }
+ }
+ }
+ }
+}
diff --git a/template/pro/src/global.tsx b/template/pro/src/global.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..626399fbf9fb09fbfe69b4556fe803797c852a2a
--- /dev/null
+++ b/template/pro/src/global.tsx
@@ -0,0 +1,83 @@
+import { Button, message, notification } from 'antd';
+
+import React from 'react';
+import { formatMessage } from 'umi-plugin-react/locale';
+import defaultSettings from '../config/defaultSettings';
+
+const { pwa } = defaultSettings;
+// if pwa is true
+if (pwa) {
+ // Notify user if offline now
+ window.addEventListener('sw.offline', () => {
+ message.warning(formatMessage({ id: 'app.pwa.offline' }));
+ });
+
+ // Pop up a prompt on the page asking the user if they want to use the latest version
+ window.addEventListener('sw.updated', (event: Event) => {
+ const e = event as CustomEvent;
+ const reloadSW = async () => {
+ // Check if there is sw whose state is waiting in ServiceWorkerRegistration
+ // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
+ const worker = e.detail && e.detail.waiting;
+ if (!worker) {
+ 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 = (
+ {
+ notification.close(key);
+ reloadSW();
+ }}
+ >
+ {formatMessage({ id: 'app.pwa.serviceworker.updated.ok' })}
+
+ );
+ 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);
+ });
+ });
+ }
+}
diff --git a/template/pro/src/layouts/BasicLayout.tsx b/template/pro/src/layouts/BasicLayout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1d7fe2e57927c7b64d93cde6adcd24ffec95561d
--- /dev/null
+++ b/template/pro/src/layouts/BasicLayout.tsx
@@ -0,0 +1,188 @@
+/**
+ * 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, {
+ MenuDataItem,
+ BasicLayoutProps as ProLayoutProps,
+ Settings,
+ DefaultFooter,
+ SettingDrawer
+} from "@ant-design/pro-layout";
+import React, { useEffect } from "react";
+import Link from "umi/link";
+import { Dispatch } from "redux";
+import { connect } from "dva";
+import { Icon } from "antd";
+import { formatMessage } from "umi-plugin-react/locale";
+
+import Authorized from "@/utils/Authorized";
+import RightContent from "@/components/GlobalHeader/RightContent";
+import { ConnectState } from "@/models/connect";
+import { isAntDesignPro } from "@/utils/utils";
+import logo from "../assets/logo.svg";
+
+export interface BasicLayoutProps extends ProLayoutProps {
+ breadcrumbNameMap: {
+ [path: string]: MenuDataItem;
+ };
+ settings: Settings;
+ dispatch: Dispatch;
+}
+export type BasicLayoutContext = { [K in "location"]: BasicLayoutProps[K] } & {
+ breadcrumbNameMap: {
+ [path: string]: MenuDataItem;
+ };
+};
+
+/**
+ * use Authorized check all menu item
+ */
+const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
+ menuList.map(item => {
+ const localItem = {
+ ...item,
+ children: item.children ? menuDataRender(item.children) : []
+ };
+ return Authorized.check(item.authority, localItem, null) as MenuDataItem;
+ });
+
+const defaultFooterDom = (
+ ,
+ href: "https://github.com/ant-design/ant-design-pro",
+ blankTarget: true
+ },
+ {
+ key: "Ant Design",
+ title: "Ant Design",
+ href: "https://ant.design",
+ blankTarget: true
+ }
+ ]}
+ />
+);
+
+const footerRender: BasicLayoutProps["footerRender"] = () => {
+ if (!isAntDesignPro()) {
+ return defaultFooterDom;
+ }
+ return (
+ <>
+ {defaultFooterDom}
+
+ >
+ );
+};
+
+const BasicLayout: React.FC = props => {
+ const { dispatch, children, settings } = props;
+
+ const [settingss, setSettingss] = useState({});
+
+ /**
+ * constructor
+ */
+
+ useEffect(() => {
+ if (dispatch) {
+ dispatch({
+ type: "user/fetchCurrent"
+ });
+ dispatch({
+ type: "settings/getSetting"
+ });
+ }
+ }, []);
+
+ /**
+ * init variables
+ */
+ const handleMenuCollapse = (payload: boolean): void => {
+ if (dispatch) {
+ dispatch({
+ type: "global/changeLayoutCollapsed",
+ payload
+ });
+ }
+ };
+
+ return (
+ <>
+ {
+ if (menuItemProps.isUrl) {
+ return defaultDom;
+ }
+ return {defaultDom};
+ }}
+ breadcrumbRender={(routers = []) => [
+ {
+ path: "/",
+ breadcrumbName: formatMessage({
+ id: "menu.home",
+ defaultMessage: "Home"
+ })
+ },
+ ...routers
+ ]}
+ itemRender={(route, params, routes, paths) => {
+ const first = routes.indexOf(route) === 0;
+ return first ? (
+ {route.breadcrumbName}
+ ) : (
+ {route.breadcrumbName}
+ );
+ }}
+ footerRender={footerRender}
+ menuDataRender={menuDataRender}
+ formatMessage={formatMessage}
+ rightContentRender={rightProps => }
+ {...props}
+ {...settings}
+ >
+ {children}
+
+ setSettingss(config)}
+ />
+ >
+ );
+};
+
+export default connect(({ global, settings }: ConnectState) => ({
+ collapsed: global.collapsed,
+ settings
+}))(BasicLayout);
diff --git a/template/pro/src/layouts/BlankLayout.tsx b/template/pro/src/layouts/BlankLayout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a5ff8c4cfa1909981083292a26ae7966f23d825e
--- /dev/null
+++ b/template/pro/src/layouts/BlankLayout.tsx
@@ -0,0 +1,5 @@
+import React from 'react';
+
+const Layout: React.FC = ({ children }) => {children}
;
+
+export default Layout;
diff --git a/template/pro/src/layouts/SecurityLayout.tsx b/template/pro/src/layouts/SecurityLayout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b485b8c3c953722ea2786575fa98d0a454e2e284
--- /dev/null
+++ b/template/pro/src/layouts/SecurityLayout.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { connect } from 'dva';
+import { Redirect } from 'umi';
+import { stringify } from 'querystring';
+import { ConnectState, ConnectProps } from '@/models/connect';
+import { CurrentUser } from '@/models/user';
+import PageLoading from '@/components/PageLoading';
+
+interface SecurityLayoutProps extends ConnectProps {
+ loading: boolean;
+ currentUser: CurrentUser;
+}
+
+interface SecurityLayoutState {
+ isReady: boolean;
+}
+
+class SecurityLayout extends React.Component {
+ state: SecurityLayoutState = {
+ 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 ;
+ }
+ if (!isLogin) {
+ return ;
+ }
+ return children;
+ }
+}
+
+export default connect(({ user, loading }: ConnectState) => ({
+ currentUser: user.currentUser,
+ loading: loading.models.user,
+}))(SecurityLayout);
diff --git a/template/pro/src/layouts/UserLayout.less b/template/pro/src/layouts/UserLayout.less
new file mode 100755
index 0000000000000000000000000000000000000000..cdc207ef66a07f9b5895182cff8c8b6d48c9c3da
--- /dev/null
+++ b/template/pro/src/layouts/UserLayout.less
@@ -0,0 +1,71 @@
+@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;
+}
diff --git a/template/pro/src/layouts/UserLayout.tsx b/template/pro/src/layouts/UserLayout.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6a792ff2dc7ed02cbf4d254dd6b965018703fdd4
--- /dev/null
+++ b/template/pro/src/layouts/UserLayout.tsx
@@ -0,0 +1,65 @@
+import { DefaultFooter, MenuDataItem, getMenuData, getPageTitle } from '@ant-design/pro-layout';
+import DocumentTitle from 'react-document-title';
+import Link from 'umi/link';
+import React from 'react';
+import { connect } from 'dva';
+import { formatMessage } from 'umi-plugin-react/locale';
+
+import SelectLang from '@/components/SelectLang';
+import { ConnectProps, ConnectState } from '@/models/connect';
+import logo from '../assets/logo.svg';
+import styles from './UserLayout.less';
+
+export interface UserLayoutProps extends ConnectProps {
+ breadcrumbNameMap: { [path: string]: MenuDataItem };
+}
+
+const UserLayout: React.SFC = props => {
+ const {
+ route = {
+ routes: [],
+ },
+ } = props;
+ const { routes = [] } = route;
+ const {
+ children,
+ location = {
+ pathname: '',
+ },
+ } = props;
+ const { breadcrumb } = getMenuData(routes);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
Ant Design
+
+
+
Ant Design 是西湖区最具影响力的 Web 设计规范
+
+ {children}
+
+
+
+
+ );
+};
+
+export default connect(({ settings }: ConnectState) => ({
+ ...settings,
+}))(UserLayout);
diff --git a/template/pro/src/locales/en-US.ts b/template/pro/src/locales/en-US.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3d57988605343a81ded6cabefbd6cdf8930dc43f
--- /dev/null
+++ b/template/pro/src/locales/en-US.ts
@@ -0,0 +1,22 @@
+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,
+};
diff --git a/template/pro/src/locales/en-US/component.ts b/template/pro/src/locales/en-US/component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3ba7eeda69c7cd6edc5fd8d205bd626531737a45
--- /dev/null
+++ b/template/pro/src/locales/en-US/component.ts
@@ -0,0 +1,5 @@
+export default {
+ 'component.tagSelect.expand': 'Expand',
+ 'component.tagSelect.collapse': 'Collapse',
+ 'component.tagSelect.all': 'All',
+};
diff --git a/template/pro/src/locales/en-US/globalHeader.ts b/template/pro/src/locales/en-US/globalHeader.ts
new file mode 100644
index 0000000000000000000000000000000000000000..60b6d4ec2d4608a8daf42407b3b5e8afd414b205
--- /dev/null
+++ b/template/pro/src/locales/en-US/globalHeader.ts
@@ -0,0 +1,17 @@
+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',
+};
diff --git a/template/pro/src/locales/en-US/menu.ts b/template/pro/src/locales/en-US/menu.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8e0026fe3adf3435f1ca9d58732de741169f6755
--- /dev/null
+++ b/template/pro/src/locales/en-US/menu.ts
@@ -0,0 +1,50 @@
+export default {
+ 'menu.welcome': 'Welcome',
+ 'menu.more-blocks': 'More Blocks',
+ 'menu.home': 'Home',
+ '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',
+};
diff --git a/template/pro/src/locales/en-US/pwa.ts b/template/pro/src/locales/en-US/pwa.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ed8d199ead182b5e0834de03410de2eb19167a88
--- /dev/null
+++ b/template/pro/src/locales/en-US/pwa.ts
@@ -0,0 +1,6 @@
+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',
+};
diff --git a/template/pro/src/locales/en-US/settingDrawer.ts b/template/pro/src/locales/en-US/settingDrawer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a644905e756eb70d4a2677d6979a5895d78ebbbb
--- /dev/null
+++ b/template/pro/src/locales/en-US/settingDrawer.ts
@@ -0,0 +1,31 @@
+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',
+};
diff --git a/template/pro/src/locales/en-US/settings.ts b/template/pro/src/locales/en-US/settings.ts
new file mode 100644
index 0000000000000000000000000000000000000000..822dd003ca048a19eef66987ee10d10853fc15d7
--- /dev/null
+++ b/template/pro/src/locales/en-US/settings.ts
@@ -0,0 +1,60 @@
+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',
+};
diff --git a/template/pro/src/locales/pt-BR.ts b/template/pro/src/locales/pt-BR.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ee3733bb575f6230726b2034975fc70400b68bec
--- /dev/null
+++ b/template/pro/src/locales/pt-BR.ts
@@ -0,0 +1,20 @@
+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,
+};
diff --git a/template/pro/src/locales/pt-BR/component.ts b/template/pro/src/locales/pt-BR/component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7cf9999c3ab4296935f65eb6d270ec4c9126c485
--- /dev/null
+++ b/template/pro/src/locales/pt-BR/component.ts
@@ -0,0 +1,5 @@
+export default {
+ 'component.tagSelect.expand': 'Expandir',
+ 'component.tagSelect.collapse': 'Diminuir',
+ 'component.tagSelect.all': 'Todas',
+};
diff --git a/template/pro/src/locales/pt-BR/globalHeader.ts b/template/pro/src/locales/pt-BR/globalHeader.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c92739919227815c35d19e1ccf39949fdb8ba255
--- /dev/null
+++ b/template/pro/src/locales/pt-BR/globalHeader.ts
@@ -0,0 +1,18 @@
+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',
+};
diff --git a/template/pro/src/locales/pt-BR/menu.ts b/template/pro/src/locales/pt-BR/menu.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3666b6b665e4464ed3a2b911fad7cf60199f001d
--- /dev/null
+++ b/template/pro/src/locales/pt-BR/menu.ts
@@ -0,0 +1,51 @@
+export default {
+ 'menu.welcome': 'Welcome',
+ 'menu.more-blocks': 'More Blocks',
+
+ 'menu.home': 'Início',
+ 'menu.login': 'Login',
+ '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',
+};
diff --git a/template/pro/src/locales/pt-BR/pwa.ts b/template/pro/src/locales/pt-BR/pwa.ts
new file mode 100644
index 0000000000000000000000000000000000000000..05cc79784227229ec18ed0b7598e6a03db6bb7d7
--- /dev/null
+++ b/template/pro/src/locales/pt-BR/pwa.ts
@@ -0,0 +1,7 @@
+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',
+};
diff --git a/template/pro/src/locales/pt-BR/settingDrawer.ts b/template/pro/src/locales/pt-BR/settingDrawer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8a10b57e2d9ddf2a4de00eba5b4da1bef0ebedb0
--- /dev/null
+++ b/template/pro/src/locales/pt-BR/settingDrawer.ts
@@ -0,0 +1,32 @@
+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',
+};
diff --git a/template/pro/src/locales/pt-BR/settings.ts b/template/pro/src/locales/pt-BR/settings.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aad2e3877550777446848c55448075d668b4c64a
--- /dev/null
+++ b/template/pro/src/locales/pt-BR/settings.ts
@@ -0,0 +1,60 @@
+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',
+};
diff --git a/template/pro/src/locales/zh-CN.ts b/template/pro/src/locales/zh-CN.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1822d7ba64680613593068536f63045798d4b8e9
--- /dev/null
+++ b/template/pro/src/locales/zh-CN.ts
@@ -0,0 +1,22 @@
+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,
+};
diff --git a/template/pro/src/locales/zh-CN/component.ts b/template/pro/src/locales/zh-CN/component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1f1feadbf6f1494e8da6a1e781a87399f1c46a8a
--- /dev/null
+++ b/template/pro/src/locales/zh-CN/component.ts
@@ -0,0 +1,5 @@
+export default {
+ 'component.tagSelect.expand': '展开',
+ 'component.tagSelect.collapse': '收起',
+ 'component.tagSelect.all': '全部',
+};
diff --git a/template/pro/src/locales/zh-CN/globalHeader.ts b/template/pro/src/locales/zh-CN/globalHeader.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9fd66a5875f088d1071b48e87b1102c128663be8
--- /dev/null
+++ b/template/pro/src/locales/zh-CN/globalHeader.ts
@@ -0,0 +1,17 @@
+export default {
+ 'component.globalHeader.search': '站内搜索',
+ 'component.globalHeader.search.example1': '搜索提示一',
+ 'component.globalHeader.search.example2': '搜索提示二',
+ 'component.globalHeader.search.example3': '搜索提示三',
+ 'component.globalHeader.help': '使用文档',
+ 'component.globalHeader.notification': '通知',
+ 'component.globalHeader.notification.empty': '你已查看所有通知',
+ 'component.globalHeader.message': '消息',
+ 'component.globalHeader.message.empty': '您已读完所有消息',
+ 'component.globalHeader.event': '待办',
+ 'component.globalHeader.event.empty': '你已完成所有待办',
+ 'component.noticeIcon.clear': '清空',
+ 'component.noticeIcon.cleared': '清空了',
+ 'component.noticeIcon.empty': '暂无数据',
+ 'component.noticeIcon.view-more': '查看更多',
+};
diff --git a/template/pro/src/locales/zh-CN/menu.ts b/template/pro/src/locales/zh-CN/menu.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f851fe967537e3af58c0bec054ee200e747bef10
--- /dev/null
+++ b/template/pro/src/locales/zh-CN/menu.ts
@@ -0,0 +1,50 @@
+export default {
+ 'menu.welcome': '欢迎',
+ 'menu.more-blocks': '更多区块',
+ 'menu.home': '首页',
+ 'menu.login': '登录',
+ 'menu.register': '注册',
+ 'menu.register.result': '注册结果',
+ 'menu.dashboard': 'Dashboard',
+ 'menu.dashboard.analysis': '分析页',
+ 'menu.dashboard.monitor': '监控页',
+ 'menu.dashboard.workplace': '工作台',
+ 'menu.exception.403': '403',
+ 'menu.exception.404': '404',
+ 'menu.exception.500': '500',
+ 'menu.form': '表单页',
+ 'menu.form.basic-form': '基础表单',
+ 'menu.form.step-form': '分步表单',
+ 'menu.form.step-form.info': '分步表单(填写转账信息)',
+ 'menu.form.step-form.confirm': '分步表单(确认转账信息)',
+ 'menu.form.step-form.result': '分步表单(完成)',
+ 'menu.form.advanced-form': '高级表单',
+ 'menu.list': '列表页',
+ 'menu.list.table-list': '查询表格',
+ 'menu.list.basic-list': '标准列表',
+ 'menu.list.card-list': '卡片列表',
+ 'menu.list.search-list': '搜索列表',
+ 'menu.list.search-list.articles': '搜索列表(文章)',
+ 'menu.list.search-list.projects': '搜索列表(项目)',
+ 'menu.list.search-list.applications': '搜索列表(应用)',
+ 'menu.profile': '详情页',
+ 'menu.profile.basic': '基础详情页',
+ 'menu.profile.advanced': '高级详情页',
+ 'menu.result': '结果页',
+ 'menu.result.success': '成功页',
+ 'menu.result.fail': '失败页',
+ 'menu.exception': '异常页',
+ 'menu.exception.not-permission': '403',
+ 'menu.exception.not-find': '404',
+ 'menu.exception.server-error': '500',
+ 'menu.exception.trigger': '触发错误',
+ 'menu.account': '个人页',
+ 'menu.account.center': '个人中心',
+ 'menu.account.settings': '个人设置',
+ 'menu.account.trigger': '触发报错',
+ 'menu.account.logout': '退出登录',
+ 'menu.editor': '图形编辑器',
+ 'menu.editor.flow': '流程编辑器',
+ 'menu.editor.mind': '脑图编辑器',
+ 'menu.editor.koni': '拓扑编辑器',
+};
diff --git a/template/pro/src/locales/zh-CN/pwa.ts b/template/pro/src/locales/zh-CN/pwa.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e9504849e7840c963295ac2f732a68136a1e48f0
--- /dev/null
+++ b/template/pro/src/locales/zh-CN/pwa.ts
@@ -0,0 +1,6 @@
+export default {
+ 'app.pwa.offline': '当前处于离线状态',
+ 'app.pwa.serviceworker.updated': '有新内容',
+ 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面',
+ 'app.pwa.serviceworker.updated.ok': '刷新',
+};
diff --git a/template/pro/src/locales/zh-CN/settingDrawer.ts b/template/pro/src/locales/zh-CN/settingDrawer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..15685a4055eb61174e39ad8eb6308cca7517af91
--- /dev/null
+++ b/template/pro/src/locales/zh-CN/settingDrawer.ts
@@ -0,0 +1,31 @@
+export default {
+ 'app.setting.pagestyle': '整体风格设置',
+ 'app.setting.pagestyle.dark': '暗色菜单风格',
+ 'app.setting.pagestyle.light': '亮色菜单风格',
+ 'app.setting.content-width': '内容区域宽度',
+ 'app.setting.content-width.fixed': '定宽',
+ 'app.setting.content-width.fluid': '流式',
+ 'app.setting.themecolor': '主题色',
+ 'app.setting.themecolor.dust': '薄暮',
+ 'app.setting.themecolor.volcano': '火山',
+ 'app.setting.themecolor.sunset': '日暮',
+ 'app.setting.themecolor.cyan': '明青',
+ 'app.setting.themecolor.green': '极光绿',
+ 'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
+ 'app.setting.themecolor.geekblue': '极客蓝',
+ 'app.setting.themecolor.purple': '酱紫',
+ 'app.setting.navigationmode': '导航模式',
+ 'app.setting.sidemenu': '侧边菜单布局',
+ 'app.setting.topmenu': '顶部菜单布局',
+ 'app.setting.fixedheader': '固定 Header',
+ 'app.setting.fixedsidebar': '固定侧边菜单',
+ 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
+ 'app.setting.hideheader': '下滑时隐藏 Header',
+ 'app.setting.hideheader.hint': '固定 Header 时可配置',
+ 'app.setting.othersettings': '其他设置',
+ 'app.setting.weakmode': '色弱模式',
+ 'app.setting.copy': '拷贝设置',
+ 'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
+ 'app.setting.production.hint':
+ '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
+};
diff --git a/template/pro/src/locales/zh-CN/settings.ts b/template/pro/src/locales/zh-CN/settings.ts
new file mode 100644
index 0000000000000000000000000000000000000000..df8af4346207ece2c290d5988381fe0f7714cb1a
--- /dev/null
+++ b/template/pro/src/locales/zh-CN/settings.ts
@@ -0,0 +1,55 @@
+export default {
+ 'app.settings.menuMap.basic': '基本设置',
+ 'app.settings.menuMap.security': '安全设置',
+ 'app.settings.menuMap.binding': '账号绑定',
+ 'app.settings.menuMap.notification': '新消息通知',
+ 'app.settings.basic.avatar': '头像',
+ 'app.settings.basic.change-avatar': '更换头像',
+ 'app.settings.basic.email': '邮箱',
+ 'app.settings.basic.email-message': '请输入您的邮箱!',
+ 'app.settings.basic.nickname': '昵称',
+ 'app.settings.basic.nickname-message': '请输入您的昵称!',
+ 'app.settings.basic.profile': '个人简介',
+ 'app.settings.basic.profile-message': '请输入个人简介!',
+ 'app.settings.basic.profile-placeholder': '个人简介',
+ 'app.settings.basic.country': '国家/地区',
+ 'app.settings.basic.country-message': '请输入您的国家或地区!',
+ 'app.settings.basic.geographic': '所在省市',
+ 'app.settings.basic.geographic-message': '请输入您的所在省市!',
+ 'app.settings.basic.address': '街道地址',
+ 'app.settings.basic.address-message': '请输入您的街道地址!',
+ 'app.settings.basic.phone': '联系电话',
+ 'app.settings.basic.phone-message': '请输入您的联系电话!',
+ 'app.settings.basic.update': '更新基本信息',
+ 'app.settings.security.strong': '强',
+ 'app.settings.security.medium': '中',
+ 'app.settings.security.weak': '弱',
+ 'app.settings.security.password': '账户密码',
+ 'app.settings.security.password-description': '当前密码强度',
+ 'app.settings.security.phone': '密保手机',
+ 'app.settings.security.phone-description': '已绑定手机',
+ 'app.settings.security.question': '密保问题',
+ 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全',
+ 'app.settings.security.email': '备用邮箱',
+ 'app.settings.security.email-description': '已绑定邮箱',
+ 'app.settings.security.mfa': 'MFA 设备',
+ 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认',
+ 'app.settings.security.modify': '修改',
+ 'app.settings.security.set': '设置',
+ 'app.settings.security.bind': '绑定',
+ 'app.settings.binding.taobao': '绑定淘宝',
+ 'app.settings.binding.taobao-description': '当前未绑定淘宝账号',
+ 'app.settings.binding.alipay': '绑定支付宝',
+ 'app.settings.binding.alipay-description': '当前未绑定支付宝账号',
+ 'app.settings.binding.dingding': '绑定钉钉',
+ 'app.settings.binding.dingding-description': '当前未绑定钉钉账号',
+ 'app.settings.binding.bind': '绑定',
+ 'app.settings.notification.password': '账户密码',
+ 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
+ 'app.settings.notification.messages': '系统消息',
+ 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知',
+ 'app.settings.notification.todo': '待办任务',
+ 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知',
+ 'app.settings.open': '开',
+ 'app.settings.close': '关',
+};
diff --git a/template/pro/src/locales/zh-TW.ts b/template/pro/src/locales/zh-TW.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6ad5f931927b7a1ba073620ca58518d098713ed6
--- /dev/null
+++ b/template/pro/src/locales/zh-TW.ts
@@ -0,0 +1,20 @@
+import component from './zh-TW/component';
+import globalHeader from './zh-TW/globalHeader';
+import menu from './zh-TW/menu';
+import pwa from './zh-TW/pwa';
+import settingDrawer from './zh-TW/settingDrawer';
+import settings from './zh-TW/settings';
+
+export default {
+ 'navBar.lang': '語言',
+ 'layout.user.link.help': '幫助',
+ 'layout.user.link.privacy': '隱私',
+ 'layout.user.link.terms': '條款',
+ 'app.preview.down.block': '下載此頁面到本地項目',
+ ...globalHeader,
+ ...menu,
+ ...settingDrawer,
+ ...settings,
+ ...pwa,
+ ...component,
+};
diff --git a/template/pro/src/locales/zh-TW/component.ts b/template/pro/src/locales/zh-TW/component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba48e299a91a29a64e2f9834cf5b0b1d3c080fb4
--- /dev/null
+++ b/template/pro/src/locales/zh-TW/component.ts
@@ -0,0 +1,5 @@
+export default {
+ 'component.tagSelect.expand': '展開',
+ 'component.tagSelect.collapse': '收起',
+ 'component.tagSelect.all': '全部',
+};
diff --git a/template/pro/src/locales/zh-TW/globalHeader.ts b/template/pro/src/locales/zh-TW/globalHeader.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ed5845185c9e040b43d7d79e914d5358175a7adb
--- /dev/null
+++ b/template/pro/src/locales/zh-TW/globalHeader.ts
@@ -0,0 +1,17 @@
+export default {
+ 'component.globalHeader.search': '站內搜索',
+ 'component.globalHeader.search.example1': '搜索提示壹',
+ 'component.globalHeader.search.example2': '搜索提示二',
+ 'component.globalHeader.search.example3': '搜索提示三',
+ 'component.globalHeader.help': '使用手冊',
+ 'component.globalHeader.notification': '通知',
+ 'component.globalHeader.notification.empty': '妳已查看所有通知',
+ 'component.globalHeader.message': '消息',
+ 'component.globalHeader.message.empty': '您已讀完所有消息',
+ 'component.globalHeader.event': '待辦',
+ 'component.globalHeader.event.empty': '妳已完成所有待辦',
+ 'component.noticeIcon.clear': '清空',
+ 'component.noticeIcon.cleared': '清空了',
+ 'component.noticeIcon.empty': '暫無資料',
+ 'component.noticeIcon.view-more': '查看更多',
+};
diff --git a/template/pro/src/locales/zh-TW/menu.ts b/template/pro/src/locales/zh-TW/menu.ts
new file mode 100644
index 0000000000000000000000000000000000000000..414affe1ff1bfd57dfe4b4ca065adb88e562fe40
--- /dev/null
+++ b/template/pro/src/locales/zh-TW/menu.ts
@@ -0,0 +1,51 @@
+export default {
+ 'menu.welcome': '歡迎',
+ 'menu.more-blocks': '更多區塊',
+
+ 'menu.home': '首頁',
+ 'menu.login': '登錄',
+ 'menu.exception.403': '403',
+ 'menu.exception.404': '404',
+ 'menu.exception.500': '500',
+ 'menu.register': '註冊',
+ 'menu.register.result': '註冊結果',
+ 'menu.dashboard': 'Dashboard',
+ 'menu.dashboard.analysis': '分析頁',
+ 'menu.dashboard.monitor': '監控頁',
+ 'menu.dashboard.workplace': '工作臺',
+ 'menu.form': '表單頁',
+ 'menu.form.basic-form': '基礎表單',
+ 'menu.form.step-form': '分步表單',
+ 'menu.form.step-form.info': '分步表單(填寫轉賬信息)',
+ 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)',
+ 'menu.form.step-form.result': '分步表單(完成)',
+ 'menu.form.advanced-form': '高級表單',
+ 'menu.list': '列表頁',
+ 'menu.list.table-list': '查詢表格',
+ 'menu.list.basic-list': '標淮列表',
+ 'menu.list.card-list': '卡片列表',
+ 'menu.list.search-list': '搜索列表',
+ 'menu.list.search-list.articles': '搜索列表(文章)',
+ 'menu.list.search-list.projects': '搜索列表(項目)',
+ 'menu.list.search-list.applications': '搜索列表(應用)',
+ 'menu.profile': '詳情頁',
+ 'menu.profile.basic': '基礎詳情頁',
+ 'menu.profile.advanced': '高級詳情頁',
+ 'menu.result': '結果頁',
+ 'menu.result.success': '成功頁',
+ 'menu.result.fail': '失敗頁',
+ 'menu.account': '個人頁',
+ 'menu.account.center': '個人中心',
+ 'menu.account.settings': '個人設置',
+ 'menu.account.trigger': '觸發報錯',
+ 'menu.account.logout': '退出登錄',
+ 'menu.exception': '异常页',
+ 'menu.exception.not-permission': '403',
+ 'menu.exception.not-find': '404',
+ 'menu.exception.server-error': '500',
+ 'menu.exception.trigger': '触发错误',
+ 'menu.editor': '圖形編輯器',
+ 'menu.editor.flow': '流程編輯器',
+ 'menu.editor.mind': '腦圖編輯器',
+ 'menu.editor.koni': '拓撲編輯器',
+};
diff --git a/template/pro/src/locales/zh-TW/pwa.ts b/template/pro/src/locales/zh-TW/pwa.ts
new file mode 100644
index 0000000000000000000000000000000000000000..108a6e489be8e7567ac6d41cd7d81d7715020b3b
--- /dev/null
+++ b/template/pro/src/locales/zh-TW/pwa.ts
@@ -0,0 +1,6 @@
+export default {
+ 'app.pwa.offline': '當前處於離線狀態',
+ 'app.pwa.serviceworker.updated': '有新內容',
+ 'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面',
+ 'app.pwa.serviceworker.updated.ok': '刷新',
+};
diff --git a/template/pro/src/locales/zh-TW/settingDrawer.ts b/template/pro/src/locales/zh-TW/settingDrawer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..24dc281fa87e613c41824a78bf6561eaa0152a6d
--- /dev/null
+++ b/template/pro/src/locales/zh-TW/settingDrawer.ts
@@ -0,0 +1,31 @@
+export default {
+ 'app.setting.pagestyle': '整體風格設置',
+ 'app.setting.pagestyle.dark': '暗色菜單風格',
+ 'app.setting.pagestyle.light': '亮色菜單風格',
+ 'app.setting.content-width': '內容區域寬度',
+ 'app.setting.content-width.fixed': '定寬',
+ 'app.setting.content-width.fluid': '流式',
+ 'app.setting.themecolor': '主題色',
+ 'app.setting.themecolor.dust': '薄暮',
+ 'app.setting.themecolor.volcano': '火山',
+ 'app.setting.themecolor.sunset': '日暮',
+ 'app.setting.themecolor.cyan': '明青',
+ 'app.setting.themecolor.green': '極光綠',
+ 'app.setting.themecolor.daybreak': '拂曉藍(默認)',
+ 'app.setting.themecolor.geekblue': '極客藍',
+ 'app.setting.themecolor.purple': '醬紫',
+ 'app.setting.navigationmode': '導航模式',
+ 'app.setting.sidemenu': '側邊菜單布局',
+ 'app.setting.topmenu': '頂部菜單布局',
+ 'app.setting.fixedheader': '固定 Header',
+ 'app.setting.fixedsidebar': '固定側邊菜單',
+ 'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置',
+ 'app.setting.hideheader': '下滑時隱藏 Header',
+ 'app.setting.hideheader.hint': '固定 Header 時可配置',
+ 'app.setting.othersettings': '其他設置',
+ 'app.setting.weakmode': '色弱模式',
+ 'app.setting.copy': '拷貝設置',
+ 'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置',
+ 'app.setting.production.hint':
+ '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件',
+};
diff --git a/template/pro/src/locales/zh-TW/settings.ts b/template/pro/src/locales/zh-TW/settings.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dd45151a8a47fcc9367704a3bb947cac24638cba
--- /dev/null
+++ b/template/pro/src/locales/zh-TW/settings.ts
@@ -0,0 +1,55 @@
+export default {
+ 'app.settings.menuMap.basic': '基本設置',
+ 'app.settings.menuMap.security': '安全設置',
+ 'app.settings.menuMap.binding': '賬號綁定',
+ 'app.settings.menuMap.notification': '新消息通知',
+ 'app.settings.basic.avatar': '頭像',
+ 'app.settings.basic.change-avatar': '更換頭像',
+ 'app.settings.basic.email': '郵箱',
+ 'app.settings.basic.email-message': '請輸入您的郵箱!',
+ 'app.settings.basic.nickname': '昵稱',
+ 'app.settings.basic.nickname-message': '請輸入您的昵稱!',
+ 'app.settings.basic.profile': '個人簡介',
+ 'app.settings.basic.profile-message': '請輸入個人簡介!',
+ 'app.settings.basic.profile-placeholder': '個人簡介',
+ 'app.settings.basic.country': '國家/地區',
+ 'app.settings.basic.country-message': '請輸入您的國家或地區!',
+ 'app.settings.basic.geographic': '所在省市',
+ 'app.settings.basic.geographic-message': '請輸入您的所在省市!',
+ 'app.settings.basic.address': '街道地址',
+ 'app.settings.basic.address-message': '請輸入您的街道地址!',
+ 'app.settings.basic.phone': '聯系電話',
+ 'app.settings.basic.phone-message': '請輸入您的聯系電話!',
+ 'app.settings.basic.update': '更新基本信息',
+ 'app.settings.security.strong': '強',
+ 'app.settings.security.medium': '中',
+ 'app.settings.security.weak': '弱',
+ 'app.settings.security.password': '賬戶密碼',
+ 'app.settings.security.password-description': '當前密碼強度',
+ 'app.settings.security.phone': '密保手機',
+ 'app.settings.security.phone-description': '已綁定手機',
+ 'app.settings.security.question': '密保問題',
+ 'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全',
+ 'app.settings.security.email': '備用郵箱',
+ 'app.settings.security.email-description': '已綁定郵箱',
+ 'app.settings.security.mfa': 'MFA 設備',
+ 'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認',
+ 'app.settings.security.modify': '修改',
+ 'app.settings.security.set': '設置',
+ 'app.settings.security.bind': '綁定',
+ 'app.settings.binding.taobao': '綁定淘寶',
+ 'app.settings.binding.taobao-description': '當前未綁定淘寶賬號',
+ 'app.settings.binding.alipay': '綁定支付寶',
+ 'app.settings.binding.alipay-description': '當前未綁定支付寶賬號',
+ 'app.settings.binding.dingding': '綁定釘釘',
+ 'app.settings.binding.dingding-description': '當前未綁定釘釘賬號',
+ 'app.settings.binding.bind': '綁定',
+ 'app.settings.notification.password': '賬戶密碼',
+ 'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知',
+ 'app.settings.notification.messages': '系統消息',
+ 'app.settings.notification.messages-description': '系統消息將以站內信的形式通知',
+ 'app.settings.notification.todo': '待辦任務',
+ 'app.settings.notification.todo-description': '待辦任務將以站內信的形式通知',
+ 'app.settings.open': '開',
+ 'app.settings.close': '關',
+};
diff --git a/template/pro/src/manifest.json b/template/pro/src/manifest.json
new file mode 100644
index 0000000000000000000000000000000000000000..839bc5b5e4a561676fca44a61674d3990b5acd48
--- /dev/null
+++ b/template/pro/src/manifest.json
@@ -0,0 +1,22 @@
+{
+ "name": "Ant Design Pro",
+ "short_name": "Ant Design Pro",
+ "display": "standalone",
+ "start_url": "./?utm_source=homescreen",
+ "theme_color": "#002140",
+ "background_color": "#001529",
+ "icons": [
+ {
+ "src": "icons/icon-192x192.png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "icons/icon-128x128.png",
+ "sizes": "128x128"
+ },
+ {
+ "src": "icons/icon-512x512.png",
+ "sizes": "512x512"
+ }
+ ]
+}
diff --git a/template/pro/src/models/connect.d.ts b/template/pro/src/models/connect.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0875800c8015c6bd6b60a6adb12e3aac03f37e6f
--- /dev/null
+++ b/template/pro/src/models/connect.d.ts
@@ -0,0 +1,40 @@
+import { AnyAction, Dispatch } from 'redux';
+import { MenuDataItem } from '@ant-design/pro-layout';
+import { RouterTypes } from 'umi';
+import { GlobalModelState } from './global';
+import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
+import { UserModelState } from './user';
+import { LoginModelType } from './login';
+
+export { GlobalModelState, SettingModelState, UserModelState };
+
+export interface Loading {
+ global: boolean;
+ effects: { [key: string]: boolean | undefined };
+ models: {
+ global?: boolean;
+ menu?: boolean;
+ setting?: boolean;
+ user?: boolean;
+ login?: boolean;
+ };
+}
+
+export interface ConnectState {
+ global: GlobalModelState;
+ loading: Loading;
+ settings: SettingModelState;
+ user: UserModelState;
+ login: LoginModelType;
+}
+
+export interface Route extends MenuDataItem {
+ routes?: Route[];
+}
+
+/**
+ * @type T: Params matched in dynamic routing
+ */
+export interface ConnectProps extends Partial> {
+ dispatch?: Dispatch;
+}
diff --git a/template/pro/src/models/global.ts b/template/pro/src/models/global.ts
new file mode 100644
index 0000000000000000000000000000000000000000..06504ad9c9afe2af50657df2e18ed07dad989672
--- /dev/null
+++ b/template/pro/src/models/global.ts
@@ -0,0 +1,149 @@
+import { Reducer } from "redux";
+import { Subscription, Effect } from "dva";
+
+import { NoticeIconData } from "@/components/NoticeIcon";
+import { queryNotices } from "@/services/user";
+import { ConnectState } from "./connect";
+
+export interface NoticeItem extends NoticeIconData {
+ id: string;
+ type: string;
+ status: string;
+}
+
+export interface GlobalModelState {
+ collapsed: boolean;
+ notices: NoticeItem[];
+}
+
+export interface GlobalModelType {
+ namespace: "global";
+ state: GlobalModelState;
+ effects: {
+ fetchNotices: Effect;
+ clearNotices: Effect;
+ changeNoticeReadState: Effect;
+ };
+ reducers: {
+ changeLayoutCollapsed: Reducer;
+ saveNotices: Reducer;
+ saveClearedNotices: Reducer;
+ };
+ subscriptions: { setup: Subscription };
+}
+
+const GlobalModel: GlobalModelType = {
+ namespace: "global",
+
+ state: {
+ collapsed: false,
+ notices: []
+ },
+
+ effects: {
+ *fetchNotices(_, { call, put, select }) {
+ const data = yield call(queryNotices);
+ yield put({
+ type: "saveNotices",
+ payload: data
+ });
+ const unreadCount: number = yield select(
+ (state: ConnectState) =>
+ state.global.notices.filter(item => !item.read).length
+ );
+ yield put({
+ type: "user/changeNotifyCount",
+ payload: {
+ totalCount: data.length,
+ unreadCount
+ }
+ });
+ },
+ *clearNotices({ payload }, { put, select }) {
+ yield put({
+ type: "saveClearedNotices",
+ payload
+ });
+ const count: number = yield select(
+ (state: ConnectState) => state.global.notices.length
+ );
+ const unreadCount: number = yield select(
+ (state: ConnectState) =>
+ state.global.notices.filter(item => !item.read).length
+ );
+ yield put({
+ type: "user/changeNotifyCount",
+ payload: {
+ totalCount: count,
+ unreadCount
+ }
+ });
+ },
+ *changeNoticeReadState({ payload }, { put, select }) {
+ const notices: NoticeItem[] = yield select((state: ConnectState) =>
+ state.global.notices.map(item => {
+ const notice = { ...item };
+ if (notice.id === payload) {
+ notice.read = true;
+ }
+ return notice;
+ })
+ );
+
+ yield put({
+ type: "saveNotices",
+ payload: notices
+ });
+
+ yield put({
+ type: "user/changeNotifyCount",
+ payload: {
+ totalCount: notices.length,
+ unreadCount: notices.filter(item => !item.read).length
+ }
+ });
+ }
+ },
+
+ reducers: {
+ changeLayoutCollapsed(
+ state = { notices: [], collapsed: true },
+ { payload }
+ ): GlobalModelState {
+ return {
+ ...state,
+ collapsed: payload
+ };
+ },
+ saveNotices(state, { payload }): GlobalModelState {
+ return {
+ collapsed: false,
+ ...state,
+ notices: payload
+ };
+ },
+ saveClearedNotices(
+ state = { notices: [], collapsed: true },
+ { payload }
+ ): GlobalModelState {
+ return {
+ collapsed: false,
+ ...state,
+ notices: state.notices.filter((item): boolean => item.type !== payload)
+ };
+ }
+ },
+
+ subscriptions: {
+ setup({ history }): void {
+ // Subscribe history(url) change, trigger `load` action if pathname is `/`
+ history.listen(({ pathname, search }): void => {
+ if (typeof window.ga !== "undefined") {
+ window.ga("send", "pageview", pathname + search);
+ }
+ });
+ }
+ }
+};
+
+export default GlobalModel;
diff --git a/template/pro/src/models/login.ts b/template/pro/src/models/login.ts
new file mode 100644
index 0000000000000000000000000000000000000000..740c8d6848cb61dc19637af1843d0e7987004db4
--- /dev/null
+++ b/template/pro/src/models/login.ts
@@ -0,0 +1,95 @@
+import { Reducer } from 'redux';
+import { routerRedux } from 'dva/router';
+import { Effect } from 'dva';
+import { stringify } from 'querystring';
+
+import { fakeAccountLogin, getFakeCaptcha } from '@/services/login';
+import { setAuthority } from '@/utils/authority';
+import { getPageQuery } from '@/utils/utils';
+
+export interface StateType {
+ status?: 'ok' | 'error';
+ type?: string;
+ currentAuthority?: 'user' | 'guest' | 'admin';
+}
+
+export interface LoginModelType {
+ namespace: string;
+ state: StateType;
+ effects: {
+ login: Effect;
+ getCaptcha: Effect;
+ logout: Effect;
+ };
+ reducers: {
+ changeLoginStatus: Reducer;
+ };
+}
+
+const Model: LoginModelType = {
+ namespace: 'login',
+
+ state: {
+ status: undefined,
+ },
+
+ effects: {
+ *login({ payload }, { call, put }) {
+ const response = yield call(fakeAccountLogin, payload);
+ yield put({
+ type: 'changeLoginStatus',
+ payload: response,
+ });
+ // Login successfully
+ if (response.status === 'ok') {
+ const urlParams = new URL(window.location.href);
+ const params = getPageQuery();
+ let { redirect } = params as { redirect: string };
+ if (redirect) {
+ const redirectUrlParams = new URL(redirect);
+ if (redirectUrlParams.origin === urlParams.origin) {
+ redirect = redirect.substr(urlParams.origin.length);
+ if (redirect.match(/^\/.*#/)) {
+ redirect = redirect.substr(redirect.indexOf('#') + 1);
+ }
+ } else {
+ window.location.href = redirect;
+ return;
+ }
+ }
+ yield put(routerRedux.replace(redirect || '/'));
+ }
+ },
+
+ *getCaptcha({ payload }, { call }) {
+ yield call(getFakeCaptcha, payload);
+ },
+ *logout(_, { put }) {
+ const { redirect } = getPageQuery();
+ // redirect
+ if (window.location.pathname !== '/user/login' && !redirect) {
+ yield put(
+ routerRedux.replace({
+ pathname: '/user/login',
+ search: stringify({
+ redirect: window.location.href,
+ }),
+ }),
+ );
+ }
+ },
+ },
+
+ reducers: {
+ changeLoginStatus(state, { payload }) {
+ setAuthority(payload.currentAuthority);
+ return {
+ ...state,
+ status: payload.status,
+ type: payload.type,
+ };
+ },
+ },
+};
+
+export default Model;
diff --git a/template/pro/src/models/setting.ts b/template/pro/src/models/setting.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7209d9b5b5b24495d7363bbd3e52d94e4a8516d7
--- /dev/null
+++ b/template/pro/src/models/setting.ts
@@ -0,0 +1,89 @@
+import { Reducer } from 'redux';
+import { message } from 'antd';
+import defaultSettings, { DefaultSettings } from '../../config/defaultSettings';
+import themeColorClient from '../components/SettingDrawer/themeColorClient';
+
+export interface SettingModelType {
+ namespace: 'settings';
+ state: DefaultSettings;
+ reducers: {
+ getSetting: Reducer;
+ changeSetting: Reducer;
+ };
+}
+
+const updateTheme = (newPrimaryColor?: string) => {
+ if (newPrimaryColor) {
+ const timeOut = 0;
+ const hideMessage = message.loading('正在切换主题!', timeOut);
+ themeColorClient.changeColor(newPrimaryColor).finally(() => hideMessage());
+ }
+};
+
+const updateColorWeak: (colorWeak: boolean) => void = colorWeak => {
+ const root = document.getElementById('root');
+ if (root) {
+ root.className = colorWeak ? 'colorWeak' : '';
+ }
+};
+
+const SettingModel: SettingModelType = {
+ namespace: 'settings',
+ state: defaultSettings,
+ reducers: {
+ getSetting(state = defaultSettings) {
+ const setting: Partial = {};
+ const urlParams = new URL(window.location.href);
+ Object.keys(state).forEach(key => {
+ if (urlParams.searchParams.has(key)) {
+ const value = urlParams.searchParams.get(key);
+ setting[key] = value === '1' ? true : value;
+ }
+ });
+ const { primaryColor, colorWeak } = setting;
+
+ if (primaryColor && state.primaryColor !== primaryColor) {
+ updateTheme(primaryColor);
+ }
+ updateColorWeak(!!colorWeak);
+ return {
+ ...state,
+ ...setting,
+ };
+ },
+ changeSetting(state = defaultSettings, { payload }) {
+ const urlParams = new URL(window.location.href);
+ Object.keys(defaultSettings).forEach(key => {
+ if (urlParams.searchParams.has(key)) {
+ urlParams.searchParams.delete(key);
+ }
+ });
+ Object.keys(payload).forEach(key => {
+ if (key === 'collapse') {
+ return;
+ }
+ let value = payload[key];
+ if (value === true) {
+ value = 1;
+ }
+ if (defaultSettings[key] !== value) {
+ urlParams.searchParams.set(key, value);
+ }
+ });
+ const { primaryColor, colorWeak, contentWidth } = payload;
+ if (primaryColor && state.primaryColor !== primaryColor) {
+ updateTheme(primaryColor);
+ }
+ if (state.contentWidth !== contentWidth && window.dispatchEvent) {
+ window.dispatchEvent(new Event('resize'));
+ }
+ updateColorWeak(!!colorWeak);
+ window.history.replaceState(null, 'setting', urlParams.href);
+ return {
+ ...state,
+ ...payload,
+ };
+ },
+ },
+};
+export default SettingModel;
diff --git a/template/pro/src/models/user.ts b/template/pro/src/models/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..360ba8e6a4ba7d5e8661df94610e6480faf48e9f
--- /dev/null
+++ b/template/pro/src/models/user.ts
@@ -0,0 +1,86 @@
+import { Effect } from 'dva';
+import { Reducer } from 'redux';
+
+import { queryCurrent, query as queryUsers } from '@/services/user';
+
+export interface CurrentUser {
+ avatar?: string;
+ name?: string;
+ title?: string;
+ group?: string;
+ signature?: string;
+ tags?: {
+ key: string;
+ label: string;
+ }[];
+ userid?: string;
+ unreadCount?: number;
+}
+
+export interface UserModelState {
+ currentUser?: CurrentUser;
+}
+
+export interface UserModelType {
+ namespace: 'user';
+ state: UserModelState;
+ effects: {
+ fetch: Effect;
+ fetchCurrent: Effect;
+ };
+ reducers: {
+ saveCurrentUser: Reducer;
+ changeNotifyCount: Reducer;
+ };
+}
+
+const UserModel: UserModelType = {
+ namespace: 'user',
+
+ state: {
+ currentUser: {},
+ },
+
+ effects: {
+ *fetch(_, { call, put }) {
+ const response = yield call(queryUsers);
+ yield put({
+ type: 'save',
+ payload: response,
+ });
+ },
+ *fetchCurrent(_, { call, put }) {
+ const response = yield call(queryCurrent);
+ yield put({
+ type: 'saveCurrentUser',
+ payload: response,
+ });
+ },
+ },
+
+ reducers: {
+ saveCurrentUser(state, action) {
+ return {
+ ...state,
+ currentUser: action.payload || {},
+ };
+ },
+ changeNotifyCount(
+ state = {
+ currentUser: {},
+ },
+ action,
+ ) {
+ return {
+ ...state,
+ currentUser: {
+ ...state.currentUser,
+ notifyCount: action.payload.totalCount,
+ unreadCount: action.payload.unreadCount,
+ },
+ };
+ },
+ },
+};
+
+export default UserModel;
diff --git a/template/pro/src/pages/404.tsx b/template/pro/src/pages/404.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..05b0a2da8d64c181f94134940171d8792a1768a9
--- /dev/null
+++ b/template/pro/src/pages/404.tsx
@@ -0,0 +1,21 @@
+import { Button, Result } from 'antd';
+import React from 'react';
+import router from 'umi/router';
+
+// 这里应该使用 antd 的 404 result 组件,
+// 但是还没发布,先来个简单的。
+
+const NoFoundPage: React.FC<{}> = () => (
+ router.push('/')}>
+ Back Home
+
+ }
+ >
+);
+
+export default NoFoundPage;
diff --git a/template/pro/src/pages/Authorized.tsx b/template/pro/src/pages/Authorized.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9c94e479231e4cfbb15bfadc8f370df5a97588ac
--- /dev/null
+++ b/template/pro/src/pages/Authorized.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import Redirect from 'umi/redirect';
+import { connect } from 'dva';
+import pathToRegexp from 'path-to-regexp';
+import Authorized from '@/utils/Authorized';
+import { ConnectProps, ConnectState, Route, UserModelState } from '@/models/connect';
+
+interface AuthComponentProps extends ConnectProps {
+ user: UserModelState;
+}
+
+const getRouteAuthority = (path: string, routeData: Route[]) => {
+ let authorities: string[] | string | undefined;
+ routeData.forEach(route => {
+ if (route.authority) {
+ authorities = route.authority;
+ }
+ // match prefix
+ if (pathToRegexp(`${route.path}(.*)`).test(path)) {
+ // exact match
+ if (route.path === path) {
+ authorities = route.authority || authorities;
+ }
+ // get children authority recursively
+ if (route.routes) {
+ authorities = getRouteAuthority(path, route.routes) || authorities;
+ }
+ }
+ });
+ return authorities;
+};
+
+const AuthComponent: React.FC = ({
+ children,
+ route = {
+ routes: [],
+ },
+ location = {
+ pathname: '',
+ },
+ user,
+}) => {
+ const { currentUser } = user;
+ const { routes = [] } = route;
+ const isLogin = currentUser && currentUser.name;
+ return (
+ : }
+ >
+ {children}
+
+ );
+};
+
+export default connect(({ user }: ConnectState) => ({
+ user,
+}))(AuthComponent);
diff --git a/template/pro/src/pages/Welcome.tsx b/template/pro/src/pages/Welcome.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..52c8549e8a19423bcf0dc76d9d2259f9637b3466
--- /dev/null
+++ b/template/pro/src/pages/Welcome.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { Card, Typography, Alert } from 'antd';
+import { PageHeaderWrapper } from '@ant-design/pro-layout';
+import { FormattedMessage } from 'umi-plugin-react/locale';
+
+const CodePreview: React.FC<{}> = ({ children }) => (
+
+
+ {children}
+
+
+);
+
+export default (): React.ReactNode => (
+
+
+
+
+
+
+
+
+ npx umi block list
+
+
+
+
+
+ npm run fetch:blocks
+
+
+ Want to add more pages? Please refer to{' '}
+
+ use block
+
+ 。
+
+
+);
diff --git a/template/pro/src/pages/document.ejs b/template/pro/src/pages/document.ejs
new file mode 100644
index 0000000000000000000000000000000000000000..ea08d4e2dfc7e7a0ebd71b2175aa0e7e9cf424dc
--- /dev/null
+++ b/template/pro/src/pages/document.ejs
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+ Ant Design Pro
+
+
+
+ Out-of-the-box mid-stage front/design solution!
+
+
+
diff --git a/template/pro/src/pages/user/login/components/Login/LoginContext.tsx b/template/pro/src/pages/user/login/components/Login/LoginContext.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ae571e0db3dd5fbbd1e5b1248947b9c9ab4ad4df
--- /dev/null
+++ b/template/pro/src/pages/user/login/components/Login/LoginContext.tsx
@@ -0,0 +1,13 @@
+import { createContext } from 'react';
+
+export interface LoginContextProps {
+ tabUtil?: {
+ addTab: (id: string) => void;
+ removeTab: (id: string) => void;
+ };
+ updateActive?: (activeItem: { [key: string]: string } | string) => void;
+}
+
+const LoginContext: React.Context = createContext({});
+
+export default LoginContext;
diff --git a/template/pro/src/pages/user/login/components/Login/LoginItem.tsx b/template/pro/src/pages/user/login/components/Login/LoginItem.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..75e0fe5a4cf5b0ebb78399f9eb8ca5827c6fe523
--- /dev/null
+++ b/template/pro/src/pages/user/login/components/Login/LoginItem.tsx
@@ -0,0 +1,196 @@
+import { Button, Col, Form, Input, Row } from 'antd';
+import React, { Component } from 'react';
+import { FormComponentProps } from 'antd/es/form';
+import { GetFieldDecoratorOptions } from 'antd/es/form/Form';
+
+import omit from 'omit.js';
+import ItemMap from './map';
+import LoginContext, { LoginContextProps } from './LoginContext';
+import styles from './index.less';
+
+type Omit = Pick>;
+
+export type WrappedLoginItemProps = Omit;
+export type LoginItemKeyType = keyof typeof ItemMap;
+export interface LoginItemType {
+ UserName: React.FC;
+ Password: React.FC;
+ Mobile: React.FC;
+ Captcha: React.FC;
+}
+
+export interface LoginItemProps extends GetFieldDecoratorOptions {
+ name?: string;
+ style?: React.CSSProperties;
+ onGetCaptcha?: (event?: MouseEvent) => void | Promise | false;
+ placeholder?: string;
+ buttonText?: React.ReactNode;
+ onPressEnter?: (e: React.KeyboardEvent) => void;
+ countDown?: number;
+ getCaptchaButtonText?: string;
+ getCaptchaSecondText?: string;
+ updateActive?: LoginContextProps['updateActive'];
+ type?: string;
+ defaultValue?: string;
+ form?: FormComponentProps['form'];
+ customProps?: { [key: string]: unknown };
+ onChange?: (e: React.ChangeEvent) => void;
+ tabUtil?: LoginContextProps['tabUtil'];
+}
+
+interface LoginItemState {
+ count: number;
+}
+
+const FormItem = Form.Item;
+
+class WrapFormItem extends Component {
+ static defaultProps = {
+ getCaptchaButtonText: 'captcha',
+ getCaptchaSecondText: 'second',
+ };
+
+ interval: number | undefined = undefined;
+
+ constructor(props: LoginItemProps) {
+ super(props);
+ this.state = {
+ count: 0,
+ };
+ }
+
+ componentDidMount() {
+ const { updateActive, name = '' } = this.props;
+ if (updateActive) {
+ updateActive(name);
+ }
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.interval);
+ }
+
+ onGetCaptcha = () => {
+ const { onGetCaptcha } = this.props;
+ const result = onGetCaptcha ? onGetCaptcha() : null;
+ if (result === false) {
+ return;
+ }
+ if (result instanceof Promise) {
+ result.then(this.runGetCaptchaCountDown);
+ } else {
+ this.runGetCaptchaCountDown();
+ }
+ };
+
+ getFormItemOptions = ({ onChange, defaultValue, customProps = {}, rules }: LoginItemProps) => {
+ const options: {
+ rules?: LoginItemProps['rules'];
+ onChange?: LoginItemProps['onChange'];
+ initialValue?: LoginItemProps['defaultValue'];
+ } = {
+ rules: rules || (customProps.rules as LoginItemProps['rules']),
+ };
+ if (onChange) {
+ options.onChange = onChange;
+ }
+ if (defaultValue) {
+ options.initialValue = defaultValue;
+ }
+ return options;
+ };
+
+ runGetCaptchaCountDown = () => {
+ const { countDown } = this.props;
+ let count = countDown || 59;
+ this.setState({ count });
+ this.interval = window.setInterval(() => {
+ count -= 1;
+ this.setState({ count });
+ if (count === 0) {
+ clearInterval(this.interval);
+ }
+ }, 1000);
+ };
+
+ render() {
+ const { count } = this.state;
+
+ // 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props tabUtil
+ const {
+ onChange,
+ customProps,
+ defaultValue,
+ rules,
+ name,
+ getCaptchaButtonText,
+ getCaptchaSecondText,
+ updateActive,
+ type,
+ form,
+ tabUtil,
+ ...restProps
+ } = this.props;
+ if (!name) {
+ return null;
+ }
+ if (!form) {
+ return null;
+ }
+ const { getFieldDecorator } = form;
+ // get getFieldDecorator props
+ const options = this.getFormItemOptions(this.props);
+ const otherProps = restProps || {};
+
+ if (type === 'Captcha') {
+ const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']);
+
+ return (
+
+
+
+ {getFieldDecorator(name, options)( )}
+
+
+
+ {count ? `${count} ${getCaptchaSecondText}` : getCaptchaButtonText}
+
+
+
+
+ );
+ }
+ return (
+
+ {getFieldDecorator(name, options)( )}
+
+ );
+ }
+}
+
+const LoginItem: Partial = {};
+
+Object.keys(ItemMap).forEach(key => {
+ const item = ItemMap[key];
+ LoginItem[key] = (props: LoginItemProps) => (
+
+ {context => (
+
+ )}
+
+ );
+});
+
+export default LoginItem as LoginItemType;
diff --git a/template/pro/src/pages/user/login/components/Login/LoginSubmit.tsx b/template/pro/src/pages/user/login/components/Login/LoginSubmit.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..280fb0fce7055c89580049aab91cabf73b1faf21
--- /dev/null
+++ b/template/pro/src/pages/user/login/components/Login/LoginSubmit.tsx
@@ -0,0 +1,23 @@
+import { Button, Form } from 'antd';
+
+import { ButtonProps } from 'antd/es/button';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+const FormItem = Form.Item;
+
+interface LoginSubmitProps extends ButtonProps {
+ className?: string;
+}
+
+const LoginSubmit: React.FC = ({ className, ...rest }) => {
+ const clsString = classNames(styles.submit, className);
+ return (
+
+
+
+ );
+};
+
+export default LoginSubmit;
diff --git a/template/pro/src/pages/user/login/components/Login/LoginTab.tsx b/template/pro/src/pages/user/login/components/Login/LoginTab.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c9da288906394a96172a1087de3cea21695310fd
--- /dev/null
+++ b/template/pro/src/pages/user/login/components/Login/LoginTab.tsx
@@ -0,0 +1,53 @@
+import React, { Component } from 'react';
+
+import { TabPaneProps } from 'antd/es/tabs';
+import { Tabs } from 'antd';
+import LoginContext, { LoginContextProps } from './LoginContext';
+
+const { TabPane } = Tabs;
+
+const generateId = (() => {
+ let i = 0;
+ return (prefix = '') => {
+ i += 1;
+ return `${prefix}${i}`;
+ };
+})();
+
+interface LoginTabProps extends TabPaneProps {
+ tabUtil: LoginContextProps['tabUtil'];
+}
+
+class LoginTab extends Component {
+ uniqueId: string = '';
+
+ constructor(props: LoginTabProps) {
+ super(props);
+ this.uniqueId = generateId('login-tab-');
+ }
+
+ componentDidMount() {
+ const { tabUtil } = this.props;
+ if (tabUtil) {
+ tabUtil.addTab(this.uniqueId);
+ }
+ }
+
+ render() {
+ const { children } = this.props;
+ return {children} ;
+ }
+}
+
+const WrapContext: React.FC & {
+ typeName: string;
+} = props => (
+
+ {value => }
+
+);
+
+// 标志位 用来判断是不是自定义组件
+WrapContext.typeName = 'LoginTab';
+
+export default WrapContext;
diff --git a/template/pro/src/pages/user/login/components/Login/index.less b/template/pro/src/pages/user/login/components/Login/index.less
new file mode 100644
index 0000000000000000000000000000000000000000..db65c4e76545a6a31f177ddc713a9775a2c16608
--- /dev/null
+++ b/template/pro/src/pages/user/login/components/Login/index.less
@@ -0,0 +1,53 @@
+@import '~antd/es/style/themes/default.less';
+
+.login {
+ :global {
+ .ant-tabs .ant-tabs-bar {
+ margin-bottom: 24px;
+ text-align: center;
+ border-bottom: 0;
+ }
+
+ .ant-form-item {
+ margin: 0 2px 24px;
+ }
+ }
+
+ .getCaptcha {
+ display: block;
+ width: 100%;
+ }
+
+ .icon {
+ margin-left: 16px;
+ color: rgba(0, 0, 0, 0.2);
+ font-size: 24px;
+ vertical-align: middle;
+ cursor: pointer;
+ transition: color 0.3s;
+
+ &:hover {
+ color: @primary-color;
+ }
+ }
+
+ .other {
+ margin-top: 24px;
+ line-height: 22px;
+ text-align: left;
+
+ .register {
+ float: right;
+ }
+ }
+
+ .prefixIcon {
+ color: @disabled-color;
+ font-size: @font-size-base;
+ }
+
+ .submit {
+ width: 100%;
+ margin-top: 24px;
+ }
+}
diff --git a/template/pro/src/pages/user/login/components/Login/index.tsx b/template/pro/src/pages/user/login/components/Login/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..00c32a1860b30a684c58b25cb6ae8ab7b0b1f3e3
--- /dev/null
+++ b/template/pro/src/pages/user/login/components/Login/index.tsx
@@ -0,0 +1,173 @@
+import { Form, Tabs } from 'antd';
+import React, { Component } from 'react';
+import { FormComponentProps } from 'antd/es/form';
+import classNames from 'classnames';
+import LoginContext, { LoginContextProps } from './LoginContext';
+import LoginItem, { LoginItemProps, LoginItemType } from './LoginItem';
+
+import LoginSubmit from './LoginSubmit';
+import LoginTab from './LoginTab';
+import styles from './index.less';
+import { LoginParamsType } from '@/services/login';
+
+export interface LoginProps {
+ defaultActiveKey?: string;
+ onTabChange?: (key: string) => void;
+ style?: React.CSSProperties;
+ onSubmit?: (error: unknown, values: LoginParamsType) => void;
+ className?: string;
+ form: FormComponentProps['form'];
+ onCreate?: (form?: FormComponentProps['form']) => void;
+ children: React.ReactElement[];
+}
+
+interface LoginState {
+ tabs?: string[];
+ type?: string;
+ active?: { [key: string]: unknown[] };
+}
+
+class Login extends Component {
+ public static Tab = LoginTab;
+
+ public static Submit = LoginSubmit;
+
+ public static UserName: React.FunctionComponent;
+
+ public static Password: React.FunctionComponent;
+
+ public static Mobile: React.FunctionComponent;
+
+ public static Captcha: React.FunctionComponent;
+
+ static defaultProps = {
+ className: '',
+ defaultActiveKey: '',
+ onTabChange: () => {},
+ onSubmit: () => {},
+ };
+
+ constructor(props: LoginProps) {
+ super(props);
+ this.state = {
+ type: props.defaultActiveKey,
+ tabs: [],
+ active: {},
+ };
+ }
+
+ componentDidMount() {
+ const { form, onCreate } = this.props;
+ if (onCreate) {
+ onCreate(form);
+ }
+ }
+
+ onSwitch = (type: string) => {
+ this.setState(
+ {
+ type,
+ },
+ () => {
+ const { onTabChange } = this.props;
+ if (onTabChange) {
+ onTabChange(type);
+ }
+ },
+ );
+ };
+
+ getContext: () => LoginContextProps = () => {
+ const { form } = this.props;
+ const { tabs = [] } = this.state;
+ return {
+ tabUtil: {
+ addTab: id => {
+ this.setState({
+ tabs: [...tabs, id],
+ });
+ },
+ removeTab: id => {
+ this.setState({
+ tabs: tabs.filter(currentId => currentId !== id),
+ });
+ },
+ },
+ form: { ...form },
+ updateActive: activeItem => {
+ const { type = '', active = {} } = this.state;
+ if (active[type]) {
+ active[type].push(activeItem);
+ } else {
+ active[type] = [activeItem];
+ }
+ this.setState({
+ active,
+ });
+ },
+ };
+ };
+
+ handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ const { active = {}, type = '' } = this.state;
+ const { form, onSubmit } = this.props;
+ const activeFields = active[type] || [];
+ if (form) {
+ form.validateFields(activeFields as string[], { force: true }, (err, values) => {
+ if (onSubmit) {
+ onSubmit(err, values);
+ }
+ });
+ }
+ };
+
+ render() {
+ const { className, children } = this.props;
+ const { type, tabs = [] } = this.state;
+ const TabChildren: React.ReactComponentElement[] = [];
+ const otherChildren: React.ReactElement[] = [];
+ React.Children.forEach(
+ children,
+ (child: React.ReactComponentElement | React.ReactElement) => {
+ if (!child) {
+ return;
+ }
+ if (child.type.typeName === 'LoginTab') {
+ TabChildren.push(child as React.ReactComponentElement);
+ } else {
+ otherChildren.push(child);
+ }
+ },
+ );
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+(Object.keys(LoginItem) as (keyof LoginItemType)[]).forEach(item => {
+ Login[item] = LoginItem[item];
+});
+
+export default Form.create()(Login);
diff --git a/template/pro/src/pages/user/login/components/Login/map.tsx b/template/pro/src/pages/user/login/components/Login/map.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..babd226f86624acd35150032eaa3288d37ebeb17
--- /dev/null
+++ b/template/pro/src/pages/user/login/components/Login/map.tsx
@@ -0,0 +1,65 @@
+import { Icon } from 'antd';
+import React from 'react';
+import styles from './index.less';
+
+export default {
+ UserName: {
+ props: {
+ size: 'large',
+ id: 'userName',
+ prefix: ,
+ placeholder: 'admin',
+ },
+ rules: [
+ {
+ required: true,
+ message: 'Please enter username!',
+ },
+ ],
+ },
+ Password: {
+ props: {
+ size: 'large',
+ prefix: ,
+ type: 'password',
+ id: 'password',
+ placeholder: '888888',
+ },
+ rules: [
+ {
+ required: true,
+ message: 'Please enter password!',
+ },
+ ],
+ },
+ Mobile: {
+ props: {
+ size: 'large',
+ prefix: ,
+ placeholder: 'mobile number',
+ },
+ rules: [
+ {
+ required: true,
+ message: 'Please enter mobile number!',
+ },
+ {
+ pattern: /^1\d{10}$/,
+ message: 'Wrong mobile number format!',
+ },
+ ],
+ },
+ Captcha: {
+ props: {
+ size: 'large',
+ prefix: ,
+ placeholder: 'captcha',
+ },
+ rules: [
+ {
+ required: true,
+ message: 'Please enter Captcha!',
+ },
+ ],
+ },
+};
diff --git a/template/pro/src/pages/user/login/index.tsx b/template/pro/src/pages/user/login/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f8b3ba9e260b30c01cea0acde3318a3076780fdc
--- /dev/null
+++ b/template/pro/src/pages/user/login/index.tsx
@@ -0,0 +1,205 @@
+import { Alert, Checkbox, Icon } from 'antd';
+import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
+import React, { Component } from 'react';
+
+import { CheckboxChangeEvent } from 'antd/es/checkbox';
+import { Dispatch, AnyAction } from 'redux';
+import { FormComponentProps } from 'antd/es/form';
+import Link from 'umi/link';
+import { connect } from 'dva';
+import { StateType } from '@/models/login';
+import LoginComponents from './components/Login';
+import styles from './style.less';
+import { LoginParamsType } from '@/services/login';
+import { ConnectState } from '@/models/connect';
+
+const { Tab, UserName, Password, Mobile, Captcha, Submit } = LoginComponents;
+
+interface LoginProps {
+ dispatch: Dispatch;
+ userLogin: StateType;
+ submitting: boolean;
+}
+interface LoginState {
+ type: string;
+ autoLogin: boolean;
+}
+
+@connect(({ login, loading }: ConnectState) => ({
+ userLogin: login,
+ submitting: loading.effects['login/login'],
+}))
+class Login extends Component {
+ loginForm: FormComponentProps['form'] | undefined | null = undefined;
+
+ state: LoginState = {
+ type: 'account',
+ autoLogin: true,
+ };
+
+ changeAutoLogin = (e: CheckboxChangeEvent) => {
+ this.setState({
+ autoLogin: e.target.checked,
+ });
+ };
+
+ handleSubmit = (err: unknown, values: LoginParamsType) => {
+ const { type } = this.state;
+ if (!err) {
+ const { dispatch } = this.props;
+ dispatch({
+ type: 'login/login',
+ payload: {
+ ...values,
+ type,
+ },
+ });
+ }
+ };
+
+ onTabChange = (type: string) => {
+ this.setState({ type });
+ };
+
+ onGetCaptcha = () =>
+ new Promise((resolve, reject) => {
+ if (!this.loginForm) {
+ return;
+ }
+ this.loginForm.validateFields(
+ ['mobile'],
+ {},
+ async (err: unknown, values: LoginParamsType) => {
+ if (err) {
+ reject(err);
+ } else {
+ const { dispatch } = this.props;
+ try {
+ const success = await ((dispatch({
+ type: 'login/getCaptcha',
+ payload: values.mobile,
+ }) as unknown) as Promise);
+ resolve(!!success);
+ } catch (error) {
+ reject(error);
+ }
+ }
+ },
+ );
+ });
+
+ renderMessage = (content: string) => (
+
+ );
+
+ render() {
+ const { userLogin, submitting } = this.props;
+ const { status, type: loginType } = userLogin;
+ const { type, autoLogin } = this.state;
+ return (
+
+
{
+ this.loginForm = form;
+ }}
+ >
+
+ {status === 'error' &&
+ loginType === 'account' &&
+ !submitting &&
+ this.renderMessage(
+ formatMessage({ id: 'user-login.login.message-invalid-credentials' }),
+ )}
+
+ {
+ e.preventDefault();
+ if (this.loginForm) {
+ this.loginForm.validateFields(this.handleSubmit);
+ }
+ }}
+ />
+
+
+ {status === 'error' &&
+ loginType === 'mobile' &&
+ !submitting &&
+ this.renderMessage(
+ formatMessage({ id: 'user-login.login.message-invalid-verification-code' }),
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default Login;
diff --git a/template/pro/src/pages/user/login/locales/en-US.ts b/template/pro/src/pages/user/login/locales/en-US.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c3e7080d05c6addaedee6ab7fbaa5889e5fe38a3
--- /dev/null
+++ b/template/pro/src/pages/user/login/locales/en-US.ts
@@ -0,0 +1,78 @@
+export default {
+ 'user-login.login.userName': 'userName',
+ 'user-login.login.password': 'password',
+ 'user-login.login.message-invalid-credentials':
+ 'Invalid username or password(admin/ant.design)',
+ 'user-login.login.message-invalid-verification-code': 'Invalid verification code',
+ 'user-login.login.tab-login-credentials': 'Credentials',
+ 'user-login.login.tab-login-mobile': 'Mobile number',
+ 'user-login.login.remember-me': 'Remember me',
+ 'user-login.login.forgot-password': 'Forgot your password?',
+ 'user-login.login.sign-in-with': 'Sign in with',
+ 'user-login.login.signup': 'Sign up',
+ 'user-login.login.login': 'Login',
+ 'user-login.register.register': 'Register',
+ 'user-login.register.get-verification-code': 'Get code',
+ 'user-login.register.sign-in': 'Already have an account?',
+ 'user-login.register-result.msg': 'Account:registered at {email}',
+ 'user-login.register-result.activation-email':
+ 'The activation email has been sent to your email address and is valid for 24 hours. Please log in to the email in time and click on the link in the email to activate the account.',
+ 'user-login.register-result.back-home': 'Back to home',
+ 'user-login.register-result.view-mailbox': 'View mailbox',
+ 'user-login.email.required': 'Please enter your email!',
+ 'user-login.email.wrong-format': 'The email address is in the wrong format!',
+ 'user-login.userName.required': 'Please enter your userName!',
+ 'user-login.password.required': 'Please enter your password!',
+ 'user-login.password.twice': 'The passwords entered twice do not match!',
+ 'user-login.strength.msg':
+ "Please enter at least 6 characters and don't use passwords that are easy to guess.",
+ 'user-login.strength.strong': 'Strength: strong',
+ 'user-login.strength.medium': 'Strength: medium',
+ 'user-login.strength.short': 'Strength: too short',
+ 'user-login.confirm-password.required': 'Please confirm your password!',
+ 'user-login.phone-number.required': 'Please enter your phone number!',
+ 'user-login.phone-number.wrong-format': 'Malformed phone number!',
+ 'user-login.verification-code.required': 'Please enter the verification code!',
+ 'user-login.title.required': 'Please enter a title',
+ 'user-login.date.required': 'Please select the start and end date',
+ 'user-login.goal.required': 'Please enter a description of the goal',
+ 'user-login.standard.required': 'Please enter a metric',
+ 'user-login.form.get-captcha': 'Get Captcha',
+ 'user-login.captcha.second': 'sec',
+ 'user-login.form.optional': ' (optional) ',
+ 'user-login.form.submit': 'Submit',
+ 'user-login.form.save': 'Save',
+ 'user-login.email.placeholder': 'Email',
+ 'user-login.password.placeholder': 'Password',
+ 'user-login.confirm-password.placeholder': 'Confirm password',
+ 'user-login.phone-number.placeholder': 'Phone number',
+ 'user-login.verification-code.placeholder': 'Verification code',
+ 'user-login.title.label': 'Title',
+ 'user-login.title.placeholder': 'Give the target a name',
+ 'user-login.date.label': 'Start and end date',
+ 'user-login.placeholder.start': 'Start date',
+ 'user-login.placeholder.end': 'End date',
+ 'user-login.goal.label': 'Goal description',
+ 'user-login.goal.placeholder': 'Please enter your work goals',
+ 'user-login.standard.label': 'Metrics',
+ 'user-login.standard.placeholder': 'Please enter a metric',
+ 'user-login.client.label': 'Client',
+ 'user-login.label.tooltip': 'Target service object',
+ 'user-login.client.placeholder':
+ 'Please describe your customer service, internal customers directly @ Name / job number',
+ 'user-login.invites.label': 'Inviting critics',
+ 'user-login.invites.placeholder':
+ 'Please direct @ Name / job number, you can invite up to 5 people',
+ 'user-login.weight.label': 'Weight',
+ 'user-login.weight.placeholder': 'Please enter weight',
+ 'user-login.public.label': 'Target disclosure',
+ 'user-login.label.help': 'Customers and invitees are shared by default',
+ 'user-login.radio.public': 'Public',
+ 'user-login.radio.partially-public': 'Partially public',
+ 'user-login.radio.private': 'Private',
+ 'user-login.publicUsers.placeholder': 'Open to',
+ 'user-login.option.A': 'Colleague A',
+ 'user-login.option.B': 'Colleague B',
+ 'user-login.option.C': 'Colleague C',
+ 'user-login.navBar.lang': 'Languages',
+};
diff --git a/template/pro/src/pages/user/login/locales/zh-CN.ts b/template/pro/src/pages/user/login/locales/zh-CN.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fe0e7f596706ad8ede54ef6c2c54c90539a021e2
--- /dev/null
+++ b/template/pro/src/pages/user/login/locales/zh-CN.ts
@@ -0,0 +1,74 @@
+export default {
+ 'user-login.login.userName': '用户名',
+ 'user-login.login.password': '密码',
+ 'user-login.login.message-invalid-credentials': '账户或密码错误(admin/ant.design)',
+ 'user-login.login.message-invalid-verification-code': '验证码错误',
+ 'user-login.login.tab-login-credentials': '账户密码登录',
+ 'user-login.login.tab-login-mobile': '手机号登录',
+ 'user-login.login.remember-me': '自动登录',
+ 'user-login.login.forgot-password': '忘记密码',
+ 'user-login.login.sign-in-with': '其他登录方式',
+ 'user-login.login.signup': '注册账户',
+ 'user-login.login.login': '登录',
+ 'user-login.register.register': '注册',
+ 'user-login.register.get-verification-code': '获取验证码',
+ 'user-login.register.sign-in': '使用已有账户登录',
+ 'user-login.register-result.msg': '你的账户:{email} 注册成功',
+ 'user-login.register-result.activation-email':
+ '激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。',
+ 'user-login.register-result.back-home': '返回首页',
+ 'user-login.register-result.view-mailbox': '查看邮箱',
+ 'user-login.email.required': '请输入邮箱地址!',
+ 'user-login.email.wrong-format': '邮箱地址格式错误!',
+ 'user-login.userName.required': '请输入用户名!',
+ 'user-login.password.required': '请输入密码!',
+ 'user-login.password.twice': '两次输入的密码不匹配!',
+ 'user-login.strength.msg': '请至少输入 6 个字符。请不要使用容易被猜到的密码。',
+ 'user-login.strength.strong': '强度:强',
+ 'user-login.strength.medium': '强度:中',
+ 'user-login.strength.short': '强度:太短',
+ 'user-login.confirm-password.required': '请确认密码!',
+ 'user-login.phone-number.required': '请输入手机号!',
+ 'user-login.phone-number.wrong-format': '手机号格式错误!',
+ 'user-login.verification-code.required': '请输入验证码!',
+ 'user-login.title.required': '请输入标题',
+ 'user-login.date.required': '请选择起止日期',
+ 'user-login.goal.required': '请输入目标描述',
+ 'user-login.standard.required': '请输入衡量标准',
+ 'user-login.form.get-captcha': '获取验证码',
+ 'user-login.captcha.second': '秒',
+ 'user-login.form.optional': '(选填)',
+ 'user-login.form.submit': '提交',
+ 'user-login.form.save': '保存',
+ 'user-login.email.placeholder': '邮箱',
+ 'user-login.password.placeholder': '至少6位密码,区分大小写',
+ 'user-login.confirm-password.placeholder': '确认密码',
+ 'user-login.phone-number.placeholder': '手机号',
+ 'user-login.verification-code.placeholder': '验证码',
+ 'user-login.title.label': '标题',
+ 'user-login.title.placeholder': '给目标起个名字',
+ 'user-login.date.label': '起止日期',
+ 'user-login.placeholder.start': '开始日期',
+ 'user-login.placeholder.end': '结束日期',
+ 'user-login.goal.label': '目标描述',
+ 'user-login.goal.placeholder': '请输入你的阶段性工作目标',
+ 'user-login.standard.label': '衡量标准',
+ 'user-login.standard.placeholder': '请输入衡量标准',
+ 'user-login.client.label': '客户',
+ 'user-login.label.tooltip': '目标的服务对象',
+ 'user-login.client.placeholder': '请描述你服务的客户,内部客户直接 @姓名/工号',
+ 'user-login.invites.label': '邀评人',
+ 'user-login.invites.placeholder': '请直接 @姓名/工号,最多可邀请 5 人',
+ 'user-login.weight.label': '权重',
+ 'user-login.weight.placeholder': '请输入',
+ 'user-login.public.label': '目标公开',
+ 'user-login.label.help': '客户、邀评人默认被分享',
+ 'user-login.radio.public': '公开',
+ 'user-login.radio.partially-public': '部分公开',
+ 'user-login.radio.private': '不公开',
+ 'user-login.publicUsers.placeholder': '公开给',
+ 'user-login.option.A': '同事甲',
+ 'user-login.option.B': '同事乙',
+ 'user-login.option.C': '同事丙',
+ 'user-login.navBar.lang': '语言',
+};
diff --git a/template/pro/src/pages/user/login/locales/zh-TW.ts b/template/pro/src/pages/user/login/locales/zh-TW.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cf0b460d2d34a578a0a294c244da0716b771c7e6
--- /dev/null
+++ b/template/pro/src/pages/user/login/locales/zh-TW.ts
@@ -0,0 +1,74 @@
+export default {
+ 'user-login.login.userName': '賬戶',
+ 'user-login.login.password': '密碼',
+ 'user-login.login.message-invalid-credentials': '賬戶或密碼錯誤(admin/ant.design)',
+ 'user-login.login.message-invalid-verification-code': '驗證碼錯誤',
+ 'user-login.login.tab-login-credentials': '賬戶密碼登錄',
+ 'user-login.login.tab-login-mobile': '手機號登錄',
+ 'user-login.login.remember-me': '自動登錄',
+ 'user-login.login.forgot-password': '忘記密碼',
+ 'user-login.login.sign-in-with': '其他登錄方式',
+ 'user-login.login.signup': '註冊賬戶',
+ 'user-login.login.login': '登錄',
+ 'user-login.register.register': '註冊',
+ 'user-login.register.get-verification-code': '獲取驗證碼',
+ 'user-login.register.sign-in': '使用已有賬戶登錄',
+ 'user-login.register-result.msg': '妳的賬戶:{email} 註冊成功',
+ 'user-login.register-result.activation-email':
+ '激活郵件已發送到妳的郵箱中,郵件有效期為24小時。請及時登錄郵箱,點擊郵件中的鏈接激活帳戶。',
+ 'user-login.register-result.back-home': '返回首頁',
+ 'user-login.register-result.view-mailbox': '查看郵箱',
+ 'user-login.email.required': '請輸入郵箱地址!',
+ 'user-login.email.wrong-format': '郵箱地址格式錯誤!',
+ 'user-login.userName.required': '請輸入賬戶!',
+ 'user-login.password.required': '請輸入密碼!',
+ 'user-login.password.twice': '兩次輸入的密碼不匹配!',
+ 'user-login.strength.msg': '請至少輸入 6 個字符。請不要使用容易被猜到的密碼。',
+ 'user-login.strength.strong': '強度:強',
+ 'user-login.strength.medium': '強度:中',
+ 'user-login.strength.short': '強度:太短',
+ 'user-login.confirm-password.required': '請確認密碼!',
+ 'user-login.phone-number.required': '請輸入手機號!',
+ 'user-login.phone-number.wrong-format': '手機號格式錯誤!',
+ 'user-login.verification-code.required': '請輸入驗證碼!',
+ 'user-login.title.required': '請輸入標題',
+ 'user-login.date.required': '請選擇起止日期',
+ 'user-login.goal.required': '請輸入目標描述',
+ 'user-login.standard.required': '請輸入衡量標淮',
+ 'user-login.form.get-captcha': '獲取驗證碼',
+ 'user-login.captcha.second': '秒',
+ 'user-login.form.optional': '(選填)',
+ 'user-login.form.submit': '提交',
+ 'user-login.form.save': '保存',
+ 'user-login.email.placeholder': '郵箱',
+ 'user-login.password.placeholder': '至少6位密碼,區分大小寫',
+ 'user-login.confirm-password.placeholder': '確認密碼',
+ 'user-login.phone-number.placeholder': '手機號',
+ 'user-login.verification-code.placeholder': '驗證碼',
+ 'user-login.title.label': '標題',
+ 'user-login.title.placeholder': '給目標起個名字',
+ 'user-login.date.label': '起止日期',
+ 'user-login.placeholder.start': '開始日期',
+ 'user-login.placeholder.end': '結束日期',
+ 'user-login.goal.label': '目標描述',
+ 'user-login.goal.placeholder': '請輸入妳的階段性工作目標',
+ 'user-login.standard.label': '衡量標淮',
+ 'user-login.standard.placeholder': '請輸入衡量標淮',
+ 'user-login.client.label': '客戶',
+ 'user-login.label.tooltip': '目標的服務對象',
+ 'user-login.client.placeholder': '請描述妳服務的客戶,內部客戶直接 @姓名/工號',
+ 'user-login.invites.label': '邀評人',
+ 'user-login.invites.placeholder': '請直接 @姓名/工號,最多可邀請 5 人',
+ 'user-login.weight.label': '權重',
+ 'user-login.weight.placeholder': '請輸入',
+ 'user-login.public.label': '目標公開',
+ 'user-login.label.help': '客戶、邀評人默認被分享',
+ 'user-login.radio.public': '公開',
+ 'user-login.radio.partially-public': '部分公開',
+ 'user-login.radio.private': '不公開',
+ 'user-login.publicUsers.placeholder': '公開給',
+ 'user-login.option.A': '同事甲',
+ 'user-login.option.B': '同事乙',
+ 'user-login.option.C': '同事丙',
+ 'user-login.navBar.lang': '語言',
+};
diff --git a/template/pro/src/pages/user/login/style.less b/template/pro/src/pages/user/login/style.less
new file mode 100644
index 0000000000000000000000000000000000000000..d9bbbf33e71c72b8f10ff30b4502cf70dad09cf2
--- /dev/null
+++ b/template/pro/src/pages/user/login/style.less
@@ -0,0 +1,39 @@
+@import '~antd/es/style/themes/default.less';
+
+.main {
+ width: 368px;
+ margin: 0 auto;
+ @media screen and (max-width: @screen-sm) {
+ width: 95%;
+ }
+
+ .icon {
+ margin-left: 16px;
+ color: rgba(0, 0, 0, 0.2);
+ font-size: 24px;
+ vertical-align: middle;
+ cursor: pointer;
+ transition: color 0.3s;
+
+ &:hover {
+ color: @primary-color;
+ }
+ }
+
+ .other {
+ margin-top: 24px;
+ line-height: 22px;
+ text-align: left;
+
+ .register {
+ float: right;
+ }
+ }
+
+ :global {
+ .antd-pro-login-submit {
+ width: 100%;
+ margin-top: 24px;
+ }
+ }
+}
diff --git a/template/pro/src/service-worker.js b/template/pro/src/service-worker.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ff92b6c9a2b601b5b88707c6a52ad1942af758b
--- /dev/null
+++ b/template/pro/src/service-worker.js
@@ -0,0 +1,66 @@
+/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable no-restricted-globals */
+/* eslint-disable no-underscore-dangle */
+/* globals workbox */
+workbox.core.setCacheNameDetails({
+ prefix: 'antd-pro',
+ suffix: 'v1',
+});
+// Control all opened tabs ASAP
+workbox.clientsClaim();
+
+/**
+ * Use precaching list generated by workbox in build process.
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
+ */
+workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
+
+/**
+ * Register a navigation route.
+ * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
+ */
+workbox.routing.registerNavigationRoute('/index.html');
+
+/**
+ * Use runtime cache:
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
+ *
+ * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
+ * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
+ */
+
+/**
+ * Handle API requests
+ */
+workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
+
+/**
+ * Handle third party requests
+ */
+workbox.routing.registerRoute(
+ /^https:\/\/gw.alipayobjects.com\//,
+ workbox.strategies.networkFirst(),
+);
+workbox.routing.registerRoute(
+ /^https:\/\/cdnjs.cloudflare.com\//,
+ workbox.strategies.networkFirst(),
+);
+workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
+
+/**
+ * Response to client after skipping waiting with MessageChannel
+ */
+addEventListener('message', event => {
+ const replyPort = event.ports[0];
+ const message = event.data;
+ if (replyPort && message && message.type === 'skip-waiting') {
+ event.waitUntil(
+ self
+ .skipWaiting()
+ .then(
+ () => replyPort.postMessage({ error: null }),
+ error => replyPort.postMessage({ error }),
+ ),
+ );
+ }
+});
diff --git a/template/pro/src/services/login.ts b/template/pro/src/services/login.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5c694dff9883cbb6302c90cb6dafb51533c1cdc3
--- /dev/null
+++ b/template/pro/src/services/login.ts
@@ -0,0 +1,19 @@
+import request from '@/utils/request';
+
+export interface LoginParamsType {
+ userName: string;
+ password: string;
+ mobile: string;
+ captcha: string;
+}
+
+export async function fakeAccountLogin(params: LoginParamsType) {
+ return request('/api/login/account', {
+ method: 'POST',
+ data: params,
+ });
+}
+
+export async function getFakeCaptcha(mobile: string) {
+ return request(`/api/login/captcha?mobile=${mobile}`);
+}
diff --git a/template/pro/src/services/user.ts b/template/pro/src/services/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..19887217b8f63496a43e5928a75478859121d92d
--- /dev/null
+++ b/template/pro/src/services/user.ts
@@ -0,0 +1,13 @@
+import request from '@/utils/request';
+
+export async function query(): Promise {
+ return request('/api/users');
+}
+
+export async function queryCurrent(): Promise {
+ return request('/api/currentUser');
+}
+
+export async function queryNotices(): Promise {
+ return request('/api/notices');
+}
diff --git a/template/pro/src/typings.d.ts b/template/pro/src/typings.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e1ae4ed54eb0d049c810f95921ce9da0efe85c8c
--- /dev/null
+++ b/template/pro/src/typings.d.ts
@@ -0,0 +1,43 @@
+declare module 'slash2';
+declare module 'antd-theme-webpack-plugin';
+
+declare module '*.css';
+declare module '*.less';
+declare module '*.scss';
+declare module '*.sass';
+declare module '*.svg';
+declare module '*.png';
+declare module '*.jpg';
+declare module '*.jpeg';
+declare module '*.gif';
+declare module '*.bmp';
+declare module '*.tiff';
+declare module 'omit.js';
+declare module 'react-copy-to-clipboard';
+declare module 'react-fittext';
+declare module '@antv/data-set';
+declare module 'nzh/cn';
+declare module 'webpack-theme-color-replacer';
+declare module 'webpack-theme-color-replacer/client';
+
+// google analytics interface
+interface GAFieldsObject {
+ eventCategory: string;
+ eventAction: string;
+ eventLabel?: string;
+ eventValue?: number;
+ nonInteraction?: boolean;
+}
+interface Window {
+ ga: (
+ command: 'send',
+ hitType: 'event' | 'pageview',
+ fieldsObject: GAFieldsObject | string,
+ ) => void;
+}
+
+declare let ga: Function;
+
+// preview.pro.ant.design only do not use in your production ;
+// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+declare let ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: 'site' | undefined;
diff --git a/template/pro/src/utils/Authorized.ts b/template/pro/src/utils/Authorized.ts
new file mode 100644
index 0000000000000000000000000000000000000000..807fc1feb12acb959ef41e09aaa8c89515bdb4d4
--- /dev/null
+++ b/template/pro/src/utils/Authorized.ts
@@ -0,0 +1,13 @@
+import RenderAuthorize from '@/components/Authorized';
+import { getAuthority } from './authority';
+/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable import/no-mutable-exports */
+let Authorized = RenderAuthorize(getAuthority());
+
+// Reload the rights component
+const reloadAuthorized = (): void => {
+ Authorized = RenderAuthorize(getAuthority());
+};
+
+export { reloadAuthorized };
+export default Authorized;
diff --git a/template/pro/src/utils/authority.test.ts b/template/pro/src/utils/authority.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..44d74bbd30b2e93647be8f69f028e9ade87d7102
--- /dev/null
+++ b/template/pro/src/utils/authority.test.ts
@@ -0,0 +1,16 @@
+import { getAuthority } from './authority';
+
+describe('getAuthority should be strong', () => {
+ it('string', () => {
+ expect(getAuthority('admin')).toEqual(['admin']);
+ });
+ it('array with double quotes', () => {
+ expect(getAuthority('"admin"')).toEqual(['admin']);
+ });
+ it('array with single item', () => {
+ expect(getAuthority('["admin"]')).toEqual(['admin']);
+ });
+ it('array with multiple items', () => {
+ expect(getAuthority('["admin", "guest"]')).toEqual(['admin', 'guest']);
+ });
+});
diff --git a/template/pro/src/utils/authority.ts b/template/pro/src/utils/authority.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ffa917884f7646a7ef46db15c8e4e2dd3bb74555
--- /dev/null
+++ b/template/pro/src/utils/authority.ts
@@ -0,0 +1,33 @@
+import { reloadAuthorized } from './Authorized';
+
+// use localStorage to store the authority info, which might be sent from server in actual project.
+export function getAuthority(str?: string): string | string[] {
+ // return localStorage.getItem('antd-pro-authority') || ['admin', 'user'];
+ const authorityString =
+ typeof str === 'undefined' ? localStorage.getItem('antd-pro-authority') : str;
+ // authorityString could be admin, "admin", ["admin"]
+ let authority;
+ try {
+ if (authorityString) {
+ authority = JSON.parse(authorityString);
+ }
+ } catch (e) {
+ authority = authorityString;
+ }
+ if (typeof authority === 'string') {
+ return [authority];
+ }
+ // preview.pro.ant.design only do not use in your production.
+ // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
+ if (!authority && ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
+ return ['admin'];
+ }
+ return authority;
+}
+
+export function setAuthority(authority: string | string[]): void {
+ const proAuthority = typeof authority === 'string' ? [authority] : authority;
+ // auto reload
+ reloadAuthorized();
+ return localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
+}
diff --git a/template/pro/src/utils/request.ts b/template/pro/src/utils/request.ts
new file mode 100644
index 0000000000000000000000000000000000000000..270dfade5f87d561aacf23572ed3e2dccc092c5c
--- /dev/null
+++ b/template/pro/src/utils/request.ts
@@ -0,0 +1,56 @@
+/**
+ * request 网络请求工具
+ * 更详细的 api 文档: https://github.com/umijs/umi-request
+ */
+import { extend } from 'umi-request';
+import { notification } from 'antd';
+
+const codeMessage = {
+ 200: '服务器成功返回请求的数据。',
+ 201: '新建或修改数据成功。',
+ 202: '一个请求已经进入后台排队(异步任务)。',
+ 204: '删除数据成功。',
+ 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+ 401: '用户没有权限(令牌、用户名、密码错误)。',
+ 403: '用户得到授权,但是访问是被禁止的。',
+ 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+ 406: '请求的格式不可得。',
+ 410: '请求的资源被永久删除,且不会再得到的。',
+ 422: '当创建一个对象时,发生一个验证错误。',
+ 500: '服务器发生错误,请检查服务器。',
+ 502: '网关错误。',
+ 503: '服务不可用,服务器暂时过载或维护。',
+ 504: '网关超时。',
+};
+
+/**
+ * 异常处理程序
+ */
+const errorHandler = (error: { response: Response }): Response => {
+ const { response } = error;
+ if (response && response.status) {
+ const errorText = codeMessage[response.status] || response.statusText;
+ const { status, url } = response;
+
+ notification.error({
+ message: `请求错误 ${status}: ${url}`,
+ description: errorText,
+ });
+ } else if (!response) {
+ notification.error({
+ description: '您的网络发生异常,无法连接服务器',
+ message: '网络异常',
+ });
+ }
+ return response;
+};
+
+/**
+ * 配置request请求时的默认参数
+ */
+const request = extend({
+ errorHandler, // 默认错误处理
+ credentials: 'include', // 默认请求是否带上cookie
+});
+
+export default request;
diff --git a/template/pro/src/utils/utils.less b/template/pro/src/utils/utils.less
new file mode 100644
index 0000000000000000000000000000000000000000..de1aa64222b6f14328d3a9e3c262ac5a31cce5af
--- /dev/null
+++ b/template/pro/src/utils/utils.less
@@ -0,0 +1,50 @@
+.textOverflow() {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+}
+
+.textOverflowMulti(@line: 3, @bg: #fff) {
+ position: relative;
+ max-height: @line * 1.5em;
+ margin-right: -1em;
+ padding-right: 1em;
+ overflow: hidden;
+ line-height: 1.5em;
+ text-align: justify;
+ &::before {
+ position: absolute;
+ right: 14px;
+ bottom: 0;
+ padding: 0 1px;
+ background: @bg;
+ content: '...';
+ }
+ &::after {
+ position: absolute;
+ right: 14px;
+ width: 1em;
+ height: 1em;
+ margin-top: 0.2em;
+ background: white;
+ content: '';
+ }
+}
+
+// mixins for clearfix
+// ------------------------
+.clearfix() {
+ zoom: 1;
+ &::before,
+ &::after {
+ display: table;
+ content: ' ';
+ }
+ &::after {
+ clear: both;
+ height: 0;
+ font-size: 0;
+ visibility: hidden;
+ }
+}
diff --git a/template/pro/src/utils/utils.test.ts b/template/pro/src/utils/utils.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c2bb32dc70dbfb874408675f9e551ca56d5ac434
--- /dev/null
+++ b/template/pro/src/utils/utils.test.ts
@@ -0,0 +1,37 @@
+import { isUrl } from './utils';
+
+describe('isUrl tests', (): void => {
+ it('should return false for invalid and corner case inputs', (): void => {
+ expect(isUrl([] as any)).toBeFalsy();
+ expect(isUrl({} as any)).toBeFalsy();
+ expect(isUrl(false as any)).toBeFalsy();
+ expect(isUrl(true as any)).toBeFalsy();
+ expect(isUrl(NaN as any)).toBeFalsy();
+ expect(isUrl(null as any)).toBeFalsy();
+ expect(isUrl(undefined as any)).toBeFalsy();
+ expect(isUrl('')).toBeFalsy();
+ });
+
+ it('should return false for invalid URLs', (): void => {
+ expect(isUrl('foo')).toBeFalsy();
+ expect(isUrl('bar')).toBeFalsy();
+ expect(isUrl('bar/test')).toBeFalsy();
+ expect(isUrl('http:/example.com/')).toBeFalsy();
+ expect(isUrl('ttp://example.com/')).toBeFalsy();
+ });
+
+ it('should return true for valid URLs', (): void => {
+ expect(isUrl('http://example.com/')).toBeTruthy();
+ expect(isUrl('https://example.com/')).toBeTruthy();
+ expect(isUrl('http://example.com/test/123')).toBeTruthy();
+ expect(isUrl('https://example.com/test/123')).toBeTruthy();
+ expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy();
+ expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy();
+ expect(isUrl('http://www.example.com/')).toBeTruthy();
+ expect(isUrl('https://www.example.com/')).toBeTruthy();
+ expect(isUrl('http://www.example.com/test/123')).toBeTruthy();
+ expect(isUrl('https://www.example.com/test/123')).toBeTruthy();
+ expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy();
+ expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy();
+ });
+});
diff --git a/template/pro/src/utils/utils.ts b/template/pro/src/utils/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..71ed4ca595483be9f48c61e96604c2aebbcbfd1e
--- /dev/null
+++ b/template/pro/src/utils/utils.ts
@@ -0,0 +1,24 @@
+import { parse } from 'querystring';
+
+/* eslint no-useless-escape:0 import/prefer-default-export:0 */
+const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
+
+export const isUrl = (path: string): boolean => reg.test(path);
+
+export const isAntDesignPro = (): boolean => {
+ if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') {
+ return true;
+ }
+ return window.location.hostname === 'preview.pro.ant.design';
+};
+
+// 给官方演示站点用,用于关闭真实开发环境不需要使用的特性
+export const isAntDesignProOrDev = (): boolean => {
+ const { NODE_ENV } = process.env;
+ if (NODE_ENV === 'development') {
+ return true;
+ }
+ return isAntDesignPro();
+};
+
+export const getPageQuery = () => parse(window.location.href.split('?')[1]);
diff --git a/template/pro/tests/run-tests.js b/template/pro/tests/run-tests.js
new file mode 100644
index 0000000000000000000000000000000000000000..ea531ef1d014e8a64688acbcd1779dad2120b9ed
--- /dev/null
+++ b/template/pro/tests/run-tests.js
@@ -0,0 +1,49 @@
+/* eslint-disable eslint-comments/disable-enable-pair */
+/* eslint-disable @typescript-eslint/no-var-requires */
+/* eslint-disable eslint-comments/no-unlimited-disable */
+const { spawn } = require('child_process');
+const { kill } = require('cross-port-killer');
+
+const env = Object.create(process.env);
+env.BROWSER = 'none';
+env.TEST = true;
+// flag to prevent multiple test
+let once = false;
+
+const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], {
+ env,
+});
+
+startServer.stderr.on('data', data => {
+ // eslint-disable-next-line
+ console.log(data.toString());
+});
+
+startServer.on('exit', () => {
+ kill(process.env.PORT || 8000);
+});
+
+console.log('Starting development server for e2e tests...');
+startServer.stdout.on('data', data => {
+ console.log(data.toString());
+ // hack code , wait umi
+ if (
+ (!once && data.toString().indexOf('Compiled successfully') >= 0) ||
+ data.toString().indexOf('Theme generated successfully') >= 0
+ ) {
+ // eslint-disable-next-line
+ once = true;
+ console.log('Development server is started, ready to run tests.');
+ const testCmd = spawn(
+ /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
+ ['test', '--', '--maxWorkers=1', '--runInBand'],
+ {
+ stdio: 'inherit',
+ },
+ );
+ testCmd.on('exit', code => {
+ startServer.kill();
+ process.exit(code);
+ });
+ }
+});
diff --git a/template/pro/tsconfig.json b/template/pro/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..638b2a0b1ab6ca51333a9018103e7d856e4cf342
--- /dev/null
+++ b/template/pro/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "esnext",
+ "lib": ["esnext", "dom"],
+ "sourceMap": true,
+ "baseUrl": ".",
+ "jsx": "react",
+ "allowSyntheticDefaultImports": true,
+ "moduleResolution": "node",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "noUnusedLocals": true,
+ "allowJs": true,
+ "experimentalDecorators": true,
+ "strict": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "tslint:latest",
+ "tslint-config-prettier"
+ ]
+}