Commit 4c714966 by ethanlamzs

init 重置与最新的开源版本一致

1 parent aa33c632
Showing 276 changed files with 17095 additions and 0 deletions
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at afc163@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
MIT License
Copyright (c) 2018 Alipay.inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
English | [简体中文](./README.zh-CN.md)
# Ant Design Pro
[![](https://img.shields.io/travis/ant-design/ant-design-pro/master.svg?style=flat-square)](https://travis-ci.org/ant-design/ant-design-pro) [![Build status](https://ci.appveyor.com/api/projects/status/67fxu2by3ibvqtat/branch/master?svg=true)](https://ci.appveyor.com/project/afc163/ant-design-pro/branch/master) [![Gitter](https://badges.gitter.im/ant-design/ant-design-pro.svg)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
An out-of-box UI solution for enterprise applications as a React boilerplate.
![](https://gw.alipayobjects.com/zos/rmsportal/xEdBqwSzvoSapmnSnYjU.png)
- Preview: http://preview.pro.ant.design
- Home Page: http://pro.ant.design
- Documentation: http://pro.ant.design/docs/getting-started
- ChangeLog: http://pro.ant.design/docs/changelog
- FAQ: http://pro.ant.design/docs/faq
## Translation Recruitment :loudspeaker:
We need your help: https://github.com/ant-design/ant-design-pro/issues/120
## Features
- :gem: **Neat Design**: Follow [Ant Design specification](http://ant.design/)
- :triangular_ruler: **Common Templates**: Typical templates for enterprise applications
- :rocket: **State of The Art Development**: Newest development stack of React/dva/antd
- :iphone: **Responsive**: Designed for varies of screen size
- :art: **Themeing**: Customizable theme with simple config
- :globe_with_meridians: **International**: Built-in i18n solution
- :gear: **Best Practice**: Solid workflow make your code health
- :1234: **Mock development**: Easy to use mock development solution
- :white_check_mark: **UI Test**: Fly safely with unit test and e2e test
## Templates
```
- Dashboard
- Analytic
- Monitor
- Workspace
- Form
- Basic Form
- Step Form
- Advanced From
- List
- Standard Table
- Standard List
- Card List
- Search List (Project/Applications/Article)
- Profile
- Simple Profile
- Advanced Profile
- Result
- Success
- Failed
- Exception
- 403
- 404
- 500
- User
- Login
- Register
- Register Result
```
## Usage
```bash
$ git clone https://github.com/ant-design/ant-design-pro.git --depth=1
$ cd ant-design-pro
$ npm install
$ npm start # visit http://localhost:8000
```
Or you can use the command tool: [ant-design-pro-cli](https://github.com/ant-design/ant-design-pro-cli)
```bash
$ npm install ant-design-pro-cli -g
$ mkdir pro-demo && cd pro-demo
$ pro new
```
More instruction at [documentation](http://pro.ant.design/docs/getting-started).
## Compatibility
Modern browsers and IE11.
## Contributing
Any Contribution of following ways will be welcome:
- Use Ant Design Pro in your daily work.
- Submit [issue](http://github.com/ant-design/ant-design-pro/issues) to report bug or ask questions.
- Propose [pull request](http://github.com/ant-design/ant-design-pro/pulls) to improve our code.
[English](./README.md) | 简体中文
# Ant Design Pro
[![](https://img.shields.io/travis/ant-design/ant-design-pro.svg?style=flat-square)](https://travis-ci.org/ant-design/ant-design-pro) [![Build status](https://ci.appveyor.com/api/projects/status/67fxu2by3ibvqtat/branch/master?svg=true)](https://ci.appveyor.com/project/afc163/ant-design-pro/branch/master) [![Gitter](https://badges.gitter.im/ant-design/ant-design-pro.svg)](https://gitter.im/ant-design/ant-design-pro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
开箱即用的中台前端/设计解决方案。
![](https://gw.alipayobjects.com/zos/rmsportal/xEdBqwSzvoSapmnSnYjU.png)
- 预览:http://preview.pro.ant.design
- 首页:http://pro.ant.design/index-cn
- 使用文档:http://pro.ant.design/docs/getting-started-cn
- 更新日志: http://pro.ant.design/docs/changelog-cn
- 常见问题:http://pro.ant.design/docs/faq-cn
## 特性
- :gem: **优雅美观**:基于 Ant Design 体系精心设计
- :triangular_ruler: **常见设计模式**:提炼自中后台应用的典型页面和场景
- :rocket: **最新技术栈**:使用 React/dva/antd 等前端前沿技术开发
- :iphone: **响应式**:针对不同屏幕大小设计
- :art: **主题**:可配置的主题满足多样化的品牌诉求
- :globe_with_meridians: **国际化**:内建业界通用的国际化方案
- :gear: **最佳实践**:良好的工程实践助您持续产出高质量代码
- :1234: **Mock 数据**:实用的本地数据调试方案
- :white_check_mark: **UI 测试**:自动化测试保障前端产品质量
## 模板
```
- Dashboard
- 分析页
- 监控页
- 工作台
- 表单页
- 基础表单页
- 分步表单页
- 高级表单页
- 列表页
- 查询表格
- 标准列表
- 卡片列表
- 搜索列表(项目/应用/文章)
- 详情页
- 基础详情页
- 高级详情页
- 结果
- 成功页
- 失败页
- 异常
- 403 无权限
- 404 找不到
- 500 服务器出错
- 帐户
- 登录
- 注册
- 注册成功
```
## 使用
```bash
$ git clone https://github.com/ant-design/ant-design-pro.git --depth=1
$ cd ant-design-pro
$ npm install
$ npm start # 访问 http://localhost:8000
```
也可以使用集成化的 [ant-design-pro-cli](https://github.com/ant-design/ant-design-pro-cli) 工具。
```bash
$ npm install ant-design-pro-cli -g
$ mkdir pro-demo && cd pro-demo
$ pro new
```
更多信息请参考 [使用文档](http://pro.ant.design/docs/getting-started)
## 兼容性
现代浏览器及 IE11。
## 参与贡献
我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :smiley::
- 在你的公司或个人项目中使用 Ant Design Pro。
- 通过 [Issue](http://github.com/ant-design/ant-design-pro/issues) 报告 bug 或进行咨询。
- 提交 [Pull Request](http://github.com/ant-design/ant-design-pro/pulls) 改进 Pro 的代码。
# Test against the latest version of this Node.js version
environment:
nodejs_version: "8"
# Install scripts. (runs after repo cloning)
install:
# Get the latest stable version of Node.js or io.js
- ps: Install-Product node $env:nodejs_version
# install modules
- npm install
# Output useful info for debugging.
- node --version
- npm --version
# Post-install test scripts.
test_script:
- npm run lint
- npm run test:all
- npm run build
# Don't actually build.
build: off
File mode changed
import { getUrlParams } from './utils';
const titles = [
'Alipay',
'Angular',
'Ant Design',
'Ant Design Pro',
'Bootstrap',
'React',
'Vue',
'Webpack',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const avatars2 = [
'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
'https://gw.alipayobjects.com/zos/rmsportal/jZUIxmJycoymBprLOUbT.png',
'https://gw.alipayobjects.com/zos/rmsportal/psOgztMplJMGpVEqfcgF.png',
'https://gw.alipayobjects.com/zos/rmsportal/ZpBqSxLxVEXfcUNoPKrz.png',
'https://gw.alipayobjects.com/zos/rmsportal/laiEnJdGHVOhJrUShBaJ.png',
'https://gw.alipayobjects.com/zos/rmsportal/UrQsqscbKEpNuJcvBZBu.png',
];
const covers = [
'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
'https://gw.alipayobjects.com/zos/rmsportal/uVZonEtjWwmUZPBQfycs.png',
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
];
const desc = [
'那是一种内在的东西, 他们到达不了,也无法触及的',
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
'生命就像一盒巧克力,结果往往出人意料',
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
'那时候我只会想自己想要什么,从不想自己拥有什么',
];
const user = [
'付小小',
'曲丽丽',
'林东东',
'周星星',
'吴加好',
'朱偏右',
'鱼酱',
'乐哥',
'谭小仪',
'仲尼',
];
export function fakeList(count) {
const list = [];
for (let i = 0; i < count; i += 1) {
list.push({
id: `fake-list-${i}`,
owner: user[i % 10],
title: titles[i % 8],
avatar: avatars[i % 8],
cover: parseInt(i / 4, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
status: ['active', 'exception', 'normal'][i % 3],
percent: Math.ceil(Math.random() * 50) + 50,
logo: avatars[i % 8],
href: 'https://ant.design',
updatedAt: new Date(new Date().getTime() - (1000 * 60 * 60 * 2 * i)),
createdAt: new Date(new Date().getTime() - (1000 * 60 * 60 * 2 * i)),
subDescription: desc[i % 5],
description: '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
activeUser: Math.ceil(Math.random() * 100000) + 100000,
newUser: Math.ceil(Math.random() * 1000) + 1000,
star: Math.ceil(Math.random() * 100) + 100,
like: Math.ceil(Math.random() * 100) + 100,
message: Math.ceil(Math.random() * 10) + 10,
content: '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
members: [
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
name: '曲丽丽',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
name: '王昭君',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
name: '董娜娜',
},
],
});
}
return list;
}
export function getFakeList(req, res, u) {
let url = u;
if (!url || Object.prototype.toString.call(url) !== '[object String]') {
url = req.url; // eslint-disable-line
}
const params = getUrlParams(url);
const count = (params.count * 1) || 20;
const result = fakeList(count);
if (res && res.json) {
res.json(result);
} else {
return result;
}
}
export const getNotice = [
{
id: 'xxx1',
title: titles[0],
logo: avatars[0],
description: '那是一种内在的东西,他们到达不了,也无法触及的',
updatedAt: new Date(),
member: '科学搬砖组',
href: '',
memberLink: '',
},
{
id: 'xxx2',
title: titles[1],
logo: avatars[1],
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
updatedAt: new Date('2017-07-24'),
member: '全组都是吴彦祖',
href: '',
memberLink: '',
},
{
id: 'xxx3',
title: titles[2],
logo: avatars[2],
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
updatedAt: new Date(),
member: '中二少女团',
href: '',
memberLink: '',
},
{
id: 'xxx4',
title: titles[3],
logo: avatars[3],
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
updatedAt: new Date('2017-07-23'),
member: '程序员日常',
href: '',
memberLink: '',
},
{
id: 'xxx5',
title: titles[4],
logo: avatars[4],
description: '凛冬将至',
updatedAt: new Date('2017-07-23'),
member: '高逼格设计天团',
href: '',
memberLink: '',
},
{
id: 'xxx6',
title: titles[5],
logo: avatars[5],
description: '生命就像一盒巧克力,结果往往出人意料',
updatedAt: new Date('2017-07-23'),
member: '骗你来学计算机',
href: '',
memberLink: '',
},
];
export const getActivities = [
{
id: 'trend-1',
updatedAt: new Date(),
user: {
name: '曲丽丽',
avatar: avatars2[0],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-2',
updatedAt: new Date(),
user: {
name: '付小小',
avatar: avatars2[1],
},
group: {
name: '高逼格设计天团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-3',
updatedAt: new Date(),
user: {
name: '林东东',
avatar: avatars2[2],
},
group: {
name: '中二少女团',
link: 'http://github.com/',
},
project: {
name: '六月迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
{
id: 'trend-4',
updatedAt: new Date(),
user: {
name: '周星星',
avatar: avatars2[4],
},
project: {
name: '5 月日常迭代',
link: 'http://github.com/',
},
template: '将 @{project} 更新至已发布状态',
},
{
id: 'trend-5',
updatedAt: new Date(),
user: {
name: '朱偏右',
avatar: avatars2[3],
},
project: {
name: '工程效能',
link: 'http://github.com/',
},
comment: {
name: '留言',
link: 'http://github.com/',
},
template: '在 @{project} 发布了 @{comment}',
},
{
id: 'trend-6',
updatedAt: new Date(),
user: {
name: '乐哥',
avatar: avatars2[5],
},
group: {
name: '程序员日常',
link: 'http://github.com/',
},
project: {
name: '品牌迭代',
link: 'http://github.com/',
},
template: '在 @{group} 新建项目 @{project}',
},
];
export default {
getNotice,
getActivities,
getFakeList,
};
import moment from 'moment';
// mock data
const visitData = [];
const beginDay = new Date().getTime();
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
y: fakeY[i],
});
}
const visitData2 = [];
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
for (let i = 0; i < fakeY2.length; i += 1) {
visitData2.push({
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
y: fakeY2[i],
});
}
const salesData = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}月`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
const searchData = [];
for (let i = 0; i < 50; i += 1) {
searchData.push({
index: i + 1,
keyword: `搜索关键词-${i}`,
count: Math.floor(Math.random() * 1000),
range: Math.floor(Math.random() * 100),
status: Math.floor((Math.random() * 10) % 2),
});
}
const salesTypeData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
const salesTypeDataOnline = [
{
x: '家用电器',
y: 244,
},
{
x: '食用酒水',
y: 321,
},
{
x: '个护健康',
y: 311,
},
{
x: '服饰箱包',
y: 41,
},
{
x: '母婴产品',
y: 121,
},
{
x: '其他',
y: 111,
},
];
const salesTypeDataOffline = [
{
x: '家用电器',
y: 99,
},
{
x: '个护健康',
y: 188,
},
{
x: '服饰箱包',
y: 344,
},
{
x: '母婴产品',
y: 255,
},
{
x: '其他',
y: 65,
},
];
const offlineData = [];
for (let i = 0; i < 10; i += 1) {
offlineData.push({
name: `门店${i}`,
cvr: Math.ceil(Math.random() * 9) / 10,
});
}
const offlineChartData = [];
for (let i = 0; i < 20; i += 1) {
offlineChartData.push({
x: (new Date().getTime()) + (1000 * 60 * 30 * i),
y1: Math.floor(Math.random() * 100) + 10,
y2: Math.floor(Math.random() * 100) + 10,
});
}
const radarOriginData = [
{
name: '个人',
ref: 10,
koubei: 8,
output: 4,
contribute: 5,
hot: 7,
},
{
name: '团队',
ref: 3,
koubei: 9,
output: 6,
contribute: 3,
hot: 1,
},
{
name: '部门',
ref: 4,
koubei: 1,
output: 6,
contribute: 5,
hot: 7,
},
];
//
const radarData = [];
const radarTitleMap = {
ref: '引用',
koubei: '口碑',
output: '产量',
contribute: '贡献',
hot: '热度',
};
radarOriginData.forEach((item) => {
Object.keys(item).forEach((key) => {
if (key !== 'name') {
radarData.push({
name: item.name,
label: radarTitleMap[key],
value: item[key],
});
}
});
});
export const getFakeChartData = {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
};
export default {
getFakeChartData,
};
export const getNotices = (req, res) => {
res.json([
{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: '通知',
},
{
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: '通知',
},
{
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: '通知',
},
{
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: '通知',
},
{
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: '通知',
},
{
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: '消息',
},
{
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: '消息',
},
{
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: '消息',
},
{
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
type: '待办',
},
{
id: '000000010',
title: '第三方紧急代码变更',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: '待办',
},
{
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
type: '待办',
},
{
id: '000000012',
title: 'ABCD 版本发布',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: '待办',
},
]);
};
export default {
getNotices,
};
const basicGoods = [
{
id: '1234561',
name: '矿泉水 550ml',
barcode: '12421432143214321',
price: '2.00',
num: '1',
amount: '2.00',
},
{
id: '1234562',
name: '凉茶 300ml',
barcode: '12421432143214322',
price: '3.00',
num: '2',
amount: '6.00',
},
{
id: '1234563',
name: '好吃的薯片',
barcode: '12421432143214323',
price: '7.00',
num: '4',
amount: '28.00',
},
{
id: '1234564',
name: '特别好吃的蛋卷',
barcode: '12421432143214324',
price: '8.50',
num: '3',
amount: '25.50',
},
];
const basicProgress = [
{
key: '1',
time: '2017-10-01 14:10',
rate: '联系客户',
status: 'processing',
operator: '取货员 ID1234',
cost: '5mins',
},
{
key: '2',
time: '2017-10-01 14:05',
rate: '取货员出发',
status: 'success',
operator: '取货员 ID1234',
cost: '1h',
},
{
key: '3',
time: '2017-10-01 13:05',
rate: '取货员接单',
status: 'success',
operator: '取货员 ID1234',
cost: '5mins',
},
{
key: '4',
time: '2017-10-01 13:00',
rate: '申请审批通过',
status: 'success',
operator: '系统',
cost: '1h',
},
{
key: '5',
time: '2017-10-01 12:00',
rate: '发起退货申请',
status: 'success',
operator: '用户',
cost: '5mins',
},
];
const advancedOperation1 = [
{
key: 'op1',
type: '订购关系生效',
name: '曲丽丽',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
{
key: 'op2',
type: '财务复审',
name: '付小小',
status: 'reject',
updatedAt: '2017-10-03 19:23:12',
memo: '不通过原因',
},
{
key: 'op3',
type: '部门初审',
name: '周毛毛',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
{
key: 'op4',
type: '提交订单',
name: '林东东',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '很棒',
},
{
key: 'op5',
type: '创建订单',
name: '汗牙牙',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
const advancedOperation2 = [
{
key: 'op1',
type: '订购关系生效',
name: '曲丽丽',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
const advancedOperation3 = [
{
key: 'op1',
type: '创建订单',
name: '汗牙牙',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
export const getProfileBasicData = {
basicGoods,
basicProgress,
};
export const getProfileAdvancedData = {
advancedOperation1,
advancedOperation2,
advancedOperation3,
};
export default {
getProfileBasicData,
getProfileAdvancedData,
};
import { getUrlParams } from './utils';
// mock tableListDataSource
let tableListDataSource = [];
for (let i = 0; i < 46; i += 1) {
tableListDataSource.push({
key: i,
disabled: ((i % 6) === 0),
href: 'https://ant.design',
avatar: ['https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png'][i % 2],
no: `TradeCode ${i}`,
title: `一个任务名称 ${i}`,
owner: '曲丽丽',
description: '这是一段描述',
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 4,
updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
progress: Math.ceil(Math.random() * 100),
});
}
export function getRule(req, res, u) {
let url = u;
if (!url || Object.prototype.toString.call(url) !== '[object String]') {
url = req.url; // eslint-disable-line
}
const params = getUrlParams(url);
let dataSource = [...tableListDataSource];
if (params.sorter) {
const s = params.sorter.split('_');
dataSource = dataSource.sort((prev, next) => {
if (s[1] === 'descend') {
return next[s[0]] - prev[s[0]];
}
return prev[s[0]] - next[s[0]];
});
}
if (params.status) {
const status = params.status.split(',');
let filterDataSource = [];
status.forEach((s) => {
filterDataSource = filterDataSource.concat(
[...dataSource].filter(data => parseInt(data.status, 10) === parseInt(s[0], 10))
);
});
dataSource = filterDataSource;
}
if (params.no) {
dataSource = dataSource.filter(data => data.no.indexOf(params.no) > -1);
}
let pageSize = 10;
if (params.pageSize) {
pageSize = params.pageSize * 1;
}
const result = {
list: dataSource,
pagination: {
total: dataSource.length,
pageSize,
current: parseInt(params.currentPage, 10) || 1,
},
};
if (res && res.json) {
res.json(result);
} else {
return result;
}
}
export function postRule(req, res, u, b) {
let url = u;
if (!url || Object.prototype.toString.call(url) !== '[object String]') {
url = req.url; // eslint-disable-line
}
const body = (b && b.body) || req.body;
const { method, no, description } = body;
switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
tableListDataSource = tableListDataSource.filter(item => no.indexOf(item.no) === -1);
break;
case 'post':
const i = Math.ceil(Math.random() * 10000);
tableListDataSource.unshift({
key: i,
href: 'https://ant.design',
avatar: ['https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png'][i % 2],
no: `TradeCode ${i}`,
title: `一个任务名称 ${i}`,
owner: '曲丽丽',
description,
callNo: Math.floor(Math.random() * 1000),
status: Math.floor(Math.random() * 10) % 2,
updatedAt: new Date(),
createdAt: new Date(),
progress: Math.ceil(Math.random() * 100),
});
break;
default:
break;
}
const result = {
list: tableListDataSource,
pagination: {
total: tableListDataSource.length,
},
};
if (res && res.json) {
res.json(result);
} else {
return result;
}
}
export default {
getRule,
postRule,
};
export const imgMap = {
user: 'https://gw.alipayobjects.com/zos/rmsportal/UjusLxePxWGkttaqqmUI.png',
a: 'https://gw.alipayobjects.com/zos/rmsportal/ZrkcSjizAKNWwJTwcadT.png',
b: 'https://gw.alipayobjects.com/zos/rmsportal/KYlwHMeomKQbhJDRUVvt.png',
c: 'https://gw.alipayobjects.com/zos/rmsportal/gabvleTstEvzkbQRfjxu.png',
d: 'https://gw.alipayobjects.com/zos/rmsportal/jvpNzacxUYLlNsHTtrAD.png',
};
// refers: https://www.sitepoint.com/get-url-parameters-with-javascript/
export function getUrlParams(url) {
const d = decodeURIComponent;
let queryString = url ? url.split('?')[1] : window.location.search.slice(1);
const obj = {};
if (queryString) {
queryString = queryString.split('#')[0]; // eslint-disable-line
const arr = queryString.split('&');
for (let i = 0; i < arr.length; i += 1) {
const a = arr[i].split('=');
let paramNum;
const paramName = a[0].replace(/\[\d*\]/, (v) => {
paramNum = v.slice(1, -1);
return '';
});
const paramValue = typeof (a[1]) === 'undefined' ? true : a[1];
if (obj[paramName]) {
if (typeof obj[paramName] === 'string') {
obj[paramName] = d([obj[paramName]]);
}
if (typeof paramNum === 'undefined') {
obj[paramName].push(d(paramValue));
} else {
obj[paramName][paramNum] = d(paramValue);
}
} else {
obj[paramName] = d(paramValue);
}
}
}
return obj;
}
export default {
getUrlParams,
imgMap,
};
{
"name": "ant-design-pro",
"version": "1.0.1",
"description": "An out-of-box UI solution for enterprise applications",
"private": true,
"scripts": {
"precommit": "npm run lint-staged",
"start": "cross-env DISABLE_ESLINT=true roadhog dev",
"start:no-proxy": "cross-env NO_PROXY=true DISABLE_ESLINT=true roadhog dev",
"build": "cross-env DISABLE_ESLINT=true roadhog build",
"site": "roadhog-api-doc static && gh-pages -d dist",
"analyze": "ANALYZE=true roadhog build",
"lint:style": "stylelint \"src/**/*.less\" --syntax less",
"lint": "eslint --ext .js src mock tests && npm run lint:style",
"lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js",
"test": "roadhog test",
"test:component": "roadhog test ./src/components",
"test:all": "node ./tests/run-tests.js"
},
"dependencies": {
"@antv/data-set": "^0.8.0",
"@babel/polyfill": "^7.0.0-beta.36",
"antd": "^3.1.0",
"babel-runtime": "^6.9.2",
"bizcharts": "^3.1.0-beta.4",
"bizcharts-plugin-slider": "^2.0.1",
"classnames": "^2.2.5",
"dva": "^2.1.0",
"dva-loading": "^1.0.4",
"enquire-js": "^0.1.1",
"fastclick": "^1.0.6",
"lodash": "^4.17.4",
"lodash-decorators": "^4.4.1",
"moment": "^2.19.1",
"numeral": "^2.0.6",
"omit.js": "^1.0.0",
"prop-types": "^15.5.10",
"qs": "^6.5.0",
"rc-drawer-menu": "^0.5.0",
"react": "^16.2.0",
"react-container-query": "^0.9.1",
"react-document-title": "^2.0.3",
"react-dom": "^16.2.0",
"react-fittext": "^1.0.0",
"url-polyfill": "^1.0.10"
},
"devDependencies": {
"babel-eslint": "^8.1.2",
"babel-plugin-dva-hmr": "^0.4.1",
"babel-plugin-import": "^1.6.3",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"cross-env": "^5.1.1",
"cross-port-killer": "^1.0.1",
"enzyme": "^3.1.0",
"eslint": "^4.14.0",
"eslint-config-airbnb": "^16.0.0",
"eslint-plugin-babel": "^4.0.0",
"eslint-plugin-compat": "^2.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-markdown": "^1.0.0-beta.6",
"eslint-plugin-react": "^7.0.1",
"gh-pages": "^1.0.0",
"husky": "^0.14.3",
"lint-staged": "^6.0.0",
"mockjs": "^1.0.1-beta3",
"pro-download": "^1.0.1",
"redbox-react": "^1.5.0",
"regenerator-runtime": "^0.11.1",
"roadhog": "^2.1.0",
"roadhog-api-doc": "^0.3.4",
"rollbar": "^2.3.4",
"stylelint": "^8.4.0",
"stylelint-config-standard": "^18.0.0"
},
"optionalDependencies": {
"nightmare": "^2.10.0"
},
"lint-staged": {
"**/*.{js,jsx}": "lint-staged:js",
"**/*.less": "stylelint --syntax less"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
]
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 47.1 (45422) - http://www.bohemiancoding.com/sketch -->
<title>Group 28 Copy 5</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="62.1023273%" y1="0%" x2="108.19718%" y2="37.8635764%" id="linearGradient-1">
<stop stop-color="#4285EB" offset="0%"></stop>
<stop stop-color="#2EC7FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="69.644116%" y1="0%" x2="54.0428975%" y2="108.456714%" id="linearGradient-2">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#148EFF" offset="37.8600687%"></stop>
<stop stop-color="#0A60FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="69.6908165%" y1="-12.9743587%" x2="16.7228981%" y2="117.391248%" id="linearGradient-3">
<stop stop-color="#FA816E" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="41.472606%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
<linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-4">
<stop stop-color="#FA8E7D" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="51.2635191%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="logo" transform="translate(-20.000000, -20.000000)">
<g id="Group-28-Copy-5" transform="translate(20.000000, 20.000000)">
<g id="Group-27-Copy-3">
<g id="Group-25" fill-rule="nonzero">
<g id="2">
<path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C99.2571609,26.9692191 101.032305,26.9692191 102.20193,28.1378823 L129.985225,55.8983314 C134.193707,60.1033528 141.017005,60.1033528 145.225487,55.8983314 C149.433969,51.69331 149.433969,44.8756232 145.225487,40.6706018 L108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-1)"></path>
<path d="M91.5880863,4.17652823 L4.17996544,91.5127728 C-0.519240605,96.2081146 -0.519240605,103.791885 4.17996544,108.487227 L91.5880863,195.823472 C96.2872923,200.518814 103.877304,200.518814 108.57651,195.823472 L145.225487,159.204632 C149.433969,154.999611 149.433969,148.181924 145.225487,143.976903 C141.017005,139.771881 134.193707,139.771881 129.985225,143.976903 L102.20193,171.737352 C101.032305,172.906015 99.2571609,172.906015 98.0875359,171.737352 L28.285908,101.993122 C27.1162831,100.824459 27.1162831,99.050775 28.285908,97.8821118 L98.0875359,28.1378823 C100.999864,25.6271836 105.751642,20.541824 112.729652,19.3524487 C117.915585,18.4685261 123.585219,20.4140239 129.738554,25.1889424 C125.624663,21.0784292 118.571995,14.0340304 108.58055,4.05574592 C103.862049,-0.537986846 96.2692618,-0.500797906 91.5880863,4.17652823 Z" id="Shape" fill="url(#linearGradient-2)"></path>
</g>
<path d="M153.685633,135.854579 C157.894115,140.0596 164.717412,140.0596 168.925894,135.854579 L195.959977,108.842726 C200.659183,104.147384 200.659183,96.5636133 195.960527,91.8688194 L168.690777,64.7181159 C164.472332,60.5180858 157.646868,60.5241425 153.435895,64.7316526 C149.227413,68.936674 149.227413,75.7543607 153.435895,79.9593821 L171.854035,98.3623765 C173.02366,99.5310396 173.02366,101.304724 171.854035,102.473387 L153.685633,120.626849 C149.47715,124.83187 149.47715,131.649557 153.685633,135.854579 Z" id="Shape" fill="url(#linearGradient-3)"></path>
</g>
<ellipse id="Combined-Shape" fill="url(#linearGradient-4)" cx="100.519339" cy="100.436681" rx="23.6001926" ry="23.580786"></ellipse>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file \ No newline at end of file
import { isUrl } from '../utils/utils';
const menuData = [{
name: 'dashboard',
icon: 'dashboard',
path: 'dashboard',
children: [{
name: '分析页',
path: 'analysis',
}, {
name: '监控页',
path: 'monitor',
}, {
name: '工作台',
path: 'workplace',
// hideInMenu: true,
}],
}, {
name: '表单页',
icon: 'form',
path: 'form',
children: [{
name: '基础表单',
path: 'basic-form',
}, {
name: '分步表单',
path: 'step-form',
}, {
name: '高级表单',
authority: 'admin',
path: 'advanced-form',
}],
}, {
name: '列表页',
icon: 'table',
path: 'list',
children: [{
name: '查询表格',
path: 'table-list',
}, {
name: '标准列表',
path: 'basic-list',
}, {
name: '卡片列表',
path: 'card-list',
}, {
name: '搜索列表',
path: 'search',
children: [{
name: '搜索列表(文章)',
path: 'articles',
}, {
name: '搜索列表(项目)',
path: 'projects',
}, {
name: '搜索列表(应用)',
path: 'applications',
}],
}],
}, {
name: '详情页',
icon: 'profile',
path: 'profile',
children: [{
name: '基础详情页',
path: 'basic',
}, {
name: '高级详情页',
path: 'advanced',
authority: 'admin',
}],
}, {
name: '结果页',
icon: 'check-circle-o',
path: 'result',
children: [{
name: '成功',
path: 'success',
}, {
name: '失败',
path: 'fail',
}],
}, {
name: '异常页',
icon: 'warning',
path: 'exception',
children: [{
name: '403',
path: '403',
}, {
name: '404',
path: '404',
}, {
name: '500',
path: '500',
}, {
name: '触发异常',
path: 'trigger',
hideInMenu: true,
}],
}, {
name: '账户',
icon: 'user',
path: 'user',
authority: 'guest',
children: [{
name: '登录',
path: 'login',
}, {
name: '注册',
path: 'register',
}, {
name: '注册结果',
path: 'register-result',
}],
}, {
name: '使用文档',
icon: 'book',
path: 'http://pro.ant.design/docs/getting-started',
target: '_blank',
}];
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);
import { createElement } from 'react';
import dynamic from 'dva/dynamic';
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')),
},
'/dashboard/analysis': {
component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')),
},
'/dashboard/monitor': {
component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/Monitor')),
},
'/dashboard/workplace': {
component: dynamicWrapper(app, ['project', 'activities', 'chart'], () => import('../routes/Dashboard/Workplace')),
// hideInBreadcrumb: true,
// name: '工作台',
// authority: 'admin',
},
'/form/basic-form': {
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/BasicForm')),
},
'/form/step-form': {
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm')),
},
'/form/step-form/info': {
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step1')),
},
'/form/step-form/confirm': {
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step2')),
},
'/form/step-form/result': {
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step3')),
},
'/form/advanced-form': {
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/AdvancedForm')),
},
'/list/table-list': {
component: dynamicWrapper(app, ['rule'], () => import('../routes/List/TableList')),
},
'/list/basic-list': {
component: dynamicWrapper(app, ['list'], () => import('../routes/List/BasicList')),
},
'/list/card-list': {
component: dynamicWrapper(app, ['list'], () => import('../routes/List/CardList')),
},
'/list/search': {
component: dynamicWrapper(app, ['list'], () => import('../routes/List/List')),
},
'/list/search/projects': {
component: dynamicWrapper(app, ['list'], () => import('../routes/List/Projects')),
},
'/list/search/applications': {
component: dynamicWrapper(app, ['list'], () => import('../routes/List/Applications')),
},
'/list/search/articles': {
component: dynamicWrapper(app, ['list'], () => import('../routes/List/Articles')),
},
'/profile/basic': {
component: dynamicWrapper(app, ['profile'], () => import('../routes/Profile/BasicProfile')),
},
'/profile/advanced': {
component: dynamicWrapper(app, ['profile'], () => import('../routes/Profile/AdvancedProfile')),
},
'/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')),
},
// '/user/:id': {
// component: dynamicWrapper(app, [], () => import('../routes/User/SomeComponent')),
// },
};
// Get name from ./menu.js or just set it in the router data.
const menuData = getFlatMenuData(getMenuData());
const routerData = {};
Object.keys(routerConfig).forEach((item) => {
const menuItem = menuData[item.replace(/^\//, '')] || {};
routerData[item] = {
...routerConfig[item],
name: routerConfig[item].name || menuItem.name,
authority: routerConfig[item].authority || menuItem.authority,
};
});
return routerData;
};
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 (
<div className={styles.activeChart}>
<NumberInfo subTitle="目标评估" total="有望达到预期" />
<div style={{ marginTop: 32 }}>
<MiniArea
animate={false}
line
borderWidth={2}
height={84}
scale={{
y: {
tickCount: 3,
},
}}
yAxis={{
tickLine: false,
label: false,
title: false,
line: false,
}}
data={activeData}
/>
</div>
{activeData && (
<div className={styles.activeChartGrid}>
<p>{[...activeData].sort()[activeData.length - 1].y + 200} 亿元</p>
<p>{[...activeData].sort()[Math.floor(activeData.length / 2)].y} 亿元</p>
</div>
)}
{activeData && (
<div className={styles.activeChartLegend}>
<span>00:00</span>
<span>{activeData[Math.floor(activeData.length / 2)].x}</span>
<span>{activeData[activeData.length - 1].x}</span>
</div>
)}
</div>
);
}
}
.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;
}
}
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;
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 (
<Authorized
authority={authority}
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
>
<Route
{...rest}
render={props => (Component ? <Component {...props} /> : render(props))}
/>
</Authorized>
);
}
}
export default AuthorizedRoute;
import React from 'react';
import 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 () => (
<PromiseRender ok={target} error={Exception} promise={authority} />
);
}
// Function 处理
if (typeof authority === 'function') {
try {
const bool = authority();
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;
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');
});
});
import React from 'react';
import { Spin } from 'antd';
export default class PromiseRender extends React.PureComponent {
state = {
component: false,
};
async componentDidMount() {
this.props.promise
.then(() => {
this.setState({
component: this.props.ok,
});
})
.catch(() => {
this.setState({
component: this.props.error,
});
});
}
render() {
const C = this.state.component;
return C ? (
<C {...this.props} />
) : (
<div
style={{
width: '100%',
height: '100%',
margin: 'auto',
paddingTop: 50,
textAlign: 'center',
}}
>
<Spin size="large" />
</div>
);
}
}
import React from 'react';
import Exception from '../Exception/index';
import CheckPermissions from './CheckPermissions';
/**
* 默认不能访问任何页面
* default is "NULL"
*/
const Exception403 = () => (
<Exception type="403" style={{ minHeight: 500, height: '80%' }} />
);
/**
* 用于判断是否拥有权限访问此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) {
return CheckPermissions(
authority,
targer,
classError || Exception403
);
};
};
export default authorize;
---
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 = <Alert message="No permission." type="error" showIcon />;
ReactDOM.render(
<Authorized authority={['user', 'admin']} noMatch={noMatch}>
<Alert message="Use Array as a parameter passed!" type="success" showIcon />
</Authorized>,
mountNode,
);
```
---
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 = <Alert message="No permission." type="error" showIcon />;
const havePermission = () => {
return false;
};
ReactDOM.render(
<Authorized authority={havePermission} noMatch={noMatch}>
<Alert
message="Use Function as a parameter passed!"
type="success"
showIcon
/>
</Authorized>,
mountNode,
);
```
---
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 = <Alert message="No permission." type="error" showIcon />;
ReactDOM.render(
<div>
<Authorized authority="admin" noMatch={noMatch}>
<Alert message="user Passed!" type="success" showIcon />
</Authorized>
</div>,
mountNode,
);
```
---
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() {
<Alert message="user Passed!" type="success" showIcon />;
}
}
ReactDOM.render(
<div>
<TestSecuredString />
</div>,
mountNode,
);
```
import * as React from 'react';
import { RouteProps } from 'react-router';
type authorityFN = () => string;
type authority = string | Array<string> | authorityFN | Promise<any>;
export type IReactComponent<P = any> =
| React.StatelessComponent<P>
| React.ComponentClass<P>
| React.ClassicComponentClass<P>;
interface Secured {
(authority: authority, error?: React.ReactNode): <T extends IReactComponent>(
target: T,
) => T;
}
export interface AuthorizedRouteProps extends RouteProps {
authority: authority;
}
export class AuthorizedRoute extends React.Component<
AuthorizedRouteProps,
any
> {}
interface check {
<T extends IReactComponent, S extends IReactComponent>(
authority: authority,
target: T,
Exception: S,
): T | S;
}
interface AuthorizedProps {
authority: authority;
noMatch?: React.ReactNode;
}
export class Authorized extends React.Component<AuthorizedProps, any> {
static Secured: Secured;
static AuthorizedRoute: typeof AuthorizedRoute;
static check: check;
}
declare function renderAuthorize(currentAuthority: string): typeof Authorized;
export default renderAuthorize;
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;
---
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 | () => boolean` | - |
| noMatch | 权限异常渲染元素,权限判断不通过时展示 | ReactNode | - |
### Authorized.AuthorizedRoute
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | array | Promise | () => boolean` | - |
| redirectPath | 权限异常时重定向的页面路由 | string | - |
其余参数与 `Route` 相同。
### Authorized.Secured
注解方式,`@Authorized.Secured(authority, error)`
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | Promise | () => boolean` | - |
| error | 权限异常时渲染元素 | ReactNode | <Exception type="403" /> |
### Authorized.check
函数形式的 Authorized,用于某些不能被 HOC 包裹的组件。 `Authorized.check(authority, target, Exception)`
注意:传入一个 Promise 时,无论正确还是错误返回的都是一个 ReactClass。
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | Promise | () => boolean` | - |
| target | 权限判断通过时渲染的元素 | `string | array | Promise | () => boolean` | - |
| Exception | 权限异常时渲染元素 | ReactNode | - |
---
order: 0
title:
zh-CN: 基础样例
en-US: Basic Usage
---
Simplest of usage.
````jsx
import AvatarList from 'ant-design-pro/lib/AvatarList';
ReactDOM.render(
<AvatarList size="mini">
<AvatarList.Item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
<AvatarList.Item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
<AvatarList.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
</AvatarList>
, mountNode);
````
import * as React from "react";
export interface AvatarItemProps {
tips: React.ReactNode;
src: string;
style?: React.CSSProperties;
}
export interface AvatarListProps {
size?: "large" | "small" | "mini" | "default";
style?: React.CSSProperties;
children:
| React.ReactElement<AvatarItem>
| Array<React.ReactElement<AvatarItem>>;
}
declare class AvatarItem extends React.Component<AvatarItemProps, any> {
constructor(props: AvatarItemProps);
}
export default class AvatarList extends React.Component<AvatarListProps, any> {
constructor(props: AvatarListProps);
static Item: typeof AvatarItem;
}
---
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 | - |
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 (
<div {...other} className={styles.avatarList}>
<ul> {childrenWithProps} </ul>
</div>
);
};
const Item = ({ src, size, tips, onClick = (() => {}) }) => {
const cls = classNames(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',
});
return (
<li className={cls} onClick={onClick} >
{
tips ? (
<Tooltip title={tips}>
<Avatar src={src} size={size} style={{ cursor: 'pointer' }} />
</Tooltip>
) : <Avatar src={src} size={size} />
}
</li>
);
};
AvatarList.Item = Item;
export default AvatarList;
@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;
}
}
}
---
title: AvatarList
subtitle: 用户头像列表
order: 1
cols: 1
---
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
## API
### AvatarList
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| size | 头像大小 | `large``small``mini`, `default` | `default` |
### AvatarList.Item
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| tips | 头像展示文案 | ReactNode\/string | - |
| src | 头像图片连接 | string | - |
import * as React from "react";
export interface BarProps {
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<BarProps, any> {}
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 (
<div className={styles.chart} style={{ height }} ref={this.handleRoot}>
<div ref={this.handleRef}>
{title && <h4 style={{ marginBottom: 20 }}>{title}</h4>}
<Chart
scale={scale}
height={title ? height - 41 : height}
forceFit={forceFit}
data={data}
padding={padding || 'auto'}
>
<Axis
name="x"
title={false}
label={autoHideXLabels ? false : {}}
tickLine={autoHideXLabels ? false : {}}
/>
<Axis name="y" min={0} />
<Tooltip showTitle={false} crosshairs={false} />
<Geom type="interval" position="x*y" color={color} tooltip={tooltip} />
</Chart>
</div>
</div>
);
}
}
export default Bar;
import * as React from "react";
export interface ChartCardProps {
title: React.ReactNode;
action?: React.ReactNode;
total?: React.ReactNode | number;
footer?: React.ReactNode;
contentHeight?: number;
avatar?: React.ReactNode;
style?: React.CSSProperties;
}
export default class ChartCard extends React.Component<ChartCardProps, any> {}
import React from 'react';
import { Card, Spin } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
const ChartCard = ({
loading = false, contentHeight, title, avatar, action, total, footer, children, ...rest
}) => {
const content = (
<div className={styles.chartCard}>
<div
className={classNames(styles.chartTop, { [styles.chartTopMargin]: (!children && !footer) })}
>
<div className={styles.avatar}>
{
avatar
}
</div>
<div className={styles.metaWrap}>
<div className={styles.meta}>
<span className={styles.title}>{title}</span>
<span className={styles.action}>{action}</span>
</div>
{
// eslint-disable-next-line
(total !== undefined) && (<div className={styles.total} dangerouslySetInnerHTML={{ __html: total }} />)
}
</div>
</div>
{
children && (
<div className={styles.content} style={{ height: contentHeight || 'auto' }}>
<div className={contentHeight && styles.contentFixed}>
{children}
</div>
</div>
)
}
{
footer && (
<div className={classNames(styles.footer, { [styles.footerMargin]: !children })}>
{footer}
</div>
)
}
</div>
);
return (
<Card
bodyStyle={{ padding: '20px 24px 8px 24px' }}
{...rest}
>
{<Spin spinning={loading} wrapperClassName={styles.spin}>{content}</Spin>}
</Card>
);
};
export default ChartCard;
@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;
}
import * as React from "react";
export interface FieldProps {
label: React.ReactNode;
value: React.ReactNode;
style?: React.CSSProperties;
}
export default class Field extends React.Component<FieldProps, any> {}
import React from 'react';
import styles from './index.less';
const Field = ({ label, value, ...rest }) => (
<div className={styles.field} {...rest}>
<span>{label}</span>
<span>{value}</span>
</div>
);
export default Field;
@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;
}
}
import * as React from "react";
export interface GaugeProps {
title: React.ReactNode;
color?: string;
height: number;
bgColor?: number;
percent: number;
style?: React.CSSProperties;
}
export default class Gauge extends React.Component<GaugeProps, any> {}
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 (
<Chart height={height} data={data} scale={cols} padding={[-16, 0, 16, 0]} forceFit={forceFit}>
<Coord type="polar" startAngle={-1.25 * Math.PI} endAngle={0.25 * Math.PI} radius={0.8} />
<Axis name="1" line={null} />
<Axis
line={null}
tickLine={null}
subTickLine={null}
name="value"
zIndex={2}
gird={null}
label={{
offset: -12,
formatter,
textStyle: {
fontSize: 12,
fill: 'rgba(0, 0, 0, 0.65)',
textAlign: 'center',
},
}}
/>
<Guide>
<Line
start={[3, 0.905]}
end={[3, 0.85]}
lineStyle={{
stroke: color,
lineDash: null,
lineWidth: 2,
}}
/>
<Line
start={[5, 0.905]}
end={[5, 0.85]}
lineStyle={{
stroke: color,
lineDash: null,
lineWidth: 3,
}}
/>
<Line
start={[7, 0.905]}
end={[7, 0.85]}
lineStyle={{
stroke: color,
lineDash: null,
lineWidth: 3,
}}
/>
<Arc
zIndex={0}
start={[0, 0.965]}
end={[10, 0.965]}
style={{
stroke: bgColor,
lineWidth: 10,
}}
/>
<Arc
zIndex={1}
start={[0, 0.965]}
end={[data[0].value, 0.965]}
style={{
stroke: color,
lineWidth: 10,
}}
/>
<Html
position={['50%', '95%']}
html={() => {
return `
<div style="width: 300px;text-align: center;font-size: 12px!important;">
<p style="font-size: 14px; color: rgba(0,0,0,0.43);margin: 0;">${title}</p>
<p style="font-size: 24px;color: rgba(0,0,0,0.85);margin: 0;">
${data[0].value * 10}%
</p>
</div>`;
}}
/>
</Guide>
<Geom
line={false}
type="point"
position="value*1"
shape="pointer"
color={color}
active={false}
/>
</Chart>
);
}
}
import * as React from "react";
// g2已经更新到3.0
// 不带的写了
export interface Axis {
title: any;
line: any;
gridAlign: any;
labels: any;
tickLine: any;
grid: any;
}
export interface MiniAreaProps {
color?: string;
height: number;
borderColor?: string;
line?: boolean;
animate?: boolean;
xAxis?: Axis;
yAxis?: Axis;
data: Array<{
x: number;
y: number;
}>;
}
export default class MiniArea extends React.Component<MiniAreaProps, any> {}
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 (
<div className={styles.miniChart} style={{ height }}>
<div className={styles.chartContent}>
{height > 0 && (
<Chart
animate={animate}
scale={scaleProps}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
>
<Axis
key="axis-x"
name="x"
label={false}
line={false}
tickLine={false}
grid={false}
{...xAxis}
/>
<Axis
key="axis-y"
name="y"
label={false}
line={false}
tickLine={false}
grid={false}
{...yAxis}
/>
<Tooltip showTitle={false} crosshairs={false} />
<Geom
type="area"
position="x*y"
color={color}
tooltip={tooltip}
shape="smooth"
style={{
fillOpacity: 1,
}}
/>
{line ? (
<Geom
type="line"
position="x*y"
shape="smooth"
color={borderColor}
size={borderWidth}
tooltip={false}
/>
) : (
<span style={{ display: 'none' }} />
)}
</Chart>
)}
</div>
</div>
);
}
}
import * as React from 'react';
export interface MiniBarProps {
color?: string;
height: number;
data: Array<{
x: number | string;
y: number;
}>;
style?: React.CSSProperties;
}
export default class MiniBar extends React.Component<MiniBarProps, any> {}
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 (
<div className={styles.miniChart} style={{ height }}>
<div className={styles.chartContent}>
<Chart
scale={scale}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
>
<Tooltip showTitle={false} crosshairs={false} />
<Geom type="interval" position="x*y" color={color} tooltip={tooltip} />
</Chart>
</div>
</div>
);
}
}
import * as React from "react";
export interface MiniProgressProps {
target: number;
color?: string;
strokeWidth?: number;
percent?: number;
style?: React.CSSProperties;
}
export default class MiniProgress extends React.Component<
MiniProgressProps,
any
> {}
import React from 'react';
import { Tooltip } from 'antd';
import styles from './index.less';
const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => (
<div className={styles.miniProgress}>
<Tooltip title={`目标值: ${target}%`}>
<div
className={styles.target}
style={{ left: (target ? `${target}%` : null) }}
>
<span style={{ backgroundColor: (color || null) }} />
<span style={{ backgroundColor: (color || null) }} />
</div>
</Tooltip>
<div className={styles.progressWrap}>
<div
className={styles.progress}
style={{
backgroundColor: (color || null),
width: (percent ? `${percent}%` : null),
height: (strokeWidth || null),
}}
/>
</div>
</div>
);
export default MiniProgress;
@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 .4s cubic-bezier(.08, .82, .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;
}
}
}
import * as React from 'react';
export interface PieProps {
animate?: boolean;
color?: string;
height: number;
hasLegend?: boolean;
padding?: [number, number, number, number];
percent?: number;
data?: Array<{
x: string | string;
y: number;
}>;
total?: string;
title?: React.ReactNode;
tooltip?: boolean;
valueFormat?: (value: string) => string;
subTitle?: React.ReactNode;
}
export default class Pie extends React.Component<PieProps, any> {}
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.getLengendData();
this.resize();
window.addEventListener('resize', this.resize);
}
componentWillReceiveProps(nextProps) {
if (this.props.data !== nextProps.data) {
this.getLengendData();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
this.resize.cancel();
}
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 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 (
<div ref={this.handleRoot} className={pieClassName} style={style}>
<ReactFitText maxFontSize={25}>
<div className={styles.chart}>
<Chart
scale={scale}
height={height}
forceFit={forceFit}
data={dv}
padding={padding}
animate={animate}
onGetG2Instance={this.getG2Instance}
>
{!!tooltip && <Tooltip showTitle={false} />}
<Coord type="theta" innerRadius={inner} />
<Geom
style={{ lineWidth, stroke: '#fff' }}
tooltip={tooltip && tooltipFormat}
type="intervalStack"
position="percent"
color={['x', percent ? formatColor : defaultColors]}
selected={selected}
/>
</Chart>
{(subTitle || total) && (
<div className={styles.total}>
{subTitle && <h4 className="pie-sub-title">{subTitle}</h4>}
{/* eslint-disable-next-line */}
{total && <div className="pie-stat" dangerouslySetInnerHTML={{ __html: total }} />}
</div>
)}
</div>
</ReactFitText>
{hasLegend && (
<ul className={styles.legend}>
{legendData.map((item, i) => (
<li key={item.x} onClick={() => this.handleLegendClick(item, i)}>
<span
className={styles.dot}
style={{ backgroundColor: !item.checked ? '#aaa' : item.color }}
/>
<span className={styles.legendTitle}>{item.x}</span>
<Divider type="vertical" />
<span className={styles.percent}>
{`${(isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}
</span>
<span
className={styles.value}
dangerouslySetInnerHTML={{
__html: valueFormat ? valueFormat(item.y) : item.y,
}}
/>
</li>
))}
</ul>
)}
</div>
);
}
}
@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;
}
}
import * as React from "react";
export interface RadarProps {
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<RadarProps, any> {}
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 (
<div className={styles.radar} style={{ height }}>
<div>
{title && <h4>{title}</h4>}
<Chart
scale={scale}
height={chartHeight}
forceFit={forceFit}
data={data}
padding={padding}
animate={animate}
onGetG2Instance={this.getG2Instance}
>
<Tooltip />
<Coord type="polar" />
<Axis
name="label"
line={null}
tickLine={null}
grid={{
lineStyle: {
lineDash: null,
},
hideFirstLine: false,
}}
/>
<Axis
name="value"
grid={{
type: 'polygon',
lineStyle: {
lineDash: null,
},
}}
/>
<Geom type="line" position="label*value" color={['name', colors]} size={1} />
<Geom
type="point"
position="label*value"
color={['name', colors]}
shape="circle"
size={3}
/>
</Chart>
{hasLegend && (
<Row className={styles.legend}>
{legendData.map((item, i) => (
<Col
span={24 / legendData.length}
key={item.name}
onClick={() => this.handleLegendClick(item, i)}
>
<div className={styles.legendItem}>
<p>
<span
className={styles.dot}
style={{ backgroundColor: !item.checked ? '#aaa' : item.color }}
/>
<span>{item.name}</span>
</p>
<h6>{item.value}</h6>
</div>
</Col>
))}
</Row>
)}
</div>
</div>
);
}
}
@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;
}
}
}
import * as React from "react";
export interface TagCloudProps {
data: Array<{
name: string;
value: number;
}>;
height: number;
style?: React.CSSProperties;
}
export default class TagCloud extends React.Component<TagCloudProps, any> {}
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 (
<div
className={classNames(styles.tagCloud, className)}
style={{ width: '100%', height }}
ref={this.saveRootRef}
>
{dv && (
<Chart
width={w}
height={h}
data={dv}
padding={0}
scale={{
x: { nice: false },
y: { nice: false },
}}
>
<Coord reflect="y" />
<Geom type="point" position="x*y" color="text" shape="cloud" />
</Chart>
)}
</div>
);
}
}
export default TagCloud;
.tagCloud {
overflow: hidden;
canvas {
transform: scale(0.25);
transform-origin: 0 0;
}
}
import * as React from "react";
export interface TimelineChartProps {
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<
TimelineChartProps,
any
> {}
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',
tickCount: 10,
mask: 'HH:MM',
range: [0, 1],
};
const cols = {
x: timeScale,
value: {
max,
min: 0,
},
};
const SliderGen = () => (
<Slider
padding={[0, padding[1] + 20, 0, padding[3]]}
width="auto"
height={26}
xAxis="x"
yAxis="y1"
scales={{ x: timeScale }}
data={data}
start={ds.state.start}
end={ds.state.end}
backgroundChart={{ type: 'line' }}
onChange={({ startValue, endValue }) => {
ds.setState('start', startValue);
ds.setState('end', endValue);
}}
/>
);
return (
<div className={styles.timelineChart} style={{ height: height + 30 }}>
<div>
{title && <h4>{title}</h4>}
<Chart height={height} padding={padding} data={dv} scale={cols} forceFit>
<Axis name="x" />
<Tooltip />
<Legend name="key" position="top" />
<Geom type="line" position="x*value" size={borderWidth} color="key" />
</Chart>
<div style={{ marginRight: -20 }}>
<SliderGen />
</div>
</div>
</div>
);
}
}
.timelineChart {
background: #fff;
}
import * as React from 'react';
export interface WaterWaveProps {
title: React.ReactNode;
color?: string;
height: number;
percent: number;
style?: React.CSSProperties;
}
export default class WaterWave extends React.Component<WaterWaveProps, any> {}
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 (
<div
className={styles.waterWave}
ref={n => (this.root = n)}
style={{ transform: `scale(${radio})` }}
>
<div style={{ width: height, height, overflow: 'hidden' }}>
<canvas
className={styles.waterWaveCanvasWrapper}
ref={n => (this.node = n)}
width={height * 2}
height={height * 2}
/>
</div>
<div className={styles.text} style={{ width: height }}>
{title && <span>{title}</span>}
<h4>{percent}%</h4>
</div>
</div>
);
}
}
@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(.5);
transform-origin: 0 0;
}
}
/* 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 (
<div ref={this.handleRoot}>{h > 0 && <WrappedComponent {...this.props} height={h} />}</div>
);
}
};
};
export default autoHeight;
---
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(
<Bar
height={200}
title="销售额趋势"
data={salesData}
/>
, mountNode);
````
---
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(
<Row>
<Col span={24}>
<ChartCard
title="销售额"
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
total={yuan(126560)}
footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
contentHeight={46}
>
<span>
周同比
<Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
</span>
<span style={{ marginLeft: 16 }}>
日环比
<Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
</span>
</ChartCard>
</Col>
<Col span={24} style={{ marginTop: 24 }}>
<ChartCard
title="移动指标"
avatar={
<img
style={{ width: 56, height: 56 }}
src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png"
alt="indicator"
/>
}
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
total={yuan(126560)}
footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
/>
</Col>
<Col span={24} style={{ marginTop: 24 }}>
<ChartCard
title="移动指标"
avatar={(
<img
alt="indicator"
style={{ width: 56, height: 56 }}
src="https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png"
/>
)}
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
total={yuan(126560)}
/>
</Col>
</Row>
, mountNode);
````
---
order: 7
title: 仪表盘
---
仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。
````jsx
import { Gauge } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
<Gauge
title="核销率"
height={164}
percent={87}
/>
, mountNode);
````
---
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(
<MiniArea
line
color="#cceafe"
height={45}
data={visitData}
/>
, mountNode);
````
---
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(
<MiniBar
height={45}
data={visitData}
/>
, mountNode);
````
---
order: 6
title: 迷你饼状图
---
通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展
现更多业务场景。
```jsx
import { Pie } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
<Pie percent={28} subTitle="中式快餐" total="28%" height={140} />,
mountNode
);
```
---
order: 3
title: 迷你进度条
---
````jsx
import { MiniProgress } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
<MiniProgress percent={78} strokeWidth={8} target={80} />
, mountNode);
````
---
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(
<Row>
<Col span={24}>
<ChartCard
title="搜索用户数量"
contentHeight={134}
>
<NumberInfo
subTitle={<span>本周访问</span>}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
<MiniArea
line
height={45}
data={visitData}
/>
</ChartCard>
</Col>
<Col span={24} style={{ marginTop: 24 }}>
<ChartCard
title="访问量"
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
total={numeral(8846).format('0,0')}
footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />}
contentHeight={46}
>
<MiniBar
height={46}
data={visitData}
/>
</ChartCard>
</Col>
<Col span={24} style={{ marginTop: 24 }}>
<ChartCard
title="线上购物转化率"
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
total="78%"
footer={
<div>
<span>
周同比
<Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
</span>
<span style={{ marginLeft: 16 }}>
日环比
<Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
</span>
</div>
}
contentHeight={46}
>
<MiniProgress percent={78} strokeWidth={8} target={80} />
</ChartCard>
</Col>
</Row>
, mountNode);
````
---
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(
<Pie
hasLegend
title="销售额"
subTitle="销售额"
total={yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))}
data={salesPieData}
valueFormat={val => yuan(val)}
height={294}
/>
, mountNode);
````
---
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(
<ChartCard title="数据比例">
<Radar
hasLegend
height={286}
data={radarData}
/>
</ChartCard>
, mountNode);
````
---
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(
<TagCloud
data={tags}
height={200}
/>
, mountNode);
````
---
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(
<TimelineChart
height={200}
data={chartData}
titleMap={{ y1: '客流量', y2: '支付笔数' }}
/>
, mountNode);
````
---
order: 8
title: 水波图
---
水波图是一种比例的展示方式,可以更直观的展示关键值的占比。
````jsx
import { WaterWave } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
<div style={{ textAlign: 'center' }}>
<WaterWave
height={161}
title="补贴资金剩余"
percent={34}
/>
</div>
, mountNode);
````
// 全局 G2 设置
import { track, setTheme } from 'bizcharts';
track(false);
const config = {
defaultColor: '#1089ff',
shape: {
interval: {
fillOpacity: 1,
},
},
};
setTheme(config);
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 };
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 => `&yen; ${numeral(val).format('0,0')}`;
export {
yuan,
Bar,
Pie,
Gauge,
Radar,
MiniBar,
MiniArea,
MiniProgress,
ChartCard,
Field,
WaterWave,
TagCloud,
TimelineChart,
};
.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;
}
}
---
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 | - |
| 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)` |
| margin | 图表内部间距 | array | \[32, 0, 32, 40\] |
| 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` |
| margin | 图表内部间距 | array | \[24, 0, 24, 0\] |
| percent | 占比 | number | - |
| tooltip | 是否显示 tooltip | boolean | true |
| valueFormat | 显示值的格式化函数 | function | - |
| title | 图表标题 | ReactNode|string | - |
| subTitle | 图表子标题 | ReactNode|string | - |
| total | 图标中央的总数 | string | - |
### Radar
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | 图表标题 | ReactNode\|string | - |
| height | 图表高度 | number | - |
| hasLegend | 是否显示 legend | boolean | `false` |
| margin | 图表内部间距 | array | \[24, 30, 16, 30\] |
| 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<name, value\> | - |
| height | 高度值 | number | - |
### TimelineChart
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| data | 标题 | Array<x, y1, y2\> | - |
| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - |
| height | 高度值 | number | 400 |
### Field
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| label | 标题 | ReactNode\|string | - |
| value | 值 | ReactNode\|string | - |
---
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(
<CountDown style={{ fontSize: 20 }} target={targetTime} />
, mountNode);
````
import * as React from "react";
export interface CountDownProps {
format?: (time: number) => void;
target: Date | number;
onEnd?: () => void;
style?: React.CSSProperties;
}
export default class CountDown extends React.Component<CountDownProps, any> {}
---
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 | -|
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,
};
}
// defaultFormat = time => (
// <span>{moment(time).format('hh:mm:ss')}</span>
// );
defaultFormat = (time) => {
const hours = 60 * 60 * 1000;
const minutes = 60 * 1000;
const h = fixedZero(Math.floor(time / hours));
const m = fixedZero(Math.floor((time - (h * hours)) / minutes));
const s = fixedZero(Math.floor((time - (h * hours) - (m * minutes)) / 1000));
return (
<span>{h}:{m}:{s}</span>
);
}
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, ...rest } = this.props;
const { lastTime } = this.state;
const result = format(lastTime);
return (<span {...rest}>{result}</span>);
}
}
export default CountDown;
---
title: CountDown
subtitle: 倒计时
cols: 1
order: 3
---
倒计时组件。
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| format | 时间格式化显示 | Function(time) | |
| target | 目标时间 | Date | - |
| onEnd | 倒计时结束回调 | funtion | -|
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 (
<Col className={clsString} {...responsive[column]} {...restProps}>
{term && <div className={styles.term}>{term}</div>}
{children && <div className={styles.detail}>{children}</div>}
</Col>
);
};
Description.defaultProps = {
term: '',
children: '',
};
Description.propTypes = {
term: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.element,
]),
};
export default Description;
import React from 'react';
import classNames from 'classnames';
import { Row } from 'antd';
import styles from './index.less';
export default ({ 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 (
<div className={clsString} {...restProps}>
{title ? <div className={styles.title}>{title}</div> : null}
<Row gutter={gutter}>
{React.Children.map(children, child => React.cloneElement(child, { column }))}
</Row>
</div>
);
};
---
order: 0
title: Basic
---
基本描述列表。
````jsx
import DescriptionList from 'ant-design-pro/lib/DescriptionList';
const { Description } = DescriptionList;
ReactDOM.render(
<DescriptionList size="large" title="title">
<Description term="Firefox">
A free, open source, cross-platform,
graphical web browser developed by the
Mozilla Corporation and hundreds of
volunteers.
</Description>
<Description term="Firefox">
A free, open source, cross-platform,
graphical web browser developed by the
Mozilla Corporation and hundreds of
volunteers.
</Description>
<Description term="Firefox">
A free, open source, cross-platform,
graphical web browser developed by the
Mozilla Corporation and hundreds of
volunteers.
</Description>
</DescriptionList>
, mountNode);
````
---
order: 1
title: Vertical
---
垂直布局。
````jsx
import DescriptionList from 'ant-design-pro/lib/DescriptionList';
const { Description } = DescriptionList;
ReactDOM.render(
<DescriptionList size="large" title="title" layout="vertical">
<Description term="Firefox">
A free, open source, cross-platform,
graphical web browser developed by the
Mozilla Corporation and hundreds of
volunteers.
</Description>
<Description term="Firefox">
A free, open source, cross-platform,
graphical web browser developed by the
Mozilla Corporation and hundreds of
volunteers.
</Description>
<Description term="Firefox">
A free, open source, cross-platform,
graphical web browser developed by the
Mozilla Corporation and hundreds of
volunteers.
</Description>
</DescriptionList>
, mountNode);
````
import * as React from 'react';
export interface DescriptionListProps {
layout?: 'horizontal' | 'vertical';
col?: number;
title: React.ReactNode;
gutter?: number;
size?: 'large' | 'small';
style?: React.CSSProperties;
}
declare class Description extends React.Component<
{
term: React.ReactNode;
style?: React.CSSProperties;
},
any
> {}
export default class DescriptionList extends React.Component<
DescriptionListProps,
any
> {
static Description: typeof Description;
}
import DescriptionList from './DescriptionList';
import Description from './Description';
DescriptionList.Description = Description;
export default DescriptionList;
@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: 22px;
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: -.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;
}
}
}
---
title:
en-US: DescriptionList
zh-CN: 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 | 列表型号,可以设置为 `large` `small` | Enum{'large', 'small'} | - |
#### 响应式规则
| 窗口宽度 | 展示列数 |
|---------------------|---------------------------------------------|
| `≥768px` | `col` |
| `≥576px` | `col < 2 ? col : 2` |
| `<576px` | `1` |
### DescriptionList.Description
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| term | 列表项标题 | ReactNode | - |
export default {
1: { xs: 24 },
2: { xs: 24, sm: 12 },
3: { xs: 24, sm: 12, md: 8 },
4: { xs: 24, sm: 12, md: 6 },
};
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 (
<div className={styles.editableItem}>
{
editable ? (
<div className={styles.wrapper}>
<Input
value={value}
onChange={this.handleChange}
onPressEnter={this.check}
/>
<Icon
type="check"
className={styles.icon}
onClick={this.check}
/>
</div>
) : (
<div className={styles.wrapper}>
<span>{value || ' '}</span>
<Icon
type="edit"
className={styles.icon}
onClick={this.edit}
/>
</div>
)
}
</div>
);
}
}
@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;
}
}
}
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 (
<div className={styles.linkGroup}>
{
links.map(link => (
createElement(linkElement, {
key: `linkGroup-item-${link.id || link.title}`,
to: link.href,
href: link.href,
}, link.title)
))
}
{
<Button size="small" type="primary" ghost onClick={onAdd} icon="plus">
添加
</Button>
}
</div>
);
}
}
export default EditableLinkGroup;
@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;
}
}
}
---
order: 1
title: 按照行数省略
---
通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。但是在这种模式下所有 `children` 将会被转换成纯文本。
并且注意在这种模式下,外容器需要有指定的宽度(或设置自身宽度)。
````jsx
import Ellipsis from 'ant-design-pro/lib/Ellipsis';
const article = <p>There were injuries alleged in three <a href="#cover">cases in 2015</a>, 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.</p>;
ReactDOM.render(
<div style={{ width: 200 }}>
<Ellipsis tooltip lines={3}>{article}</Ellipsis>
</div>
, mountNode);
````
---
order: 0
title: 按照字符数省略
---
通过设置 `length` 属性指定文本最长长度,如果超过这个长度会自动截取。
````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(
<div>
<Ellipsis length={100}>{article}</Ellipsis>
<h4 style={{ marginTop: 24 }}>Show Tooltip</h4>
<Ellipsis length={100} tooltip>{article}</Ellipsis>
</div>
, mountNode);
````
import * as React from "react";
export interface EllipsisProps {
tooltip?: boolean;
length?: number;
lines?: number;
style?: React.CSSProperties;
}
export default class Ellipsis extends React.Component<
EllipsisProps,
any
> {}
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 <span {...other}>{text}</span>;
}
const tail = '...';
let displayText;
if (length - tail.length <= 0) {
displayText = '';
} else {
displayText = text.slice(0, (length - tail.length));
}
if (tooltip) {
return <Tooltip title={text}><span>{displayText}{tail}</span></Tooltip>;
}
return (
<span {...other}>
{displayText}{tail}
</span>
);
};
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 (<span className={cls} {...restProps}>{children}</span>);
}
// length
if (!lines) {
return (<EllipsisText className={cls} length={length} text={children || ''} tooltip={tooltip} {...restProps} />);
}
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};}`;
return (
<div id={id} className={cls} {...restProps}>
<style>{style}</style>
{
tooltip ? (<Tooltip title={children}>{children}</Tooltip>) : children
}
</div>);
}
const childNode = (
<span ref={this.handleNode}>
{
(targetCount > 0) && text.substring(0, targetCount)
}
{
(targetCount > 0) && (targetCount < text.length) && '...'
}
</span>
);
return (
<div
{...restProps}
ref={this.handleRoot}
className={cls}
>
<div
ref={this.handleContent}
>
{
tooltip ? (
<Tooltip title={text}>{childNode}</Tooltip>
) : childNode
}
<div className={styles.shadow} ref={this.handleShadowChildren}>{children}</div>
<div className={styles.shadow} ref={this.handleShadow}><span>{text}</span></div>
</div>
</div>
);
}
}
.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;
-webkit-box-orient: vertical;
}
---
title:
en-US: Ellipsis
zh-CN: Ellipsis
subtitle: 文本自动省略号
cols: 1
order: 10
---
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -
lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1`
---
order: 2
title: 403
---
403 页面,配合自定义操作。
````jsx
import Exception from 'ant-design-pro/lib/Exception';
import { Button } from 'antd';
const actions = (
<div>
<Button type="primary">回到首页</Button>
<Button>查看详情</Button>
</div>
);
ReactDOM.render(
<Exception type="403" actions={actions} />
, mountNode);
````
---
order: 0
title: 404
---
404 页面。
````jsx
import Exception from 'ant-design-pro/lib/Exception';
ReactDOM.render(
<Exception type="404" />
, mountNode);
````
---
order: 1
title: 500
---
500 页面。
````jsx
import Exception from 'ant-design-pro/lib/Exception';
ReactDOM.render(
<Exception type="500" />
, mountNode);
````
import * as React from "react";
export interface ExceptionProps {
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<ExceptionProps, any> {}
import React, { createElement } from 'react';
import classNames from 'classnames';
import { Button } from 'antd';
import config from './typeConfig';
import styles from './index.less';
export default ({ className, linkElement = 'a', type, title, desc, img, actions, ...rest }) => {
const pageType = type in config ? type : '404';
const clsString = classNames(styles.exception, className);
return (
<div className={clsString} {...rest}>
<div className={styles.imgBlock}>
<div
className={styles.imgEle}
style={{ backgroundImage: `url(${img || config[pageType].img})` }}
/>
</div>
<div className={styles.content}>
<h1>{title || config[pageType].title}</h1>
<div className={styles.desc}>{desc || config[pageType].desc}</div>
<div className={styles.actions}>
{
actions ||
createElement(linkElement, {
to: '/',
href: '/',
}, <Button type="primary">返回首页</Button>)
}
</div>
</div>
</div>
);
};
@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;
}
}
}
---
title:
en-US: Exception
zh-CN: 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 | 定义链接的元素,默认为 `a` | string\|ReactElement | - |
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;
---
order: 0
title: 演示
iframe: 400
---
浮动固定页脚。
````jsx
import FooterToolbar from 'ant-design-pro/lib/FooterToolbar';
import { Button } from 'antd';
ReactDOM.render(
<div style={{ background: '#f7f7f7', padding: 24 }}>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<p>页面内容 页面内容 页面内容 页面内容</p>
<FooterToolbar extra="提示信息">
<Button>取消</Button>
<Button type="primary">提交</Button>
</FooterToolbar>
</div>
, mountNode);
````
import * as React from 'react';
export interface FooterToolbarProps {
extra: React.ReactNode;
style?: React.CSSProperties;
}
export default class FooterToolbar extends React.Component<
FooterToolbarProps,
any
> {}
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 (
<div
className={classNames(className, styles.toolbar)}
{...restProps}
>
<div className={styles.left}>{extra}</div>
<div className={styles.right}>{children}</div>
</div>
);
}
}
@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, .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;
}
}
---
title:
en-US: FooterToolbar
zh-CN: FooterToolbar
subtitle: 底部工具栏
cols: 1
order: 6
---
固定在底部的工具栏。
## 何时使用
固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
children | 工具栏内容,向右对齐 | ReactNode | -
extra | 额外信息,向左对齐 | ReactNode | -
---
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: <Icon type="github" />,
href: 'https://github.com/ant-design/ant-design-pro',
blankTarget: true,
}, {
key: '条款',
title: '条款',
href: '',
blankTarget: true,
}];
const copyright = <div>Copyright <Icon type="copyright" /> 2017 蚂蚁金服体验技术部出品</div>;
ReactDOM.render(
<div style={{ background: '#f5f5f5', overflow: 'hidden' }}>
<div style={{ height: 280 }} />
<GlobalFooter links={links} copyright={copyright} />
</div>
, mountNode);
````
import * as React from "react";
export interface GlobalFooterProps {
links?: Array<{
title: React.ReactNode;
href: string;
blankTarget?: boolean;
}>;
copyright?: React.ReactNode;
style?: React.CSSProperties;
}
export default class GlobalFooter extends React.Component<
GlobalFooterProps,
any
> {}
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
export default ({ className, links, copyright }) => {
const clsString = classNames(styles.globalFooter, className);
return (
<div className={clsString}>
{
links && (
<div className={styles.links}>
{links.map(link => (
<a
key={link.key}
target={link.blankTarget ? '_blank' : '_self'}
href={link.href}
>
{link.title}
</a>
))}
</div>
)
}
{copyright && <div className={styles.copyright}>{copyright}</div>}
</div>
);
};
@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 .3s;
&:not(:last-child) {
margin-right: 40px;
}
&:hover {
color: @text-color;
}
}
}
.copyright {
color: @text-color-secondary;
font-size: @font-size-base;
}
}
---
title:
en-US: GlobalFooter
zh-CN: GlobalFooter
subtitle: 全局页脚
cols: 1
order: 7
---
页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | -
copyright | 版权信息 | ReactNode | -
import React, { PureComponent } from 'react';
import { Layout, Menu, Icon, Spin, Tag, Dropdown, Avatar, Divider } from 'antd';
import moment from 'moment';
import groupBy from 'lodash/groupBy';
import Debounce from 'lodash-decorators/debounce';
import { Link } from 'dva/router';
import NoticeIcon from '../NoticeIcon';
import HeaderSearch from '../HeaderSearch';
import styles from './index.less';
const { Header } = Layout;
export default class GlobalHeader extends PureComponent {
componentWillUnmount() {
this.triggerResizeEvent.cancel();
}
getNoticeData() {
const { notices = [] } = this.props;
if (notices.length === 0) {
return {};
}
const newNotices = notices.map((notice) => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime).fromNow();
}
// transform id to item key
if (newNotice.id) {
newNotice.key = newNotice.id;
}
if (newNotice.extra && newNotice.status) {
const color = ({
todo: '',
processing: 'blue',
urgent: 'red',
doing: 'gold',
})[newNotice.status];
newNotice.extra = <Tag color={color} style={{ marginRight: 0 }}>{newNotice.extra}</Tag>;
}
return newNotice;
});
return groupBy(newNotices, 'type');
}
toggle = () => {
const { collapsed, onCollapse } = this.props;
onCollapse(!collapsed);
this.triggerResizeEvent();
}
@Debounce(600)
triggerResizeEvent() { // eslint-disable-line
const event = document.createEvent('HTMLEvents');
event.initEvent('resize', true, false);
window.dispatchEvent(event);
}
render() {
const {
currentUser, collapsed, fetchingNotices, isMobile, logo,
onNoticeVisibleChange, onMenuClick, onNoticeClear,
} = this.props;
const menu = (
<Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
<Menu.Item disabled><Icon type="user" />个人中心</Menu.Item>
<Menu.Item disabled><Icon type="setting" />设置</Menu.Item>
<Menu.Item key="triggerError"><Icon type="close-circle" />触发报错</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout"><Icon type="logout" />退出登录</Menu.Item>
</Menu>
);
const noticeData = this.getNoticeData();
return (
<Header className={styles.header}>
{isMobile && (
[
(
<Link to="/" className={styles.logo} key="logo">
<img src={logo} alt="logo" width="32" />
</Link>
),
<Divider type="vertical" key="line" />,
]
)}
<Icon
className={styles.trigger}
type={collapsed ? 'menu-unfold' : 'menu-fold'}
onClick={this.toggle}
/>
<div className={styles.right}>
<HeaderSearch
className={`${styles.action} ${styles.search}`}
placeholder="站内搜索"
dataSource={['搜索提示一', '搜索提示二', '搜索提示三']}
onSearch={(value) => {
console.log('input', value); // eslint-disable-line
}}
onPressEnter={(value) => {
console.log('enter', value); // eslint-disable-line
}}
/>
<NoticeIcon
className={styles.action}
count={currentUser.notifyCount}
onItemClick={(item, tabProps) => {
console.log(item, tabProps); // eslint-disable-line
}}
onClear={onNoticeClear}
onPopupVisibleChange={onNoticeVisibleChange}
loading={fetchingNotices}
popupAlign={{ offset: [20, -16] }}
>
<NoticeIcon.Tab
list={noticeData['通知']}
title="通知"
emptyText="你已查看所有通知"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
/>
<NoticeIcon.Tab
list={noticeData['消息']}
title="消息"
emptyText="您已读完所有消息"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
/>
<NoticeIcon.Tab
list={noticeData['待办']}
title="待办"
emptyText="你已完成所有待办"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
/>
</NoticeIcon>
{currentUser.name ? (
<Dropdown overlay={menu}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} />
<span className={styles.name}>{currentUser.name}</span>
</span>
</Dropdown>
) : <Spin size="small" style={{ marginLeft: 8 }} />}
</div>
</Header>
);
}
}
@import "~antd/lib/style/themes/default.less";
.header {
padding: 0 12px 0 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
position: relative;
}
:global {
.ant-layout {
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 .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 .3s;
height: 100%;
> i {
font-size: 16px;
vertical-align: middle;
}
&: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, .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;
}
}
}
}
}
---
order: 0
title: 全局搜索
---
通常放置在导航工具条右侧。(点击搜索图标预览效果)
````jsx
import HeaderSearch from 'ant-design-pro/lib/HeaderSearch';
ReactDOM.render(
<div
style={{
textAlign: 'right',
height: '64px',
lineHeight: '64px',
boxShadow: '0 1px 4px rgba(0,21,41,.12)',
padding: '0 32px',
width: '400px',
}}
>
<HeaderSearch
placeholder="站内搜索"
dataSource={['搜索提示一', '搜索提示二', '搜索提示三']}
onSearch={(value) => {
console.log('input', value); // eslint-disable-line
}}
onPressEnter={(value) => {
console.log('enter', value); // eslint-disable-line
}}
/>
</div>
, mountNode);
````
import * as React from 'react';
export interface HeaderSearchProps {
placeholder?: string;
dataSource?: Array<string>;
onSearch?: (value: string) => void;
onChange?: (value: string) => void;
onPressEnter?: (value: string) => void;
style?: React.CSSProperties;
}
export default class HeaderSearch extends React.Component<
HeaderSearchProps,
any
> {}
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: [],
};
static propTypes = {
className: PropTypes.string,
placeholder: PropTypes.string,
onSearch: PropTypes.func,
onPressEnter: PropTypes.func,
defaultActiveFirstOption: PropTypes.bool,
dataSource: PropTypes.array,
};
state = {
searchMode: false,
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;
const inputClass = classNames(styles.input, {
[styles.show]: this.state.searchMode,
});
return (
<span
className={classNames(className, styles.headerSearch)}
onClick={this.enterSearchMode}
>
<Icon type="search" key="Icon" />
<AutoComplete
key="AutoComplete"
{...restProps}
className={inputClass}
value={this.state.value}
onChange={this.onChange}
>
<Input
placeholder={placeholder}
ref={(node) => { this.input = node; }}
onKeyDown={this.onKeyDown}
onBlur={this.leaveSearchMode}
/>
</AutoComplete>
</span>
);
}
}
@import "~antd/lib/style/themes/default.less";
.headerSearch {
:global(.anticon-search) {
cursor: pointer;
font-size: 16px;
}
.input {
transition: width .3s, margin-left .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;
}
}
}
---
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) | -
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Form, Button, Row, Col } from 'antd';
import omit from 'omit.js';
import styles from './index.less';
import map from './map';
const FormItem = Form.Item;
function generator({ defaultProps, defaultRules, type }) {
return (WrappedComponent) => {
return class BasicComponent extends Component {
static contextTypes = {
form: PropTypes.object,
updateActive: PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidMount() {
if (this.context.updateActive) {
this.context.updateActive(this.props.name);
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
onGetCaptcha = () => {
let count = 59;
this.setState({ count });
if (this.props.onGetCaptcha) {
this.props.onGetCaptcha();
}
this.interval = setInterval(() => {
count -= 1;
this.setState({ count });
if (count === 0) {
clearInterval(this.interval);
}
}, 1000);
}
render() {
const { getFieldDecorator } = this.context.form;
const options = {};
let otherProps = {};
const { onChange, defaultValue, rules, name, ...restProps } = this.props;
const { count } = this.state;
options.rules = rules || defaultRules;
if (onChange) {
options.onChange = onChange;
}
if (defaultValue) {
options.initialValue = defaultValue;
}
otherProps = restProps || otherProps;
if (type === 'Captcha') {
const inputProps = omit(otherProps, ['onGetCaptcha']);
return (
<FormItem>
<Row gutter={8}>
<Col span={16}>
{getFieldDecorator(name, options)(
<WrappedComponent {...defaultProps} {...inputProps} />
)}
</Col>
<Col span={8}>
<Button
disabled={count}
className={styles.getCaptcha}
size="large"
onClick={this.onGetCaptcha}
>
{count ? `${count} s` : '获取验证码'}
</Button>
</Col>
</Row>
</FormItem>
);
}
return (
<FormItem>
{getFieldDecorator(name, options)(
<WrappedComponent {...defaultProps} {...otherProps} />
)}
</FormItem>
);
}
};
};
}
const LoginItem = {};
Object.keys(map).forEach((item) => {
LoginItem[item] = generator({
defaultProps: map[item].props,
defaultRules: map[item].rules,
type: item,
})(map[item].component);
});
export default LoginItem;
import React from 'react';
import classNames from 'classnames';
import { Button, Form } from 'antd';
import styles from './index.less';
const FormItem = Form.Item;
export default ({ className, ...rest }) => {
const clsString = classNames(styles.submit, className);
return (
<FormItem>
<Button size="large" className={clsString} type="primary" htmlType="submit" {...rest} />
</FormItem>
);
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Tabs } from 'antd';
const { TabPane } = Tabs;
const generateId = (() => {
let i = 0;
return (prefix = '') => {
i += 1;
return `${prefix}${i}`;
};
})();
export default class LoginTab extends Component {
static __ANT_PRO_LOGIN_TAB = true;
static contextTypes = {
tabUtil: PropTypes.object,
};
constructor(props) {
super(props);
this.uniqueId = generateId('login-tab-');
}
componentWillMount() {
if (this.context.tabUtil) {
this.context.tabUtil.addTab(this.uniqueId);
}
}
render() {
return <TabPane {...this.props} />;
}
}
---
order: 0
title: Standard Login
---
支持账号密码及手机号登录两种模式。
````jsx
import Login from 'ant-design-pro/lib/Login';
import { Alert, Checkbox } from 'antd';
const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
class LoginDemo extends React.Component {
state = {
notice: '',
type: 'tab2',
autoLogin: true,
}
onSubmit = (err, values) => {
console.log('value collected ->', { ...values, autoLogin: this.state.autoLogin });
if (this.state.type === 'tab1') {
this.setState({
notice: '',
}, () => {
if (!err && (values.username !== 'admin' || values.password !== '888888')) {
setTimeout(() => {
this.setState({
notice: '账号或密码错误!',
});
}, 500);
}
});
}
}
onTabChange = (key) => {
this.setState({
type: key,
});
}
changeAutoLogin = (e) => {
this.setState({
autoLogin: e.target.checked,
});
}
render() {
return (
<Login
defaultActiveKey={this.state.type}
onTabChange={this.onTabChange}
onSubmit={this.onSubmit}
>
<Tab key="tab1" tab="账号密码登录">
{
this.state.notice &&
<Alert style={{ marginBottom: 24 }} message={this.state.notice} type="error" showIcon closable />
}
<UserName name="username" />
<Password name="password" />
</Tab>
<Tab key="tab2" tab="手机号登录">
<Mobile name="mobile" />
<Captcha onGetCaptcha={() => console.log('Get captcha!')} name="captcha" />
</Tab>
<div>
<Checkbox checked={this.state.autoLogin} onChange={this.changeAutoLogin}>自动登录</Checkbox>
<a style={{ float: 'right' }} href="">忘记密码</a>
</div>
<Submit>登录</Submit>
<div>
其他登录方式
<span className="icon icon-alipay" />
<span className="icon icon-taobao" />
<span className="icon icon-weibo" />
<a style={{ float: 'right' }} href="">注册账户</a>
</div>
</Login>
);
}
}
ReactDOM.render(<LoginDemo />, mountNode);
````
<style>
#scaffold-src-components-Login-demo-basic .icon {
display: inline-block;
width: 24px;
height: 24px;
background: url('https://gw.alipayobjects.com/zos/rmsportal/itDzjUnkelhQNsycranf.svg');
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
}
#scaffold-src-components-Login-demo-basic .icon-alipay {
background-position: -24px 0;
}
#scaffold-src-components-Login-demo-basic .icon-alipay:hover {
background-position: 0 0;
}
#scaffold-src-components-Login-demo-basic .icon-taobao {
background-position: -24px -24px;
}
#scaffold-src-components-Login-demo-basic .icon-taobao:hover {
background-position: 0 -24px;
}
#scaffold-src-components-Login-demo-basic .icon-weibo {
background-position: -24px -48px;
}
#scaffold-src-components-Login-demo-basic .icon-weibo:hover {
background-position: 0 -48px;
}
</style>
import * as React from "react";
import Button from "antd/lib/button";
export interface LoginProps {
defaultActiveKey?: string;
onTabChange?: (key: string) => void;
style?: React.CSSProperties;
onSubmit?: (error: any, values: any) => void;
}
export interface TabProps {
key?: string;
tab?: React.ReactNode;
}
export class Tab extends React.Component<TabProps, any> {}
export interface LoginItemProps {
name?: string;
rules?: any[];
style?: React.CSSProperties;
onGetCaptcha?: () => void;
}
export class LoginItem extends React.Component<LoginItemProps, any> {}
export default class Login extends React.Component<LoginProps, any> {
static Tab: typeof Tab;
static UserName: typeof LoginItem;
static Password: typeof LoginItem;
static Mobile: typeof LoginItem;
static Captcha: typeof LoginItem;
static Submit: typeof Button;
}
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Form, Tabs } from 'antd';
import classNames from 'classnames';
import LoginItem from './LoginItem';
import LoginTab from './LoginTab';
import LoginSubmit from './LoginSubmit';
import styles from './index.less';
@Form.create()
class Login extends Component {
static defaultProps = {
className: '',
defaultActiveKey: '',
onTabChange: () => {},
onSubmit: () => {},
};
static propTypes = {
className: PropTypes.string,
defaultActiveKey: PropTypes.string,
onTabChange: PropTypes.func,
onSubmit: PropTypes.func,
};
static childContextTypes = {
tabUtil: PropTypes.object,
form: PropTypes.object,
updateActive: PropTypes.func,
};
state = {
type: this.props.defaultActiveKey,
tabs: [],
active: {},
};
getChildContext() {
return {
tabUtil: {
addTab: (id) => {
this.setState({
tabs: [...this.state.tabs, id],
});
},
removeTab: (id) => {
this.setState({
tabs: this.state.tabs.filter(currentId => currentId !== id),
});
},
},
form: this.props.form,
updateActive: (activeItem) => {
const { type, active } = this.state;
if (active[type]) {
active[type].push(activeItem);
} else {
active[type] = [activeItem];
}
this.setState({
active,
});
},
};
}
onSwitch = (type) => {
this.setState({
type,
});
this.props.onTabChange(type);
}
handleSubmit = (e) => {
e.preventDefault();
const { active, type } = this.state;
const activeFileds = active[type];
this.props.form.validateFields(activeFileds, { force: true },
(err, values) => {
this.props.onSubmit(err, values);
}
);
}
render() {
const { className, children } = this.props;
const { type, tabs } = this.state;
const TabChildren = [];
const otherChildren = [];
React.Children.forEach(children, (item) => {
// eslint-disable-next-line
if (item.type.__ANT_PRO_LOGIN_TAB) {
TabChildren.push(item);
} else {
otherChildren.push(item);
}
});
return (
<div className={classNames(className, styles.main)}>
<Form onSubmit={this.handleSubmit}>
{
tabs.length ? (
<div>
<Tabs
animated={false}
className={styles.tabs}
activeKey={type}
onChange={this.onSwitch}
>
{TabChildren}
</Tabs>
{otherChildren}
</div>
) : children
}
</Form>
</div>
);
}
}
Login.Tab = LoginTab;
Login.Submit = LoginSubmit;
Object.keys(LoginItem).forEach((item) => {
Login[item] = LoginItem[item];
});
export default Login;
@import "~antd/lib/style/themes/default.less";
.main {
width: 368px;
margin: 0 auto;
.tabs {
padding: 0 2px;
margin: 0 -2px;
:global {
.ant-tabs-tab {
font-size: 16px;
line-height: 24px;
}
.ant-input-affix-wrapper .ant-input:not(:first-child) {
padding-left: 34px;
}
}
}
:global {
.ant-tabs .ant-tabs-bar {
border-bottom: 0;
margin-bottom: 24px;
text-align: center;
}
.ant-form-item {
margin-bottom: 24px;
}
}
.prefixIcon {
font-size: @font-size-base;
color: @disabled-color;
}
.getCaptcha {
display: block;
width: 100%;
}
.submit {
width: 100%;
margin-top: 24px;
}
}
---
title:
en-US: Login
zh-CN: Login
subtitle: 登录
cols: 1
order: 15
---
支持多种登录方式切换,内置了几种常见的登录控件,可以灵活组合,也支持和自定义控件配合使用。
## API
### Login
参数 | 说明 | 类型 | 默认值
----|------|-----|------
defaultActiveKey | 默认激活 tab 面板的 key | String | -
onTabChange | 切换页签时的回调 | (key) => void | -
onSubmit | 点击提交时的回调 | (err, values) => void | -
### Login.Tab
参数 | 说明 | 类型 | 默认值
----|------|-----|------
key | 对应选项卡的 key | String | -
tab | 选项卡头显示文字 | ReactNode | -
### Login.UserName
参数 | 说明 | 类型 | 默认值
----|------|-----|------
name | 控件标记,提交数据中同样以此为 key | String | -
rules | 校验规则,同 Form getFieldDecorator(id, options) 中 [option.rules 的规则](getFieldDecorator(id, options)) | object[] | -
除上述属性以外,Login.UserName 还支持 antd.Input 的所有属性,并且自带默认的基础配置,包括 `placeholder` `size` `prefix` 等,这些基础配置均可被覆盖。
### Login.Password、Login.Mobile 同 Login.UserName
### Login.Captcha
参数 | 说明 | 类型 | 默认值
----|------|-----|------
onGetCaptcha | 点击获取校验码的回调 | () => void | -
除上述属性以外,Login.Captcha 支持的属性与 Login.UserName 相同。
### Login.Submit
支持 antd.Button 的所有属性。
import React from 'react';
import { Input, Icon } from 'antd';
import styles from './index.less';
const map = {
UserName: {
component: Input,
props: {
size: 'large',
prefix: <Icon type="user" className={styles.prefixIcon} />,
placeholder: 'admin',
},
rules: [{
required: true, message: '请输入账户名!',
}],
},
Password: {
component: Input,
props: {
size: 'large',
prefix: <Icon type="lock" className={styles.prefixIcon} />,
type: 'password',
placeholder: '888888',
},
rules: [{
required: true, message: '请输入密码!',
}],
},
Mobile: {
component: Input,
props: {
size: 'large',
prefix: <Icon type="mobile" className={styles.prefixIcon} />,
placeholder: '手机号',
},
rules: [{
required: true, message: '请输入手机号!',
}, {
pattern: /^1\d{10}$/, message: '手机号格式错误!',
}],
},
Captcha: {
component: Input,
props: {
size: 'large',
prefix: <Icon type="mail" className={styles.prefixIcon} />,
placeholder: '验证码',
},
rules: [{
required: true, message: '请输入验证码!',
}],
},
};
export default map;
import React from 'react';
import { Avatar, List } from 'antd';
import classNames from 'classnames';
import styles from './NoticeList.less';
export default function NoticeList({
data = [], onClick, onClear, title, locale, emptyText, emptyImage,
}) {
if (data.length === 0) {
return (
<div className={styles.notFound}>
{emptyImage ? (
<img src={emptyImage} alt="not found" />
) : null}
<div>{emptyText || locale.emptyText}</div>
</div>
);
}
return (
<div>
<List className={styles.list}>
{data.map((item, i) => {
const itemCls = classNames(styles.item, {
[styles.read]: item.read,
});
return (
<List.Item className={itemCls} key={item.key || i} onClick={() => onClick(item)}>
<List.Item.Meta
className={styles.meta}
avatar={item.avatar ? <Avatar className={styles.avatar} src={item.avatar} /> : null}
title={
<div className={styles.title}>
{item.title}
<div className={styles.extra}>{item.extra}</div>
</div>
}
description={
<div>
<div className={styles.description} title={item.description}>
{item.description}
</div>
<div className={styles.datetime}>{item.datetime}</div>
</div>
}
/>
</List.Item>
);
})}
</List>
<div className={styles.clear} onClick={onClear}>
{locale.clear}{title}
</div>
</div>
);
}
@import "~antd/lib/style/themes/default.less";
.list {
max-height: 400px;
overflow: auto;
.item {
transition: all .3s;
overflow: hidden;
cursor: pointer;
padding-left: 24px;
padding-right: 24px;
.meta {
width: 100%;
}
.avatar {
background: #fff;
margin-top: 4px;
}
&.read {
opacity: .4;
}
&:last-child {
border-bottom: 0;
}
&:hover {
background: @primary-1;
}
.title {
font-weight: normal;
margin-bottom: 8px;
}
.description {
font-size: 12px;
line-height: @line-height-base;
}
.datetime {
font-size: 12px;
margin-top: 4px;
line-height: @line-height-base;
}
.extra {
float: right;
color: @text-color-secondary;
font-weight: normal;
margin-right: 0;
margin-top: -1.5px;
}
}
}
.notFound {
text-align: center;
padding: 73px 0 88px 0;
color: @text-color-secondary;
img {
display: inline-block;
margin-bottom: 16px;
height: 76px;
}
}
.clear {
height: 46px;
line-height: 46px;
text-align: center;
color: @text-color;
border-radius: 0 0 @border-radius-base @border-radius-base;
border-top: 1px solid @border-color-split;
transition: all .3s;
cursor: pointer;
&:hover {
color: @heading-color;
}
}
---
order: 1
title: 通知图标
---
通常用在导航工具栏上。
````jsx
import NoticeIcon from 'ant-design-pro/lib/NoticeIcon';
ReactDOM.render(<NoticeIcon count={5} />, mountNode);
````
---
order: 2
title: 带浮层卡片
---
点击展开通知卡片,展现多种类型的通知,通常放在导航工具栏。
````jsx
import NoticeIcon from 'ant-design-pro/lib/NoticeIcon';
import moment from 'moment';
import groupBy from 'lodash/groupBy';
import { Tag } from 'antd';
const data = [{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: '通知',
}, {
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: '通知',
}, {
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: '通知',
}, {
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: '通知',
}, {
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: '通知',
}, {
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: '消息',
}, {
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: '消息',
}, {
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: '消息',
}, {
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
type: '待办',
}, {
id: '000000010',
title: '第三方紧急代码变更',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: '待办',
}, {
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
type: '待办',
}, {
id: '000000012',
title: 'ABCD 版本发布',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: '待办',
}];
function onItemClick(item, tabProps) {
console.log(item, tabProps);
}
function onClear(tabTitle) {
console.log(tabTitle);
}
function getNoticeData(notices) {
if (notices.length === 0) {
return {};
}
const newNotices = notices.map((notice) => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime).fromNow();
}
// transform id to item key
if (newNotice.id) {
newNotice.key = newNotice.id;
}
if (newNotice.extra && newNotice.status) {
const color = ({
todo: '',
processing: 'blue',
urgent: 'red',
doing: 'gold',
})[newNotice.status];
newNotice.extra = <Tag color={color} style={{ marginRight: 0 }}>{newNotice.extra}</Tag>;
}
return newNotice;
});
return groupBy(newNotices, 'type');
}
const noticeData = getNoticeData(data);
ReactDOM.render(
<div
style={{
textAlign: 'right',
height: '64px',
lineHeight: '64px',
boxShadow: '0 1px 4px rgba(0,21,41,.12)',
padding: '0 32px',
width: '400px',
}}
>
<NoticeIcon
className="notice-icon"
count={5}
onItemClick={onItemClick}
onClear={onClear}
popupAlign={{ offset: [20, -16] }}
>
<NoticeIcon.Tab
list={noticeData['通知']}
title="通知"
emptyText="你已查看所有通知"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
/>
<NoticeIcon.Tab
list={noticeData['消息']}
title="消息"
emptyText="您已读完所有消息"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
/>
<NoticeIcon.Tab
list={noticeData['待办']}
title="待办"
emptyText="你已完成所有待办"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
/>
</NoticeIcon>
</div>
, mountNode);
````
```css
```
import * as React from 'react';
export interface NoticeIconData {
avatar?: string;
title?: React.ReactNode;
description?: React.ReactNode;
datetime?: React.ReactNode;
extra?: React.ReactNode;
style?: React.CSSProperties;
}
export interface NoticeIconProps {
count?: number;
className?: string;
loading?: boolean;
onClear?: (tableTile: string) => void;
onItemClick?: (item: NoticeIconData, tabProps: NoticeIconProps) => void;
onTabChange?: (tableTile: string) => void;
popupAlign?: {
points?: [string, string];
offset?: [number, number];
targetOffset?: [number, number];
overflow?: any;
useCssRight?: boolean;
useCssBottom?: boolean;
useCssTransform?: boolean;
};
style?: React.CSSProperties;
onPopupVisibleChange?: (visible: boolean) => void;
popupVisible?: boolean;
locale?: { emptyText: string; clear: string };
}
export interface NoticeIconTabProps {
list?: Array<NoticeIconData>;
title?: string;
emptyText?: React.ReactNode;
emptyImage?: string;
style?: React.CSSProperties;
}
export class NoticeIconTab extends React.Component<NoticeIconTabProps, any> {}
export default class NoticeIcon extends React.Component<NoticeIconProps, any> {
static Tab: typeof NoticeIconTab;
}
import React, { PureComponent } from 'react';
import { Popover, Icon, Tabs, Badge, Spin } from 'antd';
import classNames from 'classnames';
import List from './NoticeList';
import styles from './index.less';
const { TabPane } = Tabs;
export default class NoticeIcon extends PureComponent {
static defaultProps = {
onItemClick: () => {},
onPopupVisibleChange: () => {},
onTabChange: () => {},
onClear: () => {},
loading: false,
locale: {
emptyText: '暂无数据',
clear: '清空',
},
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
};
static Tab = TabPane;
constructor(props) {
super(props);
this.state = {};
if (props.children && props.children[0]) {
this.state.tabType = props.children[0].props.title;
}
}
onItemClick = (item, tabProps) => {
const { onItemClick } = this.props;
onItemClick(item, tabProps);
}
onTabChange = (tabType) => {
this.setState({ tabType });
this.props.onTabChange(tabType);
}
getNotificationBox() {
const { children, loading, locale } = this.props;
if (!children) {
return null;
}
const panes = React.Children.map(children, (child) => {
const title = child.props.list && child.props.list.length > 0
? `${child.props.title} (${child.props.list.length})` : child.props.title;
return (
<TabPane tab={title} key={child.props.title}>
<List
{...child.props}
data={child.props.list}
onClick={item => this.onItemClick(item, child.props)}
onClear={() => this.props.onClear(child.props.title)}
title={child.props.title}
locale={locale}
/>
</TabPane>
);
});
return (
<Spin spinning={loading} delay={0}>
<Tabs className={styles.tabs} onChange={this.onTabChange}>
{panes}
</Tabs>
</Spin>
);
}
render() {
const { className, count, popupAlign, onPopupVisibleChange } = this.props;
const noticeButtonClass = classNames(className, styles.noticeButton);
const notificationBox = this.getNotificationBox();
const trigger = (
<span className={noticeButtonClass}>
<Badge count={count} className={styles.badge}>
<Icon type="bell" className={styles.icon} />
</Badge>
</span>
);
if (!notificationBox) {
return trigger;
}
const popoverProps = {};
if ('popupVisible' in this.props) {
popoverProps.visible = this.props.popupVisible;
}
return (
<Popover
placement="bottomRight"
content={notificationBox}
popupClassName={styles.popover}
trigger="click"
arrowPointAtCenter
popupAlign={popupAlign}
onVisibleChange={onPopupVisibleChange}
{...popoverProps}
>
{trigger}
</Popover>
);
}
}
@import "~antd/lib/style/themes/default.less";
.popover {
width: 336px;
:global(.ant-popover-inner-content) {
padding: 0;
}
}
.noticeButton {
cursor: pointer;
display: inline-block;
transition: all .3s;
}
.icon {
font-size: 16px;
padding: 4px;
}
.tabs {
:global {
.ant-tabs-nav-scroll {
text-align: center;
}
.ant-tabs-bar {
margin-bottom: 4px;
}
}
}
---
title:
en-US: NoticeIcon
zh-CN: NoticeIcon
subtitle: 通知菜单
cols: 1
order: 9
---
用在导航工具栏上,作为整个产品统一的通知中心。
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
count | 图标上的消息总数 | number | -
loading | 弹出卡片加载状态 | boolean | false
onClear | 点击清空按钮的回调 | function(tabTitle) | -
onItemClick | 点击列表项的回调 | function(item, tabProps) | -
onTabChange | 切换页签的回调 | function(tabTitle) | -
popupAlign | 弹出卡片的位置配置 | Object [alignConfig](https://github.com/yiminghe/dom-align#alignconfig-object-details) | -
onPopupVisibleChange | 弹出卡片显隐的回调 | function(visible) | -
popupVisible | 控制弹层显隐 | boolean | -
locale | 默认文案 | Object | `{ emptyText: '暂无数据', clear: '清空' }`
### NoticeIcon.Tab
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 消息分类的页签标题 | string | -
list | 列表数据,格式参照下表 | Array | `[]`
emptyText | 针对每个 Tab 定制空数据文案 | ReactNode | -
emptyImage | 针对每个 Tab 定制空数据图片 | string | -
### Tab data
参数 | 说明 | 类型 | 默认值
----|------|-----|------
avatar | 头像图片链接 | string | -
title | 标题 | ReactNode | -
description | 描述信息 | ReactNode | -
datetime | 时间戳 | ReactNode | -
extra | 额外信息,在列表项右上角 | ReactNode | -
---
order: 0
title: 演示
---
各种数据文案的展现方式。
````jsx
import NumberInfo from 'ant-design-pro/lib/NumberInfo';
import numeral from 'numeral';
ReactDOM.render(
<div>
<NumberInfo
subTitle={<span>本周访问</span>}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
</div>
, mountNode);
````
import * as React from 'react';
export interface NumberInfoProps {
title?: React.ReactNode | string;
subTitle?: React.ReactNode | string;
total?: React.ReactNode | string;
status?: 'up' | 'down';
theme?: string;
gap?: number;
subTotal?: number;
style?: React.CSSProperties;
}
export default class NumberInfo extends React.Component<NumberInfoProps, any> {}
import React from 'react';
import { Icon } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
export default ({
theme, title, subTitle, total, subTotal, status, suffix, gap, ...rest
}) => (
<div
className={
classNames(styles.numberInfo, {
[styles[`numberInfo${theme}`]]: theme,
})
}
{...rest}
>
{title && <div className={styles.numberInfoTitle}>{title}</div>}
{subTitle && <div className={styles.numberInfoSubTitle}>{subTitle}</div>}
<div className={styles.numberInfoValue} style={gap ? { marginTop: gap } : null}>
<span>
{total}
{suffix && <em className={styles.suffix}>{suffix}</em>}
</span>
{
(status || subTotal) && (
<span className={styles.subTotal}>
{subTotal}
{status && <Icon type={`caret-${status}`} />}
</span>
)
}
</div>
</div>
);
@import "~antd/lib/style/themes/default.less";
.numberInfo {
.suffix {
color: @text-color;
font-size: 16px;
font-style: normal;
margin-left: 4px;
}
.numberInfoTitle {
color: @text-color;
font-size: @font-size-lg;
margin-bottom: 16px;
transition: all .3s;
}
.numberInfoSubTitle {
color: @text-color-secondary;
font-size: @font-size-base;
height: 22px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.numberInfoValue {
margin-top: 4px;
font-size: 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
& > span {
color: @heading-color;
display: inline-block;
line-height: 32px;
height: 32px;
font-size: 24px;
margin-right: 32px;
}
.subTotal {
color: @text-color-secondary;
font-size: @font-size-lg;
vertical-align: top;
margin-right: 0;
i {
font-size: 12px;
transform: scale(0.82);
margin-left: 4px;
}
}
}
}
.numberInfolight {
.numberInfoValue {
& > span {
color: @text-color;
}
}
}
---
title:
en-US: NumberInfo
zh-CN: NumberInfo
subtitle: 数据文本
cols: 1
order: 10
---
常用在数据卡片中,用于突出展示某个业务数据。
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 标题 | ReactNode\|string | -
subTitle | 子标题 | ReactNode\|string | -
total | 总量 | ReactNode\|string | -
subTotal | 子总量 | ReactNode\|string | -
status | 增加状态 | 'up \| down' | -
theme | 状态样式 | string | 'light'
gap | 设置数字和描述直接的间距(像素) | number | 8
---
order: 2
title: With Image
---
带图片的页头。
````jsx
import PageHeader from 'ant-design-pro/lib/PageHeader';
const content = (
<div>
<p>段落示意:蚂蚁金服务设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。</p>
<div className="link">
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg" /> 快速开始
</a>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg" /> 产品简介
</a>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg" /> 产品文档
</a>
</div>
</div>
);
const extra = (
<div className="imgContainer">
<img style={{ width: '100%' }} alt="" src="https://gw.alipayobjects.com/zos/rmsportal/RzwpdLnhmvDJToTdfDPe.png" />
</div>
);
const breadcrumbList = [{
title: '一级菜单',
href: '/',
}, {
title: '二级菜单',
href: '/',
}, {
title: '三级菜单',
}];
ReactDOM.render(
<div>
<PageHeader
title="这是一个标题"
content={content}
extraContent={extra}
breadcrumbList={breadcrumbList}
/>
</div>
, mountNode);
````
<style>
#scaffold-src-components-PageHeader-demo-image .code-box-demo {
background: #f2f4f5;
}
#scaffold-src-components-PageHeader-demo-image .imgContainer {
margin-top: -60px;
text-align: center;
width: 195px;
}
#scaffold-src-components-PageHeader-demo-image .link {
margin-top: 16px;
}
#scaffold-src-components-PageHeader-demo-image .link a {
margin-right: 32px;
}
#scaffold-src-components-PageHeader-demo-image .link img {
vertical-align: middle;
margin-right: 8px;
}
</style>
---
order: 3
title: Simple
---
简单的页头。
````jsx
import PageHeader from 'ant-design-pro/lib/PageHeader';
const breadcrumbList = [{
title: '一级菜单',
href: '/',
}, {
title: '二级菜单',
href: '/',
}, {
title: '三级菜单',
}];
ReactDOM.render(
<div>
<PageHeader title="页面标题" breadcrumbList={breadcrumbList} />
</div>
, mountNode);
````
<style>
#scaffold-src-components-PageHeader-demo-simple .code-box-demo {
background: #f2f4f5;
}
</style>
---
order: 1
title: Standard
---
标准页头。
````jsx
import PageHeader from 'ant-design-pro/lib/PageHeader';
import DescriptionList from 'ant-design-pro/lib/DescriptionList';
import { Button, Menu, Dropdown, Icon, Row, Col } from 'antd';
const { Description } = DescriptionList;
const ButtonGroup = Button.Group;
const description = (
<DescriptionList size="small" col="2">
<Description term="创建人">曲丽丽</Description>
<Description term="订购产品">XX 服务</Description>
<Description term="创建时间">2017-07-07</Description>
<Description term="关联单据"><a href="">12421</a></Description>
</DescriptionList>
);
const menu = (
<Menu>
<Menu.Item key="1">选项一</Menu.Item>
<Menu.Item key="2">选项二</Menu.Item>
<Menu.Item key="3">选项三</Menu.Item>
</Menu>
);
const action = (
<div>
<ButtonGroup>
<Button>操作</Button>
<Button>操作</Button>
<Dropdown overlay={menu} placement="bottomRight">
<Button><Icon type="ellipsis" /></Button>
</Dropdown>
</ButtonGroup>
<Button type="primary">主操作</Button>
</div>
);
const extra = (
<Row>
<Col sm={24} md={12}>
<div style={{ color: 'rgba(0, 0, 0, 0.43)' }}>状态</div>
<div style={{ color: 'rgba(0, 0, 0, 0.85)', fontSize: 20 }}>待审批</div>
</Col>
<Col sm={24} md={12}>
<div style={{ color: 'rgba(0, 0, 0, 0.43)' }}>订单金额</div>
<div style={{ color: 'rgba(0, 0, 0, 0.85)', fontSize: 20 }}>¥ 568.08</div>
</Col>
</Row>
);
const breadcrumbList = [{
title: '一级菜单',
href: '/',
}, {
title: '二级菜单',
href: '/',
}, {
title: '三级菜单',
}];
const tabList = [{
key: 'detail',
tab: '详情',
}, {
key: 'rule',
tab: '规则',
}];
function onTabChange(key) {
console.log(key);
}
ReactDOM.render(
<div>
<PageHeader
title="单号:234231029431"
logo={<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png" />}
action={action}
content={description}
extraContent={extra}
breadcrumbList={breadcrumbList}
tabList={tabList}
tabActiveKey="detail"
onTabChange={onTabChange}
/>
</div>
, mountNode);
````
<style>
#scaffold-src-components-PageHeader-demo-standard .code-box-demo {
background: #f2f4f5;
}
</style>
---
order: 0
title: Structure
---
基本结构,具备响应式布局功能,主要断点为 768px 和 576px,拖动窗口改变大小试试看。
````jsx
import PageHeader from 'ant-design-pro/lib/PageHeader';
const breadcrumbList = [{
title: '面包屑',
}];
const tabList = [{
key: '1',
tab: '页签一',
}, {
key: '2',
tab: '页签二',
}, {
key: '3',
tab: '页签三',
}];
ReactDOM.render(
<div>
<PageHeader
className="tabs"
title={<div className="title">Title</div>}
logo={<div className="logo">logo</div>}
action={<div className="action">action</div>}
content={<div className="content">content</div>}
extraContent={<div className="extraContent">extraContent</div>}
breadcrumbList={breadcrumbList}
tabList={tabList}
tabActiveKey="1"
/>
</div>
, mountNode);
````
<style>
#scaffold-src-components-PageHeader-demo-structure .code-box-demo {
background: #f2f4f5;
}
#scaffold-src-components-PageHeader-demo-structure .logo {
background: #3ba0e9;
color: #fff;
height: 100%;
}
#scaffold-src-components-PageHeader-demo-structure .title {
background: rgba(16, 142, 233, 1);
color: #fff;
}
#scaffold-src-components-PageHeader-demo-structure .action {
background: #7dbcea;
color: #fff;
}
#scaffold-src-components-PageHeader-demo-structure .content {
background: #7dbcea;
color: #fff;
}
#scaffold-src-components-PageHeader-demo-structure .extraContent {
background: #7dbcea;
color: #fff;
}
</style>
import * as React from 'react';
export interface PageHeaderProps {
title?: React.ReactNode | string;
logo?: React.ReactNode | string;
action?: React.ReactNode | string;
content?: React.ReactNode;
extraContent?: React.ReactNode;
routes?: Array<any>;
params?: any;
breadcrumbList?: Array<{ title: React.ReactNode; href?: string }>;
tabList?: Array<{ key: string; tab: React.ReactNode }>;
tabActiveKey?: string;
onTabChange?: (key: string) => void;
linkElement?: React.ReactNode;
style?: React.CSSProperties;
}
export default class PageHeader extends React.Component<PageHeaderProps, any> {}
import React, { PureComponent, createElement } from 'react';
import PropTypes from 'prop-types';
import { Breadcrumb, Tabs } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
const { TabPane } = Tabs;
function getBreadcrumb(breadcrumbNameMap, url) {
if (breadcrumbNameMap[url]) {
return breadcrumbNameMap[url];
}
const urlWithoutSplash = url.replace(/\/$/, '');
if (breadcrumbNameMap[urlWithoutSplash]) {
return breadcrumbNameMap[urlWithoutSplash];
}
let breadcrumb = {};
Object.keys(breadcrumbNameMap).forEach((item) => {
const itemRegExpStr = `^${item.replace(/:[\w-]+/g, '[\\w-]+')}$`;
const itemRegExp = new RegExp(itemRegExpStr);
if (itemRegExp.test(url)) {
breadcrumb = breadcrumbNameMap[item];
}
});
return breadcrumb;
}
export default class PageHeader extends PureComponent {
static contextTypes = {
routes: PropTypes.array,
params: PropTypes.object,
location: PropTypes.object,
breadcrumbNameMap: PropTypes.object,
};
onChange = (key) => {
if (this.props.onTabChange) {
this.props.onTabChange(key);
}
};
getBreadcrumbProps = () => {
return {
routes: this.props.routes || this.context.routes,
params: this.props.params || this.context.params,
location: this.props.location || this.context.location,
breadcrumbNameMap: this.props.breadcrumbNameMap || this.context.breadcrumbNameMap,
};
};
itemRender = (route, params, routes, paths) => {
const { linkElement = 'a' } = this.props;
const last = routes.indexOf(route) === routes.length - 1;
return (last || !route.component)
? <span>{route.breadcrumbName}</span>
: createElement(linkElement, {
href: paths.join('/') || '/',
to: paths.join('/') || '/',
}, route.breadcrumbName);
}
render() {
const { routes, params, location, breadcrumbNameMap } = this.getBreadcrumbProps();
const {
title, logo, action, content, extraContent,
breadcrumbList, tabList, className, linkElement = 'a',
tabActiveKey,
} = this.props;
const clsString = classNames(styles.pageHeader, className);
let breadcrumb;
if (breadcrumbList && breadcrumbList.length) {
breadcrumb = (
<Breadcrumb className={styles.breadcrumb}>
{
breadcrumbList.map(item => (
<Breadcrumb.Item key={item.title}>
{item.href ? (
createElement(linkElement, {
[linkElement === 'a' ? 'href' : 'to']: item.href,
}, item.title)
) : item.title}
</Breadcrumb.Item>)
)
}
</Breadcrumb>
);
} else if (routes && params) {
breadcrumb = (
<Breadcrumb
className={styles.breadcrumb}
routes={routes.filter(route => route.breadcrumbName)}
params={params}
itemRender={this.itemRender}
/>
);
} else if (location && location.pathname) {
const pathSnippets = location.pathname.split('/').filter(i => i);
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join('/')}`;
const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url);
const isLinkable = (index !== pathSnippets.length - 1) && currentBreadcrumb.component;
return currentBreadcrumb.name && !currentBreadcrumb.hideInBreadcrumb ? (
<Breadcrumb.Item key={url}>
{createElement(
isLinkable ? linkElement : 'span',
{ [linkElement === 'a' ? 'href' : 'to']: url },
currentBreadcrumb.name,
)}
</Breadcrumb.Item>
) : null;
});
const breadcrumbItems = [(
<Breadcrumb.Item key="home">
{createElement(linkElement, {
[linkElement === 'a' ? 'href' : 'to']: '/',
}, '首页')}
</Breadcrumb.Item>
)].concat(extraBreadcrumbItems);
breadcrumb = (
<Breadcrumb className={styles.breadcrumb}>
{breadcrumbItems}
</Breadcrumb>
);
} else {
breadcrumb = null;
}
let tabDefaultValue;
if (tabActiveKey !== undefined && tabList) {
tabDefaultValue = tabList.filter(item => item.default)[0] || tabList[0];
}
const activeKeyProps = {
defaultActiveKey: tabDefaultValue && tabDefaultValue.key,
};
if (tabActiveKey !== undefined) {
activeKeyProps.activeKey = tabActiveKey;
}
return (
<div className={clsString}>
{breadcrumb}
<div className={styles.detail}>
{logo && <div className={styles.logo}>{logo}</div>}
<div className={styles.main}>
<div className={styles.row}>
{title && <h1 className={styles.title}>{title}</h1>}
{action && <div className={styles.action}>{action}</div>}
</div>
<div className={styles.row}>
{content && <div className={styles.content}>{content}</div>}
{extraContent && <div className={styles.extraContent}>{extraContent}</div>}
</div>
</div>
</div>
{
tabList &&
tabList.length && (
<Tabs
className={styles.tabs}
{...activeKeyProps}
onChange={this.onChange}
>
{
tabList.map(item => <TabPane tab={item.tab} key={item.key} />)
}
</Tabs>
)
}
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
.pageHeader {
background: @component-background;
padding: 16px 32px 0 32px;
border-bottom: @border-width-base @border-style-base @border-color-split;
.detail {
display: flex;
}
.row {
display: flex;
}
.breadcrumb {
margin-bottom: 16px;
}
.tabs {
margin: 0 0 -17px -8px;
:global {
.ant-tabs-bar {
border-bottom: @border-width-base @border-style-base @border-color-split;
}
}
}
.logo {
flex: 0 1 auto;
margin-right: 16px;
padding-top: 1px;
> img {
width: 28px;
height: 28px;
border-radius: @border-radius-base;
display: block;
}
}
.title {
font-size: 20px;
font-weight: 500;
color: @heading-color;
}
.action {
margin-left: 56px;
min-width: 266px;
:global {
.ant-btn-group:not(:last-child),
.ant-btn:not(:last-child) {
margin-right: 8px;
}
.ant-btn-group > .ant-btn {
margin-right: 0;
}
}
}
.title, .action, .content, .extraContent, .main {
flex: auto;
}
.title, .action {
margin-bottom: 16px;
}
.logo, .content, .extraContent {
margin-bottom: 16px;
}
.action, .extraContent {
text-align: right;
}
.extraContent {
margin-left: 88px;
min-width: 242px;
}
}
@media screen and (max-width: @screen-xl) {
.pageHeader {
.extraContent {
margin-left: 44px;
}
}
}
@media screen and (max-width: @screen-lg) {
.pageHeader {
.extraContent {
margin-left: 20px;
}
}
}
@media screen and (max-width: @screen-md) {
.pageHeader {
.row {
display: block;
}
.action, .extraContent {
margin-left: 0;
text-align: left;
}
}
}
@media screen and (max-width: @screen-sm) {
.pageHeader {
.detail {
display: block;
}
}
}
@media screen and (max-width: @screen-xs) {
.pageHeader {
.action {
:global {
.ant-btn-group, .ant-btn {
display: block;
margin-bottom: 8px;
}
.ant-btn-group > .ant-btn {
display: inline-block;
margin-bottom: 0;
}
}
}
}
}
---
title:
en-US: PageHeader
zh-CN: PageHeader
subtitle: 页头
cols: 1
order: 11
---
页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | title 区域 | ReactNode | - |
| logo | logo区域 | ReactNode | - |
| action | 操作区,位于 title 行的行尾 | ReactNode | - |
| content | 内容区 | ReactNode | - |
| extraContent | 额外内容区,位于content的右侧 | ReactNode | - |
| breadcrumbList | 面包屑数据,配置了此属性时 `routes` `params` `location` `breadcrumbNameMap` 无效 | array<{title: ReactNode, href?: string}> | - |
| routes | 面包屑相关属性,router 的路由栈信息 | object[] | - |
| params | 面包屑相关属性,路由的参数 | object | - |
| location | 面包屑相关属性,当前的路由信息 | object | - |
| breadcrumbNameMap | 面包屑相关属性,路由的地址-名称映射表 | object | - |
| tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | - |
| tabActiveKey | 当前高亮的 tab 项 | string | - |
| onTabChange | 切换面板的回调 | (key) => void | - |
| linkElement | 定义链接的元素,默认为 `a`,可传入 react-router 的 Link | string\|ReactElement | - |
> 面包屑的配置方式有三种,一是直接配置 `breadcrumbList`,二是结合 `react-router@2` `react-router@3`,配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router),三是结合 `react-router@4`,配置 `location` `breadcrumbNameMap`,优先级依次递减,脚手架中使用最后一种。 对于后两种用法,你也可以将 `routes` `params` 及 `location` `breadcrumbNameMap` 放到 context 中,组件会自动获取。
---
order: 1
title: Classic
---
典型结果页面。
````jsx
import Result from 'ant-design-pro/lib/Result';
import { Button, Row, Col, Icon, Steps } from 'antd';
const { Step } = Steps;
const desc1 = (
<div style={{ fontSize: 14, position: 'relative', left: 38 }}>
<div style={{ marginTop: 8, marginBottom: 4 }}>
曲丽丽
<Icon type="dingding-o" style={{ marginLeft: 8 }} />
</div>
<div style={{ marginTop: 8, marginBottom: 4 }}>2016-12-12 12:32</div>
</div>
);
const desc2 = (
<div style={{ fontSize: 14, position: 'relative', left: 38 }}>
<div style={{ marginTop: 8, marginBottom: 4 }}>
周毛毛
<Icon type="dingding-o" style={{ color: '#00A0E9', marginLeft: 8 }} />
</div>
<div style={{ marginTop: 8, marginBottom: 4 }}><a href="">催一下</a></div>
</div>
);
const extra = (
<div>
<div style={{ fontSize: 16, color: 'rgba(0, 0, 0, 0.85)', fontWeight: 500, marginBottom: 20 }}>
项目名称
</div>
<Row style={{ marginBottom: 16 }}>
<Col xs={24} sm={12} md={12} lg={12} xl={6}>
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>项目 ID:</span>
23421
</Col>
<Col xs={24} sm={12} md={12} lg={12} xl={6}>
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>负责人:</span>
曲丽丽
</Col>
<Col xs={24} sm={24} md={24} lg={24} xl={12}>
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>生效时间:</span>
2016-12-12 ~ 2017-12-12
</Col>
</Row>
<Steps progressDot current={1}>
<Step title="创建项目" description={desc1} />
<Step title="部门初审" description={desc2} />
<Step title="财务复核" />
<Step title="完成" />
</Steps>
</div>
);
const actions = (
<div>
<Button type="primary">返回列表</Button>
<Button>查看项目</Button>
<Button>打 印</Button>
</div>
);
ReactDOM.render(
<Result
type="success"
title="提交成功"
description="提交结果页用于反馈一系列操作任务的处理结果,如果仅是简单操作,使用 Message 全局提示反馈即可。本文字区域可以展示简单的补充说明,如果有类似展示“单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。"
extra={extra}
actions={actions}
style={{ width: '100%' }}
/>
, mountNode);
````
---
order: 2
title: Failed
---
提交失败。
````jsx
import Result from 'ant-design-pro/lib/Result';
import { Button, Icon } from 'antd';
const extra = (
<div>
<div style={{ fontSize: 16, color: 'rgba(0, 0, 0, 0.85)', fontWeight: 500, marginBottom: 16 }}>
您提交的内容有如下错误:
</div>
<div style={{ marginBottom: 16 }}>
<Icon style={{ color: '#f5222d', marginRight: 8 }} type="close-circle-o" />您的账户已被冻结
<a style={{ marginLeft: 16 }}>立即解冻 <Icon type="right" /></a>
</div>
<div>
<Icon style={{ color: '#f5222d', marginRight: 8 }} type="close-circle-o" />您的账户还不具备申请资格
<a style={{ marginLeft: 16 }}>立即升级 <Icon type="right" /></a>
</div>
</div>
);
const actions = <Button type="primary">返回修改</Button>;
ReactDOM.render(
<Result
type="error"
title="提交失败"
description="请核对并修改以下信息后,再重新提交。"
extra={extra}
actions={actions}
/>
, mountNode);
````
---
order: 0
title: Structure
---
结构包含 `处理结果``补充信息` 以及 `操作建议` 三个部分,其中 `处理结果``提示图标``标题``结果描述` 组成。
````jsx
import Result from 'ant-design-pro/lib/Result';
ReactDOM.render(
<Result
type="success"
title={<div style={{ background: '#7dbcea', color: '#fff' }}>标题</div>}
description={<div style={{ background: 'rgba(16, 142, 233, 1)', color: '#fff' }}>结果描述</div>}
extra="其他补充信息,自带灰底效果"
actions={<div style={{ background: '#3ba0e9', color: '#fff' }}>操作建议,一般放置按钮组</div>}
/>
, mountNode);
````
import * as React from 'react';
export interface ResultProps {
type: 'success' | 'error';
title: React.ReactNode;
description?: React.ReactNode;
extra?: React.ReactNode;
actions?: React.ReactNode;
style?: React.CSSProperties;
}
export default class Result extends React.Component<ResultProps, any> {}
import React from 'react';
import classNames from 'classnames';
import { Icon } from 'antd';
import styles from './index.less';
export default function Result({
className, type, title, description, extra, actions, ...restProps
}) {
const iconMap = {
error: <Icon className={styles.error} type="close-circle" />,
success: <Icon className={styles.success} type="check-circle" />,
};
const clsString = classNames(styles.result, className);
return (
<div className={clsString} {...restProps}>
<div className={styles.icon}>{iconMap[type]}</div>
<div className={styles.title}>{title}</div>
{description && <div className={styles.description}>{description}</div>}
{extra && <div className={styles.extra}>{extra}</div>}
{actions && <div className={styles.actions}>{actions}</div>}
</div>
);
}
@import "~antd/lib/style/themes/default.less";
.result {
text-align: center;
width: 72%;
margin: 0 auto;
.icon {
font-size: 72px;
line-height: 72px;
margin-bottom: 24px;
& > .success {
color: @success-color;
}
& > .error {
color: @error-color;
}
}
.title {
font-size: 24px;
color: @heading-color;
font-weight: 500;
line-height: 32px;
margin-bottom: 16px;
}
.description {
font-size: 14px;
line-height: 22px;
color: @text-color-secondary;
margin-bottom: 24px;
}
.extra {
background: #fafafa;
padding: 24px 40px;
border-radius: @border-radius-sm;
text-align: left;
}
.actions {
margin-top: 32px;
button:not(:last-child) {
margin-right: 8px;
}
}
}
---
title:
en-US: Result
zh-CN: Result
subtitle: 处理结果
cols: 1
order: 12
---
结果页用于对用户进行的一系列任务处理结果进行反馈。
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| type | 类型,不同类型自带对应的图标 | Enum {'success', 'error'} | - |
| title | 标题 | ReactNode | - |
| description | 结果描述 | ReactNode | - |
| extra | 补充信息,有默认的灰色背景 | ReactNode | - |
| actions | 操作建议,推荐放置跳转链接,按钮组等 | ReactNode | - |
import React, { PureComponent } from 'react';
import { Layout, Menu, Icon } from 'antd';
import { Link } from 'dva/router';
import styles from './index.less';
const { Sider } = Layout;
const { SubMenu } = Menu;
// Allow menu.js config icon as string or ReactNode
// icon: 'setting',
// icon: 'http://demo.com/icon.png',
// icon: <Icon type="setting" />,
const getIcon = (icon) => {
if (typeof icon === 'string' && icon.indexOf('http') === 0) {
return <img src={icon} alt="icon" className={styles.icon} />;
}
if (typeof icon === 'string') {
return <Icon type={icon} />;
}
return icon;
};
export default class SiderMenu extends PureComponent {
constructor(props) {
super(props);
this.menus = props.menuData;
this.state = {
openKeys: this.getDefaultCollapsedSubMenus(props),
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.location.pathname !== this.props.location.pathname) {
this.setState({
openKeys: this.getDefaultCollapsedSubMenus(nextProps),
});
}
}
getDefaultCollapsedSubMenus(props) {
const { location: { pathname } } = props || this.props;
const snippets = pathname.split('/').slice(1, -1);
const currentPathSnippets = snippets.map((item, index) => {
const arr = snippets.filter((_, i) => i <= index);
return arr.join('/');
});
let currentMenuSelectedKeys = [];
currentPathSnippets.forEach((item) => {
currentMenuSelectedKeys = currentMenuSelectedKeys.concat(this.getSelectedMenuKeys(item));
});
if (currentMenuSelectedKeys.length === 0) {
return ['dashboard'];
}
return currentMenuSelectedKeys;
}
getFlatMenuKeys(menus) {
let keys = [];
menus.forEach((item) => {
if (item.children) {
keys.push(item.path);
keys = keys.concat(this.getFlatMenuKeys(item.children));
} else {
keys.push(item.path);
}
});
return keys;
}
getSelectedMenuKeys = (path) => {
const flatMenuKeys = this.getFlatMenuKeys(this.menus);
if (flatMenuKeys.indexOf(path.replace(/^\//, '')) > -1) {
return [path.replace(/^\//, '')];
}
if (flatMenuKeys.indexOf(path.replace(/^\//, '').replace(/\/$/, '')) > -1) {
return [path.replace(/^\//, '').replace(/\/$/, '')];
}
return flatMenuKeys.filter((item) => {
const itemRegExpStr = `^${item.replace(/:[\w-]+/g, '[\\w-]+')}$`;
const itemRegExp = new RegExp(itemRegExpStr);
return itemRegExp.test(path.replace(/^\//, '').replace(/\/$/, ''));
});
}
/**
* 判断是否是http链接.返回 Link 或 a
* Judge whether it is http link.return a or Link
* @memberof SiderMenu
*/
getMenuItemPath = (item) => {
const itemPath = this.conversionPath(item.path);
const icon = getIcon(item.icon);
const { target, name } = item;
// Is it a http link
if (/^https?:\/\//.test(itemPath)) {
return (
<a href={itemPath} target={target}>
{icon}<span>{name}</span>
</a>
);
}
return (
<Link
to={itemPath}
target={target}
replace={itemPath === this.props.location.pathname}
onClick={this.props.isMobile ? () => { this.props.onCollapse(true); } : undefined}
>
{icon}<span>{name}</span>
</Link>
);
}
/**
* get SubMenu or Item
*/
getSubMenuOrItem=(item) => {
if (item.children && item.children.some(child => child.name)) {
return (
<SubMenu
title={
item.icon ? (
<span>
{getIcon(item.icon)}
<span>{item.name}</span>
</span>
) : item.name
}
key={item.key || item.path}
>
{this.getNavMenuItems(item.children)}
</SubMenu>
);
} else {
return (
<Menu.Item key={item.key || item.path}>
{this.getMenuItemPath(item)}
</Menu.Item>
);
}
}
/**
* 获得菜单子节点
* @memberof SiderMenu
*/
getNavMenuItems = (menusData) => {
if (!menusData) {
return [];
}
return menusData
.filter(item => item.name && !item.hideInMenu)
.map((item) => {
const ItemDom = this.getSubMenuOrItem(item);
return this.checkPermissionItem(item.authority, ItemDom);
})
.filter(item => !!item);
}
// conversion Path
// 转化路径
conversionPath=(path) => {
if (path && path.indexOf('http') === 0) {
return path;
} else {
return `/${path || ''}`.replace(/\/+/g, '/');
}
}
// permission to check
checkPermissionItem = (authority, ItemDom) => {
if (this.props.Authorized && this.props.Authorized.check) {
const { check } = this.props.Authorized;
return check(
authority,
ItemDom
);
}
return ItemDom;
}
handleOpenChange = (openKeys) => {
const lastOpenKey = openKeys[openKeys.length - 1];
const isMainMenu = this.menus.some(
item => lastOpenKey && (item.key === lastOpenKey || item.path === lastOpenKey)
);
this.setState({
openKeys: isMainMenu ? [lastOpenKey] : [...openKeys],
});
}
render() {
const { logo, collapsed, location: { pathname }, onCollapse } = this.props;
const { openKeys } = this.state;
// Don't show popup menu when it is been collapsed
const menuProps = collapsed ? {} : {
openKeys,
};
// if pathname can't match, use the nearest parent's key
let selectedKeys = this.getSelectedMenuKeys(pathname);
if (!selectedKeys.length) {
selectedKeys = [openKeys[openKeys.length - 1]];
}
return (
<Sider
trigger={null}
collapsible
collapsed={collapsed}
breakpoint="md"
onCollapse={onCollapse}
width={256}
className={styles.sider}
>
<div className={styles.logo} key="logo">
<Link to="/">
<img src={logo} alt="logo" />
<h1>Ant Design Pro</h1>
</Link>
</div>
<Menu
key="Menu"
theme="dark"
mode="inline"
{...menuProps}
onOpenChange={this.handleOpenChange}
selectedKeys={selectedKeys}
style={{ padding: '16px 0', width: '100%' }}
>
{this.getNavMenuItems(this.menus)}
</Menu>
</Sider>
);
}
}
import 'rc-drawer-menu/assets/index.css';
import React from 'react';
import DrawerMenu from 'rc-drawer-menu';
import SiderMenu from './SiderMenu';
export default props => (
props.isMobile ? (
<DrawerMenu
parent={null}
level={null}
iconChild={null}
open={!props.collapsed}
onMaskClick={() => { props.onCollapse(true); }}
width="256px"
>
<SiderMenu {...props} collapsed={props.isMobile ? false : props.collapsed} />
</DrawerMenu>
) : <SiderMenu {...props} />
);
@import "~antd/lib/style/themes/default.less";
@ease-in-out-circ: cubic-bezier(.78, .14, .15, .86);
.logo {
height: 64px;
position: relative;
line-height: 64px;
padding-left: (@menu-collapsed-width - 32px) / 2;
transition: all .3s;
background: #002140;
overflow: hidden;
img {
display: inline-block;
vertical-align: middle;
height: 32px;
}
h1 {
color: #fff;
display: inline-block;
vertical-align: middle;
font-size: 20px;
margin: 0 0 0 12px;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
}
}
.sider {
min-height: 100vh;
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
position: relative;
z-index: 10;
}
.icon {
width: 14px;
margin-right: 10px;
}
:global {
.drawer .drawer-content {
background: #001529;
}
}
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
export default ({ title, children, last, block, grid, ...rest }) => {
const cls = classNames(styles.standardFormRow, {
[styles.standardFormRowBlock]: block,
[styles.standardFormRowLast]: last,
[styles.standardFormRowGrid]: grid,
});
return (
<div className={cls} {...rest}>
{
title && (
<div className={styles.label}>
<span>{title}</span>
</div>
)
}
<div className={styles.content}>
{children}
</div>
</div>
);
};
@import "~antd/lib/style/themes/default.less";
.standardFormRow {
border-bottom: 1px dashed @border-color-split;
padding-bottom: 16px;
margin-bottom: 16px;
display: flex;
:global {
.ant-form-item {
margin-right: 24px;
}
.ant-form-item-label label {
color: @text-color;
margin-right: 0;
}
.ant-form-item-label,
.ant-form-item-control {
padding: 0;
line-height: 32px;
}
}
.label {
color: @heading-color;
font-size: @font-size-base;
margin-right: 24px;
flex: 0 0 auto;
text-align: right;
& > span {
display: inline-block;
height: 32px;
line-height: 32px;
&:after {
content: ':';
}
}
}
.content {
flex: 1 1 0;
:global {
.ant-form-item:last-child {
margin-right: 0;
}
}
}
}
.standardFormRowLast {
border: none;
padding-bottom: 0;
margin-bottom: 0;
}
.standardFormRowBlock {
:global {
.ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
}
}
.standardFormRowGrid {
:global {
.ant-form-item,
div.ant-form-item-control-wrapper {
display: block;
}
.ant-form-item-label {
float: left;
}
}
}
import React, { PureComponent, Fragment } from 'react';
import moment from 'moment';
import { Table, Alert, Badge, Divider } from 'antd';
import styles from './index.less';
const statusMap = ['default', 'processing', 'success', 'error'];
class StandardTable extends PureComponent {
state = {
selectedRowKeys: [],
totalCallNo: 0,
};
componentWillReceiveProps(nextProps) {
// clean state
if (nextProps.selectedRows.length === 0) {
this.setState({
selectedRowKeys: [],
totalCallNo: 0,
});
}
}
handleRowSelectChange = (selectedRowKeys, selectedRows) => {
const totalCallNo = selectedRows.reduce((sum, val) => {
return sum + parseFloat(val.callNo, 10);
}, 0);
if (this.props.onSelectRow) {
this.props.onSelectRow(selectedRows);
}
this.setState({ selectedRowKeys, totalCallNo });
}
handleTableChange = (pagination, filters, sorter) => {
this.props.onChange(pagination, filters, sorter);
}
cleanSelectedKeys = () => {
this.handleRowSelectChange([], []);
}
render() {
const { selectedRowKeys, totalCallNo } = this.state;
const { data: { list, pagination }, loading } = this.props;
const status = ['关闭', '运行中', '已上线', '异常'];
const columns = [
{
title: '规则编号',
dataIndex: 'no',
},
{
title: '描述',
dataIndex: 'description',
},
{
title: '服务调用次数',
dataIndex: 'callNo',
sorter: true,
align: 'right',
render: val => `${val} 万`,
},
{
title: '状态',
dataIndex: 'status',
filters: [
{
text: status[0],
value: 0,
},
{
text: status[1],
value: 1,
},
{
text: status[2],
value: 2,
},
{
text: status[3],
value: 3,
},
],
render(val) {
return <Badge status={statusMap[val]} text={status[val]} />;
},
},
{
title: '更新时间',
dataIndex: 'updatedAt',
sorter: true,
render: val => <span>{moment(val).format('YYYY-MM-DD HH:mm:ss')}</span>,
},
{
title: '操作',
render: () => (
<Fragment>
<a href="">配置</a>
<Divider type="vertical" />
<a href="">订阅警报</a>
</Fragment>
),
},
];
const paginationProps = {
showSizeChanger: true,
showQuickJumper: true,
...pagination,
};
const rowSelection = {
selectedRowKeys,
onChange: this.handleRowSelectChange,
getCheckboxProps: record => ({
disabled: record.disabled,
}),
};
return (
<div className={styles.standardTable}>
<div className={styles.tableAlert}>
<Alert
message={(
<div>
已选择 <a style={{ fontWeight: 600 }}>{selectedRowKeys.length}</a> 项&nbsp;&nbsp;
服务调用总计 <span style={{ fontWeight: 600 }}>{totalCallNo}</span>
<a onClick={this.cleanSelectedKeys} style={{ marginLeft: 24 }}>清空</a>
</div>
)}
type="info"
showIcon
/>
</div>
<Table
loading={loading}
rowKey={record => record.key}
rowSelection={rowSelection}
dataSource={list}
columns={columns}
pagination={paginationProps}
onChange={this.handleTableChange}
/>
</div>
);
}
}
export default StandardTable;
@import "~antd/lib/style/themes/default.less";
.standardTable {
:global {
.ant-table-pagination {
margin-top: 24px;
}
}
.tableAlert {
margin-bottom: 16px;
}
}
---
order: 1
title: 可展开和收起
---
使用 `expandable` 属性,让标签组可以收起,避免过高。
````jsx
import TagSelect from 'ant-design-pro/lib/TagSelect';
function handleFormSubmit(checkedValue) {
console.log(checkedValue);
}
ReactDOM.render(
<TagSelect onChange={handleFormSubmit} expandable>
<TagSelect.Option value="cat1">类目一</TagSelect.Option>
<TagSelect.Option value="cat2">类目二</TagSelect.Option>
<TagSelect.Option value="cat3">类目三</TagSelect.Option>
<TagSelect.Option value="cat4">类目四</TagSelect.Option>
<TagSelect.Option value="cat5">类目五</TagSelect.Option>
<TagSelect.Option value="cat6">类目六</TagSelect.Option>
<TagSelect.Option value="cat7">类目七</TagSelect.Option>
<TagSelect.Option value="cat8">类目八</TagSelect.Option>
<TagSelect.Option value="cat9">类目九</TagSelect.Option>
<TagSelect.Option value="cat10">类目十</TagSelect.Option>
<TagSelect.Option value="cat11">类目十一</TagSelect.Option>
<TagSelect.Option value="cat12">类目十二</TagSelect.Option>
</TagSelect>
, mountNode);
````
---
order: 0
title: 基础样例
---
结合 `Tag``TagSelect` 组件,方便的应用于筛选类目的业务场景中。
````jsx
import TagSelect from 'ant-design-pro/lib/TagSelect';
function handleFormSubmit(checkedValue) {
console.log(checkedValue);
}
ReactDOM.render(
<TagSelect onChange={handleFormSubmit}>
<TagSelect.Option value="cat1">类目一</TagSelect.Option>
<TagSelect.Option value="cat2">类目二</TagSelect.Option>
<TagSelect.Option value="cat3">类目三</TagSelect.Option>
<TagSelect.Option value="cat4">类目四</TagSelect.Option>
<TagSelect.Option value="cat5">类目五</TagSelect.Option>
<TagSelect.Option value="cat6">类目六</TagSelect.Option>
</TagSelect>
, mountNode);
````
import * as React from 'react';
export interface TagSelectProps {
onChange?: (value: Array<string>) => void;
expandable?: boolean;
style?: React.CSSProperties;
}
export interface TagSelectOptionProps {
value: string;
style?: React.CSSProperties;
}
export class TagSelectOption extends React.Component<
TagSelectOptionProps,
any
> {}
export default class TagSelect extends React.Component<TagSelectProps, any> {
static Option: typeof TagSelectOption;
children:
| React.ReactElement<TagSelectOption>
| Array<React.ReactElement<TagSelectOption>>;
}
import React, { Component } from 'react';
import classNames from 'classnames';
import { Tag, Icon } from 'antd';
import styles from './index.less';
const { CheckableTag } = Tag;
const TagSelectOption = ({ children, checked, onChange, value }) => (
<CheckableTag
checked={checked}
key={value}
onChange={state => onChange(value, state)}
>
{children}
</CheckableTag>
);
TagSelectOption.isTagSelectOption = true;
class TagSelect extends Component {
state = {
expand: false,
checkedTags: this.props.defaultValue || [],
};
onSelectAll = (checked) => {
const { onChange } = this.props;
let checkedTags = [];
if (checked) {
checkedTags = this.getAllTags();
}
this.setState({ checkedTags });
if (onChange) {
onChange(checkedTags);
}
}
getAllTags() {
let { children } = this.props;
children = React.Children.toArray(children);
const checkedTags = children
.filter(child => this.isTagSelectOption(child))
.map(child => child.props.value);
return checkedTags;
}
handleTagChange = (value, checked) => {
const { onChange } = this.props;
const { checkedTags } = this.state;
const index = checkedTags.indexOf(value);
if (checked && index === -1) {
checkedTags.push(value);
} else if (!checked && index > -1) {
checkedTags.splice(index, 1);
}
this.setState({ checkedTags });
if (onChange) {
onChange(checkedTags);
}
}
handleExpand = () => {
this.setState({
expand: !this.state.expand,
});
}
isTagSelectOption = (node) => {
return node && node.type && (
node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption'
);
}
render() {
const { checkedTags, expand } = this.state;
const { children, className, style, expandable } = this.props;
const checkedAll = this.getAllTags().length === checkedTags.length;
const cls = classNames(styles.tagSelect, className, {
[styles.hasExpandTag]: expandable,
[styles.expanded]: expand,
});
return (
<div className={cls} style={style}>
<CheckableTag
checked={checkedAll}
key="tag-select-__all__"
onChange={this.onSelectAll}
>
全部
</CheckableTag>
{
checkedTags && React.Children.map(children, (child) => {
if (this.isTagSelectOption(child)) {
return React.cloneElement(child, {
key: `tag-select-${child.props.value}`,
checked: checkedTags.indexOf(child.props.value) > -1,
onChange: this.handleTagChange,
});
}
return child;
})
}
{
expandable && (
<a className={styles.trigger} onClick={this.handleExpand}>
{expand ? '收起' : '展开'} <Icon type={expand ? 'up' : 'down'} />
</a>
)
}
</div>
);
}
}
TagSelect.Option = TagSelectOption;
export default TagSelect;
@import "~antd/lib/style/themes/default.less";
.tagSelect {
user-select: none;
margin-left: -8px;
position: relative;
overflow: hidden;
max-height: 32px;
line-height: 32px;
transition: all .3s;
:global {
.ant-tag {
padding: 0 8px;
margin-right: 24px;
font-size: @font-size-base;
}
}
&.expanded {
transition: all .3s;
max-height: 200px;
}
.trigger {
position: absolute;
top: 0;
right: 0;
i {
font-size: 12px;
}
}
&.hasExpandTag {
padding-right: 50px;
}
}
---
title:
en-US: TagSelect
zh-CN: TagSelect
subtitle: 标签选择器
cols: 1
order: 13
---
可进行多选,带折叠收起和展开更多功能,常用于对列表进行筛选。
## API
### TagSelect
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| onChange | 标签选择的回调函数 | Function(checkedTags) | |
| expandable | 是否展示 `展开/收起` 按钮 | Boolean | false |
### TagSelectOption
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| value | TagSelect的值 | Function(checkedTags) | - |
| children | tag的内容 | string \| ReactNode | - |
---
order: 0
title: 演示
---
在数值背后添加一个小图标来标识涨跌情况。
````jsx
import Trend from 'ant-design-pro/lib/Trend';
ReactDOM.render(
<div>
<Trend flag="up">12%</Trend>
<Trend flag="down" style={{ marginLeft: 8 }}>11%</Trend>
</div>
, mountNode);
````
import * as React from 'react';
export interface TrendProps {
colorful?: boolean;
flag: 'up' | 'down';
style?: React.CSSProperties;
}
export default class Trend extends React.Component<TrendProps, any> {}
import React from 'react';
import { Icon } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
const Trend = ({ colorful = true, flag, children, className, ...rest }) => {
const classString = classNames(styles.trendItem, {
[styles.trendItemGrey]: !colorful,
}, className);
return (
<div
{...rest}
className={classString}
title={typeof children === 'string' ? children : ''}
>
<span className={styles.value}>{children}</span>
{flag && <span className={styles[flag]}><Icon type={`caret-${flag}`} /></span>}
</div>
);
};
export default Trend;
@import "~antd/lib/style/themes/default.less";
.trendItem {
display: inline-block;
font-size: @font-size-base;
line-height: 22px;
.up,
.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(0.83);
}
}
.up {
color: @red-6;
}
.down {
color: @green-6;
top: -1px;
}
&.trendItemGrey .up,
&.trendItemGrey .down {
color: @text-color;
}
}
---
title:
en-US: Trend
zh-CN: Trend
subtitle: 趋势标记
cols: 1
order: 14
---
趋势符号,标记上升和下降趋势。通常用绿色代表“好”,红色代表“不好”,股票涨跌场景除外。
## API
```html
<Trend flag="up">50%</Trend>
```
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| colorful | 是否彩色标记 | Boolean | true |
| flag | 上升下降标识:`up|down` | string | - |
import Nightmare from 'nightmare';
describe('Homepage', () => {
it('it should have logo text', async () => {
const page = Nightmare().goto('http://localhost:8000');
const text = await page.wait('h1').evaluate(() => document.body.innerHTML).end();
expect(text).toContain('<h1>Ant Design Pro</h1>');
});
});
import Nightmare from 'nightmare';
describe('Login', () => {
let page;
beforeEach(() => {
page = Nightmare();
page
.goto('http://localhost:8000/')
.evaluate(() => {
window.localStorage.setItem('antd-pro-authority', 'guest');
})
.goto('http://localhost:8000/#/user/login');
});
it('should login with failure', async () => {
await page.type('#userName', 'mockuser')
.type('#password', 'wrong_password')
.click('button[type="submit"]')
.wait('.ant-alert-error') // should display error
.end();
});
it('should login successfully', async () => {
const text = await page.type('#userName', 'admin')
.type('#password', '888888')
.click('button[type="submit"]')
.wait('.ant-layout-sider h1') // should display error
.evaluate(() => document.body.innerHTML)
.end();
expect(text).toContain('<h1>Ant Design Pro</h1>');
});
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ant Design Pro</title>
<link rel="icon" href="/favicon.png" type="image/x-icon">
</head>
<body>
<div id="root"></div>
</body>
</html>
import '@babel/polyfill';
import 'url-polyfill';
import dva from 'dva';
import createHistory from 'history/createHashHistory';
// user BrowserHistory
// import createHistory from 'history/createBrowserHistory';
import createLoading from 'dva-loading';
import 'moment/locale/zh-cn';
import FastClick from 'fastclick';
import './rollbar';
import './index.less';
// 1. Initialize
const app = dva({
history: createHistory(),
});
// 2. Plugins
app.use(createLoading());
// 3. Register global model
app.model(require('./models/global').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
FastClick.attach(document.body);
export default app._store; // eslint-disable-line
html,
body,
:global(#root) {
height: 100%;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.globalSpin {
width: 100%;
margin: 40px 0 !important;
}
// temp fix for https://github.com/ant-design/ant-design/commit/a1fafb5b727b62cb0be29ce6e9eca8f579d4f8b7
:global {
.ant-spin-container {
overflow: visible !important;
}
}
import React from 'react';
import PropTypes from 'prop-types';
import { Layout, Icon, message } from 'antd';
import DocumentTitle from 'react-document-title';
import { connect } from 'dva';
import { Route, Redirect, Switch, routerRedux } from 'dva/router';
import { ContainerQuery } from 'react-container-query';
import classNames from 'classnames';
import { enquireScreen } from 'enquire-js';
import GlobalHeader from '../components/GlobalHeader';
import GlobalFooter from '../components/GlobalFooter';
import SiderMenu from '../components/SiderMenu';
import NotFound from '../routes/Exception/404';
import { getRoutes } from '../utils/utils';
import Authorized from '../utils/Authorized';
import { getMenuData } from '../common/menu';
import logo from '../assets/logo.svg';
const { Content } = Layout;
const { AuthorizedRoute } = Authorized;
/**
* 根据菜单取得重定向地址.
*/
const redirectData = [];
const getRedirect = (item) => {
if (item && item.children) {
if (item.children[0] && item.children[0].path) {
redirectData.push({
from: `/${item.path}`,
to: `/${item.children[0].path}`,
});
item.children.forEach((children) => {
getRedirect(children);
});
}
}
};
getMenuData().forEach(getRedirect);
const query = {
'screen-xs': {
maxWidth: 575,
},
'screen-sm': {
minWidth: 576,
maxWidth: 767,
},
'screen-md': {
minWidth: 768,
maxWidth: 991,
},
'screen-lg': {
minWidth: 992,
maxWidth: 1199,
},
'screen-xl': {
minWidth: 1200,
},
};
let isMobile;
enquireScreen((b) => {
isMobile = b;
});
class BasicLayout extends React.PureComponent {
static childContextTypes = {
location: PropTypes.object,
breadcrumbNameMap: PropTypes.object,
}
state = {
isMobile,
};
getChildContext() {
const { location, routerData } = this.props;
return {
location,
breadcrumbNameMap: routerData,
};
}
componentDidMount() {
enquireScreen((mobile) => {
this.setState({
isMobile: mobile,
});
});
this.props.dispatch({
type: 'user/fetchCurrent',
});
}
getPageTitle() {
const { routerData, location } = this.props;
const { pathname } = location;
let title = 'Ant Design Pro';
if (routerData[pathname] && routerData[pathname].name) {
title = `${routerData[pathname].name} - Ant Design Pro`;
}
return title;
}
getBashRedirect = () => {
// According to the url parameter to redirect
// 这里是重定向的,重定向到 url 的 redirect 参数所示地址
const urlParams = new URL(window.location.href);
const redirect = urlParams.searchParams.get('redirect') || '/dashboard/analysis';
// Remove the parameters in the url
urlParams.searchParams.delete('redirect');
window.history.pushState(null, 'redirect', urlParams.href);
return redirect;
}
handleMenuCollapse = (collapsed) => {
this.props.dispatch({
type: 'global/changeLayoutCollapsed',
payload: collapsed,
});
}
handleNoticeClear = (type) => {
message.success(`清空了${type}`);
this.props.dispatch({
type: 'global/clearNotices',
payload: type,
});
}
handleMenuClick = ({ key }) => {
if (key === 'triggerError') {
this.props.dispatch(routerRedux.push('/exception/trigger'));
return;
}
if (key === 'logout') {
this.props.dispatch({
type: 'login/logout',
});
}
}
handleNoticeVisibleChange = (visible) => {
if (visible) {
this.props.dispatch({
type: 'global/fetchNotices',
});
}
}
render() {
const {
currentUser, collapsed, fetchingNotices, notices, routerData, match, location,
} = this.props;
const bashRedirect = this.getBashRedirect();
const layout = (
<Layout>
<SiderMenu
logo={logo}
// 不带Authorized参数的情况下如果没有权限,会强制跳到403界面
// If you do not have the Authorized parameter
// you will be forced to jump to the 403 interface without permission
Authorized={Authorized}
menuData={getMenuData()}
collapsed={collapsed}
location={location}
isMobile={this.state.isMobile}
onCollapse={this.handleMenuCollapse}
/>
<Layout>
<GlobalHeader
logo={logo}
currentUser={currentUser}
fetchingNotices={fetchingNotices}
notices={notices}
collapsed={collapsed}
isMobile={this.state.isMobile}
onNoticeClear={this.handleNoticeClear}
onCollapse={this.handleMenuCollapse}
onMenuClick={this.handleMenuClick}
onNoticeVisibleChange={this.handleNoticeVisibleChange}
/>
<Content style={{ margin: '24px 24px 0', height: '100%' }}>
<div style={{ minHeight: 'calc(100vh - 260px)' }}>
<Switch>
{
redirectData.map(item =>
<Redirect key={item.from} exact from={item.from} to={item.to} />
)
}
{
getRoutes(match.path, routerData).map(item =>
(
<AuthorizedRoute
key={item.key}
path={item.path}
component={item.component}
exact={item.exact}
authority={item.authority}
redirectPath="/exception/403"
/>
)
)
}
<Redirect exact from="/" to={bashRedirect} />
<Route render={NotFound} />
</Switch>
</div>
<GlobalFooter
links={[{
key: 'Pro 首页',
title: 'Pro 首页',
href: 'http://pro.ant.design',
blankTarget: true,
}, {
key: 'github',
title: <Icon type="github" />,
href: 'https://github.com/ant-design/ant-design-pro',
blankTarget: true,
}, {
key: 'Ant Design',
title: 'Ant Design',
href: 'http://ant.design',
blankTarget: true,
}]}
copyright={
<div>
Copyright <Icon type="copyright" /> 2018 蚂蚁金服体验技术部出品
</div>
}
/>
</Content>
</Layout>
</Layout>
);
return (
<DocumentTitle title={this.getPageTitle()}>
<ContainerQuery query={query}>
{params => <div className={classNames(params)}>{layout}</div>}
</ContainerQuery>
</DocumentTitle>
);
}
}
export default connect(({ user, global, loading }) => ({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingNotices: loading.effects['global/fetchNotices'],
notices: global.notices,
}))(BasicLayout);
import React from 'react';
export default props => <div {...props} />;
import React from 'react';
import { Link } from 'dva/router';
import PageHeader from '../components/PageHeader';
import styles from './PageHeaderLayout.less';
export default ({ children, wrapperClassName, top, ...restProps }) => (
<div style={{ margin: '-24px -24px 0' }} className={wrapperClassName}>
{top}
<PageHeader key="pageheader" {...restProps} linkElement={Link} />
{children ? <div className={styles.content}>{children}</div> : null}
</div>
);
@import "~antd/lib/style/themes/default.less";
.content {
margin: 24px 24px 0;
}
@media screen and (max-width: @screen-sm) {
.content {
margin: 24px 0 0;
}
}
import React from 'react';
import { Link, Redirect, Switch, Route } from 'dva/router';
import DocumentTitle from 'react-document-title';
import { Icon } from 'antd';
import GlobalFooter from '../components/GlobalFooter';
import styles from './UserLayout.less';
import logo from '../assets/logo.svg';
import { getRoutes } from '../utils/utils';
const links = [{
key: 'help',
title: '帮助',
href: '',
}, {
key: 'privacy',
title: '隐私',
href: '',
}, {
key: 'terms',
title: '条款',
href: '',
}];
const copyright = <div>Copyright <Icon type="copyright" /> 2018 蚂蚁金服体验技术部出品</div>;
class UserLayout extends React.PureComponent {
getPageTitle() {
const { routerData, location } = this.props;
const { pathname } = location;
let title = 'Ant Design Pro';
if (routerData[pathname] && routerData[pathname].name) {
title = `${routerData[pathname].name} - Ant Design Pro`;
}
return title;
}
render() {
const { routerData, match } = this.props;
return (
<DocumentTitle title={this.getPageTitle()}>
<div className={styles.container}>
<div className={styles.top}>
<div className={styles.header}>
<Link to="/">
<img alt="logo" className={styles.logo} src={logo} />
<span className={styles.title}>Ant Design</span>
</Link>
</div>
<div className={styles.desc}>Ant Design 是西湖区最具影响力的 Web 设计规范</div>
</div>
<Switch>
{getRoutes(match.path, routerData).map(item =>
(
<Route
key={item.key}
path={item.path}
component={item.component}
exact={item.exact}
/>
)
)}
<Redirect exact from="/user" to="/user/login" />
</Switch>
<GlobalFooter className={styles.footer} links={links} copyright={copyright} />
</div>
</DocumentTitle>
);
}
}
export default UserLayout;
@import "~antd/lib/style/themes/default.less";
.container {
background: #f0f2f5;
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
width: 100%;
min-height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: 100%;
padding: 110px 0 144px 0;
position: relative;
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 16px;
}
.title {
font-size: 33px;
color: @heading-color;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
top: 2px;
}
.desc {
font-size: @font-size-base;
color: @text-color-secondary;
margin-top: 12px;
margin-bottom: 40px;
}
.footer {
position: absolute;
width: 100%;
bottom: 0;
}
import { queryActivities } from '../services/api';
export default {
namespace: 'activities',
state: {
list: [],
},
effects: {
*fetchList(_, { call, put }) {
const response = yield call(queryActivities);
yield put({
type: 'saveList',
payload: Array.isArray(response) ? response : [],
});
},
},
reducers: {
saveList(state, action) {
return {
...state,
list: action.payload,
};
},
},
};
import { fakeChartData } from '../services/api';
export default {
namespace: 'chart',
state: {
visitData: [],
visitData2: [],
salesData: [],
searchData: [],
offlineData: [],
offlineChartData: [],
salesTypeData: [],
salesTypeDataOnline: [],
salesTypeDataOffline: [],
radarData: [],
loading: false,
},
effects: {
*fetch(_, { call, put }) {
const response = yield call(fakeChartData);
yield put({
type: 'save',
payload: response,
});
},
*fetchSalesData(_, { call, put }) {
const response = yield call(fakeChartData);
yield put({
type: 'save',
payload: {
salesData: response.salesData,
},
});
},
},
reducers: {
save(state, { payload }) {
return {
...state,
...payload,
};
},
clear() {
return {
visitData: [],
visitData2: [],
salesData: [],
searchData: [],
offlineData: [],
offlineChartData: [],
salesTypeData: [],
salesTypeDataOnline: [],
salesTypeDataOffline: [],
radarData: [],
};
},
},
};
import { query403, query401, query404, query500 } from '../services/error';
export default {
namespace: 'error',
state: {
error: '',
isloading: false,
},
effects: {
*query403(_, { call, put }) {
yield call(query403);
yield put({
type: 'trigger',
payload: '403',
});
},
*query401(_, { call, put }) {
yield call(query401);
yield put({
type: 'trigger',
payload: '401',
});
},
*query500(_, { call, put }) {
yield call(query500);
yield put({
type: 'trigger',
payload: '500',
});
},
*query404(_, { call, put }) {
yield call(query404);
yield put({
type: 'trigger',
payload: '404',
});
},
},
reducers: {
trigger(state, action) {
return {
error: action.payload,
};
},
},
};
import { routerRedux } from 'dva/router';
import { message } from 'antd';
import { fakeSubmitForm } from '../services/api';
export default {
namespace: 'form',
state: {
step: {
payAccount: 'ant-design@alipay.com',
receiverAccount: 'test@example.com',
receiverName: 'Alex',
amount: '500',
},
},
effects: {
*submitRegularForm({ payload }, { call }) {
yield call(fakeSubmitForm, payload);
message.success('提交成功');
},
*submitStepForm({ payload }, { call, put }) {
yield call(fakeSubmitForm, payload);
yield put({
type: 'saveStepFormData',
payload,
});
yield put(routerRedux.push('/form/step-form/result'));
},
*submitAdvancedForm({ payload }, { call }) {
yield call(fakeSubmitForm, payload);
message.success('提交成功');
},
},
reducers: {
saveStepFormData(state, { payload }) {
return {
...state,
step: {
...state.step,
...payload,
},
};
},
},
};
import { queryNotices } from '../services/api';
export default {
namespace: 'global',
state: {
collapsed: false,
notices: [],
},
effects: {
*fetchNotices(_, { call, put }) {
const data = yield call(queryNotices);
yield put({
type: 'saveNotices',
payload: data,
});
yield put({
type: 'user/changeNotifyCount',
payload: data.length,
});
},
*clearNotices({ payload }, { put, select }) {
yield put({
type: 'saveClearedNotices',
payload,
});
const count = yield select(state => state.global.notices.length);
yield put({
type: 'user/changeNotifyCount',
payload: count,
});
},
},
reducers: {
changeLayoutCollapsed(state, { payload }) {
return {
...state,
collapsed: payload,
};
},
saveNotices(state, { payload }) {
return {
...state,
notices: payload,
};
},
saveClearedNotices(state, { payload }) {
return {
...state,
notices: state.notices.filter(item => item.type !== payload),
};
},
},
subscriptions: {
setup({ history }) {
// Subscribe history(url) change, trigger `load` action if pathname is `/`
return history.listen(({ pathname, search }) => {
if (typeof window.ga !== 'undefined') {
window.ga('send', 'pageview', pathname + search);
}
});
},
},
};
// Use require.context to require reducers automatically
// Ref: https://webpack.github.io/docs/context.html
const context = require.context('./', false, /\.js$/);
const keys = context.keys().filter(item => item !== './index.js');
const models = [];
for (let i = 0; i < keys.length; i += 1) {
models.push(context(keys[i]));
}
export default models;
import { queryFakeList } from '../services/api';
export default {
namespace: 'list',
state: {
list: [],
},
effects: {
*fetch({ payload }, { call, put }) {
const response = yield call(queryFakeList, payload);
yield put({
type: 'queryList',
payload: Array.isArray(response) ? response : [],
});
},
*appendFetch({ payload }, { call, put }) {
const response = yield call(queryFakeList, payload);
yield put({
type: 'appendList',
payload: Array.isArray(response) ? response : [],
});
},
},
reducers: {
queryList(state, action) {
return {
...state,
list: action.payload,
};
},
appendList(state, action) {
return {
...state,
list: state.list.concat(action.payload),
};
},
},
};
import { fakeAccountLogin } from '../services/api';
import { setAuthority } from '../utils/authority';
export default {
namespace: 'login',
state: {
status: undefined,
},
effects: {
*login({ payload }, { call, put }) {
const response = yield call(fakeAccountLogin, payload);
yield put({
type: 'changeLoginStatus',
payload: response,
});
// Login successfully
if (response.status === 'ok') {
// 非常粗暴的跳转,登陆成功之后权限会变成user或admin,会自动重定向到主页
// Login success after permission changes to admin or user
// The refresh will automatically redirect to the home page
// yield put(routerRedux.push('/'));
window.location.reload();
}
},
*logout(_, { put, select }) {
try {
// get location pathname
const urlParams = new URL(window.location.href);
const pathname = yield select(state => state.routing.location.pathname);
// add the parameters in the url
urlParams.searchParams.set('redirect', pathname);
window.history.pushState(null, 'login', urlParams.href);
} finally {
// yield put(routerRedux.push('/user/login'));
// Login out after permission changes to admin or user
// The refresh will automatically redirect to the login page
yield put({
type: 'changeLoginStatus',
payload: {
status: false,
currentAuthority: 'guest',
},
});
window.location.reload();
}
},
},
reducers: {
changeLoginStatus(state, { payload }) {
setAuthority(payload.currentAuthority);
return {
...state,
status: payload.status,
type: payload.type,
};
},
},
};
import { queryTags } from '../services/api';
export default {
namespace: 'monitor',
state: {
tags: [],
},
effects: {
*fetchTags(_, { call, put }) {
const response = yield call(queryTags);
yield put({
type: 'saveTags',
payload: response.list,
});
},
},
reducers: {
saveTags(state, action) {
return {
...state,
tags: action.payload,
};
},
},
};
import { queryBasicProfile, queryAdvancedProfile } from '../services/api';
export default {
namespace: 'profile',
state: {
basicGoods: [],
advancedOperation1: [],
advancedOperation2: [],
advancedOperation3: [],
},
effects: {
*fetchBasic(_, { call, put }) {
const response = yield call(queryBasicProfile);
yield put({
type: 'show',
payload: response,
});
},
*fetchAdvanced(_, { call, put }) {
const response = yield call(queryAdvancedProfile);
yield put({
type: 'show',
payload: response,
});
},
},
reducers: {
show(state, { payload }) {
return {
...state,
...payload,
};
},
},
};
import { queryProjectNotice } from '../services/api';
export default {
namespace: 'project',
state: {
notice: [],
},
effects: {
*fetchNotice(_, { call, put }) {
const response = yield call(queryProjectNotice);
yield put({
type: 'saveNotice',
payload: Array.isArray(response) ? response : [],
});
},
},
reducers: {
saveNotice(state, action) {
return {
...state,
notice: action.payload,
};
},
},
};
import { fakeRegister } from '../services/api';
export default {
namespace: 'register',
state: {
status: undefined,
},
effects: {
*submit(_, { call, put }) {
const response = yield call(fakeRegister);
yield put({
type: 'registerHandle',
payload: response,
});
},
},
reducers: {
registerHandle(state, { payload }) {
return {
...state,
status: payload.status,
};
},
},
};
import { queryRule, removeRule, addRule } from '../services/api';
export default {
namespace: 'rule',
state: {
data: {
list: [],
pagination: {},
},
},
effects: {
*fetch({ payload }, { call, put }) {
const response = yield call(queryRule, payload);
yield put({
type: 'save',
payload: response,
});
},
*add({ payload, callback }, { call, put }) {
const response = yield call(addRule, payload);
yield put({
type: 'save',
payload: response,
});
if (callback) callback();
},
*remove({ payload, callback }, { call, put }) {
const response = yield call(removeRule, payload);
yield put({
type: 'save',
payload: response,
});
if (callback) callback();
},
},
reducers: {
save(state, action) {
return {
...state,
data: action.payload,
};
},
},
};
import { query as queryUsers, queryCurrent } from '../services/user';
export default {
namespace: 'user',
state: {
list: [],
currentUser: {},
},
effects: {
*fetch(_, { call, put }) {
const response = yield call(queryUsers);
yield put({
type: 'save',
payload: response,
});
},
*fetchCurrent(_, { call, put }) {
const response = yield call(queryCurrent);
yield put({
type: 'saveCurrentUser',
payload: response,
});
},
},
reducers: {
save(state, action) {
return {
...state,
list: action.payload,
};
},
saveCurrentUser(state, action) {
return {
...state,
currentUser: action.payload,
};
},
changeNotifyCount(state, action) {
return {
...state,
currentUser: {
...state.currentUser,
notifyCount: action.payload,
},
};
},
},
};
import Rollbar from 'rollbar';
// Track error by rollbar.com
if (location.host === 'preview.pro.ant.design') {
Rollbar.init({
accessToken: '033ca6d7c0eb4cc1831cf470c2649971',
captureUncaught: true,
captureUnhandledRejections: true,
payload: {
environment: 'production',
},
});
}
import React from 'react';
import { routerRedux, Switch } from 'dva/router';
import { LocaleProvider, Spin } from 'antd';
import zhCN from 'antd/lib/locale-provider/zh_CN';
import dynamic from 'dva/dynamic';
import { getRouterData } from './common/router';
import Authorized from './utils/Authorized';
import styles from './index.less';
const { ConnectedRouter } = routerRedux;
const { AuthorizedRoute } = Authorized;
dynamic.setDefaultLoadingComponent(() => {
return <Spin size="large" className={styles.globalSpin} />;
});
function RouterConfig({ history, app }) {
const routerData = getRouterData(app);
const UserLayout = routerData['/user'].component;
const BasicLayout = routerData['/'].component;
return (
<LocaleProvider locale={zhCN}>
<ConnectedRouter history={history}>
<Switch>
<AuthorizedRoute
path="/user"
render={props => <UserLayout {...props} />}
authority="guest"
redirectPath="/"
/>
<AuthorizedRoute
path="/"
render={props => <BasicLayout {...props} />}
authority={['admin', 'user']}
redirectPath="/user/login"
/>
</Switch>
</ConnectedRouter>
</LocaleProvider>
);
}
export default RouterConfig;
import React, { Component } from 'react';
import { connect } from 'dva';
import {
Row,
Col,
Icon,
Card,
Tabs,
Table,
Radio,
DatePicker,
Tooltip,
Menu,
Dropdown,
} from 'antd';
import numeral from 'numeral';
import {
ChartCard,
yuan,
MiniArea,
MiniBar,
MiniProgress,
Field,
Bar,
Pie,
TimelineChart,
} from '../../components/Charts';
import Trend from '../../components/Trend';
import NumberInfo from '../../components/NumberInfo';
import { getTimeDistance } from '../../utils/utils';
import styles from './Analysis.less';
const { TabPane } = Tabs;
const { RangePicker } = DatePicker;
const rankingListData = [];
for (let i = 0; i < 7; i += 1) {
rankingListData.push({
title: `工专路 ${i} 号店`,
total: 323234,
});
}
@connect(({ chart, loading }) => ({
chart,
loading: loading.effects['chart/fetch'],
}))
export default class Analysis extends Component {
state = {
salesType: 'all',
currentTabKey: '',
rangePickerValue: getTimeDistance('year'),
};
componentDidMount() {
this.props.dispatch({
type: 'chart/fetch',
});
}
componentWillUnmount() {
const { dispatch } = this.props;
dispatch({
type: 'chart/clear',
});
}
handleChangeSalesType = (e) => {
this.setState({
salesType: e.target.value,
});
};
handleTabChange = (key) => {
this.setState({
currentTabKey: key,
});
};
handleRangePickerChange = (rangePickerValue) => {
this.setState({
rangePickerValue,
});
this.props.dispatch({
type: 'chart/fetchSalesData',
});
};
selectDate = (type) => {
this.setState({
rangePickerValue: getTimeDistance(type),
});
this.props.dispatch({
type: 'chart/fetchSalesData',
});
};
isActive(type) {
const { rangePickerValue } = this.state;
const value = getTimeDistance(type);
if (!rangePickerValue[0] || !rangePickerValue[1]) {
return;
}
if (
rangePickerValue[0].isSame(value[0], 'day') &&
rangePickerValue[1].isSame(value[1], 'day')
) {
return styles.currentDate;
}
}
render() {
const { rangePickerValue, salesType, currentTabKey } = this.state;
const { chart, loading } = this.props;
const {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
} = chart;
const salesPieData =
salesType === 'all'
? salesTypeData
: salesType === 'online' ? salesTypeDataOnline : salesTypeDataOffline;
const menu = (
<Menu>
<Menu.Item>操作一</Menu.Item>
<Menu.Item>操作二</Menu.Item>
</Menu>
);
const iconGroup = (
<span className={styles.iconGroup}>
<Dropdown overlay={menu} placement="bottomRight">
<Icon type="ellipsis" />
</Dropdown>
</span>
);
const salesExtra = (
<div className={styles.salesExtraWrap}>
<div className={styles.salesExtra}>
<a className={this.isActive('today')} onClick={() => this.selectDate('today')}>
今日
</a>
<a className={this.isActive('week')} onClick={() => this.selectDate('week')}>
本周
</a>
<a className={this.isActive('month')} onClick={() => this.selectDate('month')}>
本月
</a>
<a className={this.isActive('year')} onClick={() => this.selectDate('year')}>
全年
</a>
</div>
<RangePicker
value={rangePickerValue}
onChange={this.handleRangePickerChange}
style={{ width: 256 }}
/>
</div>
);
const columns = [
{
title: '排名',
dataIndex: 'index',
key: 'index',
},
{
title: '搜索关键词',
dataIndex: 'keyword',
key: 'keyword',
render: text => <a href="/">{text}</a>,
},
{
title: '用户数',
dataIndex: 'count',
key: 'count',
sorter: (a, b) => a.count - b.count,
className: styles.alignRight,
},
{
title: '周涨幅',
dataIndex: 'range',
key: 'range',
sorter: (a, b) => a.range - b.range,
render: (text, record) => (
<Trend flag={record.status === 1 ? 'down' : 'up'}>
<span style={{ marginRight: 4 }}>{text}%</span>
</Trend>
),
align: 'right',
},
];
const activeKey = currentTabKey || (offlineData[0] && offlineData[0].name);
const CustomTab = ({ data, currentTabKey: currentKey }) => (
<Row gutter={8} style={{ width: 138, margin: '8px 0' }}>
<Col span={12}>
<NumberInfo
title={data.name}
subTitle="转化率"
gap={2}
total={`${data.cvr * 100}%`}
theme={currentKey !== data.name && 'light'}
/>
</Col>
<Col span={12} style={{ paddingTop: 36 }}>
<Pie
animate={false}
color={currentKey !== data.name && '#BDE4FF'}
inner={0.55}
tooltip={false}
margin={[0, 0, 0, 0]}
percent={data.cvr * 100}
height={64}
/>
</Col>
</Row>
);
const topColResponsiveProps = {
xs: 24,
sm: 12,
md: 12,
lg: 12,
xl: 6,
style: { marginBottom: 24 },
};
return (
<div>
<Row gutter={24}>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title="总销售额"
action={
<Tooltip title="指标说明">
<Icon type="info-circle-o" />
</Tooltip>
}
total={yuan(126560)}
footer={<Field label="日均销售额" value={`¥${numeral(12423).format('0,0')}`} />}
contentHeight={46}
>
<Trend flag="up" style={{ marginRight: 16 }}>
周同比<span className={styles.trendText}>12%</span>
</Trend>
<Trend flag="down">
日环比<span className={styles.trendText}>11%</span>
</Trend>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title="访问量"
action={
<Tooltip title="指标说明">
<Icon type="info-circle-o" />
</Tooltip>
}
total={numeral(8846).format('0,0')}
footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />}
contentHeight={46}
>
<MiniArea color="#975FE4" data={visitData} />
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title="支付笔数"
action={
<Tooltip title="指标说明">
<Icon type="info-circle-o" />
</Tooltip>
}
total={numeral(6560).format('0,0')}
footer={<Field label="转化率" value="60%" />}
contentHeight={46}
>
<MiniBar data={visitData} />
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title="运营活动效果"
action={
<Tooltip title="指标说明">
<Icon type="info-circle-o" />
</Tooltip>
}
total="78%"
footer={
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
<Trend flag="up" style={{ marginRight: 16 }}>
周同比<span className={styles.trendText}>12%</span>
</Trend>
<Trend flag="down">
日环比<span className={styles.trendText}>11%</span>
</Trend>
</div>
}
contentHeight={46}
>
<MiniProgress percent={78} strokeWidth={8} target={80} color="#13C2C2" />
</ChartCard>
</Col>
</Row>
<Card loading={loading} bordered={false} bodyStyle={{ padding: 0 }}>
<div className={styles.salesCard}>
<Tabs tabBarExtraContent={salesExtra} size="large" tabBarStyle={{ marginBottom: 24 }}>
<TabPane tab="销售额" key="sales">
<Row>
<Col xl={16} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesBar}>
<Bar height={295} title="销售额趋势" data={salesData} />
</div>
</Col>
<Col xl={8} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesRank}>
<h4 className={styles.rankingTitle}>门店销售额排名</h4>
<ul className={styles.rankingList}>
{rankingListData.map((item, i) => (
<li key={item.title}>
<span className={i < 3 ? styles.active : ''}>{i + 1}</span>
<span>{item.title}</span>
<span>{numeral(item.total).format('0,0')}</span>
</li>
))}
</ul>
</div>
</Col>
</Row>
</TabPane>
<TabPane tab="访问量" key="views">
<Row>
<Col xl={16} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesBar}>
<Bar height={292} title="访问量趋势" data={salesData} />
</div>
</Col>
<Col xl={8} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesRank}>
<h4 className={styles.rankingTitle}>门店访问量排名</h4>
<ul className={styles.rankingList}>
{rankingListData.map((item, i) => (
<li key={item.title}>
<span className={i < 3 ? styles.active : ''}>{i + 1}</span>
<span>{item.title}</span>
<span>{numeral(item.total).format('0,0')}</span>
</li>
))}
</ul>
</div>
</Col>
</Row>
</TabPane>
</Tabs>
</div>
</Card>
<Row gutter={24}>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Card
loading={loading}
bordered={false}
title="线上热门搜索"
extra={iconGroup}
style={{ marginTop: 24 }}
>
<Row gutter={68}>
<Col sm={12} xs={24} style={{ marginBottom: 24 }}>
<NumberInfo
subTitle={
<span>
搜索用户数
<Tooltip title="指标文案">
<Icon style={{ marginLeft: 8 }} type="info-circle-o" />
</Tooltip>
</span>
}
gap={8}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
<MiniArea line height={45} data={visitData2} />
</Col>
<Col sm={12} xs={24} style={{ marginBottom: 24 }}>
<NumberInfo
subTitle="人均搜索次数"
total={2.7}
status="down"
subTotal={26.2}
gap={8}
/>
<MiniArea line height={45} data={visitData2} />
</Col>
</Row>
<Table
rowKey={record => record.index}
size="small"
columns={columns}
dataSource={searchData}
pagination={{
style: { marginBottom: 0 },
pageSize: 5,
}}
/>
</Card>
</Col>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Card
loading={loading}
className={styles.salesCard}
bordered={false}
title="销售额类别占比"
bodyStyle={{ padding: 24 }}
extra={
<div className={styles.salesCardExtra}>
{iconGroup}
<div className={styles.salesTypeRadio}>
<Radio.Group value={salesType} onChange={this.handleChangeSalesType}>
<Radio.Button value="all">全部渠道</Radio.Button>
<Radio.Button value="online">线上</Radio.Button>
<Radio.Button value="offline">门店</Radio.Button>
</Radio.Group>
</div>
</div>
}
style={{ marginTop: 24, minHeight: 509 }}
>
<h4 style={{ marginTop: 8, marginBottom: 32 }}>销售额</h4>
<Pie
hasLegend
subTitle="销售额"
total={yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))}
data={salesPieData}
valueFormat={val => yuan(val)}
height={248}
lineWidth={4}
/>
</Card>
</Col>
</Row>
<Card
loading={loading}
className={styles.offlineCard}
bordered={false}
bodyStyle={{ padding: '0 0 32px 0' }}
style={{ marginTop: 32 }}
>
<Tabs activeKey={activeKey} onChange={this.handleTabChange}>
{offlineData.map(shop => (
<TabPane tab={<CustomTab data={shop} currentTabKey={activeKey} />} key={shop.name}>
<div style={{ padding: '0 24px' }}>
<TimelineChart
height={400}
data={offlineChartData}
titleMap={{ y1: '客流量', y2: '支付笔数' }}
/>
</div>
</TabPane>
))}
</Tabs>
</Card>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.iconGroup {
i {
transition: color 0.32s;
color: @text-color-secondary;
cursor: pointer;
margin-left: 16px;
&:hover {
color: @text-color;
}
}
}
.rankingList {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
.clearfix();
margin-top: 16px;
span {
color: @text-color;
font-size: 14px;
line-height: 22px;
}
span:first-child {
background-color: @background-color-base;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 24px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
}
span.active {
//background-color: @primary-color;
background-color: #314659;
color: #fff;
}
span:last-child {
float: right;
}
}
}
.salesExtra {
display: inline-block;
margin-right: 24px;
a {
color: @text-color;
margin-left: 24px;
&:hover {
color: @primary-color;
}
&.currentDate {
color: @primary-color;
}
}
}
.salesCard {
.salesBar {
padding: 0 0 32px 32px;
}
.salesRank {
padding: 0 32px 32px 72px;
}
:global {
.ant-tabs-bar {
padding-left: 16px;
.ant-tabs-nav .ant-tabs-tab {
padding-top: 16px;
padding-bottom: 14px;
line-height: 24px;
}
}
.ant-tabs-extra-content {
padding-right: 24px;
line-height: 55px;
}
.ant-card-head {
position: relative;
}
}
}
.salesCardExtra {
height: 68px;
}
.salesTypeRadio {
position: absolute;
left: 24px;
bottom: 15px;
}
.offlineCard {
:global {
.ant-tabs-ink-bar {
bottom: auto;
}
.ant-tabs-bar {
border-bottom: none;
}
.ant-tabs-nav-container-scrolling {
padding-left: 40px;
padding-right: 40px;
}
.ant-tabs-tab-prev-icon:before {
position: relative;
left: 6px;
}
.ant-tabs-tab-next-icon:before {
position: relative;
right: 6px;
}
}
:global(.ant-tabs-tab-active) h4 {
color: @primary-color;
}
}
.trendText {
margin-left: 8px;
color: @heading-color;
}
@media screen and (max-width: @screen-lg) {
.salesExtra {
display: none;
}
.rankingList {
li {
span:first-child {
margin-right: 8px;
}
}
}
}
@media screen and (max-width: @screen-md) {
.rankingTitle {
margin-top: 16px;
}
.salesCard .salesBar {
padding: 16px;
}
}
@media screen and (max-width: @screen-sm) {
.salesExtraWrap {
display: none;
}
.salesCard {
:global {
.ant-tabs-content {
padding-top: 30px;
}
}
}
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card, Tooltip } from 'antd';
import numeral from 'numeral';
import Authorized from '../../utils/Authorized';
import { Pie, WaterWave, Gauge, TagCloud } from '../../components/Charts';
import NumberInfo from '../../components/NumberInfo';
import CountDown from '../../components/CountDown';
import ActiveChart from '../../components/ActiveChart';
import styles from './Monitor.less';
const { Secured } = Authorized;
const targetTime = new Date().getTime() + 3900000;
// use permission as a parameter
const havePermissionAsync = new Promise((resolve) => {
// Call resolve on behalf of passed
setTimeout(() => resolve(), 1000);
});
@Secured(havePermissionAsync)
@connect(({ monitor, loading }) => ({
monitor,
loading: loading.models.monitor,
}))
export default class Monitor extends PureComponent {
componentDidMount() {
this.props.dispatch({
type: 'monitor/fetchTags',
});
}
render() {
const { monitor, loading } = this.props;
const { tags } = monitor;
return (
<div>
<Row gutter={24}>
<Col xl={18} lg={24} md={24} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card title="活动实时交易情况" bordered={false}>
<Row>
<Col md={6} sm={12} xs={24}>
<NumberInfo
subTitle="今日交易总额"
suffix="元"
total={numeral(124543233).format('0,0')}
/>
</Col>
<Col md={6} sm={12} xs={24}>
<NumberInfo
subTitle="销售目标完成率"
total="92%"
/>
</Col>
<Col md={6} sm={12} xs={24}>
<NumberInfo subTitle="活动剩余时间" total={<CountDown target={targetTime} />} />
</Col>
<Col md={6} sm={12} xs={24}>
<NumberInfo
subTitle="每秒交易总额"
suffix="元"
total={numeral(234).format('0,0')}
/>
</Col>
</Row>
<div className={styles.mapChart}>
<Tooltip title="等待后期实现">
<img src="https://gw.alipayobjects.com/zos/rmsportal/HBWnDEUXCnGnGrRfrpKa.png" alt="map" />
</Tooltip>
</div>
</Card>
</Col>
<Col xl={6} lg={24} md={24} sm={24} xs={24}>
<Card title="活动情况预测" style={{ marginBottom: 24 }} bordered={false}>
<ActiveChart />
</Card>
<Card
title="券核效率"
style={{ marginBottom: 24 }}
bodyStyle={{ textAlign: 'center' }}
bordered={false}
>
<Gauge
format={(val) => {
switch (parseInt(val, 10)) {
case 20:
return '差';
case 40:
return '中';
case 60:
return '良';
case 80:
return '优';
default:
return '';
}
}}
title="跳出率"
height={180}
percent={87}
/>
</Card>
</Col>
</Row>
<Row gutter={24}>
<Col xl={12} lg={24} sm={24} xs={24}>
<Card
title="各品类占比"
style={{ marginBottom: 24 }}
bordered={false}
className={styles.pieCard}
>
<Row style={{ padding: '16px 0' }}>
<Col span={8}>
<Pie
animate={false}
percent={28}
subTitle="中式快餐"
total="28%"
height={128}
lineWidth={2}
/>
</Col>
<Col span={8}>
<Pie
animate={false}
color="#5DDECF"
percent={22}
subTitle="西餐"
total="22%"
height={128}
lineWidth={2}
/>
</Col>
<Col span={8}>
<Pie
animate={false}
color="#2FC25B"
percent={32}
subTitle="火锅"
total="32%"
height={128}
lineWidth={2}
/>
</Col>
</Row>
</Card>
</Col>
<Col xl={6} lg={12} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card title="热门搜索" loading={loading} bordered={false} bodyStyle={{ overflow: 'hidden' }}>
<TagCloud
data={tags}
height={161}
/>
</Card>
</Col>
<Col xl={6} lg={12} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card title="资源剩余" bodyStyle={{ textAlign: 'center', fontSize: 0 }} bordered={false}>
<WaterWave
height={161}
title="补贴资金剩余"
percent={34}
/>
</Card>
</Col>
</Row>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.mapChart {
padding-top: 24px;
height: 457px;
text-align: center;
img {
display: inline-block;
max-width: 100%;
max-height: 437px;
}
}
.pieCard :global(.pie-stat) {
font-size: 24px!important;
}
@media screen and (max-width: @screen-lg) {
.mapChart {
height: auto;
}
}
import React, { PureComponent } from 'react';
import moment from 'moment';
import { connect } from 'dva';
import { Link } from 'dva/router';
import { Row, Col, Card, List, Avatar } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import EditableLinkGroup from '../../components/EditableLinkGroup';
import { Radar } from '../../components/Charts';
import styles from './Workplace.less';
const links = [
{
title: '操作一',
href: '',
},
{
title: '操作二',
href: '',
},
{
title: '操作三',
href: '',
},
{
title: '操作四',
href: '',
},
{
title: '操作五',
href: '',
},
{
title: '操作六',
href: '',
},
];
const members = [
{
id: 'members-1',
title: '科学搬砖组',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
link: '',
},
{
id: 'members-2',
title: '程序员日常',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/cnrhVkzwxjPwAaCfPbdc.png',
link: '',
},
{
id: 'members-3',
title: '设计天团',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/gaOngJwsRYRaVAuXXcmB.png',
link: '',
},
{
id: 'members-4',
title: '中二少女团',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/ubnKSIfAJTxIgXOKlciN.png',
link: '',
},
{
id: 'members-5',
title: '骗你学计算机',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/WhxKECPNujWoWEFNdnJE.png',
link: '',
},
];
@connect(({ project, activities, chart, loading }) => ({
project,
activities,
chart,
projectLoading: loading.effects['project/fetchNotice'],
activitiesLoading: loading.effects['activities/fetchList'],
}))
export default class Workplace extends PureComponent {
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'project/fetchNotice',
});
dispatch({
type: 'activities/fetchList',
});
dispatch({
type: 'chart/fetch',
});
}
componentWillUnmount() {
const { dispatch } = this.props;
dispatch({
type: 'chart/clear',
});
}
renderActivities() {
const {
activities: { list },
} = this.props;
return list.map((item) => {
const events = item.template.split(/@\{([^{}]*)\}/gi).map((key) => {
if (item[key]) {
return <a href={item[key].link} key={item[key].name}>{item[key].name}</a>;
}
return key;
});
return (
<List.Item key={item.id}>
<List.Item.Meta
avatar={<Avatar src={item.user.avatar} />}
title={
<span>
<a className={styles.username}>{item.user.name}</a>
&nbsp;
<span className={styles.event}>{events}</span>
</span>
}
description={
<span className={styles.datetime} title={item.updatedAt}>
{moment(item.updatedAt).fromNow()}
</span>
}
/>
</List.Item>
);
});
}
render() {
const {
project: { notice },
projectLoading,
activitiesLoading,
chart: { radarData },
} = this.props;
const pageHeaderContent = (
<div className={styles.pageHeaderContent}>
<div className={styles.avatar}>
<Avatar size="large" src="https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png" />
</div>
<div className={styles.content}>
<div className={styles.contentTitle}>早安,曲丽丽,祝你开心每一天!</div>
<div>交互专家 | 蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED</div>
</div>
</div>
);
const extraContent = (
<div className={styles.extraContent}>
<div className={styles.statItem}>
<p>项目数</p>
<p>56</p>
</div>
<div className={styles.statItem}>
<p>团队内排名</p>
<p>8<span> / 24</span></p>
</div>
<div className={styles.statItem}>
<p>项目访问</p>
<p>2,223</p>
</div>
</div>
);
return (
<PageHeaderLayout
content={pageHeaderContent}
extraContent={extraContent}
>
<Row gutter={24}>
<Col xl={16} lg={24} md={24} sm={24} xs={24}>
<Card
className={styles.projectList}
style={{ marginBottom: 24 }}
title="进行中的项目"
bordered={false}
extra={<Link to="/">全部项目</Link>}
loading={projectLoading}
bodyStyle={{ padding: 0 }}
>
{
notice.map(item => (
<Card.Grid className={styles.projectGrid} key={item.id}>
<Card bodyStyle={{ padding: 0 }} bordered={false}>
<Card.Meta
title={(
<div className={styles.cardTitle}>
<Avatar size="small" src={item.logo} />
<Link to={item.href}>{item.title}</Link>
</div>
)}
description={item.description}
/>
<div className={styles.projectItemContent}>
<Link to={item.memberLink}>{item.member || ''}</Link>
{item.updatedAt && (
<span className={styles.datetime} title={item.updatedAt}>
{moment(item.updatedAt).fromNow()}
</span>
)}
</div>
</Card>
</Card.Grid>
))
}
</Card>
<Card
bodyStyle={{ padding: 0 }}
bordered={false}
className={styles.activeCard}
title="动态"
loading={activitiesLoading}
>
<List loading={activitiesLoading} size="large">
<div className={styles.activitiesList}>
{this.renderActivities()}
</div>
</List>
</Card>
</Col>
<Col xl={8} lg={24} md={24} sm={24} xs={24}>
<Card
style={{ marginBottom: 24 }}
title="快速开始 / 便捷导航"
bordered={false}
bodyStyle={{ padding: 0 }}
>
<EditableLinkGroup
onAdd={() => {}}
links={links}
linkElement={Link}
/>
</Card>
<Card
style={{ marginBottom: 24 }}
bordered={false}
title="XX 指数"
loading={radarData.length === 0}
>
<div className={styles.chart}>
<Radar hasLegend height={343} data={radarData} />
</div>
</Card>
<Card
bodyStyle={{ paddingTop: 12, paddingBottom: 12 }}
bordered={false}
title="团队"
>
<div className={styles.members}>
<Row gutter={48}>
{
members.map(item => (
<Col span={12} key={`members-item-${item.id}`}>
<Link to={item.link}>
<Avatar src={item.logo} size="small" />
<span className={styles.member}>{item.title}</span>
</Link>
</Col>
))
}
</Row>
</div>
</Card>
</Col>
</Row>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.activitiesList {
padding: 0 24px 8px 24px;
.username {
color: @text-color;
}
.event {
font-weight: normal;
}
}
.pageHeaderContent {
display: flex;
.avatar {
flex: 0 1 72px;
margin-bottom: 8px;
& > span {
border-radius: 72px;
display: block;
width: 72px;
height: 72px;
}
}
.content {
position: relative;
top: 4px;
margin-left: 24px;
flex: 1 1 auto;
color: @text-color-secondary;
line-height: 22px;
.contentTitle {
font-size: 20px;
line-height: 28px;
font-weight: 500;
color: @heading-color;
margin-bottom: 12px;
}
}
}
.extraContent {
.clearfix();
float: right;
white-space: nowrap;
.statItem {
padding: 0 32px;
position: relative;
display: inline-block;
> p:first-child {
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
margin-bottom: 4px;
}
> p {
color: @heading-color;
font-size: 30px;
line-height: 38px;
margin: 0;
> span {
color: @text-color-secondary;
font-size: 20px;
}
}
&:after {
background-color: @border-color-split;
position: absolute;
top: 8px;
right: 0;
width: 1px;
height: 40px;
content: '';
}
&:last-child {
padding-right: 0;
&:after {
display: none;
}
}
}
}
.members {
a {
display: block;
margin: 12px 0;
line-height: 24px;
height: 24px;
.textOverflow();
.member {
font-size: @font-size-base;
color: @text-color;
line-height: 24px;
max-width: 100px;
vertical-align: top;
margin-left: 12px;
transition: all .3s;
display: inline-block;
.textOverflow();
}
&:hover {
span {
color: @primary-color;
}
}
}
}
.projectList {
:global {
.ant-card-meta-description {
color: @text-color-secondary;
height: 44px;
line-height: 22px;
overflow: hidden;
}
}
.cardTitle {
font-size: 0;
a {
color: @heading-color;
margin-left: 12px;
line-height: 24px;
height: 24px;
display: inline-block;
vertical-align: top;
font-size: @font-size-base;
&:hover {
color: @primary-color;
}
}
}
.projectGrid {
width: 33.33%;
}
.projectItemContent {
display: flex;
margin-top: 8px;
overflow: hidden;
font-size: 12px;
height: 20px;
line-height: 20px;
.textOverflow();
a {
color: @text-color-secondary;
display: inline-block;
flex: 1 1 0;
.textOverflow();
&:hover {
color: @primary-color;
}
}
.datetime {
color: @disabled-color;
flex: 0 0 auto;
float: right;
}
}
}
.datetime {
color: @disabled-color;
}
@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) {
.activeCard {
margin-bottom: 24px;
}
.members {
margin-bottom: 0;
}
.extraContent {
margin-left: -44px;
.statItem {
padding: 0 16px;
}
}
}
@media screen and (max-width: @screen-lg) {
.activeCard {
margin-bottom: 24px;
}
.members {
margin-bottom: 0;
}
.extraContent {
float: none;
margin-right: 0;
.statItem {
padding: 0 16px;
text-align: left;
&:after {
display: none;
}
}
}
}
@media screen and (max-width: @screen-md) {
.extraContent {
margin-left: -16px;
}
.projectList {
.projectGrid {
width: 50%;
}
}
}
@media screen and (max-width: @screen-sm) {
.pageHeaderContent {
display: block;
.content {
margin-left: 0;
}
}
.extraContent {
.statItem {
float: none;
}
}
}
@media screen and (max-width: @screen-xs) {
.projectList {
.projectGrid {
width: 100%;
}
}
}
import React from 'react';
import { Link } from 'dva/router';
import Exception from '../../components/Exception';
export default () => (
<Exception type="403" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
);
import React from 'react';
import { Link } from 'dva/router';
import Exception from '../../components/Exception';
export default () => (
<Exception type="404" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
);
import React from 'react';
import { Link } from 'dva/router';
import Exception from '../../components/Exception';
export default () => (
<Exception type="500" style={{ minHeight: 500, height: '80%' }} linkElement={Link} />
);
.trigger {
background: "red";
:global(.ant-btn) {
margin-right: 8px;
margin-bottom: 12px;
}
}
import React, { PureComponent } from 'react';
import { Button, Spin, Card } from 'antd';
import { connect } from 'dva';
import styles from './style.less';
@connect(state => ({
isloading: state.error.isloading,
}))
export default class TriggerException extends PureComponent {
state={
isloading: false,
}
trigger401 = () => {
this.setState({
isloading: true,
});
this.props.dispatch({
type: 'error/query401',
});
};
trigger403 = () => {
this.setState({
isloading: true,
});
this.props.dispatch({
type: 'error/query403',
});
};
trigger500 = () => {
this.setState({
isloading: true,
});
this.props.dispatch({
type: 'error/query500',
});
};
trigger404 = () => {
this.setState({
isloading: true,
});
this.props.dispatch({
type: 'error/query404',
});
};
render() {
return (
<Card>
<Spin spinning={this.state.isloading} wrapperClassName={styles.trigger}>
<Button type="danger" onClick={this.trigger401}>
触发401
</Button>
<Button type="danger" onClick={this.trigger403}>
触发403
</Button>
<Button type="danger" onClick={this.trigger500}>
触发500
</Button>
<Button type="danger" onClick={this.trigger404}>
触发404
</Button>
</Spin>
</Card>
);
}
}
import React, { PureComponent } from 'react';
import { Card, Button, Form, Icon, Col, Row, DatePicker, TimePicker, Input, Select, Popover } from 'antd';
import { connect } from 'dva';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import FooterToolbar from '../../components/FooterToolbar';
import TableForm from './TableForm';
import styles from './style.less';
const { Option } = Select;
const { RangePicker } = DatePicker;
const fieldLabels = {
name: '仓库名',
url: '仓库域名',
owner: '仓库管理员',
approver: '审批人',
dateRange: '生效日期',
type: '仓库类型',
name2: '任务名',
url2: '任务描述',
owner2: '执行人',
approver2: '责任人',
dateRange2: '生效日期',
type2: '任务类型',
};
const tableData = [{
key: '1',
workId: '00001',
name: 'John Brown',
department: 'New York No. 1 Lake Park',
}, {
key: '2',
workId: '00002',
name: 'Jim Green',
department: 'London No. 1 Lake Park',
}, {
key: '3',
workId: '00003',
name: 'Joe Black',
department: 'Sidney No. 1 Lake Park',
}];
class AdvancedForm extends PureComponent {
state = {
width: '100%',
};
componentDidMount() {
window.addEventListener('resize', this.resizeFooterToolbar);
}
componentWillUnmount() {
window.removeEventListener('resize', this.resizeFooterToolbar);
}
resizeFooterToolbar = () => {
const sider = document.querySelectorAll('.ant-layout-sider')[0];
const width = `calc(100% - ${sider.style.width})`;
if (this.state.width !== width) {
this.setState({ width });
}
}
render() {
const { form, dispatch, submitting } = this.props;
const { getFieldDecorator, validateFieldsAndScroll, getFieldsError } = form;
const validate = () => {
validateFieldsAndScroll((error, values) => {
if (!error) {
// submit the values
dispatch({
type: 'form/submitAdvancedForm',
payload: values,
});
}
});
};
const errors = getFieldsError();
const getErrorInfo = () => {
const errorCount = Object.keys(errors).filter(key => errors[key]).length;
if (!errors || errorCount === 0) {
return null;
}
const scrollToField = (fieldKey) => {
const labelNode = document.querySelector(`label[for="${fieldKey}"]`);
if (labelNode) {
labelNode.scrollIntoView(true);
}
};
const errorList = Object.keys(errors).map((key) => {
if (!errors[key]) {
return null;
}
return (
<li key={key} className={styles.errorListItem} onClick={() => scrollToField(key)}>
<Icon type="cross-circle-o" className={styles.errorIcon} />
<div className={styles.errorMessage}>{errors[key][0]}</div>
<div className={styles.errorField}>{fieldLabels[key]}</div>
</li>
);
});
return (
<span className={styles.errorIcon}>
<Popover
title="表单校验信息"
content={errorList}
overlayClassName={styles.errorPopover}
trigger="click"
getPopupContainer={trigger => trigger.parentNode}
>
<Icon type="exclamation-circle" />
</Popover>
{errorCount}
</span>
);
};
return (
<PageHeaderLayout
title="高级表单"
content="高级表单常见于一次性输入和提交大批量数据的场景。"
wrapperClassName={styles.advancedForm}
>
<Card title="仓库管理" className={styles.card} bordered={false}>
<Form layout="vertical" hideRequiredMark>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item label={fieldLabels.name}>
{getFieldDecorator('name', {
rules: [{ required: true, message: '请输入仓库名称' }],
})(
<Input placeholder="请输入仓库名称" />
)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item label={fieldLabels.url}>
{getFieldDecorator('url', {
rules: [{ required: true, message: '请选择' }],
})(
<Input
style={{ width: '100%' }}
addonBefore="http://"
addonAfter=".com"
placeholder="请输入"
/>
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item label={fieldLabels.owner}>
{getFieldDecorator('owner', {
rules: [{ required: true, message: '请选择管理员' }],
})(
<Select placeholder="请选择管理员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
)}
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item label={fieldLabels.approver}>
{getFieldDecorator('approver', {
rules: [{ required: true, message: '请选择审批员' }],
})(
<Select placeholder="请选择审批员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item label={fieldLabels.dateRange}>
{getFieldDecorator('dateRange', {
rules: [{ required: true, message: '请选择生效日期' }],
})(
<RangePicker placeholder={['开始日期', '结束日期']} style={{ width: '100%' }} />
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item label={fieldLabels.type}>
{getFieldDecorator('type', {
rules: [{ required: true, message: '请选择仓库类型' }],
})(
<Select placeholder="请选择仓库类型">
<Option value="private">私密</Option>
<Option value="public">公开</Option>
</Select>
)}
</Form.Item>
</Col>
</Row>
</Form>
</Card>
<Card title="任务管理" className={styles.card} bordered={false}>
<Form layout="vertical" hideRequiredMark>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item label={fieldLabels.name2}>
{getFieldDecorator('name2', {
rules: [{ required: true, message: '请输入' }],
})(
<Input placeholder="请输入" />
)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item label={fieldLabels.url2}>
{getFieldDecorator('url2', {
rules: [{ required: true, message: '请选择' }],
})(
<Input placeholder="请输入" />
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item label={fieldLabels.owner2}>
{getFieldDecorator('owner2', {
rules: [{ required: true, message: '请选择管理员' }],
})(
<Select placeholder="请选择管理员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
)}
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item label={fieldLabels.approver2}>
{getFieldDecorator('approver2', {
rules: [{ required: true, message: '请选择审批员' }],
})(
<Select placeholder="请选择审批员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item label={fieldLabels.dateRange2}>
{getFieldDecorator('dateRange2', {
rules: [{ required: true, message: '请输入' }],
})(
<TimePicker
placeholder="提醒时间"
style={{ width: '100%' }}
getPopupContainer={trigger => trigger.parentNode}
/>
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item label={fieldLabels.type2}>
{getFieldDecorator('type2', {
rules: [{ required: true, message: '请选择仓库类型' }],
})(
<Select placeholder="请选择仓库类型">
<Option value="private">私密</Option>
<Option value="public">公开</Option>
</Select>
)}
</Form.Item>
</Col>
</Row>
</Form>
</Card>
<Card title="成员管理" className={styles.card} bordered={false}>
{getFieldDecorator('members', {
initialValue: tableData,
})(<TableForm />)}
</Card>
<FooterToolbar style={{ width: this.state.width }}>
{getErrorInfo()}
<Button type="primary" onClick={validate} loading={submitting}>
提交
</Button>
</FooterToolbar>
</PageHeaderLayout>
);
}
}
export default connect(({ global, loading }) => ({
collapsed: global.collapsed,
submitting: loading.effects['form/submitAdvancedForm'],
}))(Form.create()(AdvancedForm));
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import {
Form, Input, DatePicker, Select, Button, Card, InputNumber, Radio, Icon, Tooltip,
} from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import styles from './style.less';
const FormItem = Form.Item;
const { Option } = Select;
const { RangePicker } = DatePicker;
const { TextArea } = Input;
@connect(({ loading }) => ({
submitting: loading.effects['form/submitRegularForm'],
}))
@Form.create()
export default class BasicForms extends PureComponent {
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
this.props.dispatch({
type: 'form/submitRegularForm',
payload: values,
});
}
});
}
render() {
const { submitting } = this.props;
const { getFieldDecorator, getFieldValue } = this.props.form;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};
const submitFormLayout = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 10, offset: 7 },
},
};
return (
<PageHeaderLayout title="基础表单" content="表单页用于向用户收集或验证信息,基础表单常见于数据项较少的表单场景。">
<Card bordered={false}>
<Form
onSubmit={this.handleSubmit}
hideRequiredMark
style={{ marginTop: 8 }}
>
<FormItem
{...formItemLayout}
label="标题"
>
{getFieldDecorator('title', {
rules: [{
required: true, message: '请输入标题',
}],
})(
<Input placeholder="给目标起个名字" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="起止日期"
>
{getFieldDecorator('date', {
rules: [{
required: true, message: '请选择起止日期',
}],
})(
<RangePicker style={{ width: '100%' }} placeholder={['开始日期', '结束日期']} />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="目标描述"
>
{getFieldDecorator('goal', {
rules: [{
required: true, message: '请输入目标描述',
}],
})(
<TextArea style={{ minHeight: 32 }} placeholder="请输入你的阶段性工作目标" rows={4} />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="衡量标准"
>
{getFieldDecorator('standard', {
rules: [{
required: true, message: '请输入衡量标准',
}],
})(
<TextArea style={{ minHeight: 32 }} placeholder="请输入衡量标准" rows={4} />
)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
客户
<em className={styles.optional}>
(选填)
<Tooltip title="目标的服务对象">
<Icon type="info-circle-o" style={{ marginRight: 4 }} />
</Tooltip>
</em>
</span>
}
>
{getFieldDecorator('client')(
<Input placeholder="请描述你服务的客户,内部客户直接 @姓名/工号" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label={<span>邀评人<em className={styles.optional}>(选填)</em></span>}
>
{getFieldDecorator('invites')(
<Input placeholder="请直接 @姓名/工号,最多可邀请 5 人" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label={<span>权重<em className={styles.optional}>(选填)</em></span>}
>
{getFieldDecorator('weight')(
<InputNumber placeholder="请输入" min={0} max={100} />
)}
<span>%</span>
</FormItem>
<FormItem
{...formItemLayout}
label="目标公开"
help="客户、邀评人默认被分享"
>
<div>
{getFieldDecorator('public', {
initialValue: '1',
})(
<Radio.Group>
<Radio value="1">公开</Radio>
<Radio value="2">部分公开</Radio>
<Radio value="3">不公开</Radio>
</Radio.Group>
)}
<FormItem style={{ marginBottom: 0 }}>
{getFieldDecorator('publicUsers')(
<Select
mode="multiple"
placeholder="公开给"
style={{
margin: '8px 0',
display: getFieldValue('public') === '2' ? 'block' : 'none',
}}
>
<Option value="1">同事甲</Option>
<Option value="2">同事乙</Option>
<Option value="3">同事丙</Option>
</Select>
)}
</FormItem>
</div>
</FormItem>
<FormItem {...submitFormLayout} style={{ marginTop: 32 }}>
<Button type="primary" htmlType="submit" loading={submitting}>
提交
</Button>
<Button style={{ marginLeft: 8 }}>保存</Button>
</FormItem>
</Form>
</Card>
</PageHeaderLayout>
);
}
}
import React from 'react';
import { connect } from 'dva';
import { Form, Input, Button, Select, Divider } from 'antd';
import { routerRedux } from 'dva/router';
import styles from './style.less';
const { Option } = Select;
const formItemLayout = {
labelCol: {
span: 5,
},
wrapperCol: {
span: 19,
},
};
@Form.create()
class Step1 extends React.PureComponent {
render() {
const { form, dispatch, data } = this.props;
const { getFieldDecorator, validateFields } = form;
const onValidateForm = () => {
validateFields((err, values) => {
if (!err) {
dispatch({
type: 'form/saveStepFormData',
payload: values,
});
dispatch(routerRedux.push('/form/step-form/confirm'));
}
});
};
return (
<div>
<Form layout="horizontal" className={styles.stepForm} hideRequiredMark>
<Form.Item
{...formItemLayout}
label="付款账户"
>
{getFieldDecorator('payAccount', {
initialValue: data.payAccount,
rules: [{ required: true, message: '请选择付款账户' }],
})(
<Select placeholder="test@example.com">
<Option value="ant-design@alipay.com">ant-design@alipay.com</Option>
</Select>
)}
</Form.Item>
<Form.Item
{...formItemLayout}
label="收款账户"
>
<Input.Group compact>
<Select defaultValue="alipay" style={{ width: 100 }}>
<Option value="alipay">支付宝</Option>
<Option value="bank">银行账户</Option>
</Select>
{getFieldDecorator('receiverAccount', {
initialValue: data.receiverAccount,
rules: [
{ required: true, message: '请输入收款人账户' },
{ type: 'email', message: '账户名应为邮箱格式' },
],
})(
<Input style={{ width: 'calc(100% - 100px)' }} placeholder="test@example.com" />
)}
</Input.Group>
</Form.Item>
<Form.Item
{...formItemLayout}
label="收款人姓名"
>
{getFieldDecorator('receiverName', {
initialValue: data.receiverName,
rules: [{ required: true, message: '请输入收款人姓名' }],
})(
<Input placeholder="请输入收款人姓名" />
)}
</Form.Item>
<Form.Item
{...formItemLayout}
label="转账金额"
>
{getFieldDecorator('amount', {
initialValue: data.amount,
rules: [
{ required: true, message: '请输入转账金额' },
{ pattern: /^(\d+)((?:\.\d+)?)$/, message: '请输入合法金额数字' },
],
})(
<Input prefix="¥" placeholder="请输入金额" />
)}
</Form.Item>
<Form.Item
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: { span: formItemLayout.wrapperCol.span, offset: formItemLayout.labelCol.span },
}}
label=""
>
<Button type="primary" onClick={onValidateForm}>
下一步
</Button>
</Form.Item>
</Form>
<Divider style={{ margin: '40px 0 24px' }} />
<div className={styles.desc}>
<h3>说明</h3>
<h4>转账到支付宝账户</h4>
<p>如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。</p>
<h4>转账到银行卡</h4>
<p>如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。</p>
</div>
</div>
);
}
}
export default connect(({ form }) => ({
data: form.step,
}))(Step1);
import React from 'react';
import { connect } from 'dva';
import { Form, Input, Button, Alert, Divider } from 'antd';
import { routerRedux } from 'dva/router';
import { digitUppercase } from '../../../utils/utils';
import styles from './style.less';
const formItemLayout = {
labelCol: {
span: 5,
},
wrapperCol: {
span: 19,
},
};
@Form.create()
class Step2 extends React.PureComponent {
render() {
const { form, data, dispatch, submitting } = this.props;
const { getFieldDecorator, validateFields } = form;
const onPrev = () => {
dispatch(routerRedux.push('/form/step-form'));
};
const onValidateForm = (e) => {
e.preventDefault();
validateFields((err, values) => {
if (!err) {
dispatch({
type: 'form/submitStepForm',
payload: {
...data,
...values,
},
});
}
});
};
return (
<Form layout="horizontal" className={styles.stepForm}>
<Alert
closable
showIcon
message="确认转账后,资金将直接打入对方账户,无法退回。"
style={{ marginBottom: 24 }}
/>
<Form.Item
{...formItemLayout}
className={styles.stepFormText}
label="付款账户"
>
{data.payAccount}
</Form.Item>
<Form.Item
{...formItemLayout}
className={styles.stepFormText}
label="收款账户"
>
{data.receiverAccount}
</Form.Item>
<Form.Item
{...formItemLayout}
className={styles.stepFormText}
label="收款人姓名"
>
{data.receiverName}
</Form.Item>
<Form.Item
{...formItemLayout}
className={styles.stepFormText}
label="转账金额"
>
<span className={styles.money}>{data.amount}</span>
<span className={styles.uppercase}>{digitUppercase(data.amount)}</span>
</Form.Item>
<Divider style={{ margin: '24px 0' }} />
<Form.Item
{...formItemLayout}
label="支付密码"
required={false}
>
{getFieldDecorator('password', {
initialValue: '123456',
rules: [{
required: true, message: '需要支付密码才能进行支付',
}],
})(
<Input type="password" autoComplete="off" style={{ width: '80%' }} />
)}
</Form.Item>
<Form.Item
style={{ marginBottom: 8 }}
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: { span: formItemLayout.wrapperCol.span, offset: formItemLayout.labelCol.span },
}}
label=""
>
<Button type="primary" onClick={onValidateForm} loading={submitting}>
提交
</Button>
<Button onClick={onPrev} style={{ marginLeft: 8 }}>
上一步
</Button>
</Form.Item>
</Form>
);
}
}
export default connect(({ form, loading }) => ({
submitting: loading.effects['form/submitStepForm'],
data: form.step,
}))(Step2);
import React from 'react';
import { connect } from 'dva';
import { Button, Row, Col } from 'antd';
import { routerRedux } from 'dva/router';
import Result from '../../../components/Result';
import styles from './style.less';
class Step3 extends React.PureComponent {
render() {
const { dispatch, data } = this.props;
const onFinish = () => {
dispatch(routerRedux.push('/form/step-form'));
};
const information = (
<div className={styles.information}>
<Row>
<Col span={8} className={styles.label}>付款账户:</Col>
<Col span={16}>{data.payAccount}</Col>
</Row>
<Row>
<Col span={8} className={styles.label}>收款账户:</Col>
<Col span={16}>{data.receiverAccount}</Col>
</Row>
<Row>
<Col span={8} className={styles.label}>收款人姓名:</Col>
<Col span={16}>{data.receiverName}</Col>
</Row>
<Row>
<Col span={8} className={styles.label}>转账金额:</Col>
<Col span={16}><span className={styles.money}>{data.amount}</span> 元</Col>
</Row>
</div>
);
const actions = (
<div>
<Button type="primary" onClick={onFinish}>
再转一笔
</Button>
<Button>
查看账单
</Button>
</div>
);
return (
<Result
type="success"
title="操作成功"
description="预计两小时内到账"
extra={information}
actions={actions}
className={styles.result}
/>
);
}
}
export default connect(({ form }) => ({
data: form.step,
}))(Step3);
import React, { PureComponent } from 'react';
import { Route, Redirect, Switch } from 'dva/router';
import { Card, Steps } from 'antd';
import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
import NotFound from '../../Exception/404';
import { getRoutes } from '../../../utils/utils';
import styles from '../style.less';
const { Step } = Steps;
export default class StepForm extends PureComponent {
getCurrentStep() {
const { location } = this.props;
const { pathname } = location;
const pathList = pathname.split('/');
switch (pathList[pathList.length - 1]) {
case 'info': return 0;
case 'confirm': return 1;
case 'result': return 2;
default: return 0;
}
}
render() {
const { match, routerData } = this.props;
return (
<PageHeaderLayout title="分步表单" content="将一个冗长或用户不熟悉的表单任务分成多个步骤,指导用户完成。">
<Card bordered={false}>
<div>
<Steps current={this.getCurrentStep()} className={styles.steps}>
<Step title="填写转账信息" />
<Step title="确认转账信息" />
<Step title="完成" />
</Steps>
<Switch>
{
getRoutes(match.path, routerData).map(item => (
<Route
key={item.key}
path={item.path}
component={item.component}
exact={item.exact}
/>
))
}
<Redirect exact from="/form/step-form" to="/form/step-form/info" />
<Route render={NotFound} />
</Switch>
</div>
</Card>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
.stepForm {
margin: 40px auto 0;
max-width: 500px;
}
.stepFormText {
margin-bottom: 24px;
:global {
.ant-form-item-label,
.ant-form-item-control {
line-height: 22px;
}
}
}
.result {
margin: 0 auto;
max-width: 560px;
padding: 24px 0 8px;
}
.desc {
padding: 0 56px;
color: @text-color-secondary;
h3 {
font-size: 16px;
margin: 0 0 12px 0;
color: @text-color-secondary;
line-height: 32px;
}
h4 {
margin: 0 0 4px 0;
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
p {
margin-top: 0;
margin-bottom: 12px;
line-height: 22px;
}
}
@media screen and (max-width: @screen-md) {
.desc {
padding: 0;
}
}
.information {
line-height: 22px;
:global {
.ant-row:not(:last-child) {
margin-bottom: 24px;
}
}
.label {
color: @heading-color;
text-align: right;
padding-right: 8px;
}
}
.money {
font-family: "Helvetica Neue", sans-serif;
font-weight: 500;
font-size: 20px;
line-height: 14px;
}
.uppercase {
font-size: 12px;
}
import React, { PureComponent } from 'react';
import { Table, Button, Input, message, Popconfirm, Divider } from 'antd';
import styles from './style.less';
export default class TableForm extends PureComponent {
constructor(props) {
super(props);
this.state = {
data: props.value,
};
}
componentWillReceiveProps(nextProps) {
if ('value' in nextProps) {
this.setState({
data: nextProps.value,
});
}
}
getRowByKey(key, newData) {
return (newData || this.state.data).filter(item => item.key === key)[0];
}
index = 0;
cacheOriginData = {};
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFieldsAndScroll((err, values) => {
if (!err) {
this.props.dispatch({
type: 'form/submit',
payload: values,
});
}
});
}
toggleEditable(e, key) {
e.preventDefault();
const newData = this.state.data.map(item => ({ ...item }));
const target = this.getRowByKey(key, newData);
if (target) {
// 进入编辑状态时保存原始数据
if (!target.editable) {
this.cacheOriginData[key] = { ...target };
}
target.editable = !target.editable;
this.setState({ data: newData });
}
}
remove(key) {
const newData = this.state.data.filter(item => item.key !== key);
this.setState({ data: newData });
this.props.onChange(newData);
}
newMember = () => {
const newData = this.state.data.map(item => ({ ...item }));
newData.push({
key: `NEW_TEMP_ID_${this.index}`,
workId: '',
name: '',
department: '',
editable: true,
isNew: true,
});
this.index += 1;
this.setState({ data: newData });
}
handleKeyPress(e, key) {
if (e.key === 'Enter') {
this.saveRow(e, key);
}
}
handleFieldChange(e, fieldName, key) {
const newData = this.state.data.map(item => ({ ...item }));
const target = this.getRowByKey(key, newData);
if (target) {
target[fieldName] = e.target.value;
this.setState({ data: newData });
}
}
saveRow(e, key) {
e.persist();
// save field when blur input
setTimeout(() => {
if (document.activeElement.tagName === 'INPUT' &&
document.activeElement !== e.target) {
return;
}
if (this.clickedCancel) {
this.clickedCancel = false;
return;
}
const target = this.getRowByKey(key) || {};
if (!target.workId || !target.name || !target.department) {
message.error('请填写完整成员信息。');
e.target.focus();
return;
}
delete target.isNew;
this.toggleEditable(e, key);
this.props.onChange(this.state.data);
}, 10);
}
cancel(e, key) {
this.clickedCancel = true;
e.preventDefault();
const newData = this.state.data.map(item => ({ ...item }));
const target = this.getRowByKey(key, newData);
if (this.cacheOriginData[key]) {
Object.assign(target, this.cacheOriginData[key]);
target.editable = false;
delete this.cacheOriginData[key];
}
this.setState({ data: newData });
}
render() {
const columns = [{
title: '成员姓名',
dataIndex: 'name',
key: 'name',
width: '20%',
render: (text, record) => {
if (record.editable) {
return (
<Input
value={text}
autoFocus
onChange={e => this.handleFieldChange(e, 'name', record.key)}
onBlur={e => this.saveRow(e, record.key)}
onKeyPress={e => this.handleKeyPress(e, record.key)}
placeholder="成员姓名"
/>
);
}
return text;
},
}, {
title: '工号',
dataIndex: 'workId',
key: 'workId',
width: '20%',
render: (text, record) => {
if (record.editable) {
return (
<Input
value={text}
onChange={e => this.handleFieldChange(e, 'workId', record.key)}
onBlur={e => this.saveRow(e, record.key)}
onKeyPress={e => this.handleKeyPress(e, record.key)}
placeholder="工号"
/>
);
}
return text;
},
}, {
title: '所属部门',
dataIndex: 'department',
key: 'department',
width: '40%',
render: (text, record) => {
if (record.editable) {
return (
<Input
value={text}
onChange={e => this.handleFieldChange(e, 'department', record.key)}
onBlur={e => this.saveRow(e, record.key)}
onKeyPress={e => this.handleKeyPress(e, record.key)}
placeholder="所属部门"
/>
);
}
return text;
},
}, {
title: '操作',
key: 'action',
render: (text, record) => {
if (record.editable) {
if (record.isNew) {
return (
<span>
<a>保存</a>
<Divider type="vertical" />
<Popconfirm title="是否要删除此行?" onConfirm={() => this.remove(record.key)}>
<a>删除</a>
</Popconfirm>
</span>
);
}
return (
<span>
<a>保存</a>
<Divider type="vertical" />
<a onClick={e => this.cancel(e, record.key)}>取消</a>
</span>
);
}
return (
<span>
<a onClick={e => this.toggleEditable(e, record.key)}>编辑</a>
<Divider type="vertical" />
<Popconfirm title="是否要删除此行?" onConfirm={() => this.remove(record.key)}>
<a>删除</a>
</Popconfirm>
</span>
);
},
}];
return (
<div>
<Table
columns={columns}
dataSource={this.state.data}
pagination={false}
rowClassName={(record) => {
return record.editable ? styles.editable : '';
}}
/>
<Button
style={{ width: '100%', marginTop: 16, marginBottom: 8 }}
type="dashed"
onClick={this.newMember}
icon="plus"
>
新增成员
</Button>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
.card {
margin-bottom: 24px;
}
.heading {
font-size: 14px;
line-height: 22px;
margin: 0 0 16px 0;
}
.steps:global(.ant-steps) {
max-width: 750px;
margin: 16px auto;
}
.errorIcon {
cursor: pointer;
color: @error-color;
margin-right: 24px;
i {
margin-right: 4px;
}
}
.errorPopover {
:global {
.ant-popover-inner-content {
padding: 0;
max-height: 290px;
overflow: auto;
min-width: 256px;
}
}
}
.errorListItem {
list-style: none;
border-bottom: 1px solid @border-color-split;
padding: 8px 16px;
cursor: pointer;
transition: all .3s;
&:hover {
background: @primary-1;
}
&:last-child {
border: 0;
}
.errorIcon {
color: @error-color;
float: left;
margin-top: 4px;
margin-right: 12px;
padding-bottom: 22px;
}
.errorField {
font-size: 12px;
color: @text-color-secondary;
margin-top: 2px;
}
}
.editable {
td {
padding-top: 13px !important;
padding-bottom: 12.5px !important;
}
}
// custom footer for fixed footer toolbar
.advancedForm + div {
padding-bottom: 64px;
}
.advancedForm {
:global {
.ant-form .ant-row:last-child .ant-form-item {
margin-bottom: 24px;
}
.ant-table td {
transition: none !important;
}
}
}
.optional {
color: @text-color-secondary;
font-style: normal;
}
import React, { PureComponent } from 'react';
import numeral from 'numeral';
import { connect } from 'dva';
import { Row, Col, Form, Card, Select, Icon, Avatar, List, Tooltip, Dropdown, Menu } from 'antd';
import StandardFormRow from '../../components/StandardFormRow';
import TagSelect from '../../components/TagSelect';
import styles from './Applications.less';
const { Option } = Select;
const FormItem = Form.Item;
const formatWan = (val) => {
const v = val * 1;
if (!v || isNaN(v)) return '';
let result = val;
if (val > 10000) {
result = Math.floor(val / 10000);
result = <span>{result}<em className={styles.wan}></em></span>;
}
return result;
};
/* eslint react/no-array-index-key: 0 */
@Form.create()
@connect(({ list, loading }) => ({
list,
loading: loading.models.list,
}))
export default class FilterCardList extends PureComponent {
componentDidMount() {
this.props.dispatch({
type: 'list/fetch',
payload: {
count: 8,
},
});
}
handleFormSubmit = () => {
const { form, dispatch } = this.props;
// setTimeout 用于保证获取表单值是在所有表单字段更新完毕的时候
setTimeout(() => {
form.validateFields((err) => {
if (!err) {
// eslint-disable-next-line
dispatch({
type: 'list/fetch',
payload: {
count: 8,
},
});
}
});
}, 0);
}
render() {
const { list: { list }, loading, form } = this.props;
const { getFieldDecorator } = form;
const CardInfo = ({ activeUser, newUser }) => (
<div className={styles.cardInfo}>
<div>
<p>活跃用户</p>
<p>{activeUser}</p>
</div>
<div>
<p>新增用户</p>
<p>{newUser}</p>
</div>
</div>
);
const formItemLayout = {
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const itemMenu = (
<Menu>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="http://www.alipay.com/">1st menu item</a>
</Menu.Item>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="http://www.taobao.com/">2nd menu item</a>
</Menu.Item>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="http://www.tmall.com/">3d menu item</a>
</Menu.Item>
</Menu>
);
return (
<div className={styles.filterCardList}>
<Card bordered={false}>
<Form layout="inline">
<StandardFormRow title="所属类目" block style={{ paddingBottom: 11 }}>
<FormItem>
{getFieldDecorator('category')(
<TagSelect onChange={this.handleFormSubmit} expandable>
<TagSelect.Option value="cat1">类目一</TagSelect.Option>
<TagSelect.Option value="cat2">类目二</TagSelect.Option>
<TagSelect.Option value="cat3">类目三</TagSelect.Option>
<TagSelect.Option value="cat4">类目四</TagSelect.Option>
<TagSelect.Option value="cat5">类目五</TagSelect.Option>
<TagSelect.Option value="cat6">类目六</TagSelect.Option>
<TagSelect.Option value="cat7">类目七</TagSelect.Option>
<TagSelect.Option value="cat8">类目八</TagSelect.Option>
<TagSelect.Option value="cat9">类目九</TagSelect.Option>
<TagSelect.Option value="cat10">类目十</TagSelect.Option>
<TagSelect.Option value="cat11">类目十一</TagSelect.Option>
<TagSelect.Option value="cat12">类目十二</TagSelect.Option>
</TagSelect>
)}
</FormItem>
</StandardFormRow>
<StandardFormRow
title="其它选项"
grid
last
>
<Row gutter={16}>
<Col lg={8} md={10} sm={10} xs={24}>
<FormItem
{...formItemLayout}
label="作者"
>
{getFieldDecorator('author', {})(
<Select
onChange={this.handleFormSubmit}
placeholder="不限"
style={{ maxWidth: 200, width: '100%' }}
>
<Option value="lisa">王昭君</Option>
</Select>
)}
</FormItem>
</Col>
<Col lg={8} md={10} sm={10} xs={24}>
<FormItem
{...formItemLayout}
label="好评度"
>
{getFieldDecorator('rate', {})(
<Select
onChange={this.handleFormSubmit}
placeholder="不限"
style={{ maxWidth: 200, width: '100%' }}
>
<Option value="good">优秀</Option>
<Option value="normal">普通</Option>
</Select>
)}
</FormItem>
</Col>
</Row>
</StandardFormRow>
</Form>
</Card>
<List
rowKey="id"
style={{ marginTop: 24 }}
grid={{ gutter: 24, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}
loading={loading}
dataSource={list}
renderItem={item => (
<List.Item key={item.id}>
<Card
hoverable
bodyStyle={{ paddingBottom: 20 }}
actions={[
<Tooltip title="下载"><Icon type="download" /></Tooltip>,
<Tooltip title="编辑"><Icon type="edit" /></Tooltip>,
<Tooltip title="分享"><Icon type="share-alt" /></Tooltip>,
<Dropdown overlay={itemMenu}><Icon type="ellipsis" /></Dropdown>,
]}
>
<Card.Meta
avatar={<Avatar size="small" src={item.avatar} />}
title={item.title}
/>
<div className={styles.cardItemContent}>
<CardInfo
activeUser={formatWan(item.activeUser)}
newUser={numeral(item.newUser).format('0,0')}
/>
</div>
</Card>
</List.Item>
)}
/>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.filterCardList {
margin-bottom: -24px;
:global {
.ant-card-meta-content {
margin-top: 0;
}
// disabled white space
.ant-card-meta-avatar {
font-size: 0;
}
.ant-card-actions {
background: #f7f9fa;
}
}
.cardInfo {
.clearfix();
margin-top: 16px;
margin-left: 40px;
& > div {
position: relative;
text-align: left;
float: left;
width: 50%;
p {
line-height: 32px;
font-size: 24px;
margin: 0;
}
p:first-child {
color: @text-color-secondary;
font-size: 12px;
line-height: 20px;
margin-bottom: 4px;
}
}
}
}
.wan {
position: relative;
top: -2px;
font-size: @font-size-base;
font-style: normal;
line-height: 20px;
margin-left: 2px;
}
import React, { Component } from 'react';
import moment from 'moment';
import { connect } from 'dva';
import { Form, Card, Select, List, Tag, Icon, Avatar, Row, Col, Button } from 'antd';
import StandardFormRow from '../../components/StandardFormRow';
import TagSelect from '../../components/TagSelect';
import styles from './Articles.less';
const { Option } = Select;
const FormItem = Form.Item;
const pageSize = 5;
@Form.create()
@connect(({ list, loading }) => ({
list,
loading: loading.models.list,
}))
export default class SearchList extends Component {
componentDidMount() {
this.fetchMore();
}
setOwner = () => {
const { form } = this.props;
form.setFieldsValue({
owner: ['wzj'],
});
}
fetchMore = () => {
this.props.dispatch({
type: 'list/appendFetch',
payload: {
count: pageSize,
},
});
}
render() {
const { form, list: { list }, loading } = this.props;
const { getFieldDecorator } = form;
const owners = [
{
id: 'wzj',
name: '我自己',
},
{
id: 'wjh',
name: '吴家豪',
},
{
id: 'zxx',
name: '周星星',
},
{
id: 'zly',
name: '赵丽颖',
},
{
id: 'ym',
name: '姚明',
},
];
const IconText = ({ type, text }) => (
<span>
<Icon type={type} style={{ marginRight: 8 }} />
{text}
</span>
);
const ListContent = ({ data: { content, updatedAt, avatar, owner, href } }) => (
<div className={styles.listContent}>
<div className={styles.description}>{content}</div>
<div className={styles.extra}>
<Avatar src={avatar} size="small" /><a href={href}>{owner}</a> 发布在 <a href={href}>{href}</a>
<em>{moment(updatedAt).format('YYYY-MM-DD hh:mm')}</em>
</div>
</div>
);
const formItemLayout = {
wrapperCol: {
xs: { span: 24 },
sm: { span: 24 },
md: { span: 12 },
},
};
const loadMore = list.length > 0 ? (
<div style={{ textAlign: 'center', marginTop: 16 }}>
<Button onClick={this.fetchMore} style={{ paddingLeft: 48, paddingRight: 48 }}>
{loading ? <span><Icon type="loading" /> 加载中...</span> : '加载更多'}
</Button>
</div>
) : null;
return (
<div>
<Card bordered={false}>
<Form layout="inline">
<StandardFormRow title="所属类目" block style={{ paddingBottom: 11 }}>
<FormItem>
{getFieldDecorator('category')(
<TagSelect onChange={this.handleFormSubmit} expandable>
<TagSelect.Option value="cat1">类目一</TagSelect.Option>
<TagSelect.Option value="cat2">类目二</TagSelect.Option>
<TagSelect.Option value="cat3">类目三</TagSelect.Option>
<TagSelect.Option value="cat4">类目四</TagSelect.Option>
<TagSelect.Option value="cat5">类目五</TagSelect.Option>
<TagSelect.Option value="cat6">类目六</TagSelect.Option>
<TagSelect.Option value="cat7">类目七</TagSelect.Option>
<TagSelect.Option value="cat8">类目八</TagSelect.Option>
<TagSelect.Option value="cat9">类目九</TagSelect.Option>
<TagSelect.Option value="cat10">类目十</TagSelect.Option>
<TagSelect.Option value="cat11">类目十一</TagSelect.Option>
<TagSelect.Option value="cat12">类目十二</TagSelect.Option>
</TagSelect>
)}
</FormItem>
</StandardFormRow>
<StandardFormRow
title="owner"
grid
>
<Row>
<Col lg={16} md={24} sm={24} xs={24}>
<FormItem>
{getFieldDecorator('owner', {
initialValue: ['wjh', 'zxx'],
})(
<Select
mode="multiple"
style={{ maxWidth: 286, width: '100%' }}
placeholder="选择 owner"
>
{
owners.map(owner =>
<Option key={owner.id} value={owner.id}>{owner.name}</Option>
)
}
</Select>
)}
<a className={styles.selfTrigger} onClick={this.setOwner}>只看自己的</a>
</FormItem>
</Col>
</Row>
</StandardFormRow>
<StandardFormRow
title="其它选项"
grid
last
>
<Row gutter={16}>
<Col xl={8} lg={10} md={12} sm={24} xs={24}>
<FormItem
{...formItemLayout}
label="活跃用户"
>
{getFieldDecorator('user', {})(
<Select
onChange={this.handleFormSubmit}
placeholder="不限"
style={{ maxWidth: 200, width: '100%' }}
>
<Option value="lisa">李三</Option>
</Select>
)}
</FormItem>
</Col>
<Col xl={8} lg={10} md={12} sm={24} xs={24}>
<FormItem
{...formItemLayout}
label="好评度"
>
{getFieldDecorator('rate', {})(
<Select
onChange={this.handleFormSubmit}
placeholder="不限"
style={{ maxWidth: 200, width: '100%' }}
>
<Option value="good">优秀</Option>
</Select>
)}
</FormItem>
</Col>
</Row>
</StandardFormRow>
</Form>
</Card>
<Card
style={{ marginTop: 24 }}
bordered={false}
bodyStyle={{ padding: '8px 32px 32px 32px' }}
>
<List
size="large"
loading={list.length === 0 ? loading : false}
rowKey="id"
itemLayout="vertical"
loadMore={loadMore}
dataSource={list}
renderItem={item => (
<List.Item
key={item.id}
actions={[
<IconText type="star-o" text={item.star} />,
<IconText type="like-o" text={item.like} />,
<IconText type="message" text={item.message} />,
]}
extra={<div className={styles.listItemExtra} />}
>
<List.Item.Meta
title={(
<a className={styles.listItemMetaTitle} href={item.href}>{item.title}</a>
)}
description={
<span>
<Tag>Ant Design</Tag>
<Tag>设计语言</Tag>
<Tag>蚂蚁金服</Tag>
</span>
}
/>
<ListContent data={item} />
</List.Item>
)}
/>
</Card>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.listContent {
.description {
line-height: 22px;
max-width: 720px;
}
.extra {
color: @text-color-secondary;
margin-top: 16px;
line-height: 22px;
& > :global(.ant-avatar) {
vertical-align: top;
margin-right: 8px;
width: 20px;
height: 20px;
position: relative;
top: 1px;
}
& > em {
color: @disabled-color;
font-style: normal;
margin-left: 16px;
}
}
}
a.listItemMetaTitle {
color: @heading-color;
}
.listItemExtra {
width: 272px;
height: 1px;
}
.selfTrigger {
margin-left: 12px;
}
@media screen and (max-width: @screen-xs) {
.selfTrigger {
display: block;
margin-left: 0;
}
.listContent {
.extra {
& > em {
display: block;
margin-left: 0;
margin-top: 8px;
}
}
}
}
@media screen and (max-width: @screen-md) {
.selfTrigger {
display: block;
margin-left: 0;
}
}
@media screen and (max-width: @screen-lg) {
.listItemExtra {
width: 0;
height: 1px;
}
}
import React, { PureComponent } from 'react';
import moment from 'moment';
import { connect } from 'dva';
import { List, Card, Row, Col, Radio, Input, Progress, Button, Icon, Dropdown, Menu, Avatar } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import styles from './BasicList.less';
const RadioButton = Radio.Button;
const RadioGroup = Radio.Group;
const { Search } = Input;
@connect(({ list, loading }) => ({
list,
loading: loading.models.list,
}))
export default class BasicList extends PureComponent {
componentDidMount() {
this.props.dispatch({
type: 'list/fetch',
payload: {
count: 5,
},
});
}
render() {
const { list: { list }, loading } = this.props;
const Info = ({ title, value, bordered }) => (
<div className={styles.headerInfo}>
<span>{title}</span>
<p>{value}</p>
{bordered && <em />}
</div>
);
const extraContent = (
<div className={styles.extraContent}>
<RadioGroup defaultValue="all">
<RadioButton value="all">全部</RadioButton>
<RadioButton value="progress">进行中</RadioButton>
<RadioButton value="waiting">等待中</RadioButton>
</RadioGroup>
<Search
className={styles.extraContentSearch}
placeholder="请输入"
onSearch={() => ({})}
/>
</div>
);
const paginationProps = {
showSizeChanger: true,
showQuickJumper: true,
pageSize: 5,
total: 50,
};
const ListContent = ({ data: { owner, createdAt, percent, status } }) => (
<div className={styles.listContent}>
<div className={styles.listContentItem}>
<span>Owner</span>
<p>{owner}</p>
</div>
<div className={styles.listContentItem}>
<span>开始时间</span>
<p>{moment(createdAt).format('YYYY-MM-DD hh:mm')}</p>
</div>
<div className={styles.listContentItem}>
<Progress percent={percent} status={status} strokeWidth={6} style={{ width: 180 }} />
</div>
</div>
);
const menu = (
<Menu>
<Menu.Item>
<a>编辑</a>
</Menu.Item>
<Menu.Item>
<a>删除</a>
</Menu.Item>
</Menu>
);
const MoreBtn = () => (
<Dropdown overlay={menu}>
<a>
更多 <Icon type="down" />
</a>
</Dropdown>
);
return (
<PageHeaderLayout>
<div className={styles.standardList}>
<Card bordered={false}>
<Row>
<Col sm={8} xs={24}>
<Info title="我的待办" value="8个任务" bordered />
</Col>
<Col sm={8} xs={24}>
<Info title="本周任务平均处理时间" value="32分钟" bordered />
</Col>
<Col sm={8} xs={24}>
<Info title="本周完成任务数" value="24个任务" />
</Col>
</Row>
</Card>
<Card
className={styles.listCard}
bordered={false}
title="标准列表"
style={{ marginTop: 24 }}
bodyStyle={{ padding: '0 32px 40px 32px' }}
extra={extraContent}
>
<Button type="dashed" style={{ width: '100%', marginBottom: 8 }} icon="plus">
添加
</Button>
<List
size="large"
rowKey="id"
loading={loading}
pagination={paginationProps}
dataSource={list}
renderItem={item => (
<List.Item
actions={[<a>编辑</a>, <MoreBtn />]}
>
<List.Item.Meta
avatar={<Avatar src={item.logo} shape="square" size="large" />}
title={<a href={item.href}>{item.title}</a>}
description={item.subDescription}
/>
<ListContent data={item} />
</List.Item>
)}
/>
</Card>
</div>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.standardList {
:global {
.ant-card-head {
border-bottom: none;
}
.ant-card-head-title {
line-height: 32px;
padding: 24px 0;
}
.ant-card-extra {
padding: 24px 0;
}
.ant-list-pagination {
text-align: right;
margin-top: 24px;
}
.ant-avatar-lg {
width: 48px;
height: 48px;
line-height: 48px;
}
}
.headerInfo {
position: relative;
text-align: center;
& > span {
color: @text-color-secondary;
display: inline-block;
font-size: @font-size-base;
line-height: 22px;
margin-bottom: 4px;
}
& > p {
color: @heading-color;
font-size: 24px;
line-height: 32px;
margin: 0;
}
& > em {
background-color: @border-color-split;
position: absolute;
height: 56px;
width: 1px;
top: 0;
right: 0;
}
}
.listContent {
font-size: 0;
.listContentItem {
color: @text-color-secondary;
display: inline-block;
vertical-align: middle;
font-size: @font-size-base;
margin-left: 40px;
> span {
line-height: 20px;
}
> p {
margin-top: 4px;
margin-bottom: 0;
line-height: 22px;
}
}
}
.extraContentSearch {
margin-left: 16px;
width: 272px;
}
}
@media screen and (max-width: @screen-xs) {
.standardList {
:global {
.ant-list-item-content {
display: block;
flex: none;
width: 100%;
}
.ant-list-item-action {
margin-left: 0;
}
}
.listContent {
margin-left: 0;
& > div {
margin-left: 0;
}
}
.listCard {
:global {
.ant-card-head-title {
overflow: visible;
}
}
}
}
}
@media screen and (max-width: @screen-sm) {
.standardList {
.extraContentSearch {
margin-left: 0;
width: 100%;
}
.headerInfo {
margin-bottom: 16px;
& > em {
display: none;
}
}
}
}
@media screen and (max-width: @screen-md) {
.standardList {
.listContent {
& > div {
display: block;
}
& > div:last-child {
top: 0;
width: 100%;
}
}
}
.listCard {
:global {
.ant-radio-group {
display: block;
margin-bottom: 8px;
}
}
}
}
@media screen and (max-width: @screen-lg) and (min-width: @screen-md) {
.standardList {
.listContent {
& > div {
display: block;
}
& > div:last-child {
top: 0;
width: 100%;
}
}
}
}
@media screen and (max-width: @screen-xl) {
.standardList {
.listContent {
& > div {
margin-left: 24px;
}
& > div:last-child {
top: 0;
}
}
}
}
@media screen and (max-width: 1400px) {
.standardList {
.listContent {
text-align: right;
& > div:last-child {
top: 0;
}
}
}
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Card, Button, Icon, List } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import Ellipsis from '../../components/Ellipsis';
import styles from './CardList.less';
@connect(({ list, loading }) => ({
list,
loading: loading.models.list,
}))
export default class CardList extends PureComponent {
componentDidMount() {
this.props.dispatch({
type: 'list/fetch',
payload: {
count: 8,
},
});
}
render() {
const { list: { list }, loading } = this.props;
const content = (
<div className={styles.pageHeaderContent}>
<p>
段落示意:蚂蚁金服务设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,
提供跨越设计与开发的体验解决方案。
</p>
<div className={styles.contentLink}>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg" /> 快速开始
</a>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg" /> 产品简介
</a>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg" /> 产品文档
</a>
</div>
</div>
);
const extraContent = (
<div className={styles.extraImg}>
<img alt="这是一个标题" src="https://gw.alipayobjects.com/zos/rmsportal/RzwpdLnhmvDJToTdfDPe.png" />
</div>
);
return (
<PageHeaderLayout
title="卡片列表"
content={content}
extraContent={extraContent}
>
<div className={styles.cardList}>
<List
rowKey="id"
loading={loading}
grid={{ gutter: 24, lg: 3, md: 2, sm: 1, xs: 1 }}
dataSource={['', ...list]}
renderItem={item => (item ? (
<List.Item key={item.id}>
<Card hoverable className={styles.card} actions={[<a>操作一</a>, <a>操作二</a>]}>
<Card.Meta
avatar={<img alt="" className={styles.cardAvatar} src={item.avatar} />}
title={<a href="#">{item.title}</a>}
description={(
<Ellipsis className={styles.item} lines={3}>{item.description}</Ellipsis>
)}
/>
</Card>
</List.Item>
) : (
<List.Item>
<Button type="dashed" className={styles.newButton}>
<Icon type="plus" /> 新增产品
</Button>
</List.Item>
)
)}
/>
</div>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.cardList {
margin-bottom: -24px;
.card {
:global {
.ant-card-meta-title {
margin-bottom: 12px;
& > a {
color: @heading-color;
}
}
.ant-card-actions {
background: #f7f9fa;
}
.ant-card-body:hover {
.ant-card-meta-title > a {
color: @primary-color;
}
}
}
}
.item {
height: 64px;
}
}
.extraImg {
margin-top: -60px;
text-align: center;
width: 195px;
img {
width: 100%;
}
}
.newButton {
background-color: #fff;
border-color: @border-color-base;
border-radius: @border-radius-sm;
color: @text-color-secondary;
width: 100%;
height: 188px;
}
.cardAvatar {
width: 48px;
height: 48px;
border-radius: 48px;
}
.cardDescription {
.textOverflowMulti();
}
.pageHeaderContent {
position: relative;
}
.contentLink {
margin-top: 16px;
a {
margin-right: 32px;
img {
width: 24px;
}
}
img {
vertical-align: middle;
margin-right: 8px;
}
}
@media screen and (max-width: @screen-lg) {
.contentLink {
a {
margin-right: 16px;
}
}
}
@media screen and (max-width: @screen-md) {
.extraImg {
display: none;
}
}
@media screen and (max-width: @screen-sm) {
.pageHeaderContent {
padding-bottom: 30px;
}
.contentLink {
position: absolute;
left: 0;
bottom: -4px;
width: 1000px;
a {
margin-right: 16px;
}
img {
margin-right: 4px;
}
}
}
import React, { Component } from 'react';
import { routerRedux, Route, Switch } from 'dva/router';
import { connect } from 'dva';
import { Input } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import { getRoutes } from '../../utils/utils';
@connect()
export default class SearchList extends Component {
handleTabChange = (key) => {
const { dispatch, match } = this.props;
switch (key) {
case 'articles':
dispatch(routerRedux.push(`${match.url}/articles`));
break;
case 'applications':
dispatch(routerRedux.push(`${match.url}/applications`));
break;
case 'projects':
dispatch(routerRedux.push(`${match.url}/projects`));
break;
default:
break;
}
}
render() {
const tabList = [{
key: 'articles',
tab: '文章',
}, {
key: 'applications',
tab: '应用',
}, {
key: 'projects',
tab: '项目',
}];
const mainSearch = (
<div style={{ textAlign: 'center' }}>
<Input.Search
placeholder="请输入"
enterButton="搜索"
size="large"
onSearch={this.handleFormSubmit}
style={{ width: 522 }}
/>
</div>
);
const { match, routerData, location } = this.props;
const routes = getRoutes(match.path, routerData);
return (
<PageHeaderLayout
title="搜索列表"
content={mainSearch}
tabList={tabList}
activeTabKey={location.pathname.replace(`${match.path}/`, '')}
onTabChange={this.handleTabChange}
>
<Switch>
{
routes.map(item =>
(
<Route
key={item.key}
path={item.path}
component={item.component}
exact={item.exact}
/>
)
)
}
</Switch>
</PageHeaderLayout>
);
}
}
import React, { PureComponent } from 'react';
import moment from 'moment';
import { connect } from 'dva';
import { Row, Col, Form, Card, Select, List } from 'antd';
import StandardFormRow from '../../components/StandardFormRow';
import TagSelect from '../../components/TagSelect';
import AvatarList from '../../components/AvatarList';
import styles from './Projects.less';
const { Option } = Select;
const FormItem = Form.Item;
/* eslint react/no-array-index-key: 0 */
@Form.create()
@connect(({ list, loading }) => ({
list,
loading: loading.models.list,
}))
export default class CoverCardList extends PureComponent {
componentDidMount() {
this.props.dispatch({
type: 'list/fetch',
payload: {
count: 8,
},
});
}
handleFormSubmit = () => {
const { form, dispatch } = this.props;
// setTimeout 用于保证获取表单值是在所有表单字段更新完毕的时候
setTimeout(() => {
form.validateFields((err) => {
if (!err) {
// eslint-disable-next-line
dispatch({
type: 'list/fetch',
payload: {
count: 8,
},
});
}
});
}, 0);
}
render() {
const { list: { list = [] }, loading, form } = this.props;
const { getFieldDecorator } = form;
const cardList = list ? (
<List
rowKey="id"
loading={loading}
grid={{ gutter: 24, lg: 4, md: 3, sm: 2, xs: 1 }}
dataSource={list}
renderItem={item => (
<List.Item>
<Card
className={styles.card}
hoverable
cover={<img alt={item.title} src={item.cover} height={154} />}
>
<Card.Meta
title={<a href="#">{item.title}</a>}
description={item.subDescription}
/>
<div className={styles.cardItemContent}>
<span>{moment(item.updatedAt).fromNow()}</span>
<div className={styles.avatarList}>
<AvatarList size="mini">
{
item.members.map((member, i) => (
<AvatarList.Item
key={`${item.id}-avatar-${i}`}
src={member.avatar}
tips={member.name}
/>
))
}
</AvatarList>
</div>
</div>
</Card>
</List.Item>
)}
/>
) : null;
const formItemLayout = {
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
return (
<div className={styles.coverCardList}>
<Card bordered={false}>
<Form layout="inline">
<StandardFormRow title="所属类目" block style={{ paddingBottom: 11 }}>
<FormItem>
{getFieldDecorator('category')(
<TagSelect onChange={this.handleFormSubmit} expandable>
<TagSelect.Option value="cat1">类目一</TagSelect.Option>
<TagSelect.Option value="cat2">类目二</TagSelect.Option>
<TagSelect.Option value="cat3">类目三</TagSelect.Option>
<TagSelect.Option value="cat4">类目四</TagSelect.Option>
<TagSelect.Option value="cat5">类目五</TagSelect.Option>
<TagSelect.Option value="cat6">类目六</TagSelect.Option>
<TagSelect.Option value="cat7">类目七</TagSelect.Option>
<TagSelect.Option value="cat8">类目八</TagSelect.Option>
<TagSelect.Option value="cat9">类目九</TagSelect.Option>
<TagSelect.Option value="cat10">类目十</TagSelect.Option>
<TagSelect.Option value="cat11">类目十一</TagSelect.Option>
<TagSelect.Option value="cat12">类目十二</TagSelect.Option>
</TagSelect>
)}
</FormItem>
</StandardFormRow>
<StandardFormRow
title="其它选项"
grid
last
>
<Row gutter={16}>
<Col lg={8} md={10} sm={10} xs={24}>
<FormItem
{...formItemLayout}
label="作者"
>
{getFieldDecorator('author', {})(
<Select
onChange={this.handleFormSubmit}
placeholder="不限"
style={{ maxWidth: 200, width: '100%' }}
>
<Option value="lisa">王昭君</Option>
</Select>
)}
</FormItem>
</Col>
<Col lg={8} md={10} sm={10} xs={24}>
<FormItem
{...formItemLayout}
label="好评度"
>
{getFieldDecorator('rate', {})(
<Select
onChange={this.handleFormSubmit}
placeholder="不限"
style={{ maxWidth: 200, width: '100%' }}
>
<Option value="good">优秀</Option>
<Option value="normal">普通</Option>
</Select>
)}
</FormItem>
</Col>
</Row>
</StandardFormRow>
</Form>
</Card>
<div className={styles.cardList}>
{cardList}
</div>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.coverCardList {
margin-bottom: -24px;
.card {
:global {
.ant-card-meta-title {
margin-bottom: 4px;
& > a {
color: @heading-color;
}
}
.ant-card-meta-description {
height: 44px;
line-height: 22px;
overflow: hidden;
}
}
&:hover {
:global {
.ant-card-meta-title > a {
color: @primary-color;
}
}
}
}
.cardItemContent {
display: flex;
margin-top: 16px;
margin-bottom: -4px;
line-height: 20px;
height: 20px;
& > span {
color: @text-color-secondary;
flex: 1;
font-size: 12px;
}
.avatarList {
flex: 0 1 auto;
}
}
.cardList {
margin-top: 24px;
}
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card, Form, Input, Select, Icon, Button, Dropdown, Menu, InputNumber, DatePicker, Modal, message } from 'antd';
import StandardTable from '../../components/StandardTable';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import styles from './TableList.less';
const FormItem = Form.Item;
const { Option } = Select;
const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
@connect(({ rule, loading }) => ({
rule,
loading: loading.models.rule,
}))
@Form.create()
export default class TableList extends PureComponent {
state = {
addInputValue: '',
modalVisible: false,
expandForm: false,
selectedRows: [],
formValues: {},
};
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'rule/fetch',
});
}
handleStandardTableChange = (pagination, filtersArg, sorter) => {
const { dispatch } = this.props;
const { formValues } = this.state;
const filters = Object.keys(filtersArg).reduce((obj, key) => {
const newObj = { ...obj };
newObj[key] = getValue(filtersArg[key]);
return newObj;
}, {});
const params = {
currentPage: pagination.current,
pageSize: pagination.pageSize,
...formValues,
...filters,
};
if (sorter.field) {
params.sorter = `${sorter.field}_${sorter.order}`;
}
dispatch({
type: 'rule/fetch',
payload: params,
});
}
handleFormReset = () => {
const { form, dispatch } = this.props;
form.resetFields();
this.setState({
formValues: {},
});
dispatch({
type: 'rule/fetch',
payload: {},
});
}
toggleForm = () => {
this.setState({
expandForm: !this.state.expandForm,
});
}
handleMenuClick = (e) => {
const { dispatch } = this.props;
const { selectedRows } = this.state;
if (!selectedRows) return;
switch (e.key) {
case 'remove':
dispatch({
type: 'rule/remove',
payload: {
no: selectedRows.map(row => row.no).join(','),
},
callback: () => {
this.setState({
selectedRows: [],
});
},
});
break;
default:
break;
}
}
handleSelectRows = (rows) => {
this.setState({
selectedRows: rows,
});
}
handleSearch = (e) => {
e.preventDefault();
const { dispatch, form } = this.props;
form.validateFields((err, fieldsValue) => {
if (err) return;
const values = {
...fieldsValue,
updatedAt: fieldsValue.updatedAt && fieldsValue.updatedAt.valueOf(),
};
this.setState({
formValues: values,
});
dispatch({
type: 'rule/fetch',
payload: values,
});
});
}
handleModalVisible = (flag) => {
this.setState({
modalVisible: !!flag,
});
}
handleAddInput = (e) => {
this.setState({
addInputValue: e.target.value,
});
}
handleAdd = () => {
this.props.dispatch({
type: 'rule/add',
payload: {
description: this.state.addInputValue,
},
});
message.success('添加成功');
this.setState({
modalVisible: false,
});
}
renderSimpleForm() {
const { getFieldDecorator } = this.props.form;
return (
<Form onSubmit={this.handleSearch} layout="inline">
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={8} sm={24}>
<FormItem label="规则编号">
{getFieldDecorator('no')(
<Input placeholder="请输入" />
)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<FormItem label="使用状态">
{getFieldDecorator('status')(
<Select placeholder="请选择" style={{ width: '100%' }}>
<Option value="0">关闭</Option>
<Option value="1">运行中</Option>
</Select>
)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<span className={styles.submitButtons}>
<Button type="primary" htmlType="submit">查询</Button>
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>重置</Button>
<a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
展开 <Icon type="down" />
</a>
</span>
</Col>
</Row>
</Form>
);
}
renderAdvancedForm() {
const { getFieldDecorator } = this.props.form;
return (
<Form onSubmit={this.handleSearch} layout="inline">
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={8} sm={24}>
<FormItem label="规则编号">
{getFieldDecorator('no')(
<Input placeholder="请输入" />
)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<FormItem label="使用状态">
{getFieldDecorator('status')(
<Select placeholder="请选择" style={{ width: '100%' }}>
<Option value="0">关闭</Option>
<Option value="1">运行中</Option>
</Select>
)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<FormItem label="调用次数">
{getFieldDecorator('number')(
<InputNumber style={{ width: '100%' }} />
)}
</FormItem>
</Col>
</Row>
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
<Col md={8} sm={24}>
<FormItem label="更新日期">
{getFieldDecorator('date')(
<DatePicker style={{ width: '100%' }} placeholder="请输入更新日期" />
)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<FormItem label="使用状态">
{getFieldDecorator('status3')(
<Select placeholder="请选择" style={{ width: '100%' }}>
<Option value="0">关闭</Option>
<Option value="1">运行中</Option>
</Select>
)}
</FormItem>
</Col>
<Col md={8} sm={24}>
<FormItem label="使用状态">
{getFieldDecorator('status4')(
<Select placeholder="请选择" style={{ width: '100%' }}>
<Option value="0">关闭</Option>
<Option value="1">运行中</Option>
</Select>
)}
</FormItem>
</Col>
</Row>
<div style={{ overflow: 'hidden' }}>
<span style={{ float: 'right', marginBottom: 24 }}>
<Button type="primary" htmlType="submit">查询</Button>
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>重置</Button>
<a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
收起 <Icon type="up" />
</a>
</span>
</div>
</Form>
);
}
renderForm() {
return this.state.expandForm ? this.renderAdvancedForm() : this.renderSimpleForm();
}
render() {
const { rule: { data }, loading } = this.props;
const { selectedRows, modalVisible, addInputValue } = this.state;
const menu = (
<Menu onClick={this.handleMenuClick} selectedKeys={[]}>
<Menu.Item key="remove">删除</Menu.Item>
<Menu.Item key="approval">批量审批</Menu.Item>
</Menu>
);
return (
<PageHeaderLayout title="查询表格">
<Card bordered={false}>
<div className={styles.tableList}>
<div className={styles.tableListForm}>
{this.renderForm()}
</div>
<div className={styles.tableListOperator}>
<Button icon="plus" type="primary" onClick={() => this.handleModalVisible(true)}>
新建
</Button>
{
selectedRows.length > 0 && (
<span>
<Button>批量操作</Button>
<Dropdown overlay={menu}>
<Button>
更多操作 <Icon type="down" />
</Button>
</Dropdown>
</span>
)
}
</div>
<StandardTable
selectedRows={selectedRows}
loading={loading}
data={data}
onSelectRow={this.handleSelectRows}
onChange={this.handleStandardTableChange}
/>
</div>
</Card>
<Modal
title="新建规则"
visible={modalVisible}
onOk={this.handleAdd}
onCancel={() => this.handleModalVisible()}
>
<FormItem
labelCol={{ span: 5 }}
wrapperCol={{ span: 15 }}
label="描述"
>
<Input placeholder="请输入" onChange={this.handleAddInput} value={addInputValue} />
</FormItem>
</Modal>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.tableList {
.tableListOperator {
margin-bottom: 16px;
button {
margin-right: 8px;
}
}
}
.tableListForm {
:global {
.ant-form-item {
margin-bottom: 24px;
margin-right: 0;
display: flex;
> .ant-form-item-label {
width: auto;
line-height: 32px;
padding-right: 8px;
}
.ant-form-item-control {
line-height: 32px;
}
}
.ant-form-item-control-wrapper {
flex: 1;
}
}
.submitButtons {
white-space: nowrap;
margin-bottom: 24px;
}
}
@media screen and (max-width: @screen-lg) {
.tableListForm :global(.ant-form-item) {
margin-right: 24px;
}
}
@media screen and (max-width: @screen-md) {
.tableListForm :global(.ant-form-item) {
margin-right: 8px;
}
}
import React, { Component } from 'react';
import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind';
import { connect } from 'dva';
import { Button, Menu, Dropdown, Icon, Row, Col, Steps, Card, Popover, Badge, Table, Tooltip, Divider } from 'antd';
import classNames from 'classnames';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import DescriptionList from '../../components/DescriptionList';
import styles from './AdvancedProfile.less';
const { Step } = Steps;
const { Description } = DescriptionList;
const ButtonGroup = Button.Group;
const getWindowWidth = () => (window.innerWidth || document.documentElement.clientWidth);
const menu = (
<Menu>
<Menu.Item key="1">选项一</Menu.Item>
<Menu.Item key="2">选项二</Menu.Item>
<Menu.Item key="3">选项三</Menu.Item>
</Menu>
);
const action = (
<div>
<ButtonGroup>
<Button>操作</Button>
<Button>操作</Button>
<Dropdown overlay={menu} placement="bottomRight">
<Button><Icon type="ellipsis" /></Button>
</Dropdown>
</ButtonGroup>
<Button type="primary">主操作</Button>
</div>
);
const extra = (
<Row>
<Col xs={24} sm={12}>
<div className={styles.textSecondary}>状态</div>
<div className={styles.heading}>待审批</div>
</Col>
<Col xs={24} sm={12}>
<div className={styles.textSecondary}>订单金额</div>
<div className={styles.heading}>¥ 568.08</div>
</Col>
</Row>
);
const description = (
<DescriptionList className={styles.headerList} size="small" col="2">
<Description term="创建人">曲丽丽</Description>
<Description term="订购产品">XX 服务</Description>
<Description term="创建时间">2017-07-07</Description>
<Description term="关联单据"><a href="">12421</a></Description>
<Description term="生效日期">2017-07-07 ~ 2017-08-08</Description>
<Description term="备注">请于两个工作日内确认</Description>
</DescriptionList>
);
const tabList = [{
key: 'detail',
tab: '详情',
}, {
key: 'rule',
tab: '规则',
}];
const desc1 = (
<div className={classNames(styles.textSecondary, styles.stepDescription)}>
<div>
曲丽丽
<Icon type="dingding-o" style={{ marginLeft: 8 }} />
</div>
<div>2016-12-12 12:32</div>
</div>
);
const desc2 = (
<div className={styles.stepDescription}>
<div>
周毛毛
<Icon type="dingding-o" style={{ color: '#00A0E9', marginLeft: 8 }} />
</div>
<div><a href="">催一下</a></div>
</div>
);
const popoverContent = (
<div style={{ width: 160 }}>
吴加号
<span className={styles.textSecondary} style={{ float: 'right' }}>
<Badge status="default" text={<span style={{ color: 'rgba(0, 0, 0, 0.45)' }}>未响应</span>} />
</span>
<div className={styles.textSecondary} style={{ marginTop: 4 }}>耗时:2小时25分钟</div>
</div>
);
const customDot = (dot, { status }) => (status === 'process' ? (
<Popover placement="topLeft" arrowPointAtCenter content={popoverContent}>
{dot}
</Popover>
) : dot);
const operationTabList = [{
key: 'tab1',
tab: '操作日志一',
}, {
key: 'tab2',
tab: '操作日志二',
}, {
key: 'tab3',
tab: '操作日志三',
}];
const columns = [{
title: '操作类型',
dataIndex: 'type',
key: 'type',
}, {
title: '操作人',
dataIndex: 'name',
key: 'name',
}, {
title: '执行结果',
dataIndex: 'status',
key: 'status',
render: text => (
text === 'agree' ? <Badge status="success" text="成功" /> : <Badge status="error" text="驳回" />
),
}, {
title: '操作时间',
dataIndex: 'updatedAt',
key: 'updatedAt',
}, {
title: '备注',
dataIndex: 'memo',
key: 'memo',
}];
@connect(({ profile, loading }) => ({
profile,
loading: loading.effects['profile/fetchAdvanced'],
}))
export default class AdvancedProfile extends Component {
state = {
operationkey: 'tab1',
stepDirection: 'horizontal',
}
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'profile/fetchAdvanced',
});
this.setStepDirection();
window.addEventListener('resize', this.setStepDirection);
}
componentWillUnmount() {
window.removeEventListener('resize', this.setStepDirection);
this.setStepDirection.cancel();
}
onOperationTabChange = (key) => {
this.setState({ operationkey: key });
}
@Bind()
@Debounce(200)
setStepDirection() {
const { stepDirection } = this.state;
const w = getWindowWidth();
if (stepDirection !== 'vertical' && w <= 576) {
this.setState({
stepDirection: 'vertical',
});
} else if (stepDirection !== 'horizontal' && w > 576) {
this.setState({
stepDirection: 'horizontal',
});
}
}
render() {
const { stepDirection } = this.state;
const { profile, loading } = this.props;
const { advancedOperation1, advancedOperation2, advancedOperation3 } = profile;
const contentList = {
tab1: <Table
pagination={false}
loading={loading}
dataSource={advancedOperation1}
columns={columns}
/>,
tab2: <Table
pagination={false}
loading={loading}
dataSource={advancedOperation2}
columns={columns}
/>,
tab3: <Table
pagination={false}
loading={loading}
dataSource={advancedOperation3}
columns={columns}
/>,
};
return (
<PageHeaderLayout
title="单号:234231029431"
logo={<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png" />}
action={action}
content={description}
extraContent={extra}
tabList={tabList}
>
<Card title="流程进度" style={{ marginBottom: 24 }} bordered={false}>
<Steps direction={stepDirection} progressDot={customDot} current={1}>
<Step title="创建项目" description={desc1} />
<Step title="部门初审" description={desc2} />
<Step title="财务复核" />
<Step title="完成" />
</Steps>
</Card>
<Card title="用户信息" style={{ marginBottom: 24 }} bordered={false}>
<DescriptionList style={{ marginBottom: 24 }}>
<Description term="用户姓名">付小小</Description>
<Description term="会员卡号">32943898021309809423</Description>
<Description term="身份证">3321944288191034921</Description>
<Description term="联系方式">18112345678</Description>
<Description term="联系地址">曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口</Description>
</DescriptionList>
<DescriptionList style={{ marginBottom: 24 }} title="信息组">
<Description term="某某数据">725</Description>
<Description term="该数据更新时间">2017-08-08</Description>
<Description>&nbsp;</Description>
<Description term={
<span>
某某数据
<Tooltip title="数据说明">
<Icon style={{ color: 'rgba(0, 0, 0, 0.43)', marginLeft: 4 }} type="info-circle-o" />
</Tooltip>
</span>
}
>
725
</Description>
<Description term="该数据更新时间">2017-08-08</Description>
</DescriptionList>
<h4 style={{ marginBottom: 16 }}>信息组</h4>
<Card type="inner" title="多层级信息组">
<DescriptionList size="small" style={{ marginBottom: 16 }} title="组名称">
<Description term="负责人">林东东</Description>
<Description term="角色码">1234567</Description>
<Description term="所属部门">XX公司 - YY</Description>
<Description term="过期时间">2017-08-08</Description>
<Description term="描述">这段描述很长很长很长很长很长很长很长很长很长很长很长很长很长很长...</Description>
</DescriptionList>
<Divider style={{ margin: '16px 0' }} />
<DescriptionList size="small" style={{ marginBottom: 16 }} title="组名称" col="1">
<Description term="学名">
Citrullus lanatus (Thunb.) Matsum. et Nakai一年生蔓生藤本;茎、枝粗壮,具明显的棱。卷须较粗..
</Description>
</DescriptionList>
<Divider style={{ margin: '16px 0' }} />
<DescriptionList size="small" title="组名称">
<Description term="负责人">付小小</Description>
<Description term="角色码">1234568</Description>
</DescriptionList>
</Card>
</Card>
<Card title="用户近半年来电记录" style={{ marginBottom: 24 }} bordered={false}>
<div className={styles.noData}>
<Icon type="frown-o" />暂无数据
</div>
</Card>
<Card
className={styles.tabsCard}
bordered={false}
tabList={operationTabList}
onTabChange={this.onOperationTabChange}
>
{contentList[this.state.operationkey]}
</Card>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
.headerList {
margin-bottom: 4px;
}
.tabsCard {
margin-bottom: 24px;
:global {
.ant-card-head {
padding: 0 16px;
}
}
}
.noData {
color: @disabled-color;
text-align: center;
line-height: 64px;
font-size: 16px;
i {
font-size: 24px;
margin-right: 16px;
position: relative;
top: 3px;
}
}
.heading {
color: @heading-color;
font-size: 20px;
}
.stepDescription {
font-size: 14px;
position: relative;
left: 38px;
& > div {
margin-top: 8px;
margin-bottom: 4px;
}
}
.textSecondary {
color: @text-color-secondary;
}
@media screen and (max-width: @screen-sm) {
.stepDescription {
left: 8px;
}
}
import React, { Component } from 'react';
import { connect } from 'dva';
import { Card, Badge, Table, Divider } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import DescriptionList from '../../components/DescriptionList';
import styles from './BasicProfile.less';
const { Description } = DescriptionList;
const progressColumns = [{
title: '时间',
dataIndex: 'time',
key: 'time',
}, {
title: '当前进度',
dataIndex: 'rate',
key: 'rate',
}, {
title: '状态',
dataIndex: 'status',
key: 'status',
render: text => (
text === 'success' ? <Badge status="success" text="成功" /> : <Badge status="processing" text="进行中" />
),
}, {
title: '操作员ID',
dataIndex: 'operator',
key: 'operator',
}, {
title: '耗时',
dataIndex: 'cost',
key: 'cost',
}];
@connect(({ profile, loading }) => ({
profile,
loading: loading.effects['profile/fetchBasic'],
}))
export default class BasicProfile extends Component {
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'profile/fetchBasic',
});
}
render() {
const { profile, loading } = this.props;
const { basicGoods, basicProgress } = profile;
let goodsData = [];
if (basicGoods.length) {
let num = 0;
let amount = 0;
basicGoods.forEach((item) => {
num += Number(item.num);
amount += Number(item.amount);
});
goodsData = basicGoods.concat({
id: '总计',
num,
amount,
});
}
const renderContent = (value, row, index) => {
const obj = {
children: value,
props: {},
};
if (index === basicGoods.length) {
obj.props.colSpan = 0;
}
return obj;
};
const goodsColumns = [{
title: '商品编号',
dataIndex: 'id',
key: 'id',
render: (text, row, index) => {
if (index < basicGoods.length) {
return <a href="">{text}</a>;
}
return {
children: <span style={{ fontWeight: 600 }}>总计</span>,
props: {
colSpan: 4,
},
};
},
}, {
title: '商品名称',
dataIndex: 'name',
key: 'name',
render: renderContent,
}, {
title: '商品条码',
dataIndex: 'barcode',
key: 'barcode',
render: renderContent,
}, {
title: '单价',
dataIndex: 'price',
key: 'price',
align: 'right',
render: renderContent,
}, {
title: '数量(件)',
dataIndex: 'num',
key: 'num',
align: 'right',
render: (text, row, index) => {
if (index < basicGoods.length) {
return text;
}
return <span style={{ fontWeight: 600 }}>{text}</span>;
},
}, {
title: '金额',
dataIndex: 'amount',
key: 'amount',
align: 'right',
render: (text, row, index) => {
if (index < basicGoods.length) {
return text;
}
return <span style={{ fontWeight: 600 }}>{text}</span>;
},
}];
return (
<PageHeaderLayout title="基础详情页">
<Card bordered={false}>
<DescriptionList size="large" title="退款申请" style={{ marginBottom: 32 }}>
<Description term="取货单号">1000000000</Description>
<Description term="状态">已取货</Description>
<Description term="销售单号">1234123421</Description>
<Description term="子订单">3214321432</Description>
</DescriptionList>
<Divider style={{ marginBottom: 32 }} />
<DescriptionList size="large" title="用户信息" style={{ marginBottom: 32 }}>
<Description term="用户姓名">付小小</Description>
<Description term="联系电话">18100000000</Description>
<Description term="常用快递">菜鸟仓储</Description>
<Description term="取货地址">浙江省杭州市西湖区万塘路18</Description>
<Description term="备注"></Description>
</DescriptionList>
<Divider style={{ marginBottom: 32 }} />
<div className={styles.title}>退货商品</div>
<Table
style={{ marginBottom: 24 }}
pagination={false}
loading={loading}
dataSource={goodsData}
columns={goodsColumns}
rowKey="id"
/>
<div className={styles.title}>退货进度</div>
<Table
style={{ marginBottom: 16 }}
pagination={false}
loading={loading}
dataSource={basicProgress}
columns={progressColumns}
/>
</Card>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
.title {
color: @heading-color;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
import React from 'react';
import { Button, Icon, Card } from 'antd';
import Result from '../../components/Result';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
const extra = (
<div>
<div style={{ fontSize: 16, color: 'rgba(0, 0, 0, 0.85)', fontWeight: '500', marginBottom: 16 }}>
您提交的内容有如下错误:
</div>
<div style={{ marginBottom: 16 }}>
<Icon style={{ color: '#f5222d', marginRight: 8 }} type="close-circle-o" />您的账户已被冻结
<a style={{ marginLeft: 16 }}>立即解冻 <Icon type="right" /></a>
</div>
<div>
<Icon style={{ color: '#f5222d', marginRight: 8 }} type="close-circle-o" />您的账户还不具备申请资格
<a style={{ marginLeft: 16 }}>立即升级 <Icon type="right" /></a>
</div>
</div>
);
const actions = <Button type="primary">返回修改</Button>;
export default () => (
<PageHeaderLayout>
<Card bordered={false}>
<Result
type="error"
title="提交失败"
description="请核对并修改以下信息后,再重新提交。"
extra={extra}
actions={actions}
style={{ marginTop: 48, marginBottom: 16 }}
/>
</Card>
</PageHeaderLayout>
);
import React from 'react';
import { Button, Row, Col, Icon, Steps, Card } from 'antd';
import Result from '../../components/Result';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
const { Step } = Steps;
const desc1 = (
<div style={{ fontSize: 12, color: 'rgba(0, 0, 0, 0.45)', position: 'relative', left: 42 }}>
<div style={{ margin: '8px 0 4px' }}>
曲丽丽<Icon style={{ marginLeft: 8 }} type="dingding-o" />
</div>
<div>2016-12-12 12:32</div>
</div>
);
const desc2 = (
<div style={{ fontSize: 12, position: 'relative', left: 42 }}>
<div style={{ margin: '8px 0 4px' }}>
周毛毛<Icon type="dingding-o" style={{ color: '#00A0E9', marginLeft: 8 }} />
</div>
<div><a href="">催一下</a></div>
</div>
);
const extra = (
<div>
<div style={{ fontSize: 16, color: 'rgba(0, 0, 0, 0.85)', fontWeight: '500', marginBottom: 20 }}>
项目名称
</div>
<Row style={{ marginBottom: 16 }}>
<Col xs={24} sm={12} md={12} lg={12} xl={6}>
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>项目 ID</span>
23421
</Col>
<Col xs={24} sm={12} md={12} lg={12} xl={6}>
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>负责人:</span>
曲丽丽
</Col>
<Col xs={24} sm={24} md={24} lg={24} xl={12}>
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>生效时间:</span>
2016-12-12 ~ 2017-12-12
</Col>
</Row>
<Steps style={{ marginLeft: -42, width: 'calc(100% + 84px)' }} progressDot current={1}>
<Step title={<span style={{ fontSize: 14 }}>创建项目</span>} description={desc1} />
<Step title={<span style={{ fontSize: 14 }}>部门初审</span>} description={desc2} />
<Step title={<span style={{ fontSize: 14 }}>财务复核</span>} />
<Step title={<span style={{ fontSize: 14 }}>完成</span>} />
</Steps>
</div>
);
const actions = (
<div>
<Button type="primary">返回列表</Button>
<Button>查看项目</Button>
<Button> </Button>
</div>
);
export default () => (
<PageHeaderLayout>
<Card bordered={false}>
<Result
type="success"
title="提交成功"
description="提交结果页用于反馈一系列操作任务的处理结果,
如果仅是简单操作,使用 Message 全局提示反馈即可。
本文字区域可以展示简单的补充说明,如果有类似展示
“单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。"
extra={extra}
actions={actions}
style={{ marginTop: 48, marginBottom: 16 }}
/>
</Card>
</PageHeaderLayout>
);
import React from 'react';
import { shallow } from 'enzyme';
import Success from './Success';
it('renders with Result', () => {
const wrapper = shallow(<Success />);
expect(wrapper.find('Result').length).toBe(1);
expect(wrapper.find('Result').prop('type')).toBe('success');
});
import React, { Component } from 'react';
import { connect } from 'dva';
import { Link } from 'dva/router';
import { Checkbox, Alert, Icon } from 'antd';
import Login from '../../components/Login';
import styles from './Login.less';
const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
@connect(({ login, loading }) => ({
login,
submitting: loading.effects['login/login'],
}))
export default class LoginPage extends Component {
state = {
type: 'account',
autoLogin: true,
}
onTabChange = (type) => {
this.setState({ type });
}
handleSubmit = (err, values) => {
const { type } = this.state;
if (!err) {
this.props.dispatch({
type: 'login/login',
payload: {
...values,
type,
},
});
}
}
changeAutoLogin = (e) => {
this.setState({
autoLogin: e.target.checked,
});
}
renderMessage = (content) => {
return (
<Alert style={{ marginBottom: 24 }} message={content} type="error" showIcon />
);
}
render() {
const { login, submitting } = this.props;
const { type } = this.state;
return (
<div className={styles.main}>
<Login
defaultActiveKey={type}
onTabChange={this.onTabChange}
onSubmit={this.handleSubmit}
>
<Tab key="account" tab="账户密码登录">
{
login.status === 'error' &&
login.type === 'account' &&
!login.submitting &&
this.renderMessage('账户或密码错误(admin/888888)')
}
<UserName name="userName" placeholder="admin/user" />
<Password name="password" placeholder="888888/123456" />
</Tab>
<Tab key="mobile" tab="手机号登录">
{
login.status === 'error' &&
login.type === 'mobile' &&
!login.submitting &&
this.renderMessage('验证码错误')
}
<Mobile name="mobile" />
<Captcha name="captcha" />
</Tab>
<div>
<Checkbox checked={this.state.autoLogin} onChange={this.changeAutoLogin}>自动登录</Checkbox>
<a style={{ float: 'right' }} href="">忘记密码</a>
</div>
<Submit loading={submitting}>登录</Submit>
<div className={styles.other}>
其他登录方式
<Icon className={styles.icon} type="alipay-circle" />
<Icon className={styles.icon} type="taobao-circle" />
<Icon className={styles.icon} type="weibo-circle" />
<Link className={styles.register} to="/user/register">注册账户</Link>
</div>
</Login>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
.main {
width: 368px;
margin: 0 auto;
.icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color .3s;
&:hover {
color: @primary-color;
}
}
.other {
text-align: left;
margin-top: 24px;
line-height: 22px;
.register {
float: right;
}
}
}
import React, { Component } from 'react';
import { connect } from 'dva';
import { routerRedux, Link } from 'dva/router';
import { Form, Input, Button, Select, Row, Col, Popover, Progress } from 'antd';
import styles from './Register.less';
const FormItem = Form.Item;
const { Option } = Select;
const InputGroup = Input.Group;
const passwordStatusMap = {
ok: <div className={styles.success}>强度:强</div>,
pass: <div className={styles.warning}>强度:中</div>,
poor: <div className={styles.error}>强度:太短</div>,
};
const passwordProgressMap = {
ok: 'success',
pass: 'normal',
poor: 'exception',
};
@connect(({ register, loading }) => ({
register,
submitting: loading.effects['register/submit'],
}))
@Form.create()
export default class Register extends Component {
state = {
count: 0,
confirmDirty: false,
visible: false,
help: '',
prefix: '86',
};
componentWillReceiveProps(nextProps) {
if (nextProps.register.status === 'ok') {
this.props.dispatch(routerRedux.push('/user/register-result'));
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
onGetCaptcha = () => {
let count = 59;
this.setState({ count });
this.interval = setInterval(() => {
count -= 1;
this.setState({ count });
if (count === 0) {
clearInterval(this.interval);
}
}, 1000);
};
getPasswordStatus = () => {
const { form } = this.props;
const value = form.getFieldValue('password');
if (value && value.length > 9) {
return 'ok';
}
if (value && value.length > 5) {
return 'pass';
}
return 'poor';
};
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields({ force: true }, (err, values) => {
if (!err) {
this.props.dispatch({
type: 'register/submit',
payload: {
...values,
prefix: this.state.prefix,
},
});
}
});
};
handleConfirmBlur = (e) => {
const { value } = e.target;
this.setState({ confirmDirty: this.state.confirmDirty || !!value });
};
checkConfirm = (rule, value, callback) => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password')) {
callback('两次输入的密码不匹配!');
} else {
callback();
}
};
checkPassword = (rule, value, callback) => {
if (!value) {
this.setState({
help: '请输入密码!',
visible: !!value,
});
callback('error');
} else {
this.setState({
help: '',
});
if (!this.state.visible) {
this.setState({
visible: !!value,
});
}
if (value.length < 6) {
callback('error');
} else {
const { form } = this.props;
if (value && this.state.confirmDirty) {
form.validateFields(['confirm'], { force: true });
}
callback();
}
}
};
changePrefix = (value) => {
this.setState({
prefix: value,
});
};
renderPasswordProgress = () => {
const { form } = this.props;
const value = form.getFieldValue('password');
const passwordStatus = this.getPasswordStatus();
return value && value.length ? (
<div className={styles[`progress-${passwordStatus}`]}>
<Progress
status={passwordProgressMap[passwordStatus]}
className={styles.progress}
strokeWidth={6}
percent={value.length * 10 > 100 ? 100 : value.length * 10}
showInfo={false}
/>
</div>
) : null;
};
render() {
const { form, submitting } = this.props;
const { getFieldDecorator } = form;
const { count, prefix } = this.state;
return (
<div className={styles.main}>
<h3>注册</h3>
<Form onSubmit={this.handleSubmit}>
<FormItem>
{getFieldDecorator('mail', {
rules: [
{
required: true,
message: '请输入邮箱地址!',
},
{
type: 'email',
message: '邮箱地址格式错误!',
},
],
})(<Input size="large" placeholder="邮箱" />)}
</FormItem>
<FormItem help={this.state.help}>
<Popover
content={
<div style={{ padding: '4px 0' }}>
{passwordStatusMap[this.getPasswordStatus()]}
{this.renderPasswordProgress()}
<div style={{ marginTop: 10 }}>
请至少输入 6 个字符。请不要使用容易被猜到的密码。
</div>
</div>
}
overlayStyle={{ width: 240 }}
placement="right"
visible={this.state.visible}
>
{getFieldDecorator('password', {
rules: [
{
validator: this.checkPassword,
},
],
})(
<Input
size="large"
type="password"
placeholder="至少6位密码,区分大小写"
/>
)}
</Popover>
</FormItem>
<FormItem>
{getFieldDecorator('confirm', {
rules: [
{
required: true,
message: '请确认密码!',
},
{
validator: this.checkConfirm,
},
],
})(<Input size="large" type="password" placeholder="确认密码" />)}
</FormItem>
<FormItem>
<InputGroup compact>
<Select
size="large"
value={prefix}
onChange={this.changePrefix}
style={{ width: '20%' }}
>
<Option value="86">+86</Option>
<Option value="87">+87</Option>
</Select>
{getFieldDecorator('mobile', {
rules: [
{
required: true,
message: '请输入手机号!',
},
{
pattern: /^1\d{10}$/,
message: '手机号格式错误!',
},
],
})(
<Input
size="large"
style={{ width: '80%' }}
placeholder="11位手机号"
/>
)}
</InputGroup>
</FormItem>
<FormItem>
<Row gutter={8}>
<Col span={16}>
{getFieldDecorator('captcha', {
rules: [
{
required: true,
message: '请输入验证码!',
},
],
})(<Input size="large" placeholder="验证码" />)}
</Col>
<Col span={8}>
<Button
size="large"
disabled={count}
className={styles.getCaptcha}
onClick={this.onGetCaptcha}
>
{count ? `${count} s` : '获取验证码'}
</Button>
</Col>
</Row>
</FormItem>
<FormItem>
<Button
size="large"
loading={submitting}
className={styles.submit}
type="primary"
htmlType="submit"
>
注册
</Button>
<Link className={styles.login} to="/user/login">
使用已有账户登录
</Link>
</FormItem>
</Form>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
.main {
width: 368px;
margin: 0 auto;
:global {
.ant-form-item {
margin-bottom: 24px;
}
}
h3 {
font-size: 16px;
margin-bottom: 20px;
}
.getCaptcha {
display: block;
width: 100%;
}
.submit {
width: 50%;
}
.login {
float: right;
line-height: @btn-height-lg;
}
}
.success,
.warning,
.error {
transition: color 0.3s;
}
.success {
color: @success-color;
}
.warning {
color: @warning-color;
}
.error {
color: @error-color;
}
.progress-pass > .progress {
:global {
.ant-progress-bg {
background-color: @warning-color;
}
}
}
import React from 'react';
import { Button } from 'antd';
import { Link } from 'dva/router';
import Result from '../../components/Result';
import styles from './RegisterResult.less';
const title = <div className={styles.title}>你的账户:AntDesign@example.com 注册成功</div>;
const actions = (
<div className={styles.actions}>
<a href=""><Button size="large" type="primary">查看邮箱</Button></a>
<Link to="/"><Button size="large">返回首页</Button></Link>
</div>
);
export default () => (
<Result
className={styles.registerResult}
type="success"
title={title}
description="激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。"
actions={actions}
style={{ marginTop: 56 }}
/>
);
.registerResult {
:global {
.anticon {
font-size: 64px;
}
}
.title {
margin-top: 32px;
font-size: 20px;
line-height: 28px;
}
.actions {
margin-top: 40px;
a + a {
margin-left: 8px;
}
}
}
import { stringify } from 'qs';
import request from '../utils/request';
export async function queryProjectNotice() {
return request('/api/project/notice');
}
export async function queryActivities() {
return request('/api/activities');
}
export async function queryRule(params) {
return request(`/api/rule?${stringify(params)}`);
}
export async function removeRule(params) {
return request('/api/rule', {
method: 'POST',
body: {
...params,
method: 'delete',
},
});
}
export async function addRule(params) {
return request('/api/rule', {
method: 'POST',
body: {
...params,
method: 'post',
},
});
}
export async function fakeSubmitForm(params) {
return request('/api/forms', {
method: 'POST',
body: params,
});
}
export async function fakeChartData() {
return request('/api/fake_chart_data');
}
export async function queryTags() {
return request('/api/tags');
}
export async function queryBasicProfile() {
return request('/api/profile/basic');
}
export async function queryAdvancedProfile() {
return request('/api/profile/advanced');
}
export async function queryFakeList(params) {
return request(`/api/fake_list?${stringify(params)}`);
}
export async function fakeAccountLogin(params) {
return request('/api/login/account', {
method: 'POST',
body: params,
});
}
export async function fakeRegister(params) {
return request('/api/register', {
method: 'POST',
body: params,
});
}
export async function queryNotices() {
return request('/api/notices');
}
import request from '../utils/request';
export async function query404() {
return request('/api/404');
}
export async function query401() {
return request('/api/401');
}
export async function query403() {
return request('/api/403');
}
export async function query500() {
return request('/api/500');
}
import request from '../utils/request';
export async function query() {
return request('/api/users');
}
export async function queryCurrent() {
return request('/api/currentUser');
}
// https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
module.exports = {
// 'primary-color': '#10e99b',
'card-actions-background': '#f5f8fa',
};
import RenderAuthorized from '../components/Authorized';
import { getAuthority } from './authority';
const Authorized = RenderAuthorized(getAuthority());
export default Authorized;
// use localStorage to store the authority info, which might be sent from server in actual project.
export function getAuthority() {
return localStorage.getItem('antd-pro-authority') || 'admin';
}
export function setAuthority(authority) {
return localStorage.setItem('antd-pro-authority', authority);
}
import fetch from 'dva/fetch';
import { notification } from 'antd';
import { routerRedux } from 'dva/router';
import store from '../index';
const codeMessage = {
200: '服务器成功返回请求的数据',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据,的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器',
502: '网关错误',
503: '服务不可用,服务器暂时过载或维护',
504: '网关超时',
};
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const errortext = codeMessage[response.status] || response.statusText;
notification.error({
message: `请求错误 ${response.status}: ${response.url}`,
description: errortext,
});
const error = new Error(errortext);
error.name = response.status;
error.response = response;
throw error;
}
/**
* Requests a URL, returning a promise.
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
* @return {object} An object containing either "data" or "err"
*/
export default function request(url, options) {
const defaultOptions = {
credentials: 'include',
};
const newOptions = { ...defaultOptions, ...options };
if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
...newOptions.headers,
};
newOptions.body = JSON.stringify(newOptions.body);
}
return fetch(url, newOptions)
.then(checkStatus)
.then((response) => {
if (newOptions.method === 'DELETE' || response.status === 204) {
return response.text();
}
return response.json();
})
.catch((e) => {
const { dispatch } = store;
if (e.name === 401) {
dispatch({
type: 'login/logout',
});
return;
}
if (e.name === 403) {
dispatch(routerRedux.push('/exception/403'));
return;
}
if (e.name <= 504 && e.name >= 500) {
dispatch(routerRedux.push('/exception/500'));
return;
}
if (e.name >= 404 && e.name < 422) {
dispatch(routerRedux.push('/exception/404'));
}
});
}
import moment from 'moment';
export function fixedZero(val) {
return val * 1 < 10 ? `0${val}` : val;
}
export function getTimeDistance(type) {
const now = new Date();
const oneDay = 1000 * 60 * 60 * 24;
if (type === 'today') {
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
return [moment(now), moment(now.getTime() + (oneDay - 1000))];
}
if (type === 'week') {
let day = now.getDay();
now.setHours(0);
now.setMinutes(0);
now.setSeconds(0);
if (day === 0) {
day = 6;
} else {
day -= 1;
}
const beginTime = now.getTime() - (day * oneDay);
return [moment(beginTime), moment(beginTime + ((7 * oneDay) - 1000))];
}
if (type === 'month') {
const year = now.getFullYear();
const month = now.getMonth();
const nextDate = moment(now).add(1, 'months');
const nextYear = nextDate.year();
const nextMonth = nextDate.month();
return [moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`), moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000)];
}
if (type === 'year') {
const year = now.getFullYear();
return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)];
}
}
export function getPlainNode(nodeList, parentPath = '') {
const arr = [];
nodeList.forEach((node) => {
const item = node;
item.path = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
item.exact = true;
if (item.children && !item.component) {
arr.push(...getPlainNode(item.children, item.path));
} else {
if (item.children && item.component) {
item.exact = false;
}
arr.push(item);
}
});
return arr;
}
export function digitUppercase(n) {
const fraction = ['角', '分'];
const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
const unit = [
['元', '万', '亿'],
['', '拾', '佰', '仟'],
];
let num = Math.abs(n);
let s = '';
fraction.forEach((item, index) => {
s += (digit[Math.floor(num * 10 * (10 ** index)) % 10] + item).replace(/零./, '');
});
s = s || '整';
num = Math.floor(num);
for (let i = 0; i < unit[0].length && num > 0; i += 1) {
let p = '';
for (let j = 0; j < unit[1].length && num > 0; j += 1) {
p = digit[num % 10] + unit[1][j] + p;
num = Math.floor(num / 10);
}
s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s;
}
return s.replace(/(零.)*零元/, '元').replace(/(零.)+/g, '零').replace(/^整$/, '零元整');
}
function getRelation(str1, str2) {
if (str1 === str2) {
console.warn('Two path are equal!'); // eslint-disable-line
}
const arr1 = str1.split('/');
const arr2 = str2.split('/');
if (arr2.every((item, index) => item === arr1[index])) {
return 1;
} else if (arr1.every((item, index) => item === arr2[index])) {
return 2;
}
return 3;
}
export function getRoutes(path, routerData) {
let routes = Object.keys(routerData).filter(routePath =>
routePath.indexOf(path) === 0 && routePath !== path);
routes = routes.map(item => item.replace(path, ''));
let renderArr = [];
renderArr.push(routes[0]);
for (let i = 1; i < routes.length; i += 1) {
let isAdd = false;
isAdd = renderArr.every(item => getRelation(item, routes[i]) === 3);
renderArr = renderArr.filter(item => getRelation(item, routes[i]) !== 1);
if (isAdd) {
renderArr.push(routes[i]);
}
}
const renderRoutes = renderArr.map((item) => {
const exact = !routes.some(route => route !== item && getRelation(route, item) === 1);
return {
...routerData[`${path}${item}`],
key: `${path}${item}`,
path: `${path}${item}`,
exact,
};
});
return renderRoutes;
}
/* eslint no-useless-escape:0 */
const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/g;
export function isUrl(path) {
return reg.test(path);
}
.textOverflow() {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.textOverflowMulti(@line: 3, @bg: #fff) {
overflow: hidden;
position: relative;
line-height: 1.5em;
max-height: @line * 1.5em;
text-align: justify;
margin-right: -1em;
padding-right: 1em;
&:before {
background: @bg;
content: '...';
padding: 0 1px;
position: absolute;
right: 14px;
bottom: 0;
}
&:after {
background: white;
content: '';
margin-top: 0.2em;
position: absolute;
right: 14px;
width: 1em;
height: 1em;
}
}
// mixins for clearfix
// ------------------------
.clearfix() {
zoom: 1;
&:before,
&:after {
content: " ";
display: table;
}
&:after {
clear: both;
visibility: hidden;
font-size: 0;
height: 0;
}
}
const { spawn } = require('child_process');
const { kill } = require('cross-port-killer');
const env = Object.create(process.env);
env.BROWSER = 'none';
const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], {
env,
});
startServer.stderr.on('data', (data) => {
// eslint-disable-next-line
console.log(data);
});
startServer.on('exit', () => {
kill(process.env.PORT || 8000);
});
// eslint-disable-next-line
console.log('Starting development server for e2e tests...');
startServer.stdout.on('data', (data) => {
// eslint-disable-next-line
console.log(data.toString());
if (data.toString().indexOf('Compiled successfully') >= 0 ||
data.toString().indexOf('Compiled with warnings') >= 0) {
// eslint-disable-next-line
console.log('Development server is started, ready to run tests.');
const testCmd = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['test'], {
stdio: 'inherit',
});
testCmd.on('exit', () => {
startServer.kill();
});
}
});
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!