Repository: win-winFE/dms Branch: master Commit: 92cff6088759 Files: 330 Total size: 3.7 MB Directory structure: gitextract_m6r3lqby/ ├── .babelrc.js ├── .config.js ├── .gitignore ├── .webpackrc.js ├── LICENSE ├── README.md ├── app/ │ ├── assets/ │ │ ├── common/ │ │ │ ├── menu.js │ │ │ └── router.js │ │ ├── components/ │ │ │ ├── ActiveChart/ │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ ├── Authorized/ │ │ │ │ ├── Authorized.js │ │ │ │ ├── AuthorizedRoute.js │ │ │ │ ├── CheckPermissions.js │ │ │ │ ├── CheckPermissions.test.js │ │ │ │ ├── PromiseRender.js │ │ │ │ ├── Secured.js │ │ │ │ ├── demo/ │ │ │ │ │ ├── AuthorizedArray.md │ │ │ │ │ ├── AuthorizedFunction.md │ │ │ │ │ ├── basic.md │ │ │ │ │ └── secured.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ └── index.md │ │ │ ├── AvatarList/ │ │ │ │ ├── AvatarItem.d.ts │ │ │ │ ├── demo/ │ │ │ │ │ └── simple.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.en-US.md │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.zh-CN.md │ │ │ ├── Charts/ │ │ │ │ ├── Bar/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ └── index.js │ │ │ │ ├── ChartCard/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.less │ │ │ │ ├── Field/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.less │ │ │ │ ├── Gauge/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ └── index.js │ │ │ │ ├── MiniArea/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ └── index.js │ │ │ │ ├── MiniBar/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ └── index.js │ │ │ │ ├── MiniProgress/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.less │ │ │ │ ├── Pie/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.less │ │ │ │ ├── Radar/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.less │ │ │ │ ├── TagCloud/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.less │ │ │ │ ├── TimelineChart/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.less │ │ │ │ ├── WaterWave/ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.less │ │ │ │ ├── autoHeight.js │ │ │ │ ├── demo/ │ │ │ │ │ ├── bar.md │ │ │ │ │ ├── chart-card.md │ │ │ │ │ ├── gauge.md │ │ │ │ │ ├── mini-area.md │ │ │ │ │ ├── mini-bar.md │ │ │ │ │ ├── mini-pie.md │ │ │ │ │ ├── mini-progress.md │ │ │ │ │ ├── mix.md │ │ │ │ │ ├── pie.md │ │ │ │ │ ├── radar.md │ │ │ │ │ ├── tag-cloud.md │ │ │ │ │ ├── timeline-chart.md │ │ │ │ │ └── waterwave.md │ │ │ │ ├── g2.js │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.md │ │ │ ├── CountDown/ │ │ │ │ ├── demo/ │ │ │ │ │ └── simple.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.en-US.md │ │ │ │ ├── index.js │ │ │ │ └── index.zh-CN.md │ │ │ ├── DescriptionList/ │ │ │ │ ├── Description.d.ts │ │ │ │ ├── Description.js │ │ │ │ ├── DescriptionList.js │ │ │ │ ├── demo/ │ │ │ │ │ ├── basic.md │ │ │ │ │ └── vertical.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.en-US.md │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ ├── index.zh-CN.md │ │ │ │ └── responsive.js │ │ │ ├── EditableItem/ │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ ├── EditableLinkGroup/ │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ ├── Ellipsis/ │ │ │ │ ├── demo/ │ │ │ │ │ ├── line.md │ │ │ │ │ └── number.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.en-US.md │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.zh-CN.md │ │ │ ├── Exception/ │ │ │ │ ├── demo/ │ │ │ │ │ ├── 403.md │ │ │ │ │ ├── 404.md │ │ │ │ │ └── 500.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.en-US.md │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ ├── index.zh-CN.md │ │ │ │ └── typeConfig.js │ │ │ ├── FooterToolbar/ │ │ │ │ ├── demo/ │ │ │ │ │ └── basic.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.en-US.md │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.zh-CN.md │ │ │ ├── GlobalFooter/ │ │ │ │ ├── demo/ │ │ │ │ │ └── basic.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.md │ │ │ ├── GlobalHeader/ │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ ├── HeaderSearch/ │ │ │ │ ├── demo/ │ │ │ │ │ └── basic.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.md │ │ │ ├── JsonSchemaForm/ │ │ │ │ ├── components/ │ │ │ │ │ ├── AddButton.js │ │ │ │ │ ├── ErrorList.js │ │ │ │ │ ├── Form.js │ │ │ │ │ ├── IconButton.js │ │ │ │ │ ├── fields/ │ │ │ │ │ │ ├── ArrayField.js │ │ │ │ │ │ ├── BooleanField.js │ │ │ │ │ │ ├── DescriptionField.js │ │ │ │ │ │ ├── MultiSchemaField.js │ │ │ │ │ │ ├── NumberField.js │ │ │ │ │ │ ├── ObjectField.js │ │ │ │ │ │ ├── SchemaField.js │ │ │ │ │ │ ├── StringField.js │ │ │ │ │ │ ├── TitleField.js │ │ │ │ │ │ ├── UnsupportedField.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── widgets/ │ │ │ │ │ ├── AltDateTimeWidget.js │ │ │ │ │ ├── AltDateWidget.js │ │ │ │ │ ├── BaseInput.js │ │ │ │ │ ├── CheckboxWidget.js │ │ │ │ │ ├── CheckboxesWidget.js │ │ │ │ │ ├── ColorWidget.js │ │ │ │ │ ├── DateTimeWidget.js │ │ │ │ │ ├── DateWidget.js │ │ │ │ │ ├── EmailWidget.js │ │ │ │ │ ├── FileWidget.js │ │ │ │ │ ├── HiddenWidget.js │ │ │ │ │ ├── PasswordWidget.js │ │ │ │ │ ├── RadioWidget.js │ │ │ │ │ ├── RangeWidget.js │ │ │ │ │ ├── SelectWidget.js │ │ │ │ │ ├── TextWidget.js │ │ │ │ │ ├── TextareaWidget.js │ │ │ │ │ ├── URLWidget.js │ │ │ │ │ ├── UpDownWidget.js │ │ │ │ │ └── index.js │ │ │ │ ├── constants.js │ │ │ │ ├── index.js │ │ │ │ ├── types.js │ │ │ │ ├── utils.js │ │ │ │ └── validate.js │ │ │ ├── Login/ │ │ │ │ ├── LoginItem.js │ │ │ │ ├── LoginSubmit.js │ │ │ │ ├── LoginTab.js │ │ │ │ ├── demo/ │ │ │ │ │ └── basic.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.en-US.md │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ ├── index.zh-CN.md │ │ │ │ └── map.js │ │ │ ├── NoticeIcon/ │ │ │ │ ├── NoticeIconTab.d.ts │ │ │ │ ├── NoticeList.js │ │ │ │ ├── NoticeList.less │ │ │ │ ├── demo/ │ │ │ │ │ ├── basic.md │ │ │ │ │ └── popover.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.md │ │ │ ├── NumberInfo/ │ │ │ │ ├── demo/ │ │ │ │ │ └── basic.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.en-US.md │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.zh-CN.md │ │ │ ├── PageHeader/ │ │ │ │ ├── demo/ │ │ │ │ │ ├── image.md │ │ │ │ │ ├── simple.md │ │ │ │ │ ├── standard.md │ │ │ │ │ └── structure.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ ├── index.md │ │ │ │ └── index.test.js │ │ │ ├── Result/ │ │ │ │ ├── demo/ │ │ │ │ │ ├── classic.md │ │ │ │ │ ├── error.md │ │ │ │ │ └── structure.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.md │ │ │ ├── SiderMenu/ │ │ │ │ ├── SiderMenu.js │ │ │ │ ├── SilderMenu.test.js │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ ├── StandardFormRow/ │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ ├── StandardTable/ │ │ │ │ ├── index.js │ │ │ │ └── index.less │ │ │ ├── TagSelect/ │ │ │ │ ├── TagSelectOption.d.ts │ │ │ │ ├── demo/ │ │ │ │ │ ├── expandable.md │ │ │ │ │ └── simple.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.md │ │ │ ├── Trend/ │ │ │ │ ├── demo/ │ │ │ │ │ ├── basic.md │ │ │ │ │ └── reverse.md │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ ├── index.less │ │ │ │ └── index.md │ │ │ └── _utils/ │ │ │ ├── pathTools.js │ │ │ └── pathTools.test.js │ │ ├── custom/ │ │ │ └── components/ │ │ │ └── Editor.js │ │ ├── e2e/ │ │ │ ├── home.e2e.js │ │ │ └── login.e2e.js │ │ ├── index.ejs │ │ ├── index.js │ │ ├── index.less │ │ ├── layouts/ │ │ │ ├── BasicLayout.js │ │ │ ├── BlankLayout.js │ │ │ ├── PageHeaderLayout.js │ │ │ ├── PageHeaderLayout.less │ │ │ ├── UserLayout.js │ │ │ └── UserLayout.less │ │ ├── models/ │ │ │ ├── error.js │ │ │ ├── global.js │ │ │ ├── index.js │ │ │ ├── login.js │ │ │ ├── register.js │ │ │ └── user.js │ │ ├── rollbar.js │ │ ├── router.js │ │ ├── routes/ │ │ │ ├── Development/ │ │ │ │ ├── App.js │ │ │ │ ├── App.less │ │ │ │ ├── Module.js │ │ │ │ ├── Module.less │ │ │ │ ├── Param.js │ │ │ │ ├── Param.less │ │ │ │ ├── Schema.js │ │ │ │ └── Schema.less │ │ │ ├── Exception/ │ │ │ │ ├── 403.js │ │ │ │ ├── 404.js │ │ │ │ ├── 500.js │ │ │ │ ├── style.less │ │ │ │ └── triggerException.js │ │ │ ├── Operations/ │ │ │ │ ├── App.js │ │ │ │ ├── App.less │ │ │ │ ├── Data.js │ │ │ │ ├── Data.less │ │ │ │ ├── Module.js │ │ │ │ └── Module.less │ │ │ ├── Result/ │ │ │ │ ├── Error.js │ │ │ │ ├── Success.js │ │ │ │ └── Success.test.js │ │ │ └── User/ │ │ │ ├── Login.js │ │ │ ├── Login.less │ │ │ ├── Register.js │ │ │ ├── Register.less │ │ │ ├── RegisterResult.js │ │ │ └── RegisterResult.less │ │ ├── services/ │ │ │ ├── api.js │ │ │ ├── error.js │ │ │ └── user.js │ │ ├── theme.js │ │ └── utils/ │ │ ├── Authorized.js │ │ ├── authority.js │ │ ├── ca.js │ │ ├── constants.js │ │ ├── fetch.js │ │ ├── request.js │ │ ├── url.js │ │ ├── utils.js │ │ └── utils.less │ ├── controller/ │ │ ├── application.js │ │ ├── auth.js │ │ ├── data.js │ │ ├── home.js │ │ ├── module.js │ │ ├── param.js │ │ ├── put.js │ │ └── user.js │ ├── lib/ │ │ └── AliOSS.js │ ├── middleware/ │ │ └── auth.js │ ├── model/ │ │ ├── application.js │ │ ├── auth.js │ │ ├── data.js │ │ ├── module.js │ │ ├── param.js │ │ └── user.js │ ├── public/ │ │ ├── index.3e1a724e.css │ │ ├── index.3eaede4e.js │ │ └── index.html │ ├── router.js │ └── util/ │ ├── common.js │ ├── response.js │ └── utils.js ├── config/ │ ├── config.default.js │ ├── manifest.json │ └── plugin.js ├── database/ │ ├── dms.sql │ └── init.sql ├── package.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc.js ================================================ const path = require('path'); module.exports = { plugins: [ [ 'module-resolver', { alias: { components: path.join(__dirname, './src/components'), }, }, ], [ 'import', { libraryName: 'antd', style: true, // or 'css' }, ], ], }; ================================================ FILE: .config.js ================================================ module.exports = { jwtSecret: 'winwinfe', // jwt加密key dmsUploadAPI: 'http://127.0.0.1:7100/api', // dms上传服务访问地址,项目地址:https://github.com/gavin1995/dms-upload useCloud: 'OSS', // OSS、AZURE、false useServerLess: false, // ALI_CLOUD、AMAZON(暂不支持)、false cdnPrefix: 'https://dms.oss-cn-hangzhou.aliyuncs.com/', // TODO: 请重新配置cdn前缀 // mysql配置 TODO: 请修改 sequelize: { dialect: 'mysql', database: 'dms', host: '127.0.0.1', port: '3306', username: 'root', password:'root1234', timezone: '+08:00' }, // 文件上传配置 multipart: { autoFields: false, defaultCharset: 'utf8', fieldNameSize: 100, fieldSize: '100kb', fields: 10, fileSize: '10mb', files: 10, fileExtensions: [], whitelist: null, }, // redis配置 redis: { client: { port: 6379, host: '127.0.0.1', password: null, db: 0 } }, // 生产环境日志配置 log: { dir: '/opt/logs/nodejs' }, // 阿里云相关配置 TODO: 请修改 aliCloud: { ossRegion: 'oss-cn-hangzhou', ossBucket: 'dms', assessKeyId: 'assessKeyId', secretAccessKey: 'secretAccessKey', ossStaticUrl: 'https://dms.oss-cn-hangzhou.aliyuncs.com', // OSS访问地址前缀 } }; ================================================ FILE: .gitignore ================================================ node_modules .idea logs yarn-*.log .DS_Store CODE_OF_CONDUCT.md opt/ run/ dist/ config.js ================================================ FILE: .webpackrc.js ================================================ const path = require('path'); export default { entry: 'app/assets/index.js', extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]], env: { development: { extraBabelPlugins: ['dva-hmr'], }, }, alias: { components: path.resolve(__dirname, './app/assets//components/'), }, outputPath: path.resolve(__dirname, './app/public/'), ignoreMomentLocale: true, theme: './app/assets//theme.js', html: { template: './app/assets/index.ejs', }, es5ImcompatibleVersions: true, disableDynamicImport: true, publicPath: 'app/public', hash: true, manifest: { fileName: path.resolve(__dirname, './config/manifest.json') } }; ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 gavin1995 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. ================================================ FILE: README.md ================================================ DMS正在进行发布以来最大的一次更新,敬请期待... - Gavin 2021.10.18 ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/use.gif) [![issue](https://img.shields.io/github/issues/gavin1995/dms.svg)](https://github.com/gavin1995/dms) [![license](https://img.shields.io/github/license/gavin1995/dms.svg)](https://github.com/gavin1995/dms) ## 动态数据管理神器-DMS ### 重构中,敬请期待... ### 介绍 #### 什么是DMS? 基于Json Schema/UI Schema`模块化`的Json动态数据管理平台。 #### 什么是Json Schema/UI Schema? * 用于动态生成表单的Schema,参考 [Json Schema使用案例](https://mozilla-services.github.io/react-jsonschema-form/) * [官方文档](https://json-schema.org/understanding-json-schema/index.html) #### 使用场景有哪些? 无论前端、后端、移动端、运维,理论上所有需要动态配置数据的场景都可以使用。 针对前端、移动端:可以配置页面每个模块展示型数据,也可以配置各种版本号用于动态更新,各种功能开关、页面主题等。 针对后端:可以配置业务相关的ID,配置类目,城市列表,热门等。 针对运维:可以作为区分环境的配置中心等。 当然使用场景远不止这些...... #### 可以运用到生产环境吗? 当然可以,DMS存储的数据读写是完全分开的,目前支持通过Redis(使用redis获取数据方式请参考[注意](https://github.com/gavin1995/dms#%E6%B3%A8%E6%84%8F))、CDN(推荐)两种获取数据方式。即使DMS自身服务器挂掉,也不会影响数据的读取。强烈推荐使用CDN的方式,这样稳定性和使用的CDN是一样的。 #### DMS应用、模块、参数介绍 * 应用:包含一个或多个模块,包含一个或多个参数 * 模块:配置数据的最小单位 * 参数:使模块根据不同参数配置不同数据(如:每个城市展示的频道页不一样) ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/tb.png) #### DMS特性 * 实时表单预览; * 模块化(组件化)数据管理; * 支持表单数据逻辑判断、数据验证; * Schema数据自动保存(默认关闭),防止误操作及未知异常; * 支持动态增加参数,参数本身也可以为DMS生成的配置数据; * 配合[dms-upload](https://github.com/gavin1995/dms-upload)可以快速将通过表单上传的文件传入CDN/云存储 * 符合实际场景的权限控制:开发只负责schema编写,需求方配置所有数据; * 支持Schema生成所有基本表单类型及高级控件,如:日期选择器、进度条、密码框、颜色选择器等; * 实时数据预览/审核(配合[dms-fetch](https://github.com/gavin1995/dms-fetch),同时支持服务端代理请求,及浏览器端请求的数据预览与审核) * 支持anyOf * 实时错误提示,错误提示支持中文 * 运营/产品权限区分 * 统一表单图片上传管理 * 应用、模块、参数、权限管理 * 使用Redis缓存数据(需配合使用:[dms-api](https://github.com/gavin1995/dms-api)) * Json Schema/UI Schema在线编辑及生成表单预览 * 使用表单编辑动态数据及实时数据审核(配合使用[dms-fetch](https://github.com/gavin1995/dms-fetch)) * 使用CDN缓存数据,目前已支持Azure CDN(配合使用[dms-upload](https://github.com/gavin1995/dms-upload)) #### TODO - [ ] 示例项目 - [ ] [阿里云Serverless](https://serverless.aliyun.com/)支持及数据二次加工 #### 需求池 - [ ] 样式优化 - [ ] [Formily](https://github.com/alibaba/formily)接入 - [ ] webassembly前端加密 - [ ] 在线Demo - [ ] 初始化命令行交互配置 #### 最近三月完成功能 - [x] [阿里云OSS](https://cn.aliyun.com/product/oss)支持 - **2020.04.10** ### 快速开始 **请先确保已经安装好:nodejs8+、mysql、redis,并已开启相关服务** **安装DMS** ```bash > git clone https://github.com/gavin1995/dms.git > yarn # 若没有yarn,请使用 npm install ``` **创建日志目录** ```bash > mkdir /opt/logs/nodejs -p ``` **按需修改配置** * 将项目根目录下的`.config.js`改名为`config.js` * 对`config.js`按需进行配置修改 **执行初始化sql** * 使用mysql执行 dms/database/dms.sql **启动/停止/调试** 启动端口默认为:7101,需要修改请修改dms/package.json文件start部分的7101 ```bash > yarn start # 启动,若没有yarn,请使用 npm run start > yarn stop # 停止, npm run stop > yarn dev # 调试,npm run dev ``` **注册** 进入:`http://localhost:7101` 将自动跳转到登录页,选择【注册】,按要求填写相关数据,注册成功将自动跳转到【应用管理】页面 **新建示例应用** 点击【新建应用】,新建如下应用 ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/create-app-modal.png) **新建示例模块** 点击“淘宝首页”的【模块列表】,点击【新建模块】 ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/create-module-modal.png) **编写该模块Schema** 点击“首页banner”的【编辑Schema定义】,复制如下Schema到【Schema定义】中并【保存Schema】 ```json { "title": "示例", "description": "视频/图片展示配置示例", "type": "array", "minItems": 3, "items": { "type": "object", "properties": { "url": { "title": "跳转链接", "type": "string" }, "imgs": { "title": "轮播图片", "type": "string", "format": "file" } } } } ``` **添加一个参数** 进入【参数列表】,添加如下参数 ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/create-params.png) 【编辑参数】,【提交】如下参数 ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/edit-params.png) **编辑数据** 点击左侧菜单,进入【数据管理】,进入“淘宝首页”应用的【模块列表】,选择城市后点击【进入】,再选择“首页banner”的【编辑模块数据】,此时还不能上传图片、保存数据,需要启用[dms-upload](https://github.com/gavin1995/dms-upload) **启动dms-upload** ```bash > git clone https://github.com/gavin1995/dms-upload.git > yarn # npm install ``` **执行初始化sql** * 使用mysql执行 dms-upload/database/dms-upload.sql * 使用mysql执行 dms-upload/database/init.sql(用于上传时的权限验证,默认:root root1234) * 修改项目中mysql/redis相关配置dms/config/config.default.js(mysql默认密码为:root1234) **配置dms-upload** * 启动端口(默认7100):dms-upload/package.json start部分,若修改端口。请修改 dms/app/util/constants.js dmsUploadAPI 中的请求地址前缀 * 数据库配置:dms-upload/config/config.defult.js * CDN文件保存目录(默认/usr/local/services/cdn/dms):dms-upload/config/config.defult.js cdnDir * CDN文件访问地址前缀(默认//127.0.0.1:5000/dms):dms-upload/config/config.defult.js cdnPrefix **新建CDN文件(图片、json数据)保存目录** ```bash > mkdir /usr/local/services/cdn/dms/data -p # 若未使用默认cdnDir,请修改data前面部分 > mkdir /usr/local/services/cdn/dms/res -p # 若未使用默认cdnDir,请修改res前面部分 ``` **启动dms-upload** ```bash > yarn start # npm run start ``` **本地调试上传图片回显** ```bash > cd /usr/local/services/cdn > python -m SimpleHTTPServer 5000 # python3 请使用: python3 -m http.server 5000 ``` **继续回到DMS平台编辑数据** 提交下列数据 ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/edit-data.png) #### 直接访问数据(用于非js使用场景) **临时数据:提交后复制成功Toast中的链接,可以直接访问临时数据数据** ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/toast.png) **正式数据:将临时数据审核为正式数据,也可以通过Toast中的链接直接访问正式数据** ![](https://github.com/gavin1995/dms/blob/master/app/public/assets/images/module-list.png) #### 使用dms-fetch访问数据(用于js使用场景) 1.项目中安装dms-fetch(不建议,强依赖axios,说明见QA) ```bash > yarn add dms-fetch # npm install dms-fetch --save ``` 2.带参数使用示例(伪代码) ```js import { getDMSDataByCDN } from 'dms-fetch'; import ... // 复制编辑数据页面的唯一标示,下面是React应用配合使用DMS参数的示例 export default class extends React.Component { ... fetchData = async () => { const { city } = getParams(this.props.location.search); const dmsData = await getDMSDataByCDN(`/7/10/city/${city}`, this.props.location.search); this.setState({ dmsData, }); }; ... render() { ... } } ``` ### 高级 #### 配置应用参数 * 在参数列表新建参数 * 编辑参数: 1. 可以使用返回结果为 `key value 形式的对象数组` 的api生成下拉列表,配置接口地址后,请选择【使用接口地址生成参数】 2. 可以使用手动配置 `key value 形式的对象数组` ,点击+号手动添加下拉菜单项,最后选择【提交】 * 参数可以配合审核地址使用(审核地址里面使用大括号{}包装参数将自动解析,动态生成审核地址) * 使用DMS自身生成参数列表示例Schema(城市参数为例): ```json { "title": "编辑城市值", "description": "用于城市选择下拉菜单", "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "object", "required": ["key", "value"], "properties": { "key": { "type": "string", "title": "下拉菜单提交值(如:chengdu)" }, "value": { "type": "string", "title": "下拉菜单项名称(如:成都)" } }, "message": { "required": "必须完整填写表单的每一项" } } } ``` #### DMS自定义文件上传(配合使用[dms-upload](https://github.com/gavin1995/dms-upload)) ```bash # 有任何问题可以加最下面的QQ群 # dms-upload带有权限验证(该功能默认关闭,外网使用请打开相关注释) # 需要先执行`dms-upload/database/dms-upload.sql` # 执行`dms-upload/database/init.sql`后,即可通过root root1234用户授权(也可以使用/api/create创建) # 修改项目中mysql/redis相关配置`dms-upload/config/config.default.js`(mysql默认密码为:root1234) # 默认文件保存在/usr/local/services/cdn/dms目录,通过//127.0.0.1:5000/dms访问 # 修改保存路径及访问域名,请修改dms-upload/config/config.default.js: cdnDir、cdnPrefix # 建议改写dms-upload与自己公司的CDN、云存储等结合,或者独立部署一台服务器,通过lsyncd做实时文件同步 > git clone https://github.com/gavin1995/dms-upload.git # 获取dms-upload项目 > yarn # npm install > yarn start # npm run start ``` #### 数据访问(配合使用[dms-api](https://github.com/gavin1995/dms-api)) ```bash # 有任何问题可以加最下面的微信群 # 获取模块数据 # 通过dms平台的【运营配置】->【数据管理】->【模块列表】->【编辑模块数据】 # 获取到请求前缀与唯一标示,拼装在一起即可发起GET请求 > git clone https://github.com/gavin1995/dms-api.git # 获取dms-api项目 > yarn # npm install > yarn start # npm run start ``` #### 数据审核(配合使用[dms-fetch](https://github.com/gavin1995/dms-fetch)) ```bash # 有任何问题可以加最下面的微信群 # 在需要用到DMS的项目里面执行 > yarn add dms-fetch # npm install --save dms-fetch ``` #### 审核 在DMS中配置【开发配置】->【模块管理】中配置【关联审核地址】 地址支持参数匹配,如: ```bash # 配置模块是使用了city参数,则地址可以配为 https://your-app.com?_c={city} # 选择参数不同时,跳转的审核地址也会不一样 ``` #### 调试 ```bash > yarn dev # npm run dev 编译后请替换public里相关文件,并修改config/manifest.json ``` ### FAQ
如何使用CDN? 1. 直接利用nginx将相关目录映射出去 2. 使用lsyncd将相关目录同步到线上相关CDN机器、云存储等(有些CDN需要强刷,目前DMS原生支持Azure CDN强刷)

怎么使用Azure CDN? 1. 打开dms-upload/app/controller/put以下注释 const { refreshRes } = require('../util/azure'); // 10行左右 await refreshRes(fileUrl); // 51行左右 2.配置Azure CDN相关配置:dms-upload/app/util/azure.js

如果遇到未知错误、意外操作怎么办? dms自身有Schema自动保存功能,重新进入页面(刷新)即可,也可以打开控制台,每次对Schema的修改都会打印到浏览器的控制台。

为什么不建议直接使用dms-fetch? dms-fetch只是简单做了数据连接拼装的事情,建议直接将相关使用到的代码写入自己项目,统一请求处理,统一错误处理。

salt放在前端,如何做数据链接防盗?? 可以使用我朋友的前端代码加密: SecurityWorker,独立Javascript VM + 二进制混淆,几乎是不可能做到代码反向的,也就看不到salt了。

为什么数据库使用Mysql?而不用MongoDB等Json友好型存储引擎? 在生产环境中,所有请求都会走缓存/CDN。 对于用什么存储原始数据不是很重要,Mysql对于多数开发更加友好易用,且在后台配置数据时不需要过多地考虑性能问题。

Schema与数据的存储为什么不直接用Mysql5.7.8的原生JSON类型? 在生产环境中,使用到Mysql5.7.8+的公司应该是少数,考虑到大多数实际场景,所以使用TEXT类型存储。 当然有需要的同学,可以直接将相关数据字段改为JSON。

### 注意 * 用Redis可能遇到的问题:针对用DMS应用量巨大的公司,会使redis集群占用内存飙高,随着业务不断的增加,该集群的稳定性要求会不断提高,如果集群挂掉,所有压力将使mysql承接,请提前做好相应的预防措施 * 不建议直接使用dms-fetch * 若npm安装出现问题,请使用yarn * 若超管需要访问非自己创建的应用,需要先给自己授权(防止误操作) * 若dms不能使用 127.0.0.1:7100 访问dms-upload时,请修改dms/app/util/constants.js中的dmsUploadAPI ### 参与贡献 我非常欢迎你的贡献,你可以通过以下方式和我一起共建 :smiley:: - 在你的公司或个人项目中使用`dms`。 - 通过 [Issue](https://github.com/gavin1995/dms/issues) 报告 bug 或进行咨询。 - 提交 [Pull Request](https://github.com/gavin1995/dms/pulls) 改进 `dms` 的代码(注意:提交前请先执行`yarn build`产出可直接start启动的代码)。 ================================================ FILE: app/assets/common/menu.js ================================================ import { isUrl } from '../utils/utils'; const menuData = [ { name: '开发配置', icon: 'dashboard', path: 'development', children: [ { name: '应用管理', path: 'app', }, ], }, { name: '运营配置', icon: 'form', path: 'operations', children: [ { name: '数据管理', path: 'app', }, ], }, ]; function formatter(data, parentPath = '/', parentAuthority) { return data.map(item => { let { path } = item; if (!isUrl(path)) { path = parentPath + item.path; } const result = { ...item, path, authority: item.authority || parentAuthority, }; if (item.children) { result.children = formatter(item.children, `${parentPath}${item.path}/`, item.authority); } return result; }); } export const getMenuData = () => formatter(menuData); ================================================ FILE: app/assets/common/router.js ================================================ import { createElement } from 'react'; import dynamic from 'dva/dynamic'; import pathToRegexp from 'path-to-regexp'; import { getMenuData } from './menu'; let routerDataCache; const modelNotExisted = (app, model) => // eslint-disable-next-line !app._models.some(({ namespace }) => { return namespace === model.substring(model.lastIndexOf('/') + 1); }); // wrapper of dynamic const dynamicWrapper = (app, models, component) => { // () => require('module') // transformed by babel-plugin-dynamic-import-node-sync if (component.toString().indexOf('.then(') < 0) { models.forEach(model => { if (modelNotExisted(app, model)) { // eslint-disable-next-line app.model(require(`../models/${model}`).default); } }); return props => { if (!routerDataCache) { routerDataCache = getRouterData(app); } return createElement(component().default, { ...props, routerData: routerDataCache, }); }; } // () => import('module') return dynamic({ app, models: () => models.filter(model => modelNotExisted(app, model)).map(m => import(`../models/${m}.js`)), // add routerData prop component: () => { if (!routerDataCache) { routerDataCache = getRouterData(app); } return component().then(raw => { const Component = raw.default || raw; return props => createElement(Component, { ...props, routerData: routerDataCache, }); }); }, }); }; function getFlatMenuData(menus) { let keys = {}; menus.forEach(item => { if (item.children) { keys[item.path] = { ...item }; keys = { ...keys, ...getFlatMenuData(item.children) }; } else { keys[item.path] = { ...item }; } }); return keys; } export const getRouterData = (app) => { const routerConfig = { '/': { component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')), }, '/development/app': { component: dynamicWrapper(app, ['user'], () => import('../routes/Development/App')), }, '/development/module': { component: dynamicWrapper(app, [], () => import('../routes/Development/Module')), }, '/development/param': { component: dynamicWrapper(app, [], () => import('../routes/Development/Param')), }, '/development/schema': { component: dynamicWrapper(app, [], () => import('../routes/Development/Schema')), }, '/operations/app': { component: dynamicWrapper(app, [], () => import('../routes/Operations/App')), }, '/operations/module': { component: dynamicWrapper(app, [], () => import('../routes/Operations/Module')), }, '/operations/data': { component: dynamicWrapper(app, [], () => import('../routes/Operations/Data')), }, '/result/success': { component: dynamicWrapper(app, [], () => import('../routes/Result/Success')), }, '/result/fail': { component: dynamicWrapper(app, [], () => import('../routes/Result/Error')), }, '/exception/403': { component: dynamicWrapper(app, [], () => import('../routes/Exception/403')), }, '/exception/404': { component: dynamicWrapper(app, [], () => import('../routes/Exception/404')), }, '/exception/500': { component: dynamicWrapper(app, [], () => import('../routes/Exception/500')), }, '/exception/trigger': { component: dynamicWrapper(app, ['error'], () => import('../routes/Exception/triggerException') ), }, '/user': { component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')), }, '/user/login': { component: dynamicWrapper(app, ['login'], () => import('../routes/User/Login')), }, '/user/register': { component: dynamicWrapper(app, ['register'], () => import('../routes/User/Register')), }, '/user/register-result': { component: dynamicWrapper(app, [], () => import('../routes/User/RegisterResult')), } }; // Get name from ./menu.js or just set it in the router data. const menuData = getFlatMenuData(getMenuData()); // Route configuration data // eg. {name,authority ...routerConfig } const routerData = {}; // The route matches the menu Object.keys(routerConfig).forEach(path => { // Regular match item name // eg. router /user/:id === /user/chen const pathRegexp = pathToRegexp(path); const menuKey = Object.keys(menuData).find(key => pathRegexp.test(`${key}`)); let menuItem = {}; // If menuKey is not empty if (menuKey) { menuItem = menuData[menuKey]; } let router = routerConfig[path]; // If you need to configure complex parameter routing, // https://github.com/ant-design/ant-design-pro-site/blob/master/docs/router-and-nav.md#%E5%B8%A6%E5%8F%82%E6%95%B0%E7%9A%84%E8%B7%AF%E7%94%B1%E8%8F%9C%E5%8D%95 // eg . /list/:type/user/info/:id router = { ...router, name: router.name || menuItem.name, authority: router.authority || menuItem.authority, hideInBreadcrumb: router.hideInBreadcrumb || menuItem.hideInBreadcrumb, }; routerData[path] = router; }); return routerData; }; ================================================ FILE: app/assets/components/ActiveChart/index.js ================================================ import React, { Component } from 'react'; import { MiniArea } from '../Charts'; import NumberInfo from '../NumberInfo'; import styles from './index.less'; function fixedZero(val) { return val * 1 < 10 ? `0${val}` : val; } function getActiveData() { const activeData = []; for (let i = 0; i < 24; i += 1) { activeData.push({ x: `${fixedZero(i)}:00`, y: Math.floor(Math.random() * 200) + i * 50, }); } return activeData; } export default class ActiveChart extends Component { state = { activeData: getActiveData(), }; componentDidMount() { this.timer = setInterval(() => { this.setState({ activeData: getActiveData(), }); }, 1000); } componentWillUnmount() { clearInterval(this.timer); } render() { const { activeData = [] } = this.state; return (
{activeData && (

{[...activeData].sort()[activeData.length - 1].y + 200} 亿元

{[...activeData].sort()[Math.floor(activeData.length / 2)].y} 亿元

)} {activeData && (
00:00 {activeData[Math.floor(activeData.length / 2)].x} {activeData[activeData.length - 1].x}
)}
); } } ================================================ FILE: app/assets/components/ActiveChart/index.less ================================================ .activeChart { position: relative; } .activeChartGrid { p { position: absolute; top: 80px; } p:last-child { top: 115px; } } .activeChartLegend { position: relative; font-size: 0; margin-top: 8px; height: 20px; line-height: 20px; span { display: inline-block; font-size: 12px; text-align: center; width: 33.33%; } span:first-child { text-align: left; } span:last-child { text-align: right; } } ================================================ FILE: app/assets/components/Authorized/Authorized.js ================================================ import React from 'react'; import CheckPermissions from './CheckPermissions'; class Authorized extends React.Component { render() { const { children, authority, noMatch = null } = this.props; const childrenRender = typeof children === 'undefined' ? null : children; return CheckPermissions(authority, childrenRender, noMatch); } } export default Authorized; ================================================ FILE: app/assets/components/Authorized/AuthorizedRoute.js ================================================ import React from 'react'; import { Route, Redirect } from 'react-router-dom'; import Authorized from './Authorized'; class AuthorizedRoute extends React.Component { render() { const { component: Component, render, authority, redirectPath, ...rest } = this.props; return ( } />} > (Component ? : render(props))} /> ); } } export default AuthorizedRoute; ================================================ FILE: app/assets/components/Authorized/CheckPermissions.js ================================================ import React from 'react'; import PromiseRender from './PromiseRender'; import { CURRENT } from './index'; function isPromise(obj) { return ( !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function' ); } /** * 通用权限检查方法 * Common check permissions method * @param { 权限判定 Permission judgment type string |array | Promise | Function } authority * @param { 你的权限 Your permission description type:string} currentAuthority * @param { 通过的组件 Passing components } target * @param { 未通过的组件 no pass components } Exception */ const checkPermissions = (authority, currentAuthority, target, Exception) => { // 没有判定权限.默认查看所有 // Retirement authority, return target; if (!authority) { return target; } // 数组处理 if (Array.isArray(authority)) { if (authority.indexOf(currentAuthority) >= 0) { return target; } return Exception; } // string 处理 if (typeof authority === 'string') { if (authority === currentAuthority) { return target; } return Exception; } // Promise 处理 if (isPromise(authority)) { return ; } // Function 处理 if (typeof authority === 'function') { try { const bool = authority(currentAuthority); if (bool) { return target; } return Exception; } catch (error) { throw error; } } throw new Error('unsupported parameters'); }; export { checkPermissions }; const check = (authority, target, Exception) => { return checkPermissions(authority, CURRENT, target, Exception); }; export default check; ================================================ FILE: app/assets/components/Authorized/CheckPermissions.test.js ================================================ import { checkPermissions } from './CheckPermissions.js'; const target = 'ok'; const error = 'error'; describe('test CheckPermissions', () => { it('Correct string permission authentication', () => { expect(checkPermissions('user', 'user', target, error)).toEqual('ok'); }); it('Correct string permission authentication', () => { expect(checkPermissions('user', 'NULL', target, error)).toEqual('error'); }); it('authority is undefined , return ok', () => { expect(checkPermissions(null, 'NULL', target, error)).toEqual('ok'); }); it('currentAuthority is undefined , return error', () => { expect(checkPermissions('admin', null, target, error)).toEqual('error'); }); it('Wrong string permission authentication', () => { expect(checkPermissions('admin', 'user', target, error)).toEqual('error'); }); it('Correct Array permission authentication', () => { expect(checkPermissions(['user', 'admin'], 'user', target, error)).toEqual('ok'); }); it('Wrong Array permission authentication,currentAuthority error', () => { expect(checkPermissions(['user', 'admin'], 'user,admin', target, error)).toEqual('error'); }); it('Wrong Array permission authentication', () => { expect(checkPermissions(['user', 'admin'], 'guest', target, error)).toEqual('error'); }); it('Wrong Function permission authentication', () => { expect(checkPermissions(() => false, 'guest', target, error)).toEqual('error'); }); it('Correct Function permission authentication', () => { expect(checkPermissions(() => true, 'guest', target, error)).toEqual('ok'); }); }); ================================================ FILE: app/assets/components/Authorized/PromiseRender.js ================================================ import React from 'react'; import { Spin } from 'antd'; export default class PromiseRender extends React.PureComponent { state = { component: null, }; componentDidMount() { this.setRenderComponent(this.props); } componentWillReceiveProps(nextProps) { // new Props enter this.setRenderComponent(nextProps); } // set render Component : ok or error setRenderComponent(props) { const ok = this.checkIsInstantiation(props.ok); const error = this.checkIsInstantiation(props.error); props.promise .then(() => { this.setState({ component: ok, }); }) .catch(() => { this.setState({ component: error, }); }); } // Determine whether the incoming component has been instantiated // AuthorizedRoute is already instantiated // Authorized render is already instantiated, children is no instantiated // Secured is not instantiated checkIsInstantiation = target => { if (!React.isValidElement(target)) { return target; } return () => target; }; render() { const Component = this.state.component; return Component ? ( ) : (
); } } ================================================ FILE: app/assets/components/Authorized/Secured.js ================================================ import React from 'react'; import Exception from '../Exception/index'; import CheckPermissions from './CheckPermissions'; /** * 默认不能访问任何页面 * default is "NULL" */ const Exception403 = () => ; // Determine whether the incoming component has been instantiated // AuthorizedRoute is already instantiated // Authorized render is already instantiated, children is no instantiated // Secured is not instantiated const checkIsInstantiation = target => { if (!React.isValidElement(target)) { return target; } return () => target; }; /** * 用于判断是否拥有权限访问此view权限 * authority 支持传入 string ,funtion:()=>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, funtion: () => boolean | Promise * e.g. 'user' only user user can access * e.g. 'user, admin' user and admin can access * e.g. () => boolean true to be able to visit, return false can not be accessed * e.g. Promise then can not access the visit to catch * @param {string | function | Promise} authority * @param {ReactNode} error 非必需参数 */ const authorize = (authority, error) => { /** * conversion into a class * 防止传入字符串时找不到staticContext造成报错 * String parameters can cause staticContext not found error */ let classError = false; if (error) { classError = () => error; } if (!authority) { throw new Error('authority is required'); } return function decideAuthority(targer) { const component = CheckPermissions(authority, targer, classError || Exception403); return checkIsInstantiation(component); }; }; export default authorize; ================================================ FILE: app/assets/components/Authorized/demo/AuthorizedArray.md ================================================ --- order: 1 title: zh-CN: 使用数组作为参数 en-US: Use Array as a parameter --- Use Array as a parameter ```jsx import RenderAuthorized from 'ant-design-pro/lib/Authorized'; import { Alert } from 'antd'; const Authorized = RenderAuthorized('user'); const noMatch = ; ReactDOM.render( , mountNode, ); ``` ================================================ FILE: app/assets/components/Authorized/demo/AuthorizedFunction.md ================================================ --- order: 2 title: zh-CN: 使用方法作为参数 en-US: Use function as a parameter --- Use Function as a parameter ```jsx import RenderAuthorized from 'ant-design-pro/lib/Authorized'; import { Alert } from 'antd'; const Authorized = RenderAuthorized('user'); const noMatch = ; const havePermission = () => { return false; }; ReactDOM.render( , mountNode, ); ``` ================================================ FILE: app/assets/components/Authorized/demo/basic.md ================================================ --- order: 0 title: zh-CN: 基本使用 en-US: Basic use --- Basic use ```jsx import RenderAuthorized from 'ant-design-pro/lib/Authorized'; import { Alert } from 'antd'; const Authorized = RenderAuthorized('user'); const noMatch = ; ReactDOM.render(
, mountNode, ); ``` ================================================ FILE: app/assets/components/Authorized/demo/secured.md ================================================ --- order: 3 title: zh-CN: 注解基本使用 en-US: Basic use secured --- secured demo used ```jsx import RenderAuthorized from 'ant-design-pro/lib/Authorized'; import { Alert } from 'antd'; const { Secured } = RenderAuthorized('user'); @Secured('admin') class TestSecuredString extends React.Component { render() { ; } } ReactDOM.render(
, mountNode, ); ``` ================================================ FILE: app/assets/components/Authorized/index.d.ts ================================================ import * as React from 'react'; import { RouteProps } from 'react-router'; type authorityFN = (currentAuthority?: string) => boolean; type authority = string | Array | authorityFN | Promise; export type IReactComponent

= | React.StatelessComponent

| React.ComponentClass

| React.ClassicComponentClass

; interface Secured { (authority: authority, error?: React.ReactNode): (target: T) => T; } export interface AuthorizedRouteProps extends RouteProps { authority: authority; } export class AuthorizedRoute extends React.Component {} interface check { ( authority: authority, target: T, Exception: S ): T | S; } interface AuthorizedProps { authority: authority; noMatch?: React.ReactNode; } export class Authorized extends React.Component { static Secured: Secured; static AuthorizedRoute: typeof AuthorizedRoute; static check: check; } declare function renderAuthorize(currentAuthority: string): typeof Authorized; export default renderAuthorize; ================================================ FILE: app/assets/components/Authorized/index.js ================================================ import Authorized from './Authorized'; import AuthorizedRoute from './AuthorizedRoute'; import Secured from './Secured'; import check from './CheckPermissions.js'; /* eslint-disable import/no-mutable-exports */ let CURRENT = 'NULL'; Authorized.Secured = Secured; Authorized.AuthorizedRoute = AuthorizedRoute; Authorized.check = check; /** * use authority or getAuthority * @param {string|()=>String} currentAuthority */ const renderAuthorize = currentAuthority => { if (currentAuthority) { if (currentAuthority.constructor.name === 'Function') { CURRENT = currentAuthority(); } if (currentAuthority.constructor.name === 'String') { CURRENT = currentAuthority; } } else { CURRENT = 'NULL'; } return Authorized; }; export { CURRENT }; export default renderAuthorize; ================================================ FILE: app/assets/components/Authorized/index.md ================================================ --- title: en-US: Authorized zh-CN: Authorized subtitle: 权限 cols: 1 order: 15 --- 权限组件,通过比对现有权限与准入权限,决定相关元素的展示。 ## API ### RenderAuthorized `RenderAuthorized: (currentAuthority: string | () => string) => Authorized` 权限组件默认 export RenderAuthorized 函数,它接收当前权限作为参数,返回一个权限对象,该对象提供以下几种使用方式。 ### Authorized 最基础的权限控制。 | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | children | 正常渲染的元素,权限判断通过时展示 | ReactNode | - | | authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean` | - | | noMatch | 权限异常渲染元素,权限判断不通过时展示 | ReactNode | - | ### Authorized.AuthorizedRoute | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean` | - | | redirectPath | 权限异常时重定向的页面路由 | string | - | 其余参数与 `Route` 相同。 ### Authorized.Secured 注解方式,`@Authorized.Secured(authority, error)` | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean` | - | | error | 权限异常时渲染元素 | ReactNode | | ### Authorized.check 函数形式的 Authorized,用于某些不能被 HOC 包裹的组件。 `Authorized.check(authority, target, Exception)` 注意:传入一个 Promise 时,无论正确还是错误返回的都是一个 ReactClass。 | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean` | - | | target | 权限判断通过时渲染的元素 | ReactNode | - | | Exception | 权限异常时渲染元素 | ReactNode | - | ================================================ FILE: app/assets/components/AvatarList/AvatarItem.d.ts ================================================ import * as React from 'react'; export interface IAvatarItemProps { tips: React.ReactNode; src: string; style?: React.CSSProperties; } export default class AvatarItem extends React.Component { constructor(props: IAvatarItemProps); } ================================================ FILE: app/assets/components/AvatarList/demo/simple.md ================================================ --- order: 0 title: zh-CN: 基础样例 en-US: Basic Usage --- Simplest of usage. ````jsx import AvatarList from 'ant-design-pro/lib/AvatarList'; ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/AvatarList/index.d.ts ================================================ import * as React from 'react'; import AvatarItem from './AvatarItem'; export interface IAvatarListProps { size?: 'large' | 'small' | 'mini' | 'default'; style?: React.CSSProperties; children: React.ReactElement | Array>; } export default class AvatarList extends React.Component { public static Item: typeof AvatarItem; } ================================================ FILE: app/assets/components/AvatarList/index.en-US.md ================================================ --- title: AvatarList order: 1 cols: 1 --- A list of user's avatar for project or group member list frequently. If a large or small AvatarList is desired, set the `size` property to either `large` or `small` and `mini` respectively. Omit the `size` property for a AvatarList with the default size. ## API ### AvatarList | Property | Description | Type | Default | |----------|------------------------------------------|-------------|-------| | size | size of list | `large`、`small` 、`mini`, `default` | `default` | ### AvatarList.Item | Property | Description | Type | Default | |----------|------------------------------------------|-------------|-------| | tips | title tips for avatar item | ReactNode\/string | - | | src | the address of the image for an image avatar | string | - | ================================================ FILE: app/assets/components/AvatarList/index.js ================================================ import React from 'react'; import { Tooltip, Avatar } from 'antd'; import classNames from 'classnames'; import styles from './index.less'; const AvatarList = ({ children, size, ...other }) => { const childrenWithProps = React.Children.map(children, child => React.cloneElement(child, { size, }) ); return (

    {childrenWithProps}
); }; const Item = ({ src, size, tips, onClick = () => {} }) => { const cls = classNames(styles.avatarItem, { [styles.avatarItemLarge]: size === 'large', [styles.avatarItemSmall]: size === 'small', [styles.avatarItemMini]: size === 'mini', }); return (
  • {tips ? ( ) : ( )}
  • ); }; AvatarList.Item = Item; export default AvatarList; ================================================ FILE: app/assets/components/AvatarList/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .avatarList { display: inline-block; ul { display: inline-block; margin-left: 8px; font-size: 0; } } .avatarItem { display: inline-block; font-size: @font-size-base; margin-left: -8px; width: @avatar-size-base; height: @avatar-size-base; :global { .ant-avatar { border: 1px solid #fff; } } } .avatarItemLarge { width: @avatar-size-lg; height: @avatar-size-lg; } .avatarItemSmall { width: @avatar-size-sm; height: @avatar-size-sm; } .avatarItemMini { width: 20px; height: 20px; :global { .ant-avatar { width: 20px; height: 20px; line-height: 20px; } } } ================================================ FILE: app/assets/components/AvatarList/index.zh-CN.md ================================================ --- title: AvatarList subtitle: 用户头像列表 order: 1 cols: 1 --- 一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。 ## API ### AvatarList | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` | ### AvatarList.Item | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | tips | 头像展示文案 | ReactNode\/string | - | | src | 头像图片连接 | string | - | ================================================ FILE: app/assets/components/Charts/Bar/index.d.ts ================================================ import * as React from 'react'; export interface IBarProps { title: React.ReactNode; color?: string; padding?: [number, number, number, number]; height: number; data: Array<{ x: string; y: number; }>; autoLabel?: boolean; style?: React.CSSProperties; } export default class Bar extends React.Component {} ================================================ FILE: app/assets/components/Charts/Bar/index.js ================================================ import React, { Component } from 'react'; import { Chart, Axis, Tooltip, Geom } from 'bizcharts'; import Debounce from 'lodash-decorators/debounce'; import Bind from 'lodash-decorators/bind'; import autoHeight from '../autoHeight'; import styles from '../index.less'; @autoHeight() class Bar extends Component { state = { autoHideXLabels: false, }; componentDidMount() { window.addEventListener('resize', this.resize); } componentWillUnmount() { window.removeEventListener('resize', this.resize); } @Bind() @Debounce(400) resize() { if (!this.node) { return; } const canvasWidth = this.node.parentNode.clientWidth; const { data = [], autoLabel = true } = this.props; if (!autoLabel) { return; } const minWidth = data.length * 30; const { autoHideXLabels } = this.state; if (canvasWidth <= minWidth) { if (!autoHideXLabels) { this.setState({ autoHideXLabels: true, }); } } else if (autoHideXLabels) { this.setState({ autoHideXLabels: false, }); } } handleRoot = n => { this.root = n; }; handleRef = n => { this.node = n; }; render() { const { height, title, forceFit = true, data, color = 'rgba(24, 144, 255, 0.85)', padding, } = this.props; const { autoHideXLabels } = this.state; const scale = { x: { type: 'cat', }, y: { min: 0, }, }; const tooltip = [ 'x*y', (x, y) => ({ name: x, value: y, }), ]; return (
    {title &&

    {title}

    }
    ); } } export default Bar; ================================================ FILE: app/assets/components/Charts/ChartCard/index.d.ts ================================================ import * as React from 'react'; export interface IChartCardProps { title: React.ReactNode; action?: React.ReactNode; total?: React.ReactNode | number | (() => React.ReactNode | number); footer?: React.ReactNode; contentHeight?: number; avatar?: React.ReactNode; style?: React.CSSProperties; } export default class ChartCard extends React.Component {} ================================================ FILE: app/assets/components/Charts/ChartCard/index.js ================================================ import React from 'react'; import { Card, Spin } from 'antd'; import classNames from 'classnames'; import styles from './index.less'; const renderTotal = total => { let totalDom; switch (typeof total) { case 'undefined': totalDom = null; break; case 'function': totalDom =
    {total()}
    ; break; default: totalDom =
    {total}
    ; } return totalDom; }; const ChartCard = ({ loading = false, contentHeight, title, avatar, action, total, footer, children, ...rest }) => { const content = (
    {avatar}
    {title} {action}
    {renderTotal(total)}
    {children && (
    {children}
    )} {footer && (
    {footer}
    )}
    ); return ( { {content} } ); }; export default ChartCard; ================================================ FILE: app/assets/components/Charts/ChartCard/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .chartCard { position: relative; .chartTop { position: relative; overflow: hidden; width: 100%; } .chartTopMargin { margin-bottom: 12px; } .chartTopHasMargin { margin-bottom: 20px; } .metaWrap { float: left; } .avatar { position: relative; top: 4px; float: left; margin-right: 20px; img { border-radius: 100%; } } .meta { color: @text-color-secondary; font-size: @font-size-base; line-height: 22px; height: 22px; } .action { cursor: pointer; position: absolute; top: 0; right: 0; } .total { overflow: hidden; text-overflow: ellipsis; word-break: break-all; white-space: nowrap; color: @heading-color; margin-top: 4px; margin-bottom: 0; font-size: 30px; line-height: 38px; height: 38px; } .content { margin-bottom: 12px; position: relative; width: 100%; } .contentFixed { position: absolute; left: 0; bottom: 0; width: 100%; } .footer { border-top: 1px solid @border-color-split; padding-top: 9px; margin-top: 8px; & > * { position: relative; } } .footerMargin { margin-top: 20px; } } .spin :global(.ant-spin-container) { overflow: visible; } ================================================ FILE: app/assets/components/Charts/Field/index.d.ts ================================================ import * as React from 'react'; export interface IFieldProps { label: React.ReactNode; value: React.ReactNode; style?: React.CSSProperties; } export default class Field extends React.Component {} ================================================ FILE: app/assets/components/Charts/Field/index.js ================================================ import React from 'react'; import styles from './index.less'; const Field = ({ label, value, ...rest }) => (
    {label} {value}
    ); export default Field; ================================================ FILE: app/assets/components/Charts/Field/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .field { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0; span { font-size: @font-size-base; line-height: 22px; } span:last-child { margin-left: 8px; color: @heading-color; } } ================================================ FILE: app/assets/components/Charts/Gauge/index.d.ts ================================================ import * as React from 'react'; export interface IGaugeProps { title: React.ReactNode; color?: string; height: number; bgColor?: number; percent: number; style?: React.CSSProperties; } export default class Gauge extends React.Component {} ================================================ FILE: app/assets/components/Charts/Gauge/index.js ================================================ import React from 'react'; import { Chart, Geom, Axis, Coord, Guide, Shape } from 'bizcharts'; import autoHeight from '../autoHeight'; const { Arc, Html, Line } = Guide; const defaultFormatter = val => { switch (val) { case '2': return '差'; case '4': return '中'; case '6': return '良'; case '8': return '优'; default: return ''; } }; Shape.registerShape('point', 'pointer', { drawShape(cfg, group) { let point = cfg.points[0]; point = this.parsePoint(point); const center = this.parsePoint({ x: 0, y: 0, }); group.addShape('line', { attrs: { x1: center.x, y1: center.y, x2: point.x, y2: point.y, stroke: cfg.color, lineWidth: 2, lineCap: 'round', }, }); return group.addShape('circle', { attrs: { x: center.x, y: center.y, r: 6, stroke: cfg.color, lineWidth: 3, fill: '#fff', }, }); }, }); @autoHeight() export default class Gauge extends React.Component { render() { const { title, height, percent, forceFit = true, formatter = defaultFormatter, color = '#2F9CFF', bgColor = '#F0F2F5', } = this.props; const cols = { value: { type: 'linear', min: 0, max: 10, tickCount: 6, nice: true, }, }; const data = [{ value: percent / 10 }]; return ( { return `

    ${title}

    ${data[0].value * 10}%

    `; }} />
    ); } } ================================================ FILE: app/assets/components/Charts/MiniArea/index.d.ts ================================================ import * as React from 'react'; // g2已经更新到3.0 // 不带的写了 export interface IAxis { title: any; line: any; gridAlign: any; labels: any; tickLine: any; grid: any; } export interface IMiniAreaProps { color?: string; height: number; borderColor?: string; line?: boolean; animate?: boolean; xAxis?: IAxis; yAxis?: IAxis; data: Array<{ x: number; y: number; }>; } export default class MiniArea extends React.Component {} ================================================ FILE: app/assets/components/Charts/MiniArea/index.js ================================================ import React from 'react'; import { Chart, Axis, Tooltip, Geom } from 'bizcharts'; import autoHeight from '../autoHeight'; import styles from '../index.less'; @autoHeight() export default class MiniArea extends React.Component { render() { const { height, data = [], forceFit = true, color = 'rgba(24, 144, 255, 0.2)', borderColor = '#1089ff', scale = {}, borderWidth = 2, line, xAxis, yAxis, animate = true, } = this.props; const padding = [36, 5, 30, 5]; const scaleProps = { x: { type: 'cat', range: [0, 1], ...scale.x, }, y: { min: 0, ...scale.y, }, }; const tooltip = [ 'x*y', (x, y) => ({ name: x, value: y, }), ]; const chartHeight = height + 54; return (
    {height > 0 && ( {line ? ( ) : ( )} )}
    ); } } ================================================ FILE: app/assets/components/Charts/MiniBar/index.d.ts ================================================ import * as React from 'react'; export interface IMiniBarProps { color?: string; height: number; data: Array<{ x: number | string; y: number; }>; style?: React.CSSProperties; } export default class MiniBar extends React.Component {} ================================================ FILE: app/assets/components/Charts/MiniBar/index.js ================================================ import React from 'react'; import { Chart, Tooltip, Geom } from 'bizcharts'; import autoHeight from '../autoHeight'; import styles from '../index.less'; @autoHeight() export default class MiniBar extends React.Component { render() { const { height, forceFit = true, color = '#1890FF', data = [] } = this.props; const scale = { x: { type: 'cat', }, y: { min: 0, }, }; const padding = [36, 5, 30, 5]; const tooltip = [ 'x*y', (x, y) => ({ name: x, value: y, }), ]; // for tooltip not to be hide const chartHeight = height + 54; return (
    ); } } ================================================ FILE: app/assets/components/Charts/MiniProgress/index.d.ts ================================================ import * as React from 'react'; export interface IMiniProgressProps { target: number; color?: string; strokeWidth?: number; percent?: number; style?: React.CSSProperties; } export default class MiniProgress extends React.Component {} ================================================ FILE: app/assets/components/Charts/MiniProgress/index.js ================================================ import React from 'react'; import { Tooltip } from 'antd'; import styles from './index.less'; const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => (
    ); export default MiniProgress; ================================================ FILE: app/assets/components/Charts/MiniProgress/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .miniProgress { padding: 5px 0; position: relative; width: 100%; .progressWrap { background-color: @background-color-base; position: relative; } .progress { transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s; border-radius: 1px 0 0 1px; background-color: @primary-color; width: 0; height: 100%; } .target { position: absolute; top: 0; bottom: 0; span { border-radius: 100px; position: absolute; top: 0; left: 0; height: 4px; width: 2px; } span:last-child { top: auto; bottom: 0; } } } ================================================ FILE: app/assets/components/Charts/Pie/index.d.ts ================================================ import * as React from 'react'; export interface IPieProps { animate?: boolean; color?: string; height: number; hasLegend?: boolean; padding?: [number, number, number, number]; percent?: number; data?: Array<{ x: string | string; y: number; }>; total?: React.ReactNode | number | (() => React.ReactNode | number); title?: React.ReactNode; tooltip?: boolean; valueFormat?: (value: string) => string | React.ReactNode; subTitle?: React.ReactNode; } export default class Pie extends React.Component {} ================================================ FILE: app/assets/components/Charts/Pie/index.js ================================================ import React, { Component } from 'react'; import { Chart, Tooltip, Geom, Coord } from 'bizcharts'; import { DataView } from '@antv/data-set'; import { Divider } from 'antd'; import classNames from 'classnames'; import ReactFitText from 'react-fittext'; import Debounce from 'lodash-decorators/debounce'; import Bind from 'lodash-decorators/bind'; import autoHeight from '../autoHeight'; import styles from './index.less'; /* eslint react/no-danger:0 */ @autoHeight() export default class Pie extends Component { state = { legendData: [], legendBlock: false, }; componentDidMount() { this.getLegendData(); this.resize(); window.addEventListener('resize', this.resize); } componentWillReceiveProps(nextProps) { if (this.props.data !== nextProps.data) { // because of charts data create when rendered // so there is a trick for get rendered time this.setState( { legendData: [...this.state.legendData], }, () => { this.getLegendData(); } ); } } componentWillUnmount() { window.removeEventListener('resize', this.resize); this.resize.cancel(); } getG2Instance = chart => { this.chart = chart; }; // for custom lengend view getLegendData = () => { if (!this.chart) return; const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形 const items = geom.get('dataArray') || []; // 获取图形对应的 const legendData = items.map(item => { /* eslint no-underscore-dangle:0 */ const origin = item[0]._origin; origin.color = item[0].color; origin.checked = true; return origin; }); this.setState({ legendData, }); }; // for window resize auto responsive legend @Bind() @Debounce(300) resize() { const { hasLegend } = this.props; if (!hasLegend || !this.root) { window.removeEventListener('resize', this.resize); return; } if (this.root.parentNode.clientWidth <= 380) { if (!this.state.legendBlock) { this.setState({ legendBlock: true, }); } } else if (this.state.legendBlock) { this.setState({ legendBlock: false, }); } } handleRoot = n => { this.root = n; }; handleLegendClick = (item, i) => { const newItem = item; newItem.checked = !newItem.checked; const { legendData } = this.state; legendData[i] = newItem; const filteredLegendData = legendData.filter(l => l.checked).map(l => l.x); if (this.chart) { this.chart.filter('x', val => filteredLegendData.indexOf(val) > -1); } this.setState({ legendData, }); }; render() { const { valueFormat, subTitle, total, hasLegend = false, className, style, height, forceFit = true, percent = 0, color, inner = 0.75, animate = true, colors, lineWidth = 1, } = this.props; const { legendData, legendBlock } = this.state; const pieClassName = classNames(styles.pie, className, { [styles.hasLegend]: !!hasLegend, [styles.legendBlock]: legendBlock, }); const defaultColors = colors; let data = this.props.data || []; let selected = this.props.selected || true; let tooltip = this.props.tooltip || true; let formatColor; const scale = { x: { type: 'cat', range: [0, 1], }, y: { min: 0, }, }; if (percent) { selected = false; tooltip = false; formatColor = value => { if (value === '占比') { return color || 'rgba(24, 144, 255, 0.85)'; } else { return '#F0F2F5'; } }; data = [ { x: '占比', y: parseFloat(percent), }, { x: '反比', y: 100 - parseFloat(percent), }, ]; } const tooltipFormat = [ 'x*percent', (x, p) => ({ name: x, value: `${(p * 100).toFixed(2)}%`, }), ]; const padding = [12, 0, 12, 0]; const dv = new DataView(); dv.source(data).transform({ type: 'percent', field: 'y', dimension: 'x', as: 'percent', }); return (
    {!!tooltip && } {(subTitle || total) && (
    {subTitle &&

    {subTitle}

    } {/* eslint-disable-next-line */} {total && (
    {typeof total === 'function' ? total() : total}
    )}
    )}
    {hasLegend && (
      {legendData.map((item, i) => (
    • this.handleLegendClick(item, i)}> {item.x} {`${(isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`} {valueFormat ? valueFormat(item.y) : item.y}
    • ))}
    )}
    ); } } ================================================ FILE: app/assets/components/Charts/Pie/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .pie { position: relative; .chart { position: relative; } &.hasLegend .chart { width: ~'calc(100% - 240px)'; } .legend { position: absolute; right: 0; min-width: 200px; top: 50%; transform: translateY(-50%); margin: 0 20px; list-style: none; padding: 0; li { cursor: pointer; margin-bottom: 16px; height: 22px; line-height: 22px; &:last-child { margin-bottom: 0; } } } .dot { border-radius: 8px; display: inline-block; margin-right: 8px; position: relative; top: -1px; height: 8px; width: 8px; } .line { background-color: @border-color-split; display: inline-block; margin-right: 8px; width: 1px; height: 16px; } .legendTitle { color: @text-color; } .percent { color: @text-color-secondary; } .value { position: absolute; right: 0; } .title { margin-bottom: 8px; } .total { position: absolute; left: 50%; top: 50%; text-align: center; height: 62px; transform: translate(-50%, -50%); & > h4 { color: @text-color-secondary; font-size: 14px; line-height: 22px; height: 22px; margin-bottom: 8px; font-weight: normal; } & > p { color: @heading-color; display: block; font-size: 1.2em; height: 32px; line-height: 32px; white-space: nowrap; } } } .legendBlock { &.hasLegend .chart { width: 100%; margin: 0 0 32px 0; } .legend { position: relative; transform: none; } } ================================================ FILE: app/assets/components/Charts/Radar/index.d.ts ================================================ import * as React from 'react'; export interface IRadarProps { title?: React.ReactNode; height: number; padding?: [number, number, number, number]; hasLegend?: boolean; data: Array<{ name: string; label: string; value: string; }>; style?: React.CSSProperties; } export default class Radar extends React.Component {} ================================================ FILE: app/assets/components/Charts/Radar/index.js ================================================ import React, { Component } from 'react'; import { Chart, Tooltip, Geom, Coord, Axis } from 'bizcharts'; import { Row, Col } from 'antd'; import autoHeight from '../autoHeight'; import styles from './index.less'; /* eslint react/no-danger:0 */ @autoHeight() export default class Radar extends Component { state = { legendData: [], }; componentDidMount() { this.getLengendData(); } componentWillReceiveProps(nextProps) { if (this.props.data !== nextProps.data) { this.getLengendData(); } } getG2Instance = chart => { this.chart = chart; }; // for custom lengend view getLengendData = () => { if (!this.chart) return; const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形 const items = geom.get('dataArray') || []; // 获取图形对应的 const legendData = items.map(item => { // eslint-disable-next-line const origins = item.map(t => t._origin); const result = { name: origins[0].name, color: item[0].color, checked: true, value: origins.reduce((p, n) => p + n.value, 0), }; return result; }); this.setState({ legendData, }); }; handleRef = n => { this.node = n; }; handleLegendClick = (item, i) => { const newItem = item; newItem.checked = !newItem.checked; const { legendData } = this.state; legendData[i] = newItem; const filteredLegendData = legendData.filter(l => l.checked).map(l => l.name); if (this.chart) { this.chart.filter('name', val => filteredLegendData.indexOf(val) > -1); this.chart.repaint(); } this.setState({ legendData, }); }; render() { const defaultColors = [ '#1890FF', '#FACC14', '#2FC25B', '#8543E0', '#F04864', '#13C2C2', '#fa8c16', '#a0d911', ]; const { data = [], height = 0, title, hasLegend = false, forceFit = true, tickCount = 4, padding = [35, 30, 16, 30], animate = true, colors = defaultColors, } = this.props; const { legendData } = this.state; const scale = { value: { min: 0, tickCount, }, }; const chartHeight = height - (hasLegend ? 80 : 22); return (
    {title &&

    {title}

    } {hasLegend && ( {legendData.map((item, i) => ( this.handleLegendClick(item, i)} >

    {item.name}

    {item.value}
    ))}
    )}
    ); } } ================================================ FILE: app/assets/components/Charts/Radar/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .radar { .legend { margin-top: 16px; .legendItem { position: relative; text-align: center; cursor: pointer; color: @text-color-secondary; line-height: 22px; p { margin: 0; } h6 { color: @heading-color; padding-left: 16px; font-size: 24px; line-height: 32px; margin-top: 4px; margin-bottom: 0; } &:after { background-color: @border-color-split; position: absolute; top: 8px; right: 0; height: 40px; width: 1px; content: ''; } } > :last-child .legendItem:after { display: none; } .dot { border-radius: 6px; display: inline-block; margin-right: 6px; position: relative; top: -1px; height: 6px; width: 6px; } } } ================================================ FILE: app/assets/components/Charts/TagCloud/index.d.ts ================================================ import * as React from 'react'; export interface ITagCloudProps { data: Array<{ name: string; value: number; }>; height: number; style?: React.CSSProperties; } export default class TagCloud extends React.Component {} ================================================ FILE: app/assets/components/Charts/TagCloud/index.js ================================================ import React, { Component } from 'react'; import { Chart, Geom, Coord, Shape } from 'bizcharts'; import DataSet from '@antv/data-set'; import Debounce from 'lodash-decorators/debounce'; import Bind from 'lodash-decorators/bind'; import classNames from 'classnames'; import autoHeight from '../autoHeight'; import styles from './index.less'; /* eslint no-underscore-dangle: 0 */ /* eslint no-param-reassign: 0 */ const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png'; @autoHeight() class TagCloud extends Component { state = { dv: null, }; componentDidMount() { this.initTagCloud(); this.renderChart(); window.addEventListener('resize', this.resize); } componentWillReceiveProps(nextProps) { if (JSON.stringify(nextProps.data) !== JSON.stringify(this.props.data)) { this.renderChart(nextProps); } } componentWillUnmount() { this.isUnmount = true; window.removeEventListener('resize', this.resize); } resize = () => { this.renderChart(); }; saveRootRef = node => { this.root = node; }; initTagCloud = () => { function getTextAttrs(cfg) { return Object.assign( {}, { fillOpacity: cfg.opacity, fontSize: cfg.origin._origin.size, rotate: cfg.origin._origin.rotate, text: cfg.origin._origin.text, textAlign: 'center', fontFamily: cfg.origin._origin.font, fill: cfg.color, textBaseline: 'Alphabetic', }, cfg.style ); } // 给point注册一个词云的shape Shape.registerShape('point', 'cloud', { drawShape(cfg, container) { const attrs = getTextAttrs(cfg); return container.addShape('text', { attrs: Object.assign(attrs, { x: cfg.x, y: cfg.y, }), }); }, }); }; @Bind() @Debounce(500) renderChart(nextProps) { // const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C']; const { data, height } = nextProps || this.props; if (data.length < 1 || !this.root) { return; } const h = height * 4; const w = this.root.offsetWidth * 4; const onload = () => { const dv = new DataSet.View().source(data); const range = dv.range('value'); const [min, max] = range; dv.transform({ type: 'tag-cloud', fields: ['name', 'value'], imageMask: this.imageMask, font: 'Verdana', size: [w, h], // 宽高设置最好根据 imageMask 做调整 padding: 5, timeInterval: 5000, // max execute time rotate() { return 0; }, fontSize(d) { // eslint-disable-next-line return Math.pow((d.value - min) / (max - min), 2) * (70 - 20) + 20; }, }); if (this.isUnmount) { return; } this.setState({ dv, w, h, }); }; if (!this.imageMask) { this.imageMask = new Image(); this.imageMask.crossOrigin = ''; this.imageMask.src = imgUrl; this.imageMask.onload = onload; } else { onload(); } } render() { const { className, height } = this.props; const { dv, w, h } = this.state; return (
    {dv && ( )}
    ); } } export default TagCloud; ================================================ FILE: app/assets/components/Charts/TagCloud/index.less ================================================ .tagCloud { overflow: hidden; canvas { transform: scale(0.25); transform-origin: 0 0; } } ================================================ FILE: app/assets/components/Charts/TimelineChart/index.d.ts ================================================ import * as React from 'react'; export interface ITimelineChartProps { data: Array<{ x: string; y1: string; y2: string; }>; titleMap: { y1: string; y2: string }; padding?: [number, number, number, number]; height?: number; style?: React.CSSProperties; } export default class TimelineChart extends React.Component {} ================================================ FILE: app/assets/components/Charts/TimelineChart/index.js ================================================ import React from 'react'; import { Chart, Tooltip, Geom, Legend, Axis } from 'bizcharts'; import DataSet from '@antv/data-set'; import Slider from 'bizcharts-plugin-slider'; import autoHeight from '../autoHeight'; import styles from './index.less'; @autoHeight() export default class TimelineChart extends React.Component { render() { const { title, height = 400, padding = [60, 20, 40, 40], titleMap = { y1: 'y1', y2: 'y2', }, borderWidth = 2, data = [ { x: 0, y1: 0, y2: 0, }, ], } = this.props; data.sort((a, b) => a.x - b.x); let max; if (data[0] && data[0].y1 && data[0].y2) { max = Math.max( [...data].sort((a, b) => b.y1 - a.y1)[0].y1, [...data].sort((a, b) => b.y2 - a.y2)[0].y2 ); } const ds = new DataSet({ state: { start: data[0].x, end: data[data.length - 1].x, }, }); const dv = ds.createView(); dv .source(data) .transform({ type: 'filter', callback: obj => { const date = obj.x; return date <= ds.state.end && date >= ds.state.start; }, }) .transform({ type: 'map', callback(row) { const newRow = { ...row }; newRow[titleMap.y1] = row.y1; newRow[titleMap.y2] = row.y2; return newRow; }, }) .transform({ type: 'fold', fields: [titleMap.y1, titleMap.y2], // 展开字段集 key: 'key', // key字段 value: 'value', // value字段 }); const timeScale = { type: 'time', tickInterval: 60 * 60 * 1000, mask: 'HH:mm', range: [0, 1], }; const cols = { x: timeScale, value: { max, min: 0, }, }; const SliderGen = () => ( { ds.setState('start', startValue); ds.setState('end', endValue); }} /> ); return (
    {title &&

    {title}

    }
    ); } } ================================================ FILE: app/assets/components/Charts/TimelineChart/index.less ================================================ .timelineChart { background: #fff; } ================================================ FILE: app/assets/components/Charts/WaterWave/index.d.ts ================================================ import * as React from 'react'; export interface IWaterWaveProps { title: React.ReactNode; color?: string; height: number; percent: number; style?: React.CSSProperties; } export default class WaterWave extends React.Component {} ================================================ FILE: app/assets/components/Charts/WaterWave/index.js ================================================ import React, { PureComponent } from 'react'; import autoHeight from '../autoHeight'; import styles from './index.less'; /* eslint no-return-assign: 0 */ /* eslint no-mixed-operators: 0 */ // riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90 @autoHeight() export default class WaterWave extends PureComponent { state = { radio: 1, }; componentDidMount() { this.renderChart(); this.resize(); window.addEventListener('resize', this.resize); } componentWillUnmount() { cancelAnimationFrame(this.timer); if (this.node) { this.node.innerHTML = ''; } window.removeEventListener('resize', this.resize); } resize = () => { const { height } = this.props; const { offsetWidth } = this.root.parentNode; this.setState({ radio: offsetWidth < height ? offsetWidth / height : 1, }); }; renderChart() { const { percent, color = '#1890FF' } = this.props; const data = percent / 100; const self = this; if (!this.node || !data) { return; } const canvas = this.node; const ctx = canvas.getContext('2d'); const canvasWidth = canvas.width; const canvasHeight = canvas.height; const radius = canvasWidth / 2; const lineWidth = 2; const cR = radius - lineWidth; ctx.beginPath(); ctx.lineWidth = lineWidth * 2; const axisLength = canvasWidth - lineWidth; const unit = axisLength / 8; const range = 0.2; // 振幅 let currRange = range; const xOffset = lineWidth; let sp = 0; // 周期偏移量 let currData = 0; const waveupsp = 0.005; // 水波上涨速度 let arcStack = []; const bR = radius - lineWidth; const circleOffset = -(Math.PI / 2); let circleLock = true; for (let i = circleOffset; i < circleOffset + 2 * Math.PI; i += 1 / (8 * Math.PI)) { arcStack.push([radius + bR * Math.cos(i), radius + bR * Math.sin(i)]); } const cStartPoint = arcStack.shift(); ctx.strokeStyle = color; ctx.moveTo(cStartPoint[0], cStartPoint[1]); function drawSin() { ctx.beginPath(); ctx.save(); const sinStack = []; for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) { const x = sp + (xOffset + i) / unit; const y = Math.sin(x) * currRange; const dx = i; const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y; ctx.lineTo(dx, dy); sinStack.push([dx, dy]); } const startPoint = sinStack.shift(); ctx.lineTo(xOffset + axisLength, canvasHeight); ctx.lineTo(xOffset, canvasHeight); ctx.lineTo(startPoint[0], startPoint[1]); const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight); gradient.addColorStop(0, '#ffffff'); gradient.addColorStop(1, '#1890FF'); ctx.fillStyle = gradient; ctx.fill(); ctx.restore(); } function render() { ctx.clearRect(0, 0, canvasWidth, canvasHeight); if (circleLock) { if (arcStack.length) { const temp = arcStack.shift(); ctx.lineTo(temp[0], temp[1]); ctx.stroke(); } else { circleLock = false; ctx.lineTo(cStartPoint[0], cStartPoint[1]); ctx.stroke(); arcStack = null; ctx.globalCompositeOperation = 'destination-over'; ctx.beginPath(); ctx.lineWidth = lineWidth; ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1); ctx.beginPath(); ctx.save(); ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, 1); ctx.restore(); ctx.clip(); ctx.fillStyle = '#1890FF'; } } else { if (data >= 0.85) { if (currRange > range / 4) { const t = range * 0.01; currRange -= t; } } else if (data <= 0.1) { if (currRange < range * 1.5) { const t = range * 0.01; currRange += t; } } else { if (currRange <= range) { const t = range * 0.01; currRange += t; } if (currRange >= range) { const t = range * 0.01; currRange -= t; } } if (data - currData > 0) { currData += waveupsp; } if (data - currData < 0) { currData -= waveupsp; } sp += 0.07; drawSin(); } self.timer = requestAnimationFrame(render); } render(); } render() { const { radio } = this.state; const { percent, title, height } = this.props; return (
    (this.root = n)} style={{ transform: `scale(${radio})` }} >
    (this.node = n)} width={height * 2} height={height * 2} />
    {title && {title}}

    {percent}%

    ); } } ================================================ FILE: app/assets/components/Charts/WaterWave/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .waterWave { display: inline-block; position: relative; transform-origin: left; .text { position: absolute; left: 0; top: 32px; text-align: center; width: 100%; span { color: @text-color-secondary; font-size: 14px; line-height: 22px; } h4 { color: @heading-color; line-height: 32px; font-size: 24px; } } .waterWaveCanvasWrapper { transform: scale(0.5); transform-origin: 0 0; } } ================================================ FILE: app/assets/components/Charts/autoHeight.js ================================================ /* eslint eqeqeq: 0 */ import React from 'react'; function computeHeight(node) { const totalHeight = parseInt(getComputedStyle(node).height, 10); const padding = parseInt(getComputedStyle(node).paddingTop, 10) + parseInt(getComputedStyle(node).paddingBottom, 10); return totalHeight - padding; } function getAutoHeight(n) { if (!n) { return 0; } let node = n; let height = computeHeight(node); while (!height) { node = node.parentNode; if (node) { height = computeHeight(node); } else { break; } } return height; } const autoHeight = () => WrappedComponent => { return class extends React.Component { state = { computedHeight: 0, }; componentDidMount() { const { height } = this.props; if (!height) { const h = getAutoHeight(this.root); // eslint-disable-next-line this.setState({ computedHeight: h }); } } handleRoot = node => { this.root = node; }; render() { const { height } = this.props; const { computedHeight } = this.state; const h = height || computedHeight; return (
    {h > 0 && }
    ); } }; }; export default autoHeight; ================================================ FILE: app/assets/components/Charts/demo/bar.md ================================================ --- order: 4 title: 柱状图 --- 通过设置 `x`,`y` 属性,可以快速的构建出一个漂亮的柱状图,各种纬度的关系则是通过自定义的数据展现。 ````jsx import { Bar } from 'ant-design-pro/lib/Charts'; const salesData = []; for (let i = 0; i < 12; i += 1) { salesData.push({ x: `${i + 1}月`, y: Math.floor(Math.random() * 1000) + 200, }); } ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/chart-card.md ================================================ --- order: 1 title: 图表卡片 --- 用于展示图表的卡片容器,可以方便的配合其它图表套件展示丰富信息。 ```jsx import { ChartCard, yuan, Field } from 'ant-design-pro/lib/Charts'; import Trend from 'ant-design-pro/lib/Trend'; import { Row, Col, Icon, Tooltip } from 'antd'; import numeral from 'numeral'; ReactDOM.render( } total={() => ( )} footer={ } contentHeight={46} > 周同比 12% 日环比 11% } action={ } total={() => ( )} footer={ } /> } action={ } total={() => ( )} /> , mountNode, ); ``` ================================================ FILE: app/assets/components/Charts/demo/gauge.md ================================================ --- order: 7 title: 仪表盘 --- 仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。 ````jsx import { Gauge } from 'ant-design-pro/lib/Charts'; ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/mini-area.md ================================================ --- order: 2 col: 2 title: 迷你区域图 --- ````jsx import { MiniArea } from 'ant-design-pro/lib/Charts'; import moment from 'moment'; const visitData = []; const beginDay = new Date().getTime(); for (let i = 0; i < 20; i += 1) { visitData.push({ x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), y: Math.floor(Math.random() * 100) + 10, }); } ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/mini-bar.md ================================================ --- order: 2 col: 2 title: 迷你柱状图 --- 迷你柱状图更适合展示简单的区间数据,简洁的表现方式可以很好的减少大数据量的视觉展现压力。 ````jsx import { MiniBar } from 'ant-design-pro/lib/Charts'; import moment from 'moment'; const visitData = []; const beginDay = new Date().getTime(); for (let i = 0; i < 20; i += 1) { visitData.push({ x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), y: Math.floor(Math.random() * 100) + 10, }); } ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/mini-pie.md ================================================ --- order: 6 title: 迷你饼状图 --- 通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展 现更多业务场景。 ```jsx import { Pie } from 'ant-design-pro/lib/Charts'; ReactDOM.render( , mountNode ); ``` ================================================ FILE: app/assets/components/Charts/demo/mini-progress.md ================================================ --- order: 3 title: 迷你进度条 --- ````jsx import { MiniProgress } from 'ant-design-pro/lib/Charts'; ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/mix.md ================================================ --- order: 0 title: 图表套件组合展示 --- 利用 Ant Design Pro 提供的图表套件,可以灵活组合符合设计规范的图表来满足复杂的业务需求。 ````jsx import { ChartCard, Field, MiniArea, MiniBar, MiniProgress } from 'ant-design-pro/lib/Charts'; import Trend from 'ant-design-pro/lib/Trend'; import NumberInfo from 'ant-design-pro/lib/NumberInfo'; import { Row, Col, Icon, Tooltip } from 'antd'; import numeral from 'numeral'; import moment from 'moment'; const visitData = []; const beginDay = new Date().getTime(); for (let i = 0; i < 20; i += 1) { visitData.push({ x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), y: Math.floor(Math.random() * 100) + 10, }); } ReactDOM.render( 本周访问} total={numeral(12321).format('0,0')} status="up" subTotal={17.1} /> } total={numeral(8846).format('0,0')} footer={} contentHeight={46} > } total="78%" footer={
    周同比 12% 日环比 11%
    } contentHeight={46} >
    , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/pie.md ================================================ --- order: 5 title: 饼状图 --- ```jsx import { Pie, yuan } from 'ant-design-pro/lib/Charts'; const salesPieData = [ { x: '家用电器', y: 4544, }, { x: '食用酒水', y: 3321, }, { x: '个护健康', y: 3113, }, { x: '服饰箱包', y: 2341, }, { x: '母婴产品', y: 1231, }, { x: '其他', y: 1231, }, ]; ReactDOM.render( ( now.y + pre, 0)) }} /> )} data={salesPieData} valueFormat={val => } height={294} />, mountNode, ); ``` ================================================ FILE: app/assets/components/Charts/demo/radar.md ================================================ --- order: 7 title: 雷达图 --- ````jsx import { Radar, ChartCard } from 'ant-design-pro/lib/Charts'; 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], }); } }); }); ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/tag-cloud.md ================================================ --- order: 9 title: 标签云 --- 标签云是一套相关的标签以及与此相应的权重展示方式,一般典型的标签云有 30 至 150 个标签,而权重影响使用的字体大小或其他视觉效果。 ````jsx import { TagCloud } from 'ant-design-pro/lib/Charts'; const tags = []; for (let i = 0; i < 50; i += 1) { tags.push({ name: `TagClout-Title-${i}`, value: Math.floor((Math.random() * 50)) + 20, }); } ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/timeline-chart.md ================================================ --- order: 9 title: 带有时间轴的图表 --- 使用 `TimelineChart` 组件可以实现带有时间轴的柱状图展现,而其中的 `x` 属性,则是时间值的指向,默认最多支持同时展现两个指标,分别是 `y1` 和 `y2`。 ````jsx import { TimelineChart } from 'ant-design-pro/lib/Charts'; const chartData = []; for (let i = 0; i < 20; i += 1) { chartData.push({ x: (new Date().getTime()) + (1000 * 60 * 30 * i), y1: Math.floor(Math.random() * 100) + 1000, y2: Math.floor(Math.random() * 100) + 10, }); } ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Charts/demo/waterwave.md ================================================ --- order: 8 title: 水波图 --- 水波图是一种比例的展示方式,可以更直观的展示关键值的占比。 ````jsx import { WaterWave } from 'ant-design-pro/lib/Charts'; ReactDOM.render(
    , mountNode); ```` ================================================ FILE: app/assets/components/Charts/g2.js ================================================ // 全局 G2 设置 import { track, setTheme } from 'bizcharts'; track(false); const config = { defaultColor: '#1089ff', shape: { interval: { fillOpacity: 1, }, }, }; setTheme(config); ================================================ FILE: app/assets/components/Charts/index.d.ts ================================================ import * as numeral from 'numeral'; export { default as ChartCard } from './ChartCard'; export { default as Bar } from './Bar'; export { default as Pie } from './Pie'; export { default as Radar } from './Radar'; export { default as Gauge } from './Gauge'; export { default as MiniArea } from './MiniArea'; export { default as MiniBar } from './MiniBar'; export { default as MiniProgress } from './MiniProgress'; export { default as Field } from './Field'; export { default as WaterWave } from './WaterWave'; export { default as TagCloud } from './TagCloud'; export { default as TimelineChart } from './TimelineChart'; declare const yuan: (value: number | string) => string; export { yuan }; ================================================ FILE: app/assets/components/Charts/index.js ================================================ import numeral from 'numeral'; import './g2'; import ChartCard from './ChartCard'; import Bar from './Bar'; import Pie from './Pie'; import Radar from './Radar'; import Gauge from './Gauge'; import MiniArea from './MiniArea'; import MiniBar from './MiniBar'; import MiniProgress from './MiniProgress'; import Field from './Field'; import WaterWave from './WaterWave'; import TagCloud from './TagCloud'; import TimelineChart from './TimelineChart'; const yuan = val => `¥ ${numeral(val).format('0,0')}`; const Charts = { yuan, Bar, Pie, Gauge, Radar, MiniBar, MiniArea, MiniProgress, ChartCard, Field, WaterWave, TagCloud, TimelineChart, }; export { Charts as default, yuan, Bar, Pie, Gauge, Radar, MiniBar, MiniArea, MiniProgress, ChartCard, Field, WaterWave, TagCloud, TimelineChart, }; ================================================ FILE: app/assets/components/Charts/index.less ================================================ .miniChart { position: relative; width: 100%; .chartContent { position: absolute; bottom: -28px; width: 100%; > div { margin: 0 -5px; overflow: hidden; } } .chartLoading { position: absolute; top: 16px; left: 50%; margin-left: -7px; } } ================================================ FILE: app/assets/components/Charts/index.md ================================================ --- title: en-US: Charts zh-CN: Charts subtitle: 图表 order: 2 cols: 2 --- Ant Design Pro 提供的业务中常用的图表类型,都是基于 [G2](https://antv.alipay.com/g2/doc/index.html) 按照 Ant Design 图表规范封装,需要注意的是 Ant Design Pro 的图表组件以套件形式提供,可以任意组合实现复杂的业务需求。 因为结合了 Ant Design 的标准设计,本着极简的设计思想以及开箱即用的理念,简化了大量 API 配置,所以如果需要灵活定制图表,可以参考 Ant Design Pro 图表实现,自行基于 [G2](https://antv.alipay.com/g2/doc/index.html) 封装图表组件使用。 ## API ### ChartCard | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | title | 卡片标题 | ReactNode\|string | - | | action | 卡片操作 | ReactNode | - | | total | 数据总量 | ReactNode \| number \| function | - | | footer | 卡片底部 | ReactNode | - | | contentHeight | 内容区域高度 | number | - | | avatar | 右侧图标 | React.ReactNode | - | ### MiniBar | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | color | 图表颜色 | string | `#1890FF` | | height | 图表高度 | number | - | | data | 数据 | array<{x, y}> | - | ### MiniArea | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | color | 图表颜色 | string | `rgba(24, 144, 255, 0.2)` | | borderColor | 图表边颜色 | string | `#1890FF` | | height | 图表高度 | number | - | | line | 是否显示描边 | boolean | false | | animate | 是否显示动画 | boolean | true | | xAxis | [x 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - | | yAxis | [y 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - | | data | 数据 | array<{x, y}> | - | ### MiniProgress | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | target | 目标比例 | number | - | | color | 进度条颜色 | string | - | | strokeWidth | 进度条高度 | number | - | | percent | 进度比例 | number | - | ### Bar | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | title | 图表标题 | ReactNode\|string | - | | color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` | | padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` | | height | 图表高度 | number | - | | data | 数据 | array<{x, y}> | - | | autoLabel | 在宽度不足时,自动隐藏 x 轴的 label | boolean | `true` | ### Pie | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | animate | 是否显示动画 | boolean | true | | color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` | | height | 图表高度 | number | - | | hasLegend | 是否显示 legend | boolean | `false` | | padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` | | percent | 占比 | number | - | | tooltip | 是否显示 tooltip | boolean | true | | valueFormat | 显示值的格式化函数 | function | - | | title | 图表标题 | ReactNode\|string | - | | subTitle | 图表子标题 | ReactNode\|string | - | | total | 图标中央的总数 | string | function | - | ### Radar | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | title | 图表标题 | ReactNode\|string | - | | height | 图表高度 | number | - | | hasLegend | 是否显示 legend | boolean | `false` | | padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` | | data | 图标数据 | array<{name,label,value}> | - | ### Gauge | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | title | 图表标题 | ReactNode\|string | - | | height | 图表高度 | number | - | | color | 图表颜色 | string | `#2F9CFF` | | bgColor | 图表背景颜色 | string | `#F0F2F5` | | percent | 进度比例 | number | - | ### WaterWave | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | title | 图表标题 | ReactNode\|string | - | | height | 图表高度 | number | - | | color | 图表颜色 | string | `#1890FF` | | percent | 进度比例 | number | - | ### TagCloud | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | data | 标题 | Array | - | | height | 高度值 | number | - | ### TimelineChart | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | data | 标题 | Array | - | | titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - | | height | 高度值 | number | 400 | ### Field | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | label | 标题 | ReactNode\|string | - | | value | 值 | ReactNode\|string | - | ================================================ FILE: app/assets/components/CountDown/demo/simple.md ================================================ --- order: 0 title: zh-CN: 基本 en-US: Basic --- ## zh-CN 简单的倒计时组件使用。 ## en-US The simplest usage. ````jsx import CountDown from 'ant-design-pro/lib/CountDown'; const targetTime = new Date().getTime() + 3900000; ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/CountDown/index.d.ts ================================================ import * as React from 'react'; export interface ICountDownProps { format?: (time: number) => void; target: Date | number; onEnd?: () => void; style?: React.CSSProperties; } export default class CountDown extends React.Component {} ================================================ FILE: app/assets/components/CountDown/index.en-US.md ================================================ --- title: CountDown cols: 1 order: 3 --- Simple CountDown Component. ## API | Property | Description | Type | Default | |----------|------------------------------------------|-------------|-------| | format | Formatter of time | Function(time) | | | target | Target time | Date | - | | onEnd | Countdown to the end callback | funtion | -| ================================================ FILE: app/assets/components/CountDown/index.js ================================================ import React, { Component } from 'react'; function fixedZero(val) { return val * 1 < 10 ? `0${val}` : val; } class CountDown extends Component { constructor(props) { super(props); const { lastTime } = this.initTime(props); this.state = { lastTime, }; } componentDidMount() { this.tick(); } componentWillReceiveProps(nextProps) { if (this.props.target !== nextProps.target) { clearTimeout(this.timer); const { lastTime } = this.initTime(nextProps); this.setState( { lastTime, }, () => { this.tick(); } ); } } componentWillUnmount() { clearTimeout(this.timer); } timer = 0; interval = 1000; initTime = props => { let lastTime = 0; let targetTime = 0; try { if (Object.prototype.toString.call(props.target) === '[object Date]') { targetTime = props.target.getTime(); } else { targetTime = new Date(props.target).getTime(); } } catch (e) { throw new Error('invalid target prop', e); } lastTime = targetTime - new Date().getTime(); return { lastTime: lastTime < 0 ? 0 : lastTime, }; }; // defaultFormat = time => ( // {moment(time).format('hh:mm:ss')} // ); defaultFormat = time => { const hours = 60 * 60 * 1000; const minutes = 60 * 1000; const h = Math.floor(time / hours); const m = Math.floor((time - h * hours) / minutes); const s = Math.floor((time - h * hours - m * minutes) / 1000); return ( {fixedZero(h)}:{fixedZero(m)}:{fixedZero(s)} ); }; tick = () => { const { onEnd } = this.props; let { lastTime } = this.state; this.timer = setTimeout(() => { if (lastTime < this.interval) { clearTimeout(this.timer); this.setState( { lastTime: 0, }, () => { if (onEnd) { onEnd(); } } ); } else { lastTime -= this.interval; this.setState( { lastTime, }, () => { this.tick(); } ); } }, this.interval); }; render() { const { format = this.defaultFormat, onEnd, ...rest } = this.props; const { lastTime } = this.state; const result = format(lastTime); return {result}; } } export default CountDown; ================================================ FILE: app/assets/components/CountDown/index.zh-CN.md ================================================ --- title: CountDown subtitle: 倒计时 cols: 1 order: 3 --- 倒计时组件。 ## API | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | format | 时间格式化显示 | Function(time) | | | target | 目标时间 | Date | - | | onEnd | 倒计时结束回调 | funtion | -| ================================================ FILE: app/assets/components/DescriptionList/Description.d.ts ================================================ import * as React from 'react'; export default class Description extends React.Component< { term: React.ReactNode; style?: React.CSSProperties; }, any > {} ================================================ FILE: app/assets/components/DescriptionList/Description.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Col } from 'antd'; import styles from './index.less'; import responsive from './responsive'; const Description = ({ term, column, className, children, ...restProps }) => { const clsString = classNames(styles.description, className); return ( {term &&
    {term}
    } {children &&
    {children}
    } ); }; Description.defaultProps = { term: '', }; Description.propTypes = { term: PropTypes.node, }; export default Description; ================================================ FILE: app/assets/components/DescriptionList/DescriptionList.js ================================================ import React from 'react'; import classNames from 'classnames'; import { Row } from 'antd'; import styles from './index.less'; const DescriptionList = ({ className, title, col = 3, layout = 'horizontal', gutter = 32, children, size, ...restProps }) => { const clsString = classNames(styles.descriptionList, styles[layout], className, { [styles.small]: size === 'small', [styles.large]: size === 'large', }); const column = col > 4 ? 4 : col; return (
    {title ?
    {title}
    : null} {React.Children.map(children, child => child ? React.cloneElement(child, { column }) : child)}
    ); }; export default DescriptionList; ================================================ FILE: app/assets/components/DescriptionList/demo/basic.md ================================================ --- order: 0 title: zh-CN: 基本 en-US: Basic --- ## zh-CN 基本描述列表。 ## en-US Basic DescriptionList. ````jsx import DescriptionList from 'ant-design-pro/lib/DescriptionList'; const { Description } = DescriptionList; ReactDOM.render( A free, open source, cross-platform, graphical web browser developed by the Mozilla Corporation and hundreds of volunteers. A free, open source, cross-platform, graphical web browser developed by the Mozilla Corporation and hundreds of volunteers. A free, open source, cross-platform, graphical web browser developed by the Mozilla Corporation and hundreds of volunteers. , mountNode); ```` ================================================ FILE: app/assets/components/DescriptionList/demo/vertical.md ================================================ --- order: 1 title: zh-CN: 垂直型 en-US: Vertical --- ## zh-CN 垂直布局。 ## en-US Vertical layout. ````jsx import DescriptionList from 'ant-design-pro/lib/DescriptionList'; const { Description } = DescriptionList; ReactDOM.render( A free, open source, cross-platform, graphical web browser developed by the Mozilla Corporation and hundreds of volunteers. A free, open source, cross-platform, graphical web browser developed by the Mozilla Corporation and hundreds of volunteers. A free, open source, cross-platform, graphical web browser developed by the Mozilla Corporation and hundreds of volunteers. , mountNode); ```` ================================================ FILE: app/assets/components/DescriptionList/index.d.ts ================================================ import * as React from 'react'; import Description from './Description'; export interface IDescriptionListProps { layout?: 'horizontal' | 'vertical'; col?: number; title: React.ReactNode; gutter?: number; size?: 'large' | 'small'; style?: React.CSSProperties; } export default class DescriptionList extends React.Component { public static Description: typeof Description; } ================================================ FILE: app/assets/components/DescriptionList/index.en-US.md ================================================ --- title: DescriptionList cols: 1 order: 4 --- Groups display multiple read-only fields, which are common to informational displays on detail pages. ## API ### DescriptionList | Property | Description | Type | Default | |----------|------------------------------------------|-------------|---------| | layout | type of layout | Enum{'horizontal', 'vertical'} | 'horizontal' | | col | specify the maximum number of columns to display, the final columns number is determined by col setting combined with [Responsive Rules](/components/DescriptionList#Responsive-Rules) | number(0 < col <= 4) | 3 | | title | title | ReactNode | - | | gutter | specify the distance between two items, unit is `px` | number | 32 | | size | size of list | Enum{'large', 'small'} | - | #### Responsive Rules | Window Width | Columns Number | |---------------------|---------------------------------------------| | `≥768px` | `col` | | `≥576px` | `col < 2 ? col : 2` | | `<576px` | `1` | ### DescriptionList.Description | Property | Description | Type | Default | |----------|------------------------------------------|-------------|-------| | term | item title | ReactNode | - | ================================================ FILE: app/assets/components/DescriptionList/index.js ================================================ import DescriptionList from './DescriptionList'; import Description from './Description'; DescriptionList.Description = Description; export default DescriptionList; ================================================ FILE: app/assets/components/DescriptionList/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .descriptionList { // offset the padding-bottom of last row :global { .ant-row { margin-bottom: -16px; overflow: hidden; } } .title { font-size: 14px; color: @heading-color; font-weight: 500; margin-bottom: 16px; } .term { // Line-height is 22px IE dom height will calculate error line-height: 20px; padding-bottom: 16px; margin-right: 8px; color: @heading-color; white-space: nowrap; display: table-cell; &:after { content: ':'; margin: 0 8px 0 2px; position: relative; top: -0.5px; } } .detail { line-height: 22px; width: 100%; padding-bottom: 16px; color: @text-color; display: table-cell; } &.small { // offset the padding-bottom of last row :global { .ant-row { margin-bottom: -8px; } } .title { margin-bottom: 12px; color: @text-color; } .term, .detail { padding-bottom: 8px; } } &.large { .title { font-size: 16px; } } &.vertical { .term { padding-bottom: 8px; display: block; } .detail { display: block; } } } ================================================ FILE: app/assets/components/DescriptionList/index.zh-CN.md ================================================ --- title: DescriptionList subtitle: 描述列表 cols: 1 order: 4 --- 成组展示多个只读字段,常见于详情页的信息展示。 ## API ### DescriptionList | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | layout | 布局方式 | Enum{'horizontal', 'vertical'} | 'horizontal' | | col | 指定信息最多分几列展示,最终一行几列由 col 配置结合[响应式规则](/components/DescriptionList#响应式规则)决定 | number(0 < col <= 4) | 3 | | title | 列表标题 | ReactNode | - | | gutter | 列表项间距,单位为 `px` | number | 32 | | size | 列表型号 | Enum{'large', 'small'} | - | #### 响应式规则 | 窗口宽度 | 展示列数 | |---------------------|---------------------------------------------| | `≥768px` | `col` | | `≥576px` | `col < 2 ? col : 2` | | `<576px` | `1` | ### DescriptionList.Description | 参数 | 说明 | 类型 | 默认值 | |----------|------------------------------------------|-------------|-------| | term | 列表项标题 | ReactNode | - | ================================================ FILE: app/assets/components/DescriptionList/responsive.js ================================================ export default { 1: { xs: 24 }, 2: { xs: 24, sm: 12 }, 3: { xs: 24, sm: 12, md: 8 }, 4: { xs: 24, sm: 12, md: 6 }, }; ================================================ FILE: app/assets/components/EditableItem/index.js ================================================ import React, { PureComponent } from 'react'; import { Input, Icon } from 'antd'; import styles from './index.less'; export default class EditableItem extends PureComponent { state = { value: this.props.value, editable: false, }; handleChange = e => { const { value } = e.target; this.setState({ value }); }; check = () => { this.setState({ editable: false }); if (this.props.onChange) { this.props.onChange(this.state.value); } }; edit = () => { this.setState({ editable: true }); }; render() { const { value, editable } = this.state; return (
    {editable ? (
    ) : (
    {value || ' '}
    )}
    ); } } ================================================ FILE: app/assets/components/EditableItem/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .editableItem { line-height: @input-height-base; display: table; width: 100%; margin-top: (@font-size-base * @line-height-base - @input-height-base) / 2; .wrapper { display: table-row; & > * { display: table-cell; } & > *:first-child { width: 85%; } .icon { cursor: pointer; text-align: right; } } } ================================================ FILE: app/assets/components/EditableLinkGroup/index.js ================================================ import React, { PureComponent, createElement } from 'react'; import PropTypes from 'prop-types'; import { Button } from 'antd'; import styles from './index.less'; // TODO: 添加逻辑 class EditableLinkGroup extends PureComponent { static defaultProps = { links: [], onAdd: () => {}, linkElement: 'a', }; static propTypes = { links: PropTypes.array, onAdd: PropTypes.func, linkElement: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), }; render() { const { links, linkElement, onAdd } = this.props; return (
    {links.map(link => createElement( linkElement, { key: `linkGroup-item-${link.id || link.title}`, to: link.href, href: link.href, }, link.title ) )} { }
    ); } } export default EditableLinkGroup; ================================================ FILE: app/assets/components/EditableLinkGroup/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .linkGroup { padding: 20px 0 8px 24px; font-size: 0; & > a { color: @text-color; display: inline-block; font-size: @font-size-base; margin-bottom: 13px; width: 25%; &:hover { color: @primary-color; } } } ================================================ FILE: app/assets/components/Ellipsis/demo/line.md ================================================ --- order: 1 title: zh-CN: 按照行数省略 en-US: Truncate according to the number of rows --- ## zh-CN 通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。但是在这种模式下所有 `children` 将会被转换成纯文本。 并且注意在这种模式下,外容器需要有指定的宽度(或设置自身宽度)。 ## en-US `lines` attribute specifies the maximum number of rows where the text will automatically be truncated when exceeded. In this mode, all children will be converted to plain text. Also note that, in this mode, the outer container needs to have a specified width (or set its own width). ````jsx import Ellipsis from 'ant-design-pro/lib/Ellipsis'; const article =

    There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.

    ; ReactDOM.render(
    {article}
    , mountNode); ```` ================================================ FILE: app/assets/components/Ellipsis/demo/number.md ================================================ --- order: 0 title: zh-CN: 按照字符数省略 en-US: Truncate according to the number of character --- ## zh-CN 通过设置 `length` 属性指定文本最长长度,如果超过这个长度会自动截取。 ## en-US `length` attribute specifies the maximum length where the text will automatically be truncated when exceeded. ````jsx import Ellipsis from 'ant-design-pro/lib/Ellipsis'; const article = 'There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.'; ReactDOM.render(
    {article}

    Show Tooltip

    {article}
    , mountNode); ```` ================================================ FILE: app/assets/components/Ellipsis/index.d.ts ================================================ import * as React from 'react'; export interface IEllipsisProps { tooltip?: boolean; length?: number; lines?: number; style?: React.CSSProperties; className?: string; } export default class Ellipsis extends React.Component {} ================================================ FILE: app/assets/components/Ellipsis/index.en-US.md ================================================ --- title: Ellipsis cols: 1 order: 10 --- When the text is too long, the Ellipsis automatically shortens it according to its length or the maximum number of lines. ## API Property | Description | Type | Default ----|------|-----|------ tooltip | tooltip for showing the full text content when hovering over | boolean | - length | maximum number of characters in the text before being truncated | number | - lines | maximum number of rows in the text before being truncated | number | `1` ================================================ FILE: app/assets/components/Ellipsis/index.js ================================================ import React, { Component } from 'react'; import { Tooltip } from 'antd'; import classNames from 'classnames'; import styles from './index.less'; /* eslint react/no-did-mount-set-state: 0 */ /* eslint no-param-reassign: 0 */ const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined; const EllipsisText = ({ text, length, tooltip, ...other }) => { if (typeof text !== 'string') { throw new Error('Ellipsis children must be string.'); } if (text.length <= length || length < 0) { return {text}; } const tail = '...'; let displayText; if (length - tail.length <= 0) { displayText = ''; } else { displayText = text.slice(0, length - tail.length); } if (tooltip) { return ( {displayText} {tail} ); } return ( {displayText} {tail} ); }; export default class Ellipsis extends Component { state = { text: '', targetCount: 0, }; componentDidMount() { if (this.node) { this.computeLine(); } } componentWillReceiveProps(nextProps) { if (this.props.lines !== nextProps.lines) { this.computeLine(); } } computeLine = () => { const { lines } = this.props; if (lines && !isSupportLineClamp) { const text = this.shadowChildren.innerText; const lineHeight = parseInt(getComputedStyle(this.root).lineHeight, 10); const targetHeight = lines * lineHeight; this.content.style.height = `${targetHeight}px`; const totalHeight = this.shadowChildren.offsetHeight; const shadowNode = this.shadow.firstChild; if (totalHeight <= targetHeight) { this.setState({ text, targetCount: text.length, }); return; } // bisection const len = text.length; const mid = Math.floor(len / 2); const count = this.bisection(targetHeight, mid, 0, len, text, shadowNode); this.setState({ text, targetCount: count, }); } }; bisection = (th, m, b, e, text, shadowNode) => { const suffix = '...'; let mid = m; let end = e; let begin = b; shadowNode.innerHTML = text.substring(0, mid) + suffix; let sh = shadowNode.offsetHeight; if (sh <= th) { shadowNode.innerHTML = text.substring(0, mid + 1) + suffix; sh = shadowNode.offsetHeight; if (sh > th) { return mid; } else { begin = mid; mid = Math.floor((end - begin) / 2) + begin; return this.bisection(th, mid, begin, end, text, shadowNode); } } else { if (mid - 1 < 0) { return mid; } shadowNode.innerHTML = text.substring(0, mid - 1) + suffix; sh = shadowNode.offsetHeight; if (sh <= th) { return mid - 1; } else { end = mid; mid = Math.floor((end - begin) / 2) + begin; return this.bisection(th, mid, begin, end, text, shadowNode); } } }; handleRoot = n => { this.root = n; }; handleContent = n => { this.content = n; }; handleNode = n => { this.node = n; }; handleShadow = n => { this.shadow = n; }; handleShadowChildren = n => { this.shadowChildren = n; }; render() { const { text, targetCount } = this.state; const { children, lines, length, className, tooltip, ...restProps } = this.props; const cls = classNames(styles.ellipsis, className, { [styles.lines]: lines && !isSupportLineClamp, [styles.lineClamp]: lines && isSupportLineClamp, }); if (!lines && !length) { return ( {children} ); } // length if (!lines) { return ( ); } const id = `antd-pro-ellipsis-${`${new Date().getTime()}${Math.floor(Math.random() * 100)}`}`; // support document.body.style.webkitLineClamp if (isSupportLineClamp) { const style = `#${id}{-webkit-line-clamp:${lines};-webkit-box-orient: vertical;}`; return (
    {tooltip ? ( {children} ) : ( children )}
    ); } const childNode = ( {targetCount > 0 && text.substring(0, targetCount)} {targetCount > 0 && targetCount < text.length && '...'} ); return (
    {tooltip ? ( {childNode} ) : ( childNode )}
    {children}
    {text}
    ); } } ================================================ FILE: app/assets/components/Ellipsis/index.less ================================================ .ellipsis { overflow: hidden; display: inline-block; word-break: break-all; width: 100%; } .lines { position: relative; .shadow { display: block; position: relative; color: transparent; opacity: 0; z-index: -999; } } .lineClamp { position: relative; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; } ================================================ FILE: app/assets/components/Ellipsis/index.zh-CN.md ================================================ --- title: Ellipsis subtitle: 文本自动省略号 cols: 1 order: 10 --- 文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 ## API 参数 | 说明 | 类型 | 默认值 ----|------|-----|------ tooltip | 移动到文本展示完整内容的提示 | boolean | - length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1` ================================================ FILE: app/assets/components/Exception/demo/403.md ================================================ --- order: 2 title: zh-CN: 403 en-US: 403 --- ## zh-CN 403 页面,配合自定义操作。 ## en-US 403 page with custom operations. ````jsx import Exception from 'ant-design-pro/lib/Exception'; import { Button } from 'antd'; const actions = (
    ); ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Exception/demo/404.md ================================================ --- order: 0 title: zh-CN: 404 en-US: 404 --- ## zh-CN 404 页面。 ## en-US 404 page. ````jsx import Exception from 'ant-design-pro/lib/Exception'; ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Exception/demo/500.md ================================================ --- order: 1 title: zh-CN: 500 en-US: 500 --- ## zh-CN 500 页面。 ## en-US 500 page. ````jsx import Exception from 'ant-design-pro/lib/Exception'; ReactDOM.render( , mountNode); ```` ================================================ FILE: app/assets/components/Exception/index.d.ts ================================================ import * as React from 'react'; export interface IExceptionProps { type?: '403' | '404' | '500'; title?: React.ReactNode; desc?: React.ReactNode; img?: string; actions?: React.ReactNode; linkElement?: React.ReactNode; style?: React.CSSProperties; } export default class Exception extends React.Component {} ================================================ FILE: app/assets/components/Exception/index.en-US.md ================================================ --- title: Exception cols: 1 order: 5 --- Exceptions page is used to provide feedback on specific abnormal state. Usually, it contains an explanation of the error status, and provides users with suggestions or operations, to prevent users from feeling lost and confused. ## API Property | Description | Type | Default ---------|-------------|------|-------- type | type of exception, the corresponding default `title`, `desc`, `img` will be given if set, which can be overridden by explicit setting of `title`, `desc`, `img` | Enum {'403', '404', '500'} | - title | title | ReactNode | - desc | supplementary description | ReactNode | - img | the url of background image | string | - actions | suggested operations, a default 'Home' link will show if not set | ReactNode | - linkElement | to specify the element of link | string\|ReactElement | 'a' ================================================ FILE: app/assets/components/Exception/index.js ================================================ import React, { createElement } from 'react'; import classNames from 'classnames'; import { Button } from 'antd'; import config from './typeConfig'; import styles from './index.less'; const Exception = ({ className, linkElement = 'a', type, title, desc, img, actions, ...rest }) => { const pageType = type in config ? type : '404'; const clsString = classNames(styles.exception, className); return (

    {title || config[pageType].title}

    {desc || config[pageType].desc}
    {actions || createElement( linkElement, { to: '/', href: '/', }, )}
    ); }; export default Exception; ================================================ FILE: app/assets/components/Exception/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .exception { display: flex; align-items: center; height: 100%; .imgBlock { flex: 0 0 62.5%; width: 62.5%; padding-right: 152px; zoom: 1; &:before, &:after { content: ' '; display: table; } &:after { clear: both; visibility: hidden; font-size: 0; height: 0; } } .imgEle { height: 360px; width: 100%; max-width: 430px; float: right; background-repeat: no-repeat; background-position: 50% 50%; background-size: contain; } .content { flex: auto; h1 { color: #434e59; font-size: 72px; font-weight: 600; line-height: 72px; margin-bottom: 24px; } .desc { color: @text-color-secondary; font-size: 20px; line-height: 28px; margin-bottom: 16px; } .actions { button:not(:last-child) { margin-right: 8px; } } } } @media screen and (max-width: @screen-xl) { .exception { .imgBlock { padding-right: 88px; } } } @media screen and (max-width: @screen-sm) { .exception { display: block; text-align: center; .imgBlock { padding-right: 0; margin: 0 auto 24px; } } } @media screen and (max-width: @screen-xs) { .exception { .imgBlock { margin-bottom: -24px; overflow: hidden; } } } ================================================ FILE: app/assets/components/Exception/index.zh-CN.md ================================================ --- title: Exception subtitle: 异常 cols: 1 order: 5 --- 异常页用于对页面特定的异常状态进行反馈。通常,它包含对错误状态的阐述,并向用户提供建议或操作,避免用户感到迷失和困惑。 ## API | 参数 | 说明 | 类型 | 默认值 | |-------------|------------------------------------------|-------------|-------| | type | 页面类型,若配置,则自带对应类型默认的 `title`,`desc`,`img`,此默认设置可以被 `title`,`desc`,`img` 覆盖 | Enum {'403', '404', '500'} | - | | title | 标题 | ReactNode | - | | desc | 补充描述 | ReactNode | - | | img | 背景图片地址 | string | - | | actions | 建议操作,配置此属性时默认的『返回首页』按钮不生效 | ReactNode | - | | linkElement | 定义链接的元素 | string\|ReactElement | 'a' | ================================================ FILE: app/assets/components/Exception/typeConfig.js ================================================ const config = { 403: { img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg', title: '403', desc: '抱歉,你无权访问该页面', }, 404: { img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg', title: '404', desc: '抱歉,你访问的页面不存在', }, 500: { img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg', title: '500', desc: '抱歉,服务器出错了', }, }; export default config; ================================================ FILE: app/assets/components/FooterToolbar/demo/basic.md ================================================ --- order: 0 title: zh-CN: 演示 en-US: demo iframe: 400 --- ## zh-CN 浮动固定页脚。 ## en-US Fixed to the footer. ````jsx import FooterToolbar from 'ant-design-pro/lib/FooterToolbar'; import { Button } from 'antd'; ReactDOM.render(

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    Content Content Content Content

    , mountNode); ```` ================================================ FILE: app/assets/components/FooterToolbar/index.d.ts ================================================ import * as React from 'react'; export interface IFooterToolbarProps { extra: React.ReactNode; style?: React.CSSProperties; } export default class FooterToolbar extends React.Component {} ================================================ FILE: app/assets/components/FooterToolbar/index.en-US.md ================================================ --- title: FooterToolbar cols: 1 order: 6 --- A toolbar fixed at the bottom. ## Usage It is fixed at the bottom of the content area and does not move along with the scroll bar, which is usually used for data collection and submission for long pages. ## API Property | Description | Type | Default ---------|-------------|------|-------- children | toolbar content, align to the right | ReactNode | - extra | extra information, align to the left | ReactNode | - ================================================ FILE: app/assets/components/FooterToolbar/index.js ================================================ import React, { Component } from 'react'; import classNames from 'classnames'; import styles from './index.less'; export default class FooterToolbar extends Component { render() { const { children, className, extra, ...restProps } = this.props; return (
    {extra}
    {children}
    ); } } ================================================ FILE: app/assets/components/FooterToolbar/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .toolbar { position: fixed; width: 100%; bottom: 0; right: 0; height: 56px; line-height: 56px; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03); background: #fff; border-top: 1px solid @border-color-split; padding: 0 24px; z-index: 9; &:after { content: ''; display: block; clear: both; } .left { float: left; } .right { float: right; } button + button { margin-left: 8px; } } ================================================ FILE: app/assets/components/FooterToolbar/index.zh-CN.md ================================================ --- title: FooterToolbar subtitle: 底部工具栏 cols: 1 order: 6 --- 固定在底部的工具栏。 ## 何时使用 固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。 ## API 参数 | 说明 | 类型 | 默认值 ----|------|-----|------ children | 工具栏内容,向右对齐 | ReactNode | - extra | 额外信息,向左对齐 | ReactNode | - ================================================ FILE: app/assets/components/GlobalFooter/demo/basic.md ================================================ --- order: 0 title: 演示 iframe: 400 --- 基本页脚。 ````jsx import GlobalFooter from 'ant-design-pro/lib/GlobalFooter'; import { Icon } from 'antd'; const links = [{ key: '帮助', title: '帮助', href: '', }, { key: 'github', title: , href: 'https://github.com/ant-design/ant-design-pro', blankTarget: true, }, { key: '条款', title: '条款', href: '', blankTarget: true, }]; const copyright =
    Copyright 2017 蚂蚁金服体验技术部出品
    ; ReactDOM.render(
    , mountNode); ```` ================================================ FILE: app/assets/components/GlobalFooter/index.d.ts ================================================ import * as React from 'react'; export interface IGlobalFooterProps { links?: Array<{ key?: string; title: React.ReactNode; href: string; blankTarget?: boolean; }>; copyright?: React.ReactNode; style?: React.CSSProperties; } export default class GlobalFooter extends React.Component {} ================================================ FILE: app/assets/components/GlobalFooter/index.js ================================================ import React from 'react'; import classNames from 'classnames'; import styles from './index.less'; const GlobalFooter = ({ className, links, copyright }) => { const clsString = classNames(styles.globalFooter, className); return (
    {links && (
    {links.map(link => ( {link.title} ))}
    )} {copyright &&
    {copyright}
    }
    ); }; export default GlobalFooter; ================================================ FILE: app/assets/components/GlobalFooter/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .globalFooter { padding: 0 16px; margin: 48px 0 24px 0; text-align: center; .links { margin-bottom: 8px; a { color: @text-color-secondary; transition: all 0.3s; &:not(:last-child) { margin-right: 40px; } &:hover { color: @text-color; } } } .copyright { color: @text-color-secondary; font-size: @font-size-base; } } ================================================ FILE: app/assets/components/GlobalFooter/index.md ================================================ --- title: en-US: GlobalFooter zh-CN: GlobalFooter subtitle: 全局页脚 cols: 1 order: 7 --- 页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。 ## API 参数 | 说明 | 类型 | 默认值 ----|------|-----|------ links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - copyright | 版权信息 | ReactNode | - ================================================ FILE: app/assets/components/GlobalHeader/index.js ================================================ import React, { PureComponent } from 'react'; import { Icon, Divider, Tooltip } from 'antd'; import Debounce from 'lodash-decorators/debounce'; import {Link, routerRedux} from 'dva/router'; import ca from '../../utils/ca'; import styles from './index.less'; import { connect } from "dva/index"; @connect() export default class GlobalHeader extends PureComponent { componentWillUnmount() { this.triggerResizeEvent.cancel(); } toggle = () => { const { collapsed, onCollapse } = this.props; onCollapse(!collapsed); this.triggerResizeEvent(); }; /* eslint-disable*/ @Debounce(600) triggerResizeEvent() { const event = document.createEvent('HTMLEvents'); event.initEvent('resize', true, false); window.dispatchEvent(event); } handleLogout = async () => { const res = await ca.get('/api/userLogout'); if (!res) return; const { dispatch } = this.props; dispatch(routerRedux.push('/user/login')); }; render() { const { collapsed, isMobile, logo, currentUser, } = this.props; return (
    {isMobile && [ logo , , ]}
    欢迎 {currentUser.real_name}
    ); } } ================================================ FILE: app/assets/components/GlobalHeader/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .header { height: 64px; padding: 0 12px 0 0; background: #fff; box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); position: relative; } :global { .ant-layout { min-height: 100vh; overflow-x: hidden; } } .logo { height: 64px; line-height: 58px; vertical-align: top; display: inline-block; padding: 0 0 0 24px; cursor: pointer; font-size: 20px; img { display: inline-block; vertical-align: middle; } } .menu { :global(.anticon) { margin-right: 8px; } :global(.ant-dropdown-menu-item) { width: 160px; } } i.trigger { font-size: 20px; line-height: 64px; cursor: pointer; transition: all 0.3s, padding 0s; padding: 0 24px; &:hover { background: @primary-1; } } .right { float: right; height: 100%; .action { cursor: pointer; padding: 0 12px; display: inline-block; transition: all 0.3s; height: 100%; > i { font-size: 16px; vertical-align: middle; color: @text-color; } &:hover, &:global(.ant-popover-open) { background: @primary-1; } } .search { padding: 0; margin: 0 12px; &:hover { background: transparent; } } .account { .avatar { margin: 20px 8px 20px 0; color: @primary-color; background: rgba(255, 255, 255, 0.85); vertical-align: middle; } } } @media only screen and (max-width: @screen-md) { .header { :global(.ant-divider-vertical) { vertical-align: unset; } .name { display: none; } i.trigger { padding: 0 12px; } .logo { padding-right: 12px; position: relative; } .right { position: absolute; right: 12px; top: 0; background: #fff; .account { .avatar { margin-right: 0; } } } } } ================================================ FILE: app/assets/components/HeaderSearch/demo/basic.md ================================================ --- order: 0 title: 全局搜索 --- 通常放置在导航工具条右侧。(点击搜索图标预览效果) ````jsx import HeaderSearch from 'ant-design-pro/lib/HeaderSearch'; ReactDOM.render(
    { console.log('input', value); // eslint-disable-line }} onPressEnter={(value) => { console.log('enter', value); // eslint-disable-line }} />
    , mountNode); ```` ================================================ FILE: app/assets/components/HeaderSearch/index.d.ts ================================================ import * as React from 'react'; export interface IHeaderSearchProps { placeholder?: string; dataSource?: string[]; onSearch?: (value: string) => void; onChange?: (value: string) => void; onPressEnter?: (value: string) => void; style?: React.CSSProperties; } export default class HeaderSearch extends React.Component {} ================================================ FILE: app/assets/components/HeaderSearch/index.js ================================================ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { Input, Icon, AutoComplete } from 'antd'; import classNames from 'classnames'; import styles from './index.less'; export default class HeaderSearch extends PureComponent { static defaultProps = { defaultActiveFirstOption: false, onPressEnter: () => {}, onSearch: () => {}, className: '', placeholder: '', dataSource: [], defaultOpen: false, }; static propTypes = { className: PropTypes.string, placeholder: PropTypes.string, onSearch: PropTypes.func, onPressEnter: PropTypes.func, defaultActiveFirstOption: PropTypes.bool, dataSource: PropTypes.array, defaultOpen: PropTypes.bool, }; state = { searchMode: this.props.defaultOpen, value: '', }; componentWillUnmount() { clearTimeout(this.timeout); } onKeyDown = e => { if (e.key === 'Enter') { this.timeout = setTimeout(() => { this.props.onPressEnter(this.state.value); // Fix duplicate onPressEnter }, 0); } }; onChange = value => { this.setState({ value }); if (this.props.onChange) { this.props.onChange(); } }; enterSearchMode = () => { this.setState({ searchMode: true }, () => { if (this.state.searchMode) { this.input.focus(); } }); }; leaveSearchMode = () => { this.setState({ searchMode: false, value: '', }); }; render() { const { className, placeholder, ...restProps } = this.props; delete restProps.defaultOpen; // for rc-select not affected const inputClass = classNames(styles.input, { [styles.show]: this.state.searchMode, }); return ( { this.input = node; }} onKeyDown={this.onKeyDown} onBlur={this.leaveSearchMode} /> ); } } ================================================ FILE: app/assets/components/HeaderSearch/index.less ================================================ @import '~antd/lib/style/themes/default.less'; .headerSearch { :global(.anticon-search) { cursor: pointer; font-size: 16px; } .input { transition: width 0.3s, margin-left 0.3s; width: 0; background: transparent; border-radius: 0; :global(.ant-select-selection) { background: transparent; } input { border: 0; padding-left: 0; padding-right: 0; box-shadow: none !important; } &, &:hover, &:focus { border-bottom: 1px solid @border-color-base; } &.show { width: 210px; margin-left: 8px; } } } ================================================ FILE: app/assets/components/HeaderSearch/index.md ================================================ --- title: en-US: HeaderSearch zh-CN: HeaderSearch subtitle: 顶部搜索框 cols: 1 order: 8 --- 通常作为全局搜索的入口,放置在导航工具条右侧。 ## API 参数 | 说明 | 类型 | 默认值 ----|------|-----|------ placeholder | 占位文字 | string | - dataSource | 当前提示内容列表 | string[] | - onSearch | 选择某项或按下回车时的回调 | function(value) | - onChange | 输入搜索字符的回调 | function(value) | - onPressEnter | 按下回车时的回调 | function(value) | - defaultOpen | 输入框首次显示是否打开 | boolean | false ================================================ FILE: app/assets/components/JsonSchemaForm/components/AddButton.js ================================================ import React from "react"; import IconButton from "./IconButton"; export default function AddButton({ className, onClick, disabled }) { return (

    ); } ================================================ FILE: app/assets/components/JsonSchemaForm/components/ErrorList.js ================================================ import React from "react"; export default function ErrorList(props) { const { errors } = props; return (

    Errors

      {errors.map((error, i) => { return (
    • {error.stack}
    • ); })}
    ); } ================================================ FILE: app/assets/components/JsonSchemaForm/components/Form.js ================================================ import React, { Component } from "react"; import PropTypes from "prop-types"; import { default as DefaultErrorList } from "./ErrorList"; import { getDefaultFormState, retrieveSchema, shouldRender, toIdSchema, setState, getDefaultRegistry, deepEquals, } from "../utils"; import validateFormData, { toErrorList } from "../validate"; export default class Form extends Component { static defaultProps = { uiSchema: {}, noValidate: false, liveValidate: false, disabled: false, safeRenderCompletion: false, noHtml5Validate: false, ErrorList: DefaultErrorList, }; constructor(props) { super(props); this.state = this.getStateFromProps(props); if ( this.props.onChange && !deepEquals(this.state.formData, this.props.formData) ) { this.props.onChange(this.state); } this.formElement = null; } componentWillReceiveProps(nextProps) { const nextState = this.getStateFromProps(nextProps); if ( !deepEquals(nextState.formData, nextProps.formData) && !deepEquals(nextState.formData, this.state.formData) && this.props.onChange ) { this.props.onChange(nextState); } this.setState(nextState); } getStateFromProps(props) { const state = this.state || {}; const schema = "schema" in props ? props.schema : this.props.schema; const uiSchema = "uiSchema" in props ? props.uiSchema : this.props.uiSchema; const edit = typeof props.formData !== "undefined"; const liveValidate = props.liveValidate || this.props.liveValidate; const mustValidate = edit && !props.noValidate && liveValidate; const { definitions } = schema; const formData = getDefaultFormState(schema, props.formData, definitions); const retrievedSchema = retrieveSchema(schema, definitions, formData); const additionalMetaSchemas = props.additionalMetaSchemas; const { errors, errorSchema } = mustValidate ? this.validate(formData, schema, additionalMetaSchemas) : { errors: state.errors || [], errorSchema: state.errorSchema || {}, }; const idSchema = toIdSchema( retrievedSchema, uiSchema["ui:rootFieldId"], definitions, formData, props.idPrefix ); return { schema, uiSchema, idSchema, formData, edit, errors, errorSchema, additionalMetaSchemas, }; } shouldComponentUpdate(nextProps, nextState) { return shouldRender(this, nextProps, nextState); } validate( formData, schema = this.props.schema, additionalMetaSchemas = this.props.additionalMetaSchemas ) { const { validate, transformErrors } = this.props; const { definitions } = this.getRegistry(); const resolvedSchema = retrieveSchema(schema, definitions, formData); return validateFormData( formData, resolvedSchema, validate, transformErrors, additionalMetaSchemas ); } renderErrors() { const { errors, errorSchema, schema, uiSchema } = this.state; const { ErrorList, showErrorList, formContext } = this.props; if (errors.length && showErrorList != false) { return ( ); } return null; } onChange = (formData, newErrorSchema) => { const mustValidate = !this.props.noValidate && this.props.liveValidate; let state = { formData }; if (mustValidate) { const { errors, errorSchema } = this.validate(formData); state = { ...state, errors, errorSchema }; } else if (!this.props.noValidate && newErrorSchema) { state = { ...state, errorSchema: newErrorSchema, errors: toErrorList(newErrorSchema), }; } setState(this, state, () => { if (this.props.onChange) { this.props.onChange(this.state); } }); }; onBlur = (...args) => { if (this.props.onBlur) { this.props.onBlur(...args); } }; onFocus = (...args) => { if (this.props.onFocus) { this.props.onFocus(...args); } }; onSubmit = event => { event.preventDefault(); event.persist(); if (!this.props.noValidate) { const { errors, errorSchema } = this.validate(this.state.formData); if (Object.keys(errors).length > 0) { setState(this, { errors, errorSchema }, () => { if (this.props.onError) { this.props.onError(errors); } else { console.error("Form validation failed", errors); } }); return; } } this.setState({ errors: [], errorSchema: {} }, () => { if (this.props.onSubmit) { this.props.onSubmit({ ...this.state, status: "submitted" }, event); } }); }; getRegistry() { // For BC, accept passed SchemaField and TitleField props and pass them to // the "fields" registry one. const { fields, widgets } = getDefaultRegistry(); return { fields: { ...fields, ...this.props.fields }, widgets: { ...widgets, ...this.props.widgets }, ArrayFieldTemplate: this.props.ArrayFieldTemplate, ObjectFieldTemplate: this.props.ObjectFieldTemplate, FieldTemplate: this.props.FieldTemplate, definitions: this.props.schema.definitions || {}, formContext: this.props.formContext || {}, }; } submit() { if (this.formElement) { this.formElement.dispatchEvent(new Event("submit", { cancelable: true })); } } render() { const { children, safeRenderCompletion, id, idPrefix, className, name, method, target, action, autocomplete, enctype, acceptcharset, noHtml5Validate, disabled, } = this.props; const { schema, uiSchema, formData, errorSchema, idSchema } = this.state; const registry = this.getRegistry(); const _SchemaField = registry.fields.SchemaField; return (
    { this.formElement = form; }}> {this.renderErrors()} <_SchemaField schema={schema} uiSchema={uiSchema} errorSchema={errorSchema} idSchema={idSchema} idPrefix={idPrefix} formData={formData} onChange={this.onChange} onBlur={this.onBlur} onFocus={this.onFocus} registry={registry} safeRenderCompletion={safeRenderCompletion} disabled={disabled} /> {children ? ( children ) : (
    )}
    ); } } if (process.env.NODE_ENV !== "production") { Form.propTypes = { schema: PropTypes.object.isRequired, uiSchema: PropTypes.object, formData: PropTypes.any, widgets: PropTypes.objectOf( PropTypes.oneOfType([PropTypes.func, PropTypes.object]) ), fields: PropTypes.objectOf(PropTypes.func), ArrayFieldTemplate: PropTypes.func, ObjectFieldTemplate: PropTypes.func, FieldTemplate: PropTypes.func, ErrorList: PropTypes.func, onChange: PropTypes.func, onError: PropTypes.func, showErrorList: PropTypes.bool, onSubmit: PropTypes.func, id: PropTypes.string, className: PropTypes.string, name: PropTypes.string, method: PropTypes.string, target: PropTypes.string, action: PropTypes.string, autocomplete: PropTypes.string, enctype: PropTypes.string, acceptcharset: PropTypes.string, noValidate: PropTypes.bool, noHtml5Validate: PropTypes.bool, liveValidate: PropTypes.bool, validate: PropTypes.func, transformErrors: PropTypes.func, safeRenderCompletion: PropTypes.bool, formContext: PropTypes.object, additionalMetaSchemas: PropTypes.arrayOf(PropTypes.object), }; } ================================================ FILE: app/assets/components/JsonSchemaForm/components/IconButton.js ================================================ import React from "react"; export default function IconButton(props) { const { type = "default", icon, className, ...otherProps } = props; return ( ); } ================================================ FILE: app/assets/components/JsonSchemaForm/components/fields/ArrayField.js ================================================ import AddButton from "../AddButton"; import IconButton from "../IconButton"; import React, { Component } from "react"; import includes from "core-js/library/fn/array/includes"; import * as types from "../../types"; import UnsupportedField from "./UnsupportedField"; import { getWidget, getDefaultFormState, getUiOptions, isMultiSelect, isFilesArray, isFixedItems, allowAdditionalItems, optionsList, retrieveSchema, toIdSchema, getDefaultRegistry, } from "../../utils"; function ArrayFieldTitle({ TitleField, idSchema, title, required }) { if (!title) { return null; } const id = `${idSchema.$id}__title`; return ; } function ArrayFieldDescription({ DescriptionField, idSchema, description }) { if (!description) { return null; } const id = `${idSchema.$id}__description`; return ; } // Used in the two templates function DefaultArrayItem(props) { const btnStyle = { flex: 1, paddingLeft: 6, paddingRight: 6, fontWeight: "bold", }; return (
    {props.children}
    {props.hasToolbar && (
    {(props.hasMoveUp || props.hasMoveDown) && ( )} {(props.hasMoveUp || props.hasMoveDown) && ( )} {props.hasRemove && ( )}
    )}
    ); } function DefaultFixedArrayFieldTemplate(props) { return (
    {(props.uiSchema["ui:description"] || props.schema.description) && (
    {props.uiSchema["ui:description"] || props.schema.description}
    )}
    {props.items && props.items.map(DefaultArrayItem)}
    {props.canAdd && ( )}
    ); } function DefaultNormalArrayFieldTemplate(props) { return (
    {(props.uiSchema["ui:description"] || props.schema.description) && ( )}
    {props.items && props.items.map(p => DefaultArrayItem(p))}
    {props.canAdd && ( )}
    ); } class ArrayField extends Component { static defaultProps = { uiSchema: {}, formData: [], idSchema: {}, required: false, disabled: false, readonly: false, autofocus: false, }; get itemTitle() { const { schema } = this.props; return schema.items.title || schema.items.description || "Item"; } isItemRequired(itemSchema) { if (Array.isArray(itemSchema.type)) { // While we don't yet support composite/nullable jsonschema types, it's // future-proof to check for requirement against these. return !includes(itemSchema.type, "null"); } // All non-null array item types are inherently required by design return itemSchema.type !== "null"; } canAddItem(formItems) { const { schema, uiSchema } = this.props; let { addable } = getUiOptions(uiSchema); if (addable !== false) { // if ui:options.addable was not explicitly set to false, we can add // another item if we have not exceeded maxItems yet if (schema.maxItems !== undefined) { addable = formItems.length < schema.maxItems; } else { addable = true; } } return addable; } onAddClick = event => { event.preventDefault(); const { schema, formData, registry = getDefaultRegistry() } = this.props; const { definitions } = registry; let itemSchema = schema.items; if (isFixedItems(schema) && allowAdditionalItems(schema)) { itemSchema = schema.additionalItems; } this.props.onChange([ ...formData, getDefaultFormState(itemSchema, undefined, definitions), ]); }; onDropIndexClick = index => { return event => { if (event) { event.preventDefault(); } const { formData, onChange } = this.props; // refs #195: revalidate to ensure properly reindexing errors let newErrorSchema; if (this.props.errorSchema) { newErrorSchema = {}; const errorSchema = this.props.errorSchema; for (let i in errorSchema) { i = parseInt(i); if (i < index) { newErrorSchema[i] = errorSchema[i]; } else if (i > index) { newErrorSchema[i - 1] = errorSchema[i]; } } } onChange(formData.filter((_, i) => i !== index), newErrorSchema); }; }; onReorderClick = (index, newIndex) => { return event => { if (event) { event.preventDefault(); event.target.blur(); } const { formData, onChange } = this.props; let newErrorSchema; if (this.props.errorSchema) { newErrorSchema = {}; const errorSchema = this.props.errorSchema; for (let i in errorSchema) { if (i == index) { newErrorSchema[newIndex] = errorSchema[index]; } else if (i == newIndex) { newErrorSchema[index] = errorSchema[newIndex]; } else { newErrorSchema[i] = errorSchema[i]; } } } function reOrderArray() { // Copy item let newFormData = formData.slice(); // Moves item from index to newIndex newFormData.splice(index, 1); newFormData.splice(newIndex, 0, formData[index]); return newFormData; } onChange(reOrderArray(), newErrorSchema); }; }; onChangeForIndex = index => { return (value, errorSchema) => { const { formData, onChange } = this.props; const newFormData = formData.map((item, i) => { // We need to treat undefined items as nulls to have validation. // See https://github.com/tdegrunt/jsonschema/issues/206 const jsonValue = typeof value === "undefined" ? null : value; return index === i ? jsonValue : item; }); onChange( newFormData, errorSchema && this.props.errorSchema && { ...this.props.errorSchema, [index]: errorSchema, } ); }; }; onSelectChange = value => { this.props.onChange(value); }; render() { const { schema, uiSchema, idSchema, registry = getDefaultRegistry(), } = this.props; const { definitions } = registry; if (!schema.hasOwnProperty("items")) { return ( ); } if (isFixedItems(schema)) { return this.renderFixedArray(); } if (isFilesArray(schema, uiSchema, definitions)) { return this.renderFiles(); } if (isMultiSelect(schema, definitions)) { return this.renderMultiSelect(); } return this.renderNormalArray(); } renderNormalArray() { const { schema, uiSchema, formData, errorSchema, idSchema, name, required, disabled, readonly, autofocus, registry = getDefaultRegistry(), onBlur, onFocus, idPrefix, rawErrors, } = this.props; const title = schema.title === undefined ? name : schema.title; const { ArrayFieldTemplate, definitions, fields, formContext } = registry; const { TitleField, DescriptionField } = fields; const itemsSchema = retrieveSchema(schema.items, definitions); const arrayProps = { canAdd: this.canAddItem(formData), items: Array.isArray(formData) && formData.map((item, index) => { const itemSchema = retrieveSchema(schema.items, definitions, item); const itemErrorSchema = errorSchema ? errorSchema[index] : undefined; const itemIdPrefix = idSchema.$id + "_" + index; const itemIdSchema = toIdSchema( itemSchema, itemIdPrefix, definitions, item, idPrefix ); return this.renderArrayFieldItem({ index, canMoveUp: index > 0, canMoveDown: index < formData.length - 1, itemSchema: itemSchema, itemIdSchema, itemErrorSchema, itemData: item, itemUiSchema: uiSchema.items, autofocus: autofocus && index === 0, onBlur, onFocus, }); }), className: `field field-array field-array-of-${itemsSchema.type}`, DescriptionField, disabled, idSchema, uiSchema, onAddClick: this.onAddClick, readonly, required, schema, title, TitleField, formContext, formData, rawErrors, }; // Check if a custom render function was passed in const Component = ArrayFieldTemplate || DefaultNormalArrayFieldTemplate; return ; } renderMultiSelect() { const { schema, idSchema, uiSchema, formData, disabled, readonly, autofocus, onBlur, onFocus, registry = getDefaultRegistry(), rawErrors, } = this.props; const items = this.props.formData; const { widgets, definitions, formContext } = registry; const itemsSchema = retrieveSchema(schema.items, definitions, formData); const enumOptions = optionsList(itemsSchema); const { widget = "select", ...options } = { ...getUiOptions(uiSchema), enumOptions, }; const Widget = getWidget(schema, widget, widgets); if (!Widget) return null; return ( ); } renderFiles() { const { schema, uiSchema, idSchema, name, disabled, readonly, autofocus, onBlur, onFocus, registry = getDefaultRegistry(), rawErrors, } = this.props; const title = schema.title || name; const items = this.props.formData; const { widgets, formContext } = registry; const { widget = "files", ...options } = getUiOptions(uiSchema); const Widget = getWidget(schema, widget, widgets); if (!Widget) return null; return ( ); } renderFixedArray() { const { schema, uiSchema, formData, errorSchema, idPrefix, idSchema, name, required, disabled, readonly, autofocus, registry = getDefaultRegistry(), onBlur, onFocus, rawErrors, } = this.props; const title = schema.title || name; let items = this.props.formData; const { ArrayFieldTemplate, definitions, fields, formContext } = registry; const { TitleField } = fields; const itemSchemas = schema.items.map((item, index) => retrieveSchema(item, definitions, formData[index]) ); const additionalSchema = allowAdditionalItems(schema) ? retrieveSchema(schema.additionalItems, definitions, formData) : null; if (!items || items.length < itemSchemas.length) { // to make sure at least all fixed items are generated items = items || []; items = items.concat(new Array(itemSchemas.length - items.length)); } // These are the props passed into the render function const arrayProps = { canAdd: this.canAddItem(items) && additionalSchema, className: "field field-array field-array-fixed-items", disabled, idSchema, formData, items: items.map((item, index) => { const additional = index >= itemSchemas.length; const itemSchema = additional ? retrieveSchema(schema.additionalItems, definitions, item) : itemSchemas[index]; const itemIdPrefix = idSchema.$id + "_" + index; const itemIdSchema = toIdSchema( itemSchema, itemIdPrefix, definitions, item, idPrefix ); const itemUiSchema = additional ? uiSchema.additionalItems || {} : Array.isArray(uiSchema.items) ? uiSchema.items[index] : uiSchema.items || {}; const itemErrorSchema = errorSchema ? errorSchema[index] : undefined; return this.renderArrayFieldItem({ index, canRemove: additional, canMoveUp: index >= itemSchemas.length + 1, canMoveDown: additional && index < items.length - 1, itemSchema, itemData: item, itemUiSchema, itemIdSchema, itemErrorSchema, autofocus: autofocus && index === 0, onBlur, onFocus, }); }), onAddClick: this.onAddClick, readonly, required, schema, uiSchema, title, TitleField, formContext, rawErrors, }; // Check if a custom template template was passed in const Template = ArrayFieldTemplate || DefaultFixedArrayFieldTemplate; return