Commit d7ddf3ab authored by nikogu's avatar nikogu

Extract scaffold to independent git repo

parent 653d86f7
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
{
"parser": "babel-eslint",
"extends": "airbnb",
"rules": {
"generator-star-spacing": [0],
"consistent-return": [0],
"react/forbid-prop-types": [0],
"react/jsx-filename-extension": [1, { "extensions": [".js"] }],
"global-require": [1],
"import/prefer-default-export": [0],
"react/jsx-no-bind": [0],
"react/prop-types": [0],
"react/prefer-stateless-function": [0],
"no-else-return": [0],
"no-restricted-syntax": [0],
"import/no-extraneous-dependencies": [0],
"no-use-before-define": [0],
"jsx-a11y/no-static-element-interactions": [0],
"jsx-a11y/no-noninteractive-element-interactions": [0],
"no-nested-ternary": [0],
"arrow-body-style": [0],
"import/extensions": [0],
"no-bitwise": [0],
"no-cond-assign": [0],
"import/no-unresolved": [0],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "ignore"
}],
"require-yield": [1]
},
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true
}
}
}
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
# production
/dist
# misc
.DS_Store
npm-debug.log*
{
"entry": "src/index.js",
"env": {
"development": {
"extraBabelPlugins": [
"dva-hmr",
"transform-runtime",
"transform-decorators-legacy",
["import", { "libraryName": "antd", "style": true }]
]
},
"production": {
"extraBabelPlugins": [
"transform-runtime",
"transform-decorators-legacy",
["import", { "libraryName": "antd", "style": true }]
]
}
},
"theme": {
"font-size-base": "14px",
"badge-font-size": "12px",
"btn-font-size-lg": "@font-size-base",
"layout-body-background": "#f5f5f5"
}
}
import mockjs from 'mockjs';
import { getRule, postRule } from './mock/rule';
import { getActivities, getNotice, getFakeList } from './mock/api';
import { getFakeChartData } from './mock/chart';
import { imgMap } from './mock/utils';
import { getProfileData } from './mock/profile';
import { getNotices } from './mock/notices';
import { format, delay } from 'roadhog-api-doc';
// 代码中会兼容本地 service mock 以及部署站点的静态数据
const proxy = {
// 支持值为 Object 和 Array
'GET /api/currentUser': {
$desc: "获取当前用户接口",
$params: {
pageSize: {
desc: '分页',
exp: 2,
},
},
$body: {
name: 'momo.zxy',
avatar: imgMap.user,
userid: '00000001',
notifyCount: 12,
},
},
// GET POST 可省略
'GET /api/users': [{
key: '1',
name: 'John Brown',
age: 32,
address: 'New York No. 1 Lake Park',
}, {
key: '2',
name: 'Jim Green',
age: 42,
address: 'London No. 1 Lake Park',
}, {
key: '3',
name: 'Joe Black',
age: 32,
address: 'Sidney No. 1 Lake Park',
}],
'GET /api/project/notice': getNotice,
'GET /api/activities': getActivities,
'GET /api/rule': getRule,
'POST /api/rule': {
$params: {
pageSize: {
desc: '分页',
exp: 2,
},
},
$body: postRule,
},
'POST /api/forms': (req, res) => {
res.send('Ok');
},
'GET /api/tags': mockjs.mock({
'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }]
}),
'GET /api/fake_list': getFakeList,
'GET /api/fake_chart_data': getFakeChartData,
'GET /api/profile': getProfileData,
'POST /api/login/account': (req, res) => {
res.send({ status: 'error', type: 'account' });
},
'POST /api/login/mobile': (req, res) => {
res.send({ status: 'ok', type: 'mobile' });
},
'POST /api/register': (req, res) => {
res.send({ status: 'ok' });
},
'GET /api/notices': getNotices,
};
export default delay(proxy, 1000);
import { imgMap, getUrlParams } from './utils';
export function fakeList(count) {
const titles = [
'凤蝶',
'AntDesignPro',
'DesignLab',
'Basement',
'AntDesign',
'云雀',
'体验云',
'AntDesignMobile',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/hYjIZrUoBfNxOAYBVDfc.png', // 凤蝶
'https://gw.alipayobjects.com/zos/rmsportal/HHWPIzPLCLYmVuPivyiA.png', // 云雀
'https://gw.alipayobjects.com/zos/rmsportal/irqByKtOdKfDojxIWTXF.png', // Basement
'https://gw.alipayobjects.com/zos/rmsportal/VcmdbCBcwPTGYgbYeMzX.png', // DesignLab
];
const covers = [
'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png',
'https://gw.alipayobjects.com/zos/rmsportal/xMPpMvGSIXusgtgUPAdw.png',
'https://gw.alipayobjects.com/zos/rmsportal/hQReiajgtqzIVFjLXjHp.png',
'https://gw.alipayobjects.com/zos/rmsportal/nczfTaXEzhSpvgZZjBev.png',
];
const list = [];
for (let i = 0; i < count; i += 1) {
list.push({
id: `fake-list-${i}`,
owner: '曲丽丽',
title: titles[i % 8],
avatar: avatars[i % 4],
cover: covers[i % 4],
status: ['active', 'exception', 'normal'][i % 3],
percent: Math.ceil(Math.random() * 50) + 50,
logo: ['https://gw.alipayobjects.com/zos/rmsportal/KoJjkdbuTFxzJmmjuDVR.png', 'https://gw.alipayobjects.com/zos/rmsportal/UxGORCvEXJEsxOfEKZiA.png'][i % 2],
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: '一句话描述一句话描述',
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: '段落示意:蚂蚁金服设计平台 design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
members: [
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png',
name: '王昭君',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png',
name: '王昭君',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png',
name: '王昭君',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.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;
}
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: '消息列表体验优化',
logo: imgMap.b,
description: '这是一条描述信息这是一条描述信息',
updatedAt: new Date(),
member: '蜂鸟项目组',
},
{
id: 'xxx2',
title: 'XX 平台',
logo: imgMap.c,
description: '这是一条描述信息',
updatedAt: new Date('2017-07-24 11:00:00'),
member: '凤蝶精英小分队',
},
{
id: 'xxx3',
title: '消息列表体验优化',
logo: imgMap.a,
description: '这是一条描述信息这是一条描述信息',
updatedAt: new Date(),
member: '蜂鸟项目组',
},
{
id: 'xxx4',
title: '文档中心1',
logo: imgMap.a,
description: '这是一条描述信息这是一条描述信息',
updatedAt: new Date('2017-07-23 06:23:00'),
member: '成都超级小分队',
},
{
id: 'xxx5',
title: '文档中心2',
logo: imgMap.b,
description: '这是一条描述信息这是一条描述信息',
updatedAt: new Date('2017-07-23 06:23:00'),
member: '成都超级小分队',
},
{
id: 'xxx6',
title: '智能运营中心',
logo: imgMap.c,
description: '这是一条描述信息这是一条描述信息',
updatedAt: new Date('2017-07-23 06:23:00'),
member: '成都超级小分队',
},
];
export const getActivities = [
{
id: 'trend-1',
updatedAt: new Date(),
user: {
name: '林东东',
avatar: imgMap.a,
},
action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)',
},
{
id: 'trend-2',
updatedAt: new Date(),
user: {
name: '林嘻嘻',
avatar: imgMap.c,
},
action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)',
},
{
id: 'trend-3',
updatedAt: new Date(),
user: {
name: '林囡囡',
avatar: imgMap.b,
},
action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)',
},
{
id: 'trend-4',
updatedAt: new Date(),
user: {
name: '林贝贝',
avatar: imgMap.c,
},
action: '在 [5 月日常迭代](http://github.com/) 更新至已发布状态',
},
{
id: 'trend-5',
updatedAt: new Date(),
user: {
name: '林忠忠',
avatar: imgMap.a,
},
action: '在 [工程效能](http://github.com/) 发布了 [留言](http://github.com/)',
},
{
id: 'trend-6',
updatedAt: new Date(),
user: {
name: '林呜呜',
avatar: imgMap.d,
},
action: '在 [云雀](http://github.com/) 新建项目 [品牌迭代](http://github.com/)',
},
];
export default {
getNotice,
getActivities,
getFakeList,
};
import moment from 'moment';
// mock data
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,
});
}
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,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
radarData,
};
export default {
getFakeChartData,
};
export default {
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: 'urgent',
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: '待办',
}]);
},
};
const operation1 = [
{
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 operation2 = [
{
key: 'op1',
type: '订购关系生效',
name: '曲丽丽',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
const operation3 = [
{
key: 'op1',
type: '创建订单',
name: '汗牙牙',
status: 'agree',
updatedAt: '2017-10-03 19:23:12',
memo: '-',
},
];
export const getProfileData = {
operation1,
operation2,
operation3,
};
export default {
getProfileData,
};
import { getUrlParams } from './utils';
// mock tableListDataSource
let tableListDataSource = [];
for (let i = 0; i < 46; i += 1) {
tableListDataSource.push({
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(`2017-07-${Math.floor(i / 2) + 1} ${Math.floor(i / 2) + 1}:00:00`),
createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1} ${Math.floor(i / 2) + 1}:00:00`),
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;
}
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 s = params.status.split(',');
if (s.length === 1) {
dataSource = dataSource.filter(data => parseInt(data.status, 10) === parseInt(s[0], 10));
}
}
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;
}
const body = (b && b.body) || req.body;
const method = body.method;
switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
const no = body.no;
tableListDataSource = tableListDataSource.filter(item => no.indexOf(item.no) === -1);
break;
case 'post':
const description = body.description;
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/YdMCpIJULitXfqHCFPbF.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];
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-admin",
"private": true,
"scripts": {
"start": "roadhog server",
"build": "roadhog build",
"lint": "eslint --ext .js src test",
"precommit": "npm run lint"
},
"dependencies": {
"antd": "next",
"dva": "^1.2.1",
"g-cloud": "^1.0.2-beta",
"g2": "^2.3.8",
"g2-plugin-slider": "^1.2.1",
"lodash": "^4.17.4",
"marked": "^0.3.6",
"numeral": "^2.0.6",
"prop-types": "^15.5.10",
"qs": "^6.5.0",
"react": "^15.4.0",
"react-document-title": "^2.0.3",
"react-dom": "^15.4.0",
"react-redux": "4.x || 5.x",
"react-router": "2.x || 3.x"
},
"devDependencies": {
"babel-eslint": "^7.1.1",
"babel-plugin-dva-hmr": "^0.3.2",
"babel-plugin-import": "^1.2.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-runtime": "^6.9.0",
"babel-runtime": "^6.9.2",
"eslint": "^3.0.0",
"eslint-config-airbnb": "latest",
"eslint-plugin-babel": "^4.0.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^5.0.1",
"eslint-plugin-react": "^7.0.1",
"expect": "^1.20.2",
"husky": "^0.13.4",
"mockjs": "^1.0.1-beta3",
"redbox-react": "^1.3.2",
"roadhog": "^1.0.2",
"roadhog-api-doc": "^0.1.0"
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ant Design Pro</title>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div id="root"></div>
<script src="index.js"></script>
</body>
</html>
import BasicLayout from '../layouts/BasicLayout';
import UserLayout from '../layouts/UserLayout';
import Analysis from '../routes/Dashboard/Analysis';
import Monitor from '../routes/Dashboard/Monitor';
import Workplace from '../routes/Dashboard/Workplace';
import TableList from '../routes/List/TableList';
import CoverCardList from '../routes/List/CoverCardList';
import CardList from '../routes/List/CardList';
import FilterCardList from '../routes/List/FilterCardList';
import SearchList from '../routes/List/SearchList';
import BasicList from '../routes/List/BasicList';
import Profile from '../routes/Profile';
import BasicForm from '../routes/Forms/BasicForm';
import AdvancedForm from '../routes/Forms/AdvancedForm';
import StepForm from '../routes/Forms/StepForm';
import Step2 from '../routes/Forms/StepForm/Step2';
import Step3 from '../routes/Forms/StepForm/Step3';
import Exception403 from '../routes/Exception/403';
import Exception404 from '../routes/Exception/404';
import Exception500 from '../routes/Exception/500';
import Success from '../routes/Result/Success';
import Error from '../routes/Result/Error';
import Login from '../routes/User/Login';
import Register from '../routes/User/Register';
import RegisterResult from '../routes/User/RegisterResult';
function userAdapter(userData) {
userData.children.forEach((item) => {
if (item.children) {
userAdapter(item);
} else {
const userItem = item;
userItem.target = '_blank';
userItem.noRoute = true;
}
});
return userData;
}
export const user = [{
name: '帐户',
icon: 'setting',
path: 'user',
children: [{
name: '登录',
path: 'login',
component: Login,
icon: 'setting',
}, {
name: '注册',
path: 'register',
component: Register,
icon: 'setting',
}, {
name: '注册结果',
path: 'register-result',
component: RegisterResult,
icon: 'setting',
}],
}];
export const menus = [{
name: 'Dashboard',
icon: 'setting',
path: 'dashboard',
children: [{
name: '分析页',
path: 'analysis',
component: Analysis,
icon: 'setting',
}, {
name: '监控页',
path: 'monitor',
component: Monitor,
icon: 'setting',
}, {
name: '工作台',
path: 'workplace',
component: Workplace,
icon: 'setting',
}],
}, {
name: '表单页',
path: 'form',
icon: 'setting',
children: [{
name: '基础表单',
path: 'basic-form',
component: BasicForm,
icon: 'setting',
}, {
name: '分步表单',
path: 'step-form',
component: StepForm,
icon: 'setting',
children: [{
path: 'confirm',
component: Step2,
}, {
path: 'result',
component: Step3,
}],
}, {
name: '高级表单',
path: 'advanced-form',
component: AdvancedForm,
icon: 'setting',
}],
}, {
name: '列表页',
path: 'list',
icon: 'setting',
children: [{
name: '标准表格(表格查询)',
path: 'table-list',
component: TableList,
icon: 'setting',
}, {
name: '标准列表',
path: 'basic-list',
component: BasicList,
icon: 'setting',
}, {
name: '卡片列表',
path: 'card-list',
component: CardList,
icon: 'setting',
}, {
name: '卡片列表(封面)',
path: 'cover-card-list',
component: CoverCardList,
icon: 'setting',
}, {
name: '带筛选卡片列表',
path: 'filter-card-list',
component: FilterCardList,
icon: 'setting',
}, {
name: '搜索列表',
path: 'search',
component: SearchList,
icon: 'setting',
}],
}, {
name: '详情页',
path: 'profile',
component: Profile,
icon: 'setting',
}, {
name: '结果',
path: 'result',
icon: 'setting',
children: [{
name: '成功',
path: 'success',
component: Success,
icon: 'setting',
}, {
name: '失败',
path: 'fail',
component: Error,
icon: 'setting',
}],
}, {
name: '错误',
path: 'error',
icon: 'setting',
children: [{
name: '403',
path: '403',
component: Exception403,
icon: 'setting',
}, {
name: '404',
path: '404',
component: Exception404,
icon: 'setting',
}, {
name: '500',
path: '500',
component: Exception500,
icon: 'setting',
}],
}, userAdapter(JSON.parse(JSON.stringify(user[0])))];
export default [{
component: BasicLayout,
name: '首页',
children: menus,
path: '',
}, {
component: UserLayout,
name: '账户',
children: user,
}];
import React from 'react';
import moment from 'moment';
import marked from 'marked';
import { Avatar } from 'antd';
import styles from './index.less';
/* eslint react/no-danger:0 */
export default ({ data: { user, updatedAt, action } }) => (
<div
className={styles.activitiesItem}
>
<div className={styles.avatar}>
{
user.link && <a href={user.link} target="_blank">
<Avatar src={user.avatar} />
</a>
}
{
!user.link && <img src={user.avatar} alt={user.title} />
}
</div>
<div className={styles.content}>
<div>
<span className={styles.name}>{user.name}</span>
<div dangerouslySetInnerHTML={{ __html: marked(action) }} />
</div>
<p>{moment(updatedAt).fromNow()}</p>
</div>
</div>
);
@import "~antd/lib/style/themes/default.less";
.activitiesItem {
padding: 24px 24px 0 24px;
position: relative;
.avatar {
position: absolute;
top: 24px;
left: 24px;
img {
display: block;
border-radius: 32px;
width: 32px;
height: 32px;
}
}
.content {
border-bottom: 1px solid @border-color-split;
padding-left: 48px;
padding-bottom: 24px;
font-size: @font-size-base;
a {
color: @primary-color;
}
& > div {
line-height: 22px;
.name {
margin-right: 4px;
font-weight: 500;
}
div, p {
display: inline-block;
}
}
& > p {
margin-top: 4px;
line-height: 22px;
}
}
}
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',
});
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;
overflow: hidden;
font-size: @font-size-base;
margin-left: -8px;
width: @avatar-size-base;
height: @avatar-size-base;
}
.avatarItemLarge {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
.avatarItemSmall {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
import React, { PureComponent } from 'react';
import G2 from 'g2';
import styles from '../index.less';
class Bar extends PureComponent {
componentDidMount() {
this.renderChart(this.props.data);
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
this.renderChart(nextProps.data);
}
}
handleRef = (n) => {
this.node = n;
}
renderChart(data) {
const { height = 0, fit = true, color = '#33abfb', margin = [32, 0, 32, 40] } = this.props;
if (!data || (data && data.length < 1)) {
return;
}
// clean
this.node.innerHTML = '';
const Frame = G2.Frame;
const frame = new Frame(data);
const chart = new G2.Chart({
container: this.node,
forceFit: fit,
height: height - 22,
legend: null,
plotCfg: {
margin,
},
});
chart.axis('x', {
title: false,
});
chart.axis('y', {
title: false,
line: false,
tickLine: false,
});
chart.source(frame, {
x: {
type: 'cat',
},
y: {
min: 0,
},
});
chart.tooltip({
title: null,
crosshairs: false,
map: {
name: 'x',
},
});
chart.interval().position('x*y').color(color);
chart.render();
}
render() {
const { height, title } = this.props;
return (
<div className={styles.chart} style={{ height }}>
<div>
{ title && <h4>{title}</h4>}
<div ref={this.handleRef} />
</div>
</div>
);
}
}
export default Bar;
import React from 'react';
import { Card } from 'antd';
import styles from './index.less';
const ChartCard = ({ contentHeight, title, action, total, footer, children, ...rest }) => (
<Card
bodyStyle={{ padding: '20px 24px 8px 24px' }}
{...rest}
>
<div className={styles.chartCard}>
<div className={styles.meta}>
<span className={styles.title}>{title}</span>
<span className={styles.action}>{action}</span>
</div>
{
// eslint-disable-next-line
total && <p className={styles.total} dangerouslySetInnerHTML={{ __html: total }} />
}
<div className={styles.content} style={{ height: contentHeight || 'auto' }}>
<div className={contentHeight && styles.contentFixed}>
{children}
</div>
</div>
{
footer && <div className={styles.footer}>
{footer}
</div>
}
</div>
</Card>
);
export default ChartCard;
@import "~antd/lib/style/themes/default.less";
@import "../../../utils/utils.less";
.chartCard {
position: relative;
.meta {
color: @text-color-secondary;
font-size: @font-size-base;
position: relative;
line-height: 22px;
height: 22px;
}
.action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.total {
.textOverflow();
color: @heading-color;
margin-top: 8px;
font-size: 30px;
line-height: 38px;
height: 38px;
}
.content {
position: relative;
width: 100%;
}
.contentFixed {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
.footer {
border-top: 1px solid @border-color-split;
padding-top: 8px;
margin-top: 11px;
& > * {
position: relative;
}
}
}
import React from 'react';
import styles from './index.less';
const Field = ({ label, value, ...rest }) => (
<p className={styles.field} {...rest}>
<span>{label}</span>
<span>{value}</span>
</p>
);
export default Field;
@import "~antd/lib/style/themes/default.less";
@import "../../../utils/utils.less";
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
span {
font-size: @font-size-base;
line-height: 22px;
}
span:last-child {
font-weight: 600;
margin-left: 8px;
}
}
import React, { PureComponent } from 'react';
import G2 from 'g2';
const Shape = G2.Shape;
/* eslint no-underscore-dangle: 0 */
class Gauge extends PureComponent {
componentDidMount() {
this.renderChart();
}
componentWillReceiveProps(nextProps) {
this.renderChart(nextProps);
}
handleRef = (n) => {
this.node = n;
}
initChart(nextProps) {
const { title, color = '#00b1f8' } = nextProps || this.props;
Shape.registShape('point', 'dashBoard', {
drawShape(cfg, group) {
const originPoint = cfg.points[0];
const point = this.parsePoint({ x: originPoint.x, y: 0.4 });
const center = this.parsePoint({
x: 0,
y: 0,
});
const shape = group.addShape('polygon', {
attrs: {
points: [
[center.x, center.y],
[point.x + 8, point.y],
[point.x + 8, point.y - 2],
[center.x, center.y - 2],
],
radius: 2,
lineWidth: 2,
arrow: false,
fill: color,
},
});
group.addShape('Marker', {
attrs: {
symbol: 'circle',
lineWidth: 2,
fill: color,
radius: 8,
x: center.x,
y: center.y,
},
});
group.addShape('Marker', {
attrs: {
symbol: 'circle',
lineWidth: 2,
fill: '#fff',
radius: 5,
x: center.x,
y: center.y,
},
});
const origin = cfg.origin;
group.addShape('text', {
attrs: {
x: center.x,
y: center.y + 80,
text: `${origin._origin.value}%`,
textAlign: 'center',
fontSize: 24,
fill: 'rgba(0, 0, 0, 0.85)',
},
});
group.addShape('text', {
attrs: {
x: center.x,
y: center.y + 45,
text: title,
textAlign: 'center',
fontSize: 14,
fill: 'rgba(0, 0, 0, 0.43)',
},
});
return shape;
},
});
}
renderChart(nextProps) {
const { height, color = '#00b1f8', bgColor = '#d3f3fe', title, percent } = nextProps || this.props;
const data = [{ name: title, value: percent }];
if (this.chart) {
this.chart.clear();
}
if (this.node) {
this.node.innerHTML = '';
}
this.initChart(nextProps);
const chart = new G2.Chart({
container: this.node,
forceFit: true,
height,
animate: false,
plotCfg: {
margin: [10, 0, 30, 0],
},
});
chart.source(data);
chart.tooltip(false);
chart.coord('gauge', {
startAngle: -1.2 * Math.PI,
endAngle: 0.20 * Math.PI,
});
chart.col('value', {
type: 'linear',
nice: true,
min: 0,
max: 100,
tickCount: 6,
subTick: false,
});
chart.axis('value', {
tickLine: {
stroke: color,
},
labelOffset: -12,
formatter(val) {
switch (val * 1) {
case 20:
return '差';
case 40:
return '中';
case 60:
return '良';
case 80:
return '优';
default:
return '';
}
},
});
chart.point().position('value').shape('dashBoard');
draw(data);
/* eslint no-shadow: 0 */
function draw(data) {
const val = data[0].value;
const lineWidth = 18;
chart.guide().clear();
chart.guide().arc(() => {
return [0, 0.95];
}, () => {
return [val, 0.95];
}, {
stroke: color,
lineWidth,
});
chart.guide().arc(() => {
return [val, 0.95];
}, (arg) => {
return [arg.max, 0.95];
}, {
stroke: bgColor,
lineWidth,
});
chart.changeData(data);
}
this.chart = chart;
}
render() {
return (
<div ref={this.handleRef} />
);
}
}
export default Gauge;
import React from 'react';
import { Icon } from 'antd';
const IconUp = ({ color }) => (
<Icon
style={{
color: (color === false) ? 'rgba(0,0,0,0.43)' : '#00a854',
fontSize: 12,
transform: 'scale(0.83)',
}}
type="caret-up"
/>
);
const IconDown = ({ color }) => (
<Icon
style={{
color: (color === false) ? 'rgba(0,0,0,0.43)' : '#f04134',
fontSize: 12,
transform: 'scale(0.83)',
}}
type="caret-down"
/>
);
export default {
IconUp,
IconDown,
};
import React, { PureComponent } from 'react';
import G2 from 'g2';
import styles from '../index.less';
class MiniArea extends PureComponent {
componentDidMount() {
this.renderChart(this.props.data);
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
this.renderChart(nextProps.data);
}
}
handleRef = (n) => {
this.node = n;
}
renderChart(data) {
const { height = 0, fit = true, color = '#33abfb', line, xAxis, yAxis } = this.props;
if (!data || (data && data.length < 1)) {
return;
}
// clean
this.node.innerHTML = '';
const chart = new G2.Chart({
container: this.node,
forceFit: fit,
height: height + 54,
plotCfg: {
margin: [36, 0, 30, 0],
},
legend: null,
});
if (!xAxis && !yAxis) {
chart.axis(false);
}
if (xAxis) {
chart.axis('x', xAxis);
} else {
chart.axis('x', false);
}
if (yAxis) {
chart.axis('y', yAxis);
} else {
chart.axis('y', false);
}
chart.source(data, {
x: {
type: 'cat',
range: [0, 1],
...xAxis,
},
y: {
min: 0,
...yAxis,
},
});
chart.tooltip({
title: null,
crosshairs: false,
map: {
name: 'x',
},
});
chart.area().position('x*y').color(color).shape('smooth');
if (line) {
chart.line().position('x*y').color(color).shape('smooth');
}
chart.render();
}
render() {
const { height } = this.props;
return (
<div className={styles.miniChart} style={{ height }}>
<div>
<div ref={this.handleRef} />
</div>
</div>
);
}
}
export default MiniArea;
import React, { PureComponent } from 'react';
import G2 from 'g2';
import styles from '../index.less';
class MiniBar extends PureComponent {
componentDidMount() {
this.renderChart(this.props.data);
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
this.renderChart(nextProps.data);
}
}
handleRef = (n) => {
this.node = n;
}
renderChart(data) {
const { height = 0, fit = true, color = '#33ABFB' } = this.props;
if (!data || (data && data.length < 1)) {
return;
}
// clean
this.node.innerHTML = '';
const Frame = G2.Frame;
const frame = new Frame(data);
const chart = new G2.Chart({
container: this.node,
forceFit: fit,
height: height + 54,
plotCfg: {
margin: [36, 0, 30, 0],
},
legend: null,
});
chart.axis(false);
chart.source(frame, {
x: {
type: 'cat',
},
y: {
min: 0,
},
});
chart.tooltip({
title: null,
crosshairs: false,
map: {
name: 'x',
},
});
chart.interval().position('x*y').color(color);
chart.render();
}
render() {
const { height } = this.props;
return (
<div className={styles.miniChart} style={{ height }}>
<div>
<div ref={this.handleRef} />
</div>
</div>
);
}
}
export default MiniBar;
import React from 'react';
import styles from './index.less';
const MiniProgress = ({ target, color, strokeWidth, percent }) => (
<div className={styles.miniProgress}>
<div
className={styles.target}
style={{ left: (target ? `${target}%` : null) }}
>
<span style={{ backgroundColor: (color || null) }} />
<span style={{ backgroundColor: (color || null) }} />
</div>
<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";
@import "../../../utils/utils.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 React from 'react';
import { Icon } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
export default ({ theme, title, subTitle, total, subTotal, status, ...rest }) => (
<div
className={
classNames(styles.numberInfo, {
[styles[`numberInfo${theme}`]]: theme,
})
}
{...rest}
>
{
title && <h4>{title}</h4>
}
<h6>{subTitle}</h6>
<div>
<span>{total}</span>
{
(status || subTotal) && <span className={styles.subTotal}>
{
status && <Icon type={`caret-${status}`} />
}
{subTotal}
</span>
}
</div>
</div>
);
@import "~antd/lib/style/themes/default.less";
@import "../../../utils/utils.less";
.numberInfo {
h4 {
color: @heading-color;
margin-bottom: 16px;
}
h6 {
color: @text-color-secondary;
font-size: @font-size-base;
height: 22px;
line-height: 22px;
.textOverflow();
}
& > div {
margin-top: 8px;
font-size: 0;
.textOverflow();
& > 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-base;
vertical-align: top;
i {
font-size: 12px;
transform: scale(0.82);
margin-right: 4px;
}
}
}
}
.numberInfolight {
& > div {
& > span {
color: @text-color;
}
}
}
import React, { Component } from 'react';
import G2 from 'g2';
import styles from './index.less';
/* eslint react/no-danger:0 */
class Pie extends Component {
state = {
legendData: [],
left: undefined,
}
componentDidMount() {
this.renderChart(this.props.data);
}
componentWillReceiveProps(nextProps) {
this.renderChart(nextProps.data);
}
handleRef = (n) => {
this.node = n;
}
handleTotalRef = (n) => {
this.totalNode = n;
}
handleLegendClick = (item, i) => {
const newItem = item;
newItem.checked = !newItem.checked;
const legendData = this.state.legendData;
legendData[i] = newItem;
if (this.chart) {
const filterItem = legendData.filter(l => l.checked).map(l => l.x);
this.chart.filter('x', filterItem);
this.chart.repaint();
}
this.setState({
legendData,
});
}
renderChart(data) {
const {
title, height = 0,
hasLegend, fit = true,
margin, percent, color,
inner = 0.75,
animate = true,
} = this.props;
let selected = this.props.selected || true;
let tooltip = this.props.tooltips || true;
let formatColor;
if (percent) {
selected = false;
tooltip = false;
formatColor = (value) => {
if (value === '占比') {
return color || '#0096fa';
} else {
return '#e9e9e9';
}
};
/* eslint no-param-reassign: */
data = [
{
x: '占比',
y: parseFloat(percent),
},
{
x: '反比',
y: 100 - parseFloat(percent),
},
];
}
if (!data || (data && data.length < 1)) {
return;
}
let m = margin;
if (!margin) {
if (hasLegend) {
m = [24, 240, 24, 0];
} else if (percent) {
m = [0, 0, 0, 0];
} else {
m = [24, 0, 24, 0];
}
}
const h = title ? (height + m[0] + m[2] + (-46)) : (height + m[0] + m[2]);
// clean
this.node.innerHTML = '';
const Stat = G2.Stat;
const chart = new G2.Chart({
container: this.node,
forceFit: fit,
height: h,
plotCfg: {
margin: m,
},
animate,
});
if (!tooltip) {
chart.tooltip(false);
} else {
chart.tooltip({
title: null,
});
}
chart.axis(false);
chart.legend(false);
chart.source(data, {
x: {
type: 'cat',
range: [0, 1],
},
y: {
min: 0,
},
});
chart.coord('theta', {
inner,
});
chart.intervalStack().position(Stat.summary.percent('y')).color('x', formatColor).selected(selected);
chart.render();
this.chart = chart;
let legendData = [];
if (hasLegend) {
const geom = chart.getGeoms()[0]; // 获取所有的图形
const items = geom.getData(); // 获取图形对应的数据
legendData = items.map((item) => {
/* eslint no-underscore-dangle:0 */
const origin = item._origin;
origin.color = item.color;
origin.checked = true;
return origin;
});
}
this.setState({
legendData,
}, () => {
let left = 0;
if (this.totalNode) {
left = -((this.totalNode.offsetWidth / 2) + ((margin || m)[1] / 2));
}
this.setState({
left,
});
});
}
render() {
const { height, title, valueFormat, subTitle, total, hasLegend } = this.props;
const { legendData, left } = this.state;
const mt = -(((legendData.length * 38) - 16) / 2);
return (
<div className={styles.pie} style={{ height }}>
<div>
{ title && <h4 className={styles.title}>{title}</h4>}
<div className={styles.content}>
<div ref={this.handleRef} />
{
(subTitle || total) && <div
className={styles.total}
ref={this.handleTotalRef}
style={{ marginLeft: left, opacity: left ? 1 : 0 }}
>
{
subTitle && <h4>{subTitle}</h4>
}
{
// eslint-disable-next-line
total && <p dangerouslySetInnerHTML={{ __html: total }} />
}
</div>
}
{
hasLegend && <ul className={styles.legend} style={{ marginTop: mt }}>
{
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>
<span className={styles.line} />
<span className={styles.percent}>{`${(item['..percent'] * 100).toFixed(2)}%`}</span>
<span
className={styles.value}
dangerouslySetInnerHTML={{
__html: valueFormat ? valueFormat(item.y) : item.y,
}}
/>
</li>
))
}
</ul>
}
</div>
</div>
</div>
);
}
}
export default Pie;
@import "~antd/lib/style/themes/default.less";
@import "../../../utils/utils.less";
.pie {
.content {
position: relative;
}
.legend {
position: absolute;
top: 50%;
right: 0;
min-width: 200px;
li {
cursor: pointer;
margin-bottom: 16px;
height: 22px;
line-height: 22px;
}
}
.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;
margin-right: 8px;
}
.percent {
color: @text-color-secondary;
}
.value {
position: absolute;
right: 0;
}
.total {
opacity: 0;
position: absolute;
left: 50%;
top: 50%;
margin-top: -34px;
text-align: center;
height: 62px;
& > h4 {
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
height: 22px;
margin-bottom: 8px;
}
& > p {
color: @heading-color;
display: block;
font-size: 24px;
height: 32px;
line-height: 32px;
}
}
}
import React, { PureComponent } from 'react';
import G2 from 'g2';
import { Row, Col } from 'antd';
import styles from './index.less';
/* eslint react/no-danger:0 */
class Radar extends PureComponent {
state = {
legendData: [],
}
componentDidMount() {
this.renderChart(this.props.data);
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
this.renderChart(nextProps.data);
}
}
handleRef = (n) => {
this.node = n;
}
handleLegendClick = (item, i) => {
const newItem = item;
newItem.checked = !newItem.checked;
const legendData = this.state.legendData;
legendData[i] = newItem;
if (this.chart) {
const filterItem = legendData.filter(l => l.checked).map(l => l.name);
this.chart.filter('name', filterItem);
this.chart.repaint();
}
this.setState({
legendData,
});
}
renderChart(data) {
const { height = 0,
hasLegend = true,
fit = true,
tickCount = 4,
margin = [16, 0, 16, 0] } = this.props;
if (!data || (data && data.length < 1)) {
return;
}
// clean
this.node.innerHTML = '';
const chart = new G2.Chart({
container: this.node,
forceFit: fit,
height: height - 22,
plotCfg: {
margin,
},
});
this.chart = chart;
chart.source(data, {
value: {
min: 0,
tickCount,
},
});
chart.coord('polar');
chart.legend(false);
chart.axis('label', {
line: null,
});
chart.axis('value', {
grid: {
type: 'polygon',
},
});
chart.line().position('label*value').color('name');
chart.point().position('label*value').color('name').shape('circle');
chart.render();
if (hasLegend) {
const geom = chart.getGeoms()[0]; // 获取所有的图形
const items = geom.getData(); // 获取图形对应的数据
const legendData = items.map((item) => {
/* eslint no-underscore-dangle:0 */
const origin = item._origin;
const result = {
name: origin[0].name,
color: item.color,
checked: true,
value: origin.reduce((p, n) => p + n.value, 0),
};
return result;
});
this.setState({
legendData,
});
}
}
render() {
const { height, title, hasLegend } = this.props;
const { legendData } = this.state;
return (
<div className={styles.radar} style={{ height }}>
<div>
{ title && <h4>{title}</h4>}
<div ref={this.handleRef} />
{
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>
{
i !== (legendData.length - 1) && <div className={styles.split} />
}
</div>
</Col>
))
}
</Row>
}
</div>
</div>
);
}
}
export default Radar;
@import "~antd/lib/style/themes/default.less";
@import "../../../utils/utils.less";
.radar {
.legend {
margin-top: 16px;
.legendItem {
position: relative;
text-align: center;
p {
cursor: pointer;
}
h6 {
color: @heading-color;
font-size: 24px;
line-height: 32px;
margin-top: 2px;
}
.split {
background-color: @border-color-split;
position: absolute;
top: 8px;
right: 0;
height: 40px;
width: 1px;
}
}
.dot {
border-radius: 8px;
display: inline-block;
margin-right: 8px;
position: relative;
top: -1px;
height: 8px;
width: 8px;
}
}
}
import React from 'react';
import { Icon } from 'antd';
import styles from './index.less';
const Item = ({ title, flag, children, ...rest }) => (
<div {...rest} className={styles.trendItem}>
<span className={styles.title}>{title}</span>
{ flag && <span className={styles[flag]}><Icon type={`caret-${flag}`} /></span>}
<span className={styles.value}>{children}</span>
</div>
);
const Trend = ({ colorType, children, ...rest }) => (
<div className={colorType ? (styles[`trend${colorType}`] || styles.trend) : styles.trend} {...rest}>
{children}
</div>
);
Trend.Item = Item;
export default Trend;
@import "~antd/lib/style/themes/default.less";
@import "../../../utils/utils.less";
.trend {
font-size: 0;
height: 22px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.trendItem {
display: inline-block;
margin-right: 16px;
color: @text-color;
font-size: @font-size-base;
line-height: 22px;
height: 22px;
.title {
margin-right: 4px;
}
.value {
color: @text-color;
font-weight: 600;
}
.up, .down {
color: #00a854;
margin-right: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(0.83);
}
}
.down {
color: #f04134;
top: -1px;
}
}
.trendItem:last-child {
margin-right: 0;
}
}
.trendgray {
.trend();
.trendItem {
color: @text-color-secondary;
}
}
import React, { PureComponent } from 'react';
import styles from './index.less';
/* eslint no-return-assign: 0 */
// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
class WaterWave extends PureComponent {
static defaultProps = {
height: 160,
}
state = {
radio: 1,
}
componentDidMount() {
this.renderChart();
this.resize();
window.addEventListener('resize', () => {
this.resize();
});
}
resize() {
const { height } = this.props;
const realWidth = this.root.parentNode.offsetWidth;
if (realWidth < this.props.height) {
const radio = realWidth / height;
this.setState({
radio,
});
} else {
this.setState({
radio: 1,
});
}
}
renderChart() {
const { percent, color = '#19AFFA' } = this.props;
const data = percent / 100;
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;
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]);
ctx.fillStyle = color;
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 = '#108ee9';
}
} 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();
}
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})` }}>
<canvas ref={n => (this.node = n)} width={height} height={height} />
<div className={styles.text} style={{ width: height }}>
{
title && <span>{title}</span>
}
<h4>{percent}%</h4>
</div>
</div>
);
}
}
export default WaterWave;
@import "~antd/lib/style/themes/default.less";
@import "../../../utils/utils.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;
}
}
}
import numeral from 'numeral';
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 Trend from './Trend';
import Field from './Field';
import NumberInfo from './NumberInfo';
import WaterWave from './WaterWave';
import { IconUp, IconDown } from './Icon';
const yuan = val => `&yen; ${numeral(val).format('0,0')}`;
export default {
IconUp,
IconDown,
yuan,
Bar,
Pie,
Gauge,
Radar,
MiniBar,
MiniArea,
MiniProgress,
ChartCard,
Trend,
Field,
NumberInfo,
WaterWave,
};
.miniChart {
position: relative;
width: 100%;
& > div {
position: absolute;
bottom: -34px;
width: 100%;
}
}
import React, { Component } from 'react';
function fixedZero(val) {
return val * 1 < 10 ? `0${val}` : val;
}
class Countdown extends Component {
constructor(props) {
super(props);
const { targetTime, lastTime } = this.initTime(props);
this.state = {
targetTime,
lastTime,
};
}
componentDidMount() {
this.tick();
}
componentWillReceiveProps(nextProps) {
if (this.props.target !== nextProps.target) {
const { targetTime, lastTime } = this.initTime(nextProps);
this.setState({
lastTime,
targetTime,
});
}
}
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,
targetTime,
};
}
// 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 } = this.props;
const { lastTime } = this.state;
const result = format(lastTime);
return result;
}
}
export default Countdown;
import React from 'react';
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>
);
};
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, ...restProps }) => {
const clsString = classNames(styles.descriptionList, styles[layout], className);
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';
const { Description } = DescriptionList;
ReactDOM.render(
<DescriptionList 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';
const { Description } = DescriptionList;
ReactDOM.render(
<DescriptionList 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 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 {
color: @heading-color;
font-weight: 600;
margin-bottom: 16px;
}
.term {
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 {
padding-bottom: 16px;
color: @text-color;
display: table-cell;
}
&.vertical {
.term {
padding-bottom: 8px;
display: block;
}
.detail {
display: block;
}
}
}
---
category: Components
type: General
title: DescriptionList
subtitle: 描述列表
cols: 1
---
描述列表用来展示一系列文本信息。
## API
### DescriptionList
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| layout | 布局方式 | Enum{'horizontal', 'vertical'} | 'horizontal' |
| col | 指定信息分几列展示 | number(0 < col <= 4) | 3 |
| title | 列表标题 | ReactNode | - |
| gutter | 列表项间距,单位为 `px` | number | 32 |
### 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 PropTypes from 'prop-types';
import { Link } from 'dva/router';
import { Button, Icon } from 'antd';
import styles from './index.less';
// TODO: 添加逻辑
class EditableLinkGroup extends PureComponent {
static defaultProps = {
links: [],
onAdd: () => {
},
}
state = {
links: this.props.links,
};
handleOnClick() {
const { onAdd } = this.props;
onAdd();
}
render() {
const { links } = this.state;
return (
<div className={styles.linkGroup}>
{
links.map(link => <Link key={`linkGroup-item-${link.id || link.title}`} to={link.href}>{link.title}</Link>)
}
{
<Button size="small" onClick={() => this.handleOnClick()}>
<Icon type="plus" /> 添加
</Button>
}
</div>
);
}
}
EditableLinkGroup.propTypes = {
links: PropTypes.array,
onAdd: PropTypes.func,
};
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;
margin-right: 32px;
&:hover {
color: @primary-color;
}
}
& > button {
border-color: @primary-color;
color: @primary-color;
i {
position: relative;
top: -1px;
}
span {
margin-left: 0 !important;
position: relative;
top: -1px;
}
}
}
---
order: 2
title: 403
---
403 页面,配合自定义操作。
````jsx
import { Exception } from 'ant-design-pro';
import { Button } from 'antd';
const actions = (
<div>
<Button type="primary" size="large">回到首页</Button>
<Button size="large">查看详情</Button>
</div>
);
ReactDOM.render(
<Exception type="403" actions={actions} />
, mountNode);
````
---
order: 0
title: 404
---
404 页面。
````jsx
import { Exception } from 'ant-design-pro';
ReactDOM.render(
<Exception type="404" />
, mountNode);
````
---
order: 1
title: 500
---
500 页面。
````jsx
import { Exception } from 'ant-design-pro';
ReactDOM.render(
<Exception type="500" />
, mountNode);
````
import React from 'react';
import classNames from 'classnames';
import { Button } from 'antd';
import { Link } from 'react-router';
import config from './typeConfig';
import styles from './index.less';
export default ({ className, type, title, desc, img, actions }) => {
const pageType = type in config ? type : '404';
const clsString = classNames(styles.exception, className);
return (
<div className={clsString}>
<div className={styles.imgBlock}>
<img src={img || config[pageType].img} alt="" />
</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 || <Link to="/"><Button size="large" type="primary">返回首页</Button></Link>}
</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%;
text-align: right;
padding-right: 152px;
}
.content {
flex: auto;
h1 {
color: @text-color;
font-size: 68px;
line-height: 68px;
margin-bottom: 16px;
}
.desc {
color: @text-color-secondary;
font-size: 20px;
margin-bottom: 16px;
}
.actions {
button:not(:last-child) {
margin-right: 8px;
}
}
}
}
---
category: Components
type: General
title: Exception
subtitle: 异常
cols: 1
---
异常页用于对页面特定的异常状态进行反馈。通常,它包含对错误状态的阐述,并向用户提供建议或操作,避免用户感到迷失和困惑。
## API
| 参数 | 说明 | 类型 | 默认值 |
|-------------|------------------------------------------|-------------|-------|
| type | 页面类型,若配置,则自带对应类型默认的 `title``desc``img`,此默认设置可以被 `title``desc``img` 覆盖 | Enum {'403', '404', '500'} | - |
| title | 标题 | ReactNode | - |
| desc | 补充描述 | ReactNode | - |
| img | 背景图片地址 | string | - |
| actions | 建议操作,配置此属性时默认的『返回首页』按钮不生效 | ReactNode | - |
const config = {
403: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/byTGXmzwJVwgotvxHQsU.svg',
title: '403',
desc: '对不起,你没有权限',
},
404: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/GdXXOjtMMzaPfCziUVYt.svg',
title: '404',
desc: '你要找的页面不存在',
},
500: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/OpTUNDbQGfEWLubSrJap.svg',
title: '500',
desc: '服务器错误,我们正在维修',
},
};
export default config;
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import styles from './index.less';
export default class FooterToolbar extends Component {
static contextTypes = {
layoutCollapsed: PropTypes.bool,
};
state = {
width: '',
};
componentDidMount() {
this.syncWidth();
}
componentWillReceiveProps() {
this.syncWidth();
}
syncWidth() {
const sider = document.querySelectorAll('.ant-layout-sider')[0];
if (sider) {
this.setState({
width: `calc(100% - ${sider.style.width})`,
});
}
}
render() {
const { children, style, className, extra, ...restProps } = this.props;
return (
<div
className={classNames(className, styles.toolbar)}
ref={this.getRefNode}
style={{
width: this.state.width,
...style,
}}
{...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: @shadow-1-up;
background: #fff;
padding: 0 28px;
transition: all .3s;
&:after {
content: "";
display: block;
clear: both;
}
.left {
float: left;
}
.right {
float: right;
}
button + button {
margin-left: 8px;
}
}
---
category: Components
type: General
title: FooterToolbar
subtitle: 底部固定工具栏
cols: 1
---
## API
---
order: 0
title: Basic
---
基本页脚。
````jsx
import { GlobalFooter } from 'ant-design-pro';
import { Icon } from 'antd';
const links = [{
title: '帮助',
href: '',
}, {
title: '隐私',
href: '',
}, {
title: '条款',
href: '',
blankTarget: true,
}];
const copyright = <div>Copyright <Icon type="copyright" /> 2017 蚂蚁金服体验技术部出品</div>;
ReactDOM.render(
<GlobalFooter links={links} copyright={copyright} />
, mountNode);
````
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.title} 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: 32px 28px 16px;
text-align: center;
.links {
margin-bottom: 8px;
a {
color: @text-color-secondary;
&:not(:last-child) {
margin-right: 40px;
}
}
}
.copyright {
color: @text-color-secondary;
font-size: @font-size-base;
}
}
---
category: Components
type: General
title: GlobalFooter
subtitle: 全局页脚
cols: 1
---
页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - |
| copyright | 版权信息 | ReactNode | - |
import React, { PureComponent } from 'react';
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,
};
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 });
}
enterSearchMode = () => {
this.setState({ searchMode: true }, () => {
if (this.state.searchMode) {
this.input.refs.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={className} onClick={this.enterSearchMode}>
<Icon type="search" />
<AutoComplete
className={inputClass}
value={this.state.value}
onChange={this.onChange}
onSelect={this.onSelect}
{...restProps}
>
<Input
placeholder={placeholder}
ref={(node) => { this.input = node; }}
onKeyDown={this.onKeyDown}
onBlur={this.leaveSearchMode}
/>
</AutoComplete>
</span>
);
}
}
.input {
transition: all .3s;
width: 0;
background: transparent;
border-radius: 0;
:global(.ant-select-selection) {
background: transparent;
}
input {
border: 0;
padding-left: 0;
padding-right: 0;
color: #fff;
&::placeholder {
color: rgba(255, 255, 255, .5);
}
}
&,
&:hover,
&:focus {
border-bottom: 1px solid #fff;
}
&.show {
width: 210px;
margin-left: 8px;
}
}
import React, { Component } from 'react';
import { Tooltip } from 'antd';
import styles from './index.less';
/* eslint no-return-assign: 0 */
class MapChart extends Component {
getRect() {
// 0.4657 = 708 / 1520 (img origin size)
const width = this.root.offsetWidth;
const height = width * 0.4657;
return {
width,
height,
};
}
render() {
return (
<div className={styles.mapChart} ref={n => (this.root = n)}>
<Tooltip title="等待实现">
<div className={styles.canvas} ref={n => (this.root = n)}>
<img src="https://gw.alipayobjects.com/zos/rmsportal/fBcAYoxWIjlUXwDjqvzg.png" alt="map" />
<div ref={n => (this.node = n)} />
</div>
</Tooltip>
</div>
);
}
}
export default MapChart;
.mapChart {
background-color: #fff;
position: relative;
.canvas {
width: 100%;
& > img {
width: 100%;
}
}
}
import React from 'react';
import { Avatar, Icon } from 'antd';
import classNames from 'classnames';
import styles from './NoticeList.less';
export default function NoticeList({ data = [], onClick, onClear, title, locale }) {
if (data.length === 0) {
return (
<div className={styles.notFound}>
<Icon type="frown-o" />
{locale.emptyText}
</div>
);
}
return (
<div>
<ul className={styles.list}>
{data.map((item, i) => {
const itemCls = classNames(styles.item, {
[styles.read]: item.read,
});
return (
<li className={itemCls} key={item.key || i} onClick={() => onClick(item)}>
<div className={styles.wrapper}>
{item.avatar ? <Avatar className={styles.avatar} src={item.avatar} /> : null}
<div className={styles.content}>
<h4 className={styles.title} title={item.title}>{item.title}</h4>
<div className={styles.description} title={item.description}>
{item.description}
</div>
<div className={styles.datetime}>{item.datetime}</div>
<div className={styles.extra}>{item.extra}</div>
</div>
</div>
</li>
);
})}
</ul>
<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;
.wrapper {
margin: 0 32px;
padding: 12px 0;
border-bottom: 1px solid @border-color-split;
}
&.read {
opacity: .4;
}
&:last-child .wrapper {
border-bottom: 0;
}
&:hover {
background: @primary-1;
}
.content {
position: relative;
overflow: hidden;
}
.avatar {
margin-right: 16px;
float: left;
margin-top: 4px;
background: #fff;
}
.title {
font-weight: normal;
color: @text-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.description {
color: @text-color-secondary;
font-size: 12px;
margin-top: 8px;
}
.datetime {
color: @text-color-secondary;
font-size: 12px;
margin-top: 4px;
}
.extra {
position: absolute;
right: 0;
top: 0;
color: @text-color-secondary;
font-size: 12px;
}
}
}
.notFound {
text-align: center;
height: 120px;
line-height: 120px;
font-size: 14px;
color: @text-color-secondary;
> i {
font-size: 16px;
margin-right: 8px;
vertical-align: middle;
margin-top: -1px;
}
}
.clear {
height: 46px;
line-height: 46px;
text-align: center;
color: @text-color-secondary;
border-radius: 0 0 @border-radius-base @border-radius-base;
border-top: 1px solid @border-color-split;
transition: all .3s;
cursor: pointer;
&:hover {
color: @text-color;
}
}
---
order: 1
title: 通知图标
---
通常用在全局导航上。
````jsx
import { NoticeIcon } from 'ant-design-pro';
ReactDOM.render(<NoticeIcon count={5} />, mountNode);
````
---
order: 2
title: 带浮层卡片
---
点击展开通知卡片,展现多种类型的通知。
````jsx
import { NoticeIcon } from 'ant-design-pro';
import moment from 'moment';
const data = [{
key: '1',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: moment('2017-08-07').fromNow(),
}, {
key: '2',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: moment('2017-08-07').fromNow(),
}, {
key: '3',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: moment('2017-08-07').fromNow(),
}];
ReactDOM.render(
<div style={{ width: 300, textAlign: 'right' }}>
<NoticeIcon count={10}>
<NoticeIcon.Tab list={data} title="通知" />
<NoticeIcon.Tab list={data} title="消息" />
<NoticeIcon.Tab list={[]} title="待办" />
</NoticeIcon>
</div>
, mountNode);
````
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: '清空',
},
};
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 = children.map((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
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 } = 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;
}
return (
<Popover
placement="bottomRight"
content={notificationBox}
popupClassName={styles.popover}
trigger="click"
arrowPointAtCenter
popupAlign={popupAlign}
onVisibleChange={this.props.onPopupVisibleChange}
>
{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: 20px;
}
.tabs {
:global {
.ant-tabs-nav-container {
font-size: 14px;
}
.ant-tabs-nav-scroll {
text-align: center;
}
.ant-tabs-bar {
margin-bottom: 0;
}
.ant-tabs-nav .ant-tabs-tab {
padding-top: 16px;
padding-bottom: 16px;
}
}
}
---
category: Components
type: General
title: NoticeIcon
subtitle: 通知菜单
cols: 1
---
用在顶部导航上,作为整个产品统一的通知中心。
## 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) | -
locale | 默认文案 | Object | `{ emptyText: '暂无数据', clear: '清空' }`
### NoticeIcon.Tab
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 消息分类的页签标题 | string | -
data | 列表数据,格式参照下表 | Array | `[]`
### Tab data
参数 | 说明 | 类型 | 默认值
----|------|-----|------
avatar | 头像图片链接 | string | -
title | 标题 | ReactNode | -
description | 描述信息 | ReactNode | -
datetime | 时间戳 | ReactNode | -
extra | 额外信息,在列表项右上角 | ReactNode | -
---
order: 2
title: With Image
---
带图片的页头。
````jsx
import { PageHeader } from 'ant-design-pro';
const content = (
<div>
<p>段落示意:蚂蚁金服务设计平台-design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,
提供跨越设计与开发的体验解决方案。</p>
<div className="link">
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/wUTAfuNZjhmCIxEPxQVY.svg" /> 快速开始
</a>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/qsmGbwvxTAjXfkkrZYov.svg" /> 产品简介
</a>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/UGEHGuwlGDalIJlbsNxL.svg" /> 产品文档
</a>
</div>
</div>
);
const extra = (
<div className="imgContainer">
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/RWDkuWwBqMPLpNqGdxDp.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 .imgContainer {
text-align: center;
}
#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';
const breadcrumbList = [{
title: '一级菜单',
href: '/',
}, {
title: '二级菜单',
href: '/',
}, {
title: '三级菜单',
}];
ReactDOM.render(
<div>
<PageHeader title="页面标题" breadcrumbList={breadcrumbList} />
</div>
, mountNode);
````
---
order: 1
title: Standard
---
标准页头。
````jsx
import { PageHeader } from 'ant-design-pro';
import { Button, Menu, Dropdown, Icon, Row, Col } from 'antd';
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>
<Button size="large" type="primary">主操作</Button>
<Button size="large">次操作</Button>
<Dropdown overlay={menu}>
<Button size="large">
更多 <Icon type="down" />
</Button>
</Dropdown>
</div>
);
const extra = (
<Row>
<Col span={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 span={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/JcBAEvlHGhVvBekIJCWT.svg" />}
action={action}
content="DescriptionList 占位"
extraContent={extra}
breadcrumbList={breadcrumbList}
tabList={tabList}
onTabChange={onTabChange}
/>
</div>
, mountNode);
````
---
order: 0
title: Structure
---
基本结构,可以形成多种组合。
````jsx
import { PageHeader } from 'ant-design-pro';
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}
/>
</div>
, mountNode);
````
<style>
#scaffold-src-components-PageHeader-demo-structure .code-box-demo {
background: #eee;
}
#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 React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Breadcrumb, Tabs } from 'antd';
import { Link } from 'dva/router';
import classNames from 'classnames';
import styles from './index.less';
const TabPane = Tabs.TabPane;
function itemRender(route, params, routes, paths) {
const last = routes.indexOf(route) === routes.length - 1;
return (last || !route.component)
? <span>{route.breadcrumbName}</span>
: <Link to={paths.join('/') || '/'}>{route.breadcrumbName}</Link>;
}
export default class PageHeader extends PureComponent {
static contextTypes = {
routes: PropTypes.array,
params: 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,
};
};
render() {
const { routes, params } = this.getBreadcrumbProps();
const { title, logo, action, content, extraContent,
breadcrumbList, tabList, className } = this.props;
const clsString = classNames(styles.pageHeader, className);
let breadcrumb;
if (routes && params) {
breadcrumb = (
<Breadcrumb
className={styles.breadcrumb}
routes={routes.filter(route => route.breadcrumbName)}
params={params}
itemRender={itemRender}
/>
);
} else if (breadcrumbList && breadcrumbList.length) {
breadcrumb = (
<Breadcrumb className={styles.breadcrumb}>
{
breadcrumbList.map(item => (
<Breadcrumb.Item>
{item.href ? <a href="">{item.title}</a> : item.title}
</Breadcrumb.Item>)
)
}
</Breadcrumb>
);
} else {
breadcrumb = null;
}
const tabDefaultValue = tabList && tabList.filter(item => item.default)[0];
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}
defaultActiveKey={(tabDefaultValue && tabDefaultValue.key)}
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: 18px 28px 0 36px;
border-bottom: @border-width-base @border-style-base @border-color-split;
.detail {
display: flex;
}
.row {
display: flex;
}
.breadcrumb {
margin-bottom: 18px;
}
.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;
}
.title {
font-size: 20px;
font-weight: 500;
color: @heading-color;
}
.action {
margin-left: 56px;
min-width: 266px;
button:not(:last-child) {
margin-right: 8px;
}
}
.title, .action, .content, .extraContent, .main {
flex: auto;
}
.title, .action {
margin-bottom: 16px;
}
.logo, .content, .extraContent {
margin-bottom: 12px;
}
.action, .extraContent {
text-align: right;
}
.extraContent {
margin-left: 88px;
min-width: 242px;
}
}
@media screen and (max-width: @screen-md) {
.pageHeader {
.extraContent {
margin-left: 44px;
}
}
}
@media screen and (max-width: @screen-sm) {
.pageHeader {
.extraContent {
margin-left: 24px;
}
}
}
@media screen and (max-width: @screen-xs) {
.pageHeader {
.extraContent {
margin-left: 8px;
}
}
}
---
category: Components
type: General
title: PageHeader
subtitle: 页头
cols: 1
---
页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | title 区域 | ReactNode | - |
| logo | logo区域 | ReactNode | - |
| action | 操作区,位于 title 行的行尾 | ReactNode | - |
| content | 内容区 | ReactNode | - |
| extraContent | 额外内容区,位于content的右侧 | ReactNode | - |
| routes | 面包屑相关属性,router 的路由栈信息 | object[] | - |
| params | 面包屑相关属性,路由的参数 | object | - |
| breadcrumbList | 面包屑数据,配置了 `routes` `params` 时此属性无效 | array<{title: ReactNode, href?: string}> | - |
| tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | - |
| onTabChange | 切换面板的回调 | (key) => void | - |
> 面包屑的配置方式有两种,一是结合 `react-router`,通过配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router);二是直接配置 `breadcrumbList`。 你也可以将 `routes` 及 `params` 放到 context 中,`PageHeader` 组件会自动获取。
import React from 'react';
import { Radio } from 'antd';
import styles from './index.less';
const RadioButton = Radio.Button;
export default props => (<div className={styles.radioText}>
<RadioButton {...props} />
</div>);
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.radioText {
display: inline;
:global {
.ant-radio-button-wrapper {
border: none;
padding: 0 12px;
}
}
}
---
order: 1
title: Classic
---
典型结果页面。
````jsx
import { Result } from 'ant-design-pro';
import { Button, Row, Col, Icon, Steps } from 'antd';
const Step = Steps.Step;
const desc1 = (
<div style={{ fontSize: 14 }}>
<div style={{ marginTop: 4, marginBottom: 8 }}>曲丽丽 <Icon type="dingding-o" /></div>
<div>2016-12-12 12:32</div>
</div>
);
const desc2 = (
<div style={{ fontSize: 14 }}>
<div style={{ marginTop: 4, marginBottom: 8 }}>周毛毛 <Icon type="dingding-o" style={{ color: '#00A0E9' }} /></div>
<div><a href="">催一下</a></div>
</div>
);
const extra = (
<div>
<div style={{ fontSize: 16, color: 'rgba(0, 0, 0, 0.65)', fontWeight: '600', marginBottom: 16 }}>
项目名称
</div>
<Row style={{ color: 'rgba(0, 0, 0, 0.65)', marginBottom: 20 }}>
<Col span={6}>项目 ID:23421</Col>
<Col span={6}>负责人:曲丽丽</Col>
<Col span={12}>生效时间: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 size="large" type="primary">返回列表</Button>
<Button size="large">查看项目</Button>
<Button size="large">打 印</Button>
</div>
);
ReactDOM.render(
<Result
type="success"
title="提交成功"
description="提交结果页用于反馈一系列操作任务的处理结果,如果仅是简单操作,使用 Message 全局提示反馈即可。本文字区域可以展示简单的补充说明,如果有类似展示“单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。"
extra={extra}
actions={actions}
/>
, mountNode);
````
---
order: 2
title: Failed
---
提交失败。
````jsx
import { Result } from 'ant-design-pro';
import { Button, Icon } from 'antd';
const extra = (
<div>
<div style={{ fontSize: 16, color: 'rgba(0, 0, 0, 0.65)', fontWeight: '600', marginBottom: 16 }}>
您提交的内容有如下错误:
</div>
<div style={{ marginBottom: 8 }}>
<Icon style={{ color: '#f04134', marginRight: 8 }} type="close-circle" />您的账户已被冻结
<a style={{ marginLeft: 24 }}>立即解冻 <Icon type="right" /></a>
</div>
<div>
<Icon style={{ color: '#f04134', marginRight: 8 }} type="close-circle" />您的账户还不具备申请资格
<a style={{ marginLeft: 24 }}>立即升级 <Icon type="right" /></a>
</div>
</div>
);
const actions = <Button size="large" 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';
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 React from 'react';
import classNames from 'classnames';
import { Icon } from 'antd';
import styles from './index.less';
export default ({ 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;
.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;
color: @text-color-secondary;
margin-bottom: 24px;
}
.extra {
background: rgba(245, 245, 245, 0.5);
padding: 24px 40px;
margin-bottom: 32px;
border-radius: @border-radius-sm;
text-align: left;
}
.actions button:not(:last-child) {
margin-right: 8px;
}
}
---
category: Components
type: General
title: Result
subtitle: 处理结果
cols: 1
---
结果页用于对用户进行的一系列任务处理结果进行反馈。
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| type | 类型,不同类型自带对应的图标 | Enum {'success', 'error'} | - |
| title | 标题 | ReactNode | - |
| description | 结果描述 | ReactNode | - |
| extra | 补充信息,有默认的灰色背景 | ReactNode | - |
| actions | 操作建议,推荐放置跳转链接,按钮组等 | ReactNode | - |
import React from 'react';
import { Button, Input } from 'antd';
import styles from './index.less';
export default ({ onSearch = () => ({}), text = '搜索', ...reset }) => (
<div className={styles.search}>
<Input
placeholder="请输入"
size="large"
{...reset}
addonAfter={<Button onClick={onSearch} type="primary">{text}</Button>}
/>
</div>
);
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.search {
display: inline-block;
:global {
.ant-input-group-addon {
border: none;
padding: 0;
}
.ant-input-group .ant-input {
width: 522px;
}
}
input {
border-right: none;
height: 40px;
line-height: 40px;
}
button {
border-radius: 0 @border-radius-base @border-radius-base 0;
width: 86px;
height: 40px;
}
}
@media screen and (max-width: @screen-sm) {
.search {
:global {
.ant-input-group .ant-input {
width: 300px;
}
}
}
}
@media screen and (max-width: @screen-xs) {
.search {
:global {
.ant-input-group .ant-input {
width: 200px;
}
}
}
}
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";
@import "../../utils/utils.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: 16px;
}
}
.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 } from 'react';
import moment from 'moment';
import { Table, Alert, Badge } from 'antd';
import styles from './index.less';
class StandardTable extends PureComponent {
state = {
selectedRowKeys: [],
selectedRows: [],
totalCallNo: 0,
loading: false,
};
componentWillReceiveProps(nextProps) {
// clean state
if (nextProps.selectedRows.length === 0) {
this.setState({
selectedRows: [],
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, selectedRows, 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,
render: val => (
<p style={{ textAlign: 'center' }}>
{val}
</p>
),
},
{
title: '状态',
dataIndex: 'status',
filters: [
{
text: status[0],
value: 0,
},
{
text: status[1],
value: 1,
},
],
render(val) {
if (val === 0) {
return <Badge status="default" text={status[val]} />;
} else {
return <Badge status="processing" text={status[val]} />;
}
},
},
{
title: '更新时间',
dataIndex: 'updatedAt',
sorter: true,
render: val => <span>{moment(val).format('YYYY-MM-DD HH:mm:ss')}</span>,
},
{
title: '操作',
render: () => (
<p>
<a href="">配置</a>
<span className={styles.splitLine} />
<a href="">订阅警报</a>
</p>
),
},
];
const paginationProps = {
showSizeChanger: true,
showQuickJumper: true,
...pagination,
};
const rowSelection = {
selectedRowKeys,
onChange: this.handleRowSelectChange,
};
return (
<div className={styles.standardTable}>
<div className={styles.tableAlert}>
<Alert
message={(
<p>
已选择 <a>{selectedRowKeys.length}</a> 项&nbsp;&nbsp;
服务调用总计 <span style={{ fontWeight: 600 }}>{totalCallNo}</span>
<a onClick={this.cleanSelectedKeys} style={{ marginLeft: 8 }}>清空</a>
</p>
)}
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";
@import "../../utils/utils.less";
.standardTable {
:global {
.ant-table-pagination {
margin-bottom: 0;
}
}
.tableAlert {
margin-bottom: 16px;
}
.splitLine {
background: @border-color-split;
display: inline-block;
margin: 0 8px;
width: 1px;
height: 12px;
}
}
import React, { PureComponent } from 'react';
import G2 from 'g2';
import Cloud from 'g-cloud';
/* eslint no-underscore-dangle: 0 */
/* eslint no-param-reassign: 0 */
/* eslint no-return-assign: 0 */
class TagCloud extends PureComponent {
componentDidMount() {
this.initTagCloud();
this.renderChart(this.props.data);
}
componentWillReceiveProps(nextProps) {
if (this.props.data !== nextProps.data) {
this.renderChart(nextProps.data);
}
}
initTagCloud = () => {
const Util = G2.Util;
const Shape = G2.Shape;
function getTextAttrs(cfg) {
const textAttrs = Util.mix(true, {}, {
fillOpacity: cfg.opacity,
fontSize: cfg.size,
rotate: 0, // cfg.origin._origin.rotate,
text: cfg.origin._origin.text,
textAlign: 'center',
fill: cfg.color,
textBaseline: 'Alphabetic',
}, cfg.style);
return textAttrs;
}
// 给point注册一个词云的shape
Shape.registShape('point', 'cloud', {
drawShape(cfg, container) {
cfg.points = this.parsePoints(cfg.points);
const attrs = getTextAttrs(cfg);
const shape = container.addShape('text', {
attrs: Util.mix(attrs, {
x: cfg.points[0].x,
y: cfg.points[0].y,
}),
});
return shape;
},
});
}
renderChart(data) {
if (!data || data.length < 1) {
return;
}
const { height } = this.props;
let width = 0;
if (this.root) {
width = this.root.offsetWidth;
}
// clean
if (this.node) {
this.node.innerHTML = '';
}
data.sort((a, b) => b.value - a.value);
const max = data[0].value;
const min = data[data.length - 1].value;
// 构造一个词云布局对象
const layout = new Cloud({
words: data,
width,
height,
// 设定文字大小配置函数(默认为12-40px的随机大小)
size: words => (((words.value - min) / (max - min)) * 10) + 12,
// 设定文字内容
text: words => words.name,
});
// 执行词云布局函数,并在回调函数中调用G2对结果进行绘制
layout.exec((texts) => {
const chart = new G2.Chart({
container: this.node,
width,
height,
plotCfg: {
margin: 0,
},
});
chart.legend(false);
chart.axis(false);
chart.tooltip(false);
chart.source(texts);
// 将词云坐标系调整为G2的坐标系
chart.coord().reflect();
chart
.point()
.position('x*y')
.color('text')
.size('size', size => size)
.shape('cloud')
.style({
fontStyle: texts[0].style,
fontFamily: texts[0].font,
fontWeight: texts[0].weight,
});
chart.render();
});
}
render() {
return (
<div ref={n => (this.root = n)} style={{ width: '100%' }}>
<div ref={n => (this.node = n)} />
</div>
);
}
}
export default TagCloud;
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { Tag, Icon } from 'antd';
import styles from './index.less';
const CheckableTag = Tag.CheckableTag;
const TagSelectOption = ({ children, checked, onChange, value }) => (
<CheckableTag
checked={checked}
key={value}
onChange={state => onChange(value, state)}
>
{children}
</CheckableTag>
);
TagSelectOption.defaultProps = {
displayName: 'TagSelectOption',
};
const TagSelectExpand = ({ children }) => (
<div>{children}</div>
);
TagSelectExpand.defaultProps = {
displayName: 'TagSelectExpand',
};
class TagSelect extends PureComponent {
static defaultProps = {
initialValue: [],
};
state = {
checkedAll: false,
expand: false,
checkedTags: this.props.initialValue || [],
};
onSelectAll = (checked) => {
const { onChange } = this.props;
let checkedTags = [];
let expand = this.state.expand;
if (checked) {
const tags = this.getAllTags();
checkedTags = tags.list;
expand = tags.expand;
}
this.setState({
checkedAll: checked,
checkedTags,
expand,
});
if (onChange) {
onChange(checkedTags);
}
}
getAllTags() {
let expand = this.state.expand;
const { children } = this.props;
let checkedTags = children.filter(child => child.props.displayName === 'TagSelectOption').map(child => child.props.value);
const expandChild = children.filter(child => child.props.displayName === 'TagSelectExpand')[0];
if (expandChild) {
checkedTags = checkedTags.concat(
expandChild.props.children.map(child => child.props.value)
);
expand = true;
}
return {
list: checkedTags,
expand,
};
}
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);
}
const tags = this.getAllTags();
let checkedAll = false;
if (tags.list.length === checkedTags.length) {
checkedAll = true;
}
this.setState({
checkedAll,
checkedTags,
});
if (onChange) {
onChange(checkedTags);
}
}
handleExpand = () => {
this.setState({
expand: !this.state.expand,
});
}
render() {
const { checkedTags, checkedAll, expand } = this.state;
const { children } = this.props;
const expandNode = children.filter(child => child.props.displayName === 'TagSelectExpand')[0];
const cls = classNames(styles.tagSelect, {
[styles.expandTag]: expandNode,
});
return (
<div className={cls}>
<CheckableTag
checked={checkedAll}
key="tag-select-__all__"
onChange={this.onSelectAll}
>
全部
</CheckableTag>
{
children.filter(child => child.props.displayName === 'TagSelectOption').map(child => React.cloneElement(child, {
key: `tag-select-${child.props.value}`,
checked: checkedTags.indexOf(child.props.value) > -1,
onChange: this.handleTagChange,
}))
}
{
expandNode && <a className={styles.trigger} onClick={this.handleExpand}>
{ expand ? '收起' : '展开'} <Icon type={expand ? 'up' : 'down'} />
</a>
}
{
expandNode && <div className={expand ? styles.expand : styles.fold}>
{
expandNode.props.children.map(child => React.cloneElement(child, {
key: `tag-select-${child.props.value}`,
checked: checkedTags.indexOf(child.props.value) > -1,
onChange: this.handleTagChange,
}))
}
</div>
}
</div>
);
}
}
TagSelect.Option = TagSelectOption;
TagSelect.Expand = TagSelectExpand;
export default TagSelect;
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.tagSelect {
user-select: none;
margin-left: -8px;
position: relative;
.expand {
transition: all 0.32s ease;
overflow: hidden;
max-height: 100px;
}
.fold {
.expand();
max-height: 0;
}
.trigger {
position: absolute;
top: 0;
right: 0;
}
}
.expandTag {
padding-right: 50px;
}
import React, { Component } from 'react';
import G2 from 'g2';
import Slider from 'g2-plugin-slider';
import styles from './index.less';
class TimelineChart extends Component {
componentDidMount() {
this.renderChart(this.props.data);
}
componentWillReceiveProps(nextProps) {
if (nextProps.data !== this.props.data) {
this.renderChart(nextProps.data);
}
}
sliderId = `timeline-chart-slider-${Math.random() * 1000}`
handleRef = (n) => {
this.node = n;
}
renderChart(data) {
const { height = 400, margin = [60, 40, 40, 40], titleMap } = this.props;
if (!data || (data && data.length < 1)) {
return;
}
// clean
if (this.sliderId) {
document.getElementById(this.sliderId).innerHTML = '';
}
this.node.innerHTML = '';
const chart = new G2.Chart({
container: this.node,
forceFit: true,
height,
plotCfg: {
margin,
},
});
chart.axis('x', {
title: false,
});
chart.axis('y1', {
title: false,
});
chart.axis('y2', false);
chart.legend({
mode: false,
position: 'top',
});
chart.source(data, {
x: {
type: 'timeCat',
tickCount: 16,
mask: 'HH:MM',
range: [0, 1],
},
y1: {
alias: titleMap.y1,
min: 0,
},
y2: {
alias: titleMap.y2,
min: 0,
},
});
chart.line().position('x*y1').color('#4FAAEB');
chart.line().position('x*y2').color('#9AD681');
/* eslint new-cap:0 */
const slider = new Slider({
domId: this.sliderId,
height: 26,
xDim: 'x',
yDim: 'y1',
charts: [chart],
});
slider.render();
}
render() {
const { height, title } = this.props;
return (
<div className={styles.timelineChart} style={{ height }}>
<div>
{ title && <h4>{title}</h4>}
<div ref={this.handleRef} />
<div id={this.sliderId} />
</div>
</div>
);
}
}
export default TimelineChart;
.timelineChart {
background: #fff;
}
import dva from 'dva';
// import { browserHistory } from 'dva/router';
import 'moment/locale/zh-cn';
import models from './models';
import './index.less';
// 1. Initialize
const app = dva({
// history: browserHistory,
});
// 2. Plugins
// app.use({});
// 3. Model
models.forEach((m) => {
app.model(m);
});
// 4. Router
app.router(require('./router'));
// 5. Start
app.start('#root');
html, body, :global(#root) {
height: 100%;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// temporary font size patch
:global(.ant-tag) {
font-size: 12px;
}
import React from 'react';
import PropTypes from 'prop-types';
import { Layout, Menu, Icon, Avatar, Dropdown, Tag, message } from 'antd';
import DocumentTitle from 'react-document-title';
import { connect } from 'dva';
import { Link, routerRedux } from 'dva/router';
import moment from 'moment';
import groupBy from 'lodash/groupBy';
import styles from './BasicLayout.less';
import HeaderSearch from '../components/HeaderSearch';
import NoticeIcon from '../components/NoticeIcon';
import GlobalFooter from '../components/GlobalFooter';
import { menus } from '../common/nav';
const { Header, Sider, Content } = Layout;
const { SubMenu } = Menu;
class BasicLayout extends React.PureComponent {
static childContextTypes = {
routes: PropTypes.array,
params: PropTypes.object,
}
state = {
mode: 'inline',
};
getChildContext() {
const { routes, params } = this.props;
return { routes, params };
}
componentDidMount() {
this.props.dispatch({
type: 'user/fetchCurrent',
});
}
onCollapse = (collapsed) => {
this.props.dispatch({
type: 'global/changeLayoutCollapsed',
payload: collapsed,
});
}
onMenuClick = ({ key }) => {
if (key === 'logout') {
this.props.dispatch(routerRedux.push('/user/login'));
}
}
getDefaultCollapsedSubMenus() {
const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys()];
currentMenuSelectedKeys.splice(-1, 1);
return currentMenuSelectedKeys;
}
getCurrentMenuSelectedKeys() {
const { location: { pathname } } = this.props;
const keys = pathname.split('/').slice(1);
if (keys.length === 1 && keys[0] === '') {
return [menus[0].key];
}
return keys;
}
getNavMenuItems(menusData, parentPath = '') {
return menusData.map((item) => {
if (!item.name) {
return null;
}
const itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
if (item.children && item.children.some(child => child.name)) {
return (
<SubMenu
title={
<span>
<Icon type={item.icon} />
<span>{item.name}</span>
</span>
}
key={item.key || item.path}
>
{this.getNavMenuItems(item.children, itemPath)}
</SubMenu>
);
}
return (
<Menu.Item key={item.key || item.path}>
<Link to={itemPath} target={item.target || '_self'}>
<Icon type={item.icon} />
<span>{item.name}</span>
</Link>
</Menu.Item>
);
});
}
getPageTitle() {
const { routes } = this.props;
for (let i = routes.length - 1; i >= 0; i -= 1) {
if (routes[i].breadcrumbName) {
return `${routes[i].breadcrumbName} - Ant Design Pro`;
}
}
return 'Ant Design Pro';
}
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 = ({
processing: 'blue',
urgent: 'red',
doing: 'yellow',
})[newNotice.status];
newNotice.extra = <Tag color={`${color}-inverse`}>{newNotice.extra}</Tag>;
}
return newNotice;
});
return groupBy(newNotices, 'type');
}
toggle = () => {
const { collapsed } = this.props;
this.props.dispatch({
type: 'global/changeLayoutCollapsed',
payload: !collapsed,
});
}
handleNoticeClear = (type) => {
message.success(`清空了${type}`);
this.props.dispatch({
type: 'global/clearNotices',
payload: type,
});
}
handleNoticeVisibleChange = (visible) => {
if (visible) {
this.props.dispatch({
type: 'global/fetchNotices',
});
}
}
render() {
const { children, currentUser, collapsed, fetchingNotices } = this.props;
const menu = (
<Menu className={styles.menu} selectedKeys={[]} onClick={this.onMenuClick}>
<Menu.Item><Icon type="user" />个人中心</Menu.Item>
<Menu.Item><Icon type="setting" />设置</Menu.Item>
<Menu.Divider />
<Menu.Item key="logout"><Icon type="logout" />退出登录</Menu.Item>
</Menu>
);
const noticeData = this.getNoticeData();
return (
<DocumentTitle title={this.getPageTitle()}>
<Layout>
<Sider
trigger={null}
collapsible
collapsed={collapsed}
collapsedWidth={80}
breakpoint="md"
onCollapse={this.onCollapse}
style={{ minHeight: '100vh' }}
width={272}
>
<div className={styles.logo}>
<Link to="/">
<img src="https://gw.alipayobjects.com/zos/rmsportal/osjtaBtmmQzWRvMbcKeb.svg" alt="logo" />
<h1>Ant Design Pro</h1>
</Link>
</div>
<Menu
theme="dark"
mode="inline"
defaultOpenKeys={this.getDefaultCollapsedSubMenus()}
selectedKeys={this.getCurrentMenuSelectedKeys()}
style={{ margin: '24px 0', width: '100%' }}
>
{this.getNavMenuItems(menus)}
</Menu>
</Sider>
<Layout>
<Header className={styles.header}>
<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={this.handleNoticeClear}
onPopupVisibleChange={this.handleNoticeVisibleChange}
loading={fetchingNotices}
popupAlign={{ offset: [20, -16] }}
>
<NoticeIcon.Tab list={noticeData['通知']} title="通知" />
<NoticeIcon.Tab list={noticeData['消息']} title="消息" />
<NoticeIcon.Tab list={noticeData['待办']} title="待办" />
</NoticeIcon>
<Dropdown overlay={menu}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar size="small" className={styles.avatar} src={currentUser.avatar} />
{currentUser.name}
</span>
</Dropdown>
</div>
</Header>
<Content style={{ margin: 24, height: '100%' }}>
{children}
<GlobalFooter
links={[{
title: '帮助',
href: '',
}, {
title: '隐私',
href: '',
}, {
title: '条款',
href: '',
blankTarget: true,
}]}
copyright={<div>Copyright <Icon type="copyright" /> 2017 蚂蚁金服体验技术部出品</div>}
/>
</Content>
</Layout>
</Layout>
</DocumentTitle>
);
}
}
export default connect(state => ({
currentUser: state.user.currentUser,
collapsed: state.global.collapsed,
fetchingNotices: state.global.fetchingNotices,
notices: state.global.notices,
}))(BasicLayout);
@import "~antd/lib/style/themes/default.less";
.header {
background: @primary-color;
padding: 0 16px 0 0;
color: #fff;
}
.logo {
height: 64px;
position: relative;
line-height: 64px;
padding: 0 24px;
background: @primary-color;
overflow: hidden;
img {
display: inline-block;
vertical-align: middle;
height: 32px;
}
h1 {
color: #fff;
display: inline-block;
vertical-align: middle;
font-size: 22px;
margin-left: 12px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin-top: -3px;
}
}
:global(.ant-layout-sider-collapsed) .logo > a {
width: 32px;
}
.trigger {
font-size: 20px;
line-height: 64px;
cursor: pointer;
transition: all .3s;
color: #fff;
padding: 0 28px;
vertical-align: middle;
&:hover {
background: @primary-7;
}
}
.right {
float: right;
height: 100%;
.action {
cursor: pointer;
padding: 0 12px;
display: inline-block;
transition: all .3s;
height: 100%;
> i {
font-size: 20px;
vertical-align: middle;
}
&:global(.ant-popover-open),
&:hover {
background: @primary-7;
}
}
.search:hover {
background: transparent;
}
.account {
.avatar {
margin: 20px 8px 20px 0;
color: @primary-color;
background: rgba(255, 255, 255, .85);
vertical-align: middle;
}
}
}
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
padding-left: 16px;
padding-right: 16px;
width: 190px;
}
}
:global {
.ant-layout {
overflow-x: hidden;
}
}
import React from 'react';
export default props => <div {...props} />;
import React from 'react';
import PageHeader from '../components/PageHeader';
export default ({ children, ...restProps }) => (
<div style={{ margin: -24 }}>
<PageHeader {...restProps} />
{children ? <div style={{ margin: 24 }}>{children}</div> : null}
</div>
);
import React from 'react';
import PropTypes from 'prop-types';
import DocumentTitle from 'react-document-title';
import { Icon } from 'antd';
import GlobalFooter from '../components/GlobalFooter';
import styles from './UserLayout.less';
const links = [{
title: '帮助',
href: '',
}, {
title: '隐私',
href: '',
}, {
title: '条款',
href: '',
}];
const copyright = <div>Copyright <Icon type="copyright" /> 2017 蚂蚁金服体验技术部出品</div>;
class UserLayout extends React.PureComponent {
static childContextTypes = {
routes: PropTypes.array,
params: PropTypes.object,
}
getChildContext() {
const { routes, params } = this.props;
return { routes, params };
}
getPageTitle() {
const { routes } = this.props;
for (let i = routes.length - 1; i >= 0; i -= 1) {
if (routes[i].breadcrumbName) {
return `${routes[i].breadcrumbName} - Ant Design Pro`;
}
}
return 'Ant Design Pro';
}
render() {
const { children } = this.props;
return (
<DocumentTitle title={this.getPageTitle()}>
<div className={styles.container}>
<div className={styles.top}>
<div className={styles.header}>
<img alt="" className={styles.logo} src="https://gw.alipayobjects.com/zos/rmsportal/NGCCBOENpgTXpBWUIPnI.svg" />
<span className={styles.title}>Ant Design</span>
</div>
<p className={styles.desc}>Ant Design 是东半球最具影响力的 Web 设计规范</p>
</div>
{children}
<GlobalFooter className={styles.footer} links={links} copyright={copyright} />
</div>
</DocumentTitle>
);
}
}
export default UserLayout;
@import "~antd/lib/style/themes/default.less";
.container {
background: @background-color-base;
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/bOjjckIwLKuWCswKAghg.svg');
width: 100%;
min-height: 100%;
background-repeat: no-repeat;
background-position: center;
background-size: 85%;
padding: 110px 0 144px 0;
position: relative;
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 12px;
}
.title {
font-size: 33px;
color: @heading-color;
}
.desc {
font-size: @font-size-lg;
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: [],
loading: true,
},
effects: {
*fetchList({ payload }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryActivities);
yield put({
type: 'saveList',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
},
reducers: {
saveList(state, action) {
return {
...state,
list: action.payload,
};
},
changeLoading(state, action) {
return {
...state,
loading: action.payload,
};
},
},
};
import { fakeChartData } from '../services/api';
export default {
namespace: 'chart',
state: {
visitData: [],
salesData: [],
searchData: [],
offlineData: [],
offlineChartData: [],
salesTypeData: [],
salesTypeDataOnline: [],
salesTypeDataOffline: [],
radarData: [],
},
effects: {
*fetch({ payload }, { call, put }) {
const response = yield call(fakeChartData);
yield put({
type: 'save',
payload: response,
});
},
*fetchSalesData({ payload }, { call, put }) {
const response = yield call(fakeChartData);
yield put({
type: 'save',
payload: {
salesData: response.salesData,
},
});
},
},
reducers: {
save(state, { payload }) {
return {
...state,
...payload,
};
},
setter(state, { payload }) {
return {
...state,
...payload,
};
},
clear() {
return {
visitData: [],
salesData: [],
searchData: [],
offlineData: [],
offlineChartData: [],
salesTypeData: [],
salesTypeDataOnline: [],
salesTypeDataOffline: [],
radarData: [],
};
},
},
};
import { routerRedux } from 'dva/router';
import { message } from 'antd';
import { fakeSubmitForm } from '../services/api';
export default {
namespace: 'form',
state: {
step: {
},
regularFormSubmitting: false,
stepFormSubmitting: false,
advancedFormSubmitting: false,
},
effects: {
*submitRegularForm({ payload }, { call, put }) {
yield put({
type: 'changeRegularFormSubmitting',
payload: true,
});
yield call(fakeSubmitForm, payload);
yield put({
type: 'changeRegularFormSubmitting',
payload: false,
});
message.success('提交成功');
},
*submitStepForm({ payload }, { call, put }) {
yield put({
type: 'changeStepFormSubmitting',
payload: true,
});
yield call(fakeSubmitForm, payload);
yield put({
type: 'saveStepFormData',
payload,
});
yield put({
type: 'changeStepFormSubmitting',
payload: false,
});
yield put(routerRedux.push('/form/step-form/result'));
},
*submitAdvancedForm({ payload }, { call, put }) {
yield put({
type: 'changeAdvancedFormSubmitting',
payload: true,
});
yield call(fakeSubmitForm, payload);
yield put({
type: 'changeAdvancedFormSubmitting',
payload: false,
});
message.success('提交成功');
},
},
reducers: {
saveStepFormData(state, { payload }) {
return {
...state,
step: {
...state.step,
...payload,
},
};
},
changeRegularFormSubmitting(state, { payload }) {
return {
...state,
regularFormSubmitting: payload,
};
},
changeStepFormSubmitting(state, { payload }) {
return {
...state,
stepFormSubmitting: payload,
};
},
changeAdvancedFormSubmitting(state, { payload }) {
return {
...state,
advancedFormSubmitting: payload,
};
},
},
};
import { queryNotices } from '../services/api';
export default {
namespace: 'global',
state: {
collapsed: false,
notices: [],
fetchingNotices: false,
},
effects: {
*fetchNotices({ payload }, { call, put }) {
yield put({
type: 'changeNoticeLoading',
payload: true,
});
const data = yield call(queryNotices);
yield put({
type: 'saveNotices',
payload: data,
});
},
},
reducers: {
changeLayoutCollapsed(state, { payload }) {
return {
...state,
collapsed: payload,
};
},
saveNotices(state, { payload }) {
return {
...state,
notices: payload,
fetchingNotices: false,
};
},
clearNotices(state, { payload }) {
return {
...state,
notices: state.notices.filter(item => item.type !== payload),
};
},
changeNoticeLoading(state, { payload }) {
return {
...state,
fetchingNotices: payload,
};
},
},
};
// 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: [],
loading: true,
},
effects: {
*fetch({ payload, callback }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryFakeList, payload);
yield put({
type: 'save',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
if (callback) {
callback();
}
},
},
reducers: {
save(state, action) {
return {
...state,
list: action.payload,
};
},
changeLoading(state, action) {
return {
...state,
loading: action.payload,
};
},
},
};
import { fakeAccountLogin, fakeMobileLogin } from '../services/api';
export default {
namespace: 'login',
state: {
status: undefined,
},
effects: {
*accountSubmit({ payload }, { call, put }) {
yield put({
type: 'changeSubmitting',
payload: true,
});
const response = yield call(fakeAccountLogin);
yield put({
type: 'loginHandle',
payload: response,
});
yield put({
type: 'changeSubmitting',
payload: false,
});
},
*mobileSubmit({ payload }, { call, put }) {
yield put({
type: 'changeSubmitting',
payload: true,
});
const response = yield call(fakeMobileLogin);
yield put({
type: 'loginHandle',
payload: response,
});
yield put({
type: 'changeSubmitting',
payload: false,
});
},
},
reducers: {
loginHandle(state, { payload }) {
return {
...state,
status: payload.status,
type: payload.type,
};
},
changeSubmitting(state, { payload }) {
return {
...state,
submitting: payload,
};
},
},
};
import { queryTags } from '../services/api';
export default {
namespace: 'monitor',
state: {
tags: [],
},
effects: {
*fetchTags({ payload }, { call, put }) {
const response = yield call(queryTags);
yield put({
type: 'saveTags',
payload: response.list,
});
},
},
reducers: {
saveTags(state, action) {
return {
...state,
tags: action.payload,
};
},
},
};
import { queryProfile } from '../services/api';
export default {
namespace: 'profile',
state: {
operation1: [],
operation2: [],
operation3: [],
loading: true,
},
effects: {
*fetch({ payload }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryProfile);
yield put({
type: 'show',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
},
reducers: {
show(state, { payload }) {
return {
...state,
...payload,
};
},
changeLoading(state, { payload }) {
return {
...state,
loading: payload,
};
},
},
};
import { queryProjectNotice } from '../services/api';
export default {
namespace: 'project',
state: {
notice: [],
loading: true,
},
effects: {
*fetchNotice({ payload }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryProjectNotice);
yield put({
type: 'saveNotice',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
},
reducers: {
saveNotice(state, action) {
return {
...state,
notice: action.payload,
};
},
changeLoading(state, action) {
return {
...state,
loading: action.payload,
};
},
},
};
import { fakeRegister } from '../services/api';
export default {
namespace: 'register',
state: {
status: undefined,
},
effects: {
*submit({ payload }, { call, put }) {
yield put({
type: 'changeSubmitting',
payload: true,
});
const response = yield call(fakeRegister);
yield put({
type: 'registerHandle',
payload: response,
});
yield put({
type: 'changeSubmitting',
payload: false,
});
},
},
reducers: {
registerHandle(state, { payload }) {
return {
...state,
status: payload.status,
};
},
changeSubmitting(state, { payload }) {
return {
...state,
submitting: payload,
};
},
},
};
import { queryRule, removeRule, addRule } from '../services/api';
export default {
namespace: 'rule',
state: {
data: {
list: [],
pagination: {},
},
loading: true,
},
effects: {
*fetch({ payload }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryRule, payload);
yield put({
type: 'save',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
*add({ payload, callback }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(addRule, payload);
yield put({
type: 'save',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
if (callback) callback();
},
*remove({ payload, callback }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(removeRule, payload);
yield put({
type: 'save',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
if (callback) callback();
},
},
reducers: {
save(state, action) {
return {
...state,
data: action.payload,
};
},
changeLoading(state, action) {
return {
...state,
loading: action.payload,
};
},
},
};
import { query as queryUsers, queryCurrent } from '../services/user';
export default {
namespace: 'user',
state: {
list: [],
loading: false,
currentUser: {},
},
effects: {
*fetch({ payload }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryUsers);
yield put({
type: 'save',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
*fetchCurrent({ payload }, { call, put }) {
const response = yield call(queryCurrent);
yield put({
type: 'saveCurrentUser',
payload: response,
});
},
},
reducers: {
save(state, action) {
return {
...state,
list: action.payload,
};
},
changeLoading(state, action) {
return {
...state,
loading: action.payload,
};
},
saveCurrentUser(state, action) {
return {
...state,
currentUser: action.payload,
};
},
},
};
import React from 'react';
import { Router, Route, Redirect } from 'dva/router';
import navData from './common/nav';
function getRoutes(data, level = 0) {
return data.map((item, i) => {
let children;
if (item.children) {
children = getRoutes(item.children, level + 1);
}
let homePageRedirect;
if (level === 1 && i === 0) {
let indexPath;
// First children router
if (item.children && item.children[0]) {
indexPath = `/${item.path}/${item.children[0].path}`;
} else {
indexPath = item.path;
}
homePageRedirect = <Redirect from="/" to={indexPath} />;
}
if (item.noRoute) {
return null;
}
return (
<Route
key={item.key || item.path || ''}
path={item.path}
breadcrumbName={item.name}
component={item.component}
>
{homePageRedirect}
{children}
</Route>
);
});
}
function RouterConfig({ history }) {
return (
<Router history={history}>
{getRoutes(navData)}
</Router>
);
}
export default RouterConfig;
.normal {
font-family: Georgia, sans-serif;
margin-top: 3em;
text-align: center;
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card, Table, Icon } from 'antd';
const columns = [{
title: 'Name',
dataIndex: 'name',
key: 'name',
}, {
title: 'Age',
dataIndex: 'age',
key: 'age',
}, {
title: 'Address',
dataIndex: 'address',
key: 'address',
}, {
title: 'Action',
key: 'action',
render: (text, record) => (
<span>
<a href="">Action {record.name}</a>
<span className="ant-divider" />
<a href="">Delete</a>
<span className="ant-divider" />
<a href="" className="ant-dropdown-link">
More actions <Icon type="down" />
</a>
</span>
),
}];
class Dashboard extends PureComponent {
componentDidMount() {
this.props.dispatch({
type: 'user/fetch',
});
}
render() {
const { user: { list, loading } } = this.props;
return (
<div>
<Row gutter={24}>
<Col span={8}>
<Card bordered={false}>
<p>卡片内容</p>
<p>卡片内容</p>
<p>卡片内容</p>
</Card>
</Col>
<Col span={8}>
<Card bordered={false}>
<p>卡片内容</p>
<p>卡片内容</p>
<p>卡片内容</p>
</Card>
</Col>
<Col span={8}>
<Card bordered={false}>
<p>卡片内容</p>
<p>卡片内容</p>
<p>卡片内容</p>
</Card>
</Col>
</Row>
<Row gutter={24} style={{ marginTop: 24 }}>
<Col span={12}>
<Card bordered={false}>
<p>卡片内容</p>
<p>卡片内容</p>
<p>卡片内容</p>
</Card>
</Col>
<Col span={12}>
<Card bordered={false}>
<p>卡片内容</p>
<p>卡片内容</p>
<p>卡片内容</p>
</Card>
</Col>
</Row>
<Row gutter={24} style={{ marginTop: 24 }}>
<Col span={24}>
<Card
title="业务表格"
bordered={false}
extra={<Icon type="setting" />}
>
<Table dataSource={list} loading={loading} columns={columns} />
</Card>
</Col>
</Row>
</div>
);
}
}
export default connect(state => ({
user: state.user,
}))(Dashboard);
import React, { Component } from 'react';
import { connect } from 'dva';
import { Row, Col, Icon, Card, Tabs, Table, Radio, DatePicker, Tooltip } from 'antd';
import numeral from 'numeral';
import { ChartCard, Trend, yuan, MiniArea, MiniBar, MiniProgress, Field, Bar, Pie, NumberInfo, IconUp, IconDown } from '../../components/Charts';
import TimelineChart from '../../components/TimelineChart';
import { getTimeDistance } from '../../utils/utils';
import styles from './Analysis.less';
const TabPane = Tabs.TabPane;
const { RangePicker } = DatePicker;
const rankingListData = [];
for (let i = 0; i < 7; i += 1) {
rankingListData.push({
title: `工专路 ${i} 号店`,
total: 323234,
});
}
@connect(state => ({
chart: state.chart,
}))
export default class Analysis extends Component {
state = {
salesType: 'all',
currentTabKey: '',
rangePickerValue: [],
}
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,
});
}
selectDate = (type) => {
this.setState({
rangePickerValue: getTimeDistance(type),
});
this.props.dispatch({
type: 'chart/fetchSalesData',
});
}
render() {
const { rangePickerValue, salesType, currentTabKey } = this.state;
const { chart } = this.props;
const {
visitData,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
} = chart;
const salesPieData = salesType === 'all' ?
salesTypeData
:
(salesType === 'online' ? salesTypeDataOnline : salesTypeDataOffline);
const iconGroup = (
<span className={styles.iconGroup}>
<Icon type="camera-o" /><Icon type="export" /><Icon type="ellipsis" />
</span>
);
const salesExtra = (<div className={styles.salesExtraWrap}>
<div className={styles.salesExtra}>
<a onClick={() => this.selectDate('today')}>今日</a>
<a onClick={() => this.selectDate('week')}>本周</a>
<a onClick={() => this.selectDate('month')}>本月</a>
<a 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,
},
{
title: '周涨幅',
dataIndex: 'range',
key: 'range',
sorter: (a, b) => a.range - b.range,
render: (text, record) => (
<span style={{ textAlign: 'right' }}>{text}% {record.status === 1 ? <IconDown /> : <IconUp />}</span>
),
},
];
const CustomTab = ({ data, currentTabKey: currentKey }) => (
<Row gutter={8} style={{ width: 138, margin: '8px 28px' }}>
<Col span={12}>
<NumberInfo
title={data.name}
subTitle="转化率"
total={`${data.cvr * 100}%`}
theme={(currentKey !== data.name) && 'light'}
/>
</Col>
<Col span={12} style={{ paddingTop: 36 }}>
<Pie
animate={(currentKey === data.name)}
color={(currentKey !== data.name) && '#99d5fd'}
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: 6,
style: { marginBottom: 24 },
};
return (
<div>
<Row gutter={24}>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title="销售额"
action={<Tooltip title="我是一段说明"><Icon type="exclamation-circle-o" /></Tooltip>}
total={yuan(126560)}
footer={<Field label="日均销售额" value={numeral(12423).format('0,0')} />}
contentHeight={46}
>
<Trend colorType="gray">
<Trend.Item title="周同比" flag="up">12.3%</Trend.Item>
<Trend.Item title="日环比" flag="down">11%</Trend.Item>
</Trend>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title="访问量"
action={<Tooltip title="访问量是关键指标"><Icon type="exclamation-circle-o" /></Tooltip>}
total={numeral(8846).format('0,0')}
footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />}
contentHeight={46}
>
<MiniArea
line
color="#AF7CE9"
height={46}
data={visitData}
/>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title="支付笔数"
action={<Tooltip title="支付笔数反应交易质量"><Icon type="exclamation-circle-o" /></Tooltip>}
total={numeral(6560).format('0,0')}
footer={<Field label="转化率" value="60%" />}
contentHeight={46}
>
<MiniBar
height={46}
data={visitData}
/>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title="线上购物转化率"
action={<Tooltip title="购买效率"><Icon type="exclamation-circle-o" /></Tooltip>}
total="78%"
footer={<Trend>
<Trend.Item title="周同比" flag="up">12.3%</Trend.Item>
<Trend.Item title="日环比" flag="down">11%</Trend.Item>
</Trend>}
contentHeight={46}
>
<MiniProgress percent={78} strokeWidth={8} target={80} color="#5DD1DD" />
</ChartCard>
</Col>
</Row>
<Card
bordered={false}
bodyStyle={{ padding: 0 }}
>
<div className={styles.salesCard}>
<Tabs tabBarExtraContent={salesExtra}>
<TabPane tab="销售额" key="sales">
<Row gutter={72}>
<Col sm={16} xs={24}>
<Bar
height={292}
title="销售额趋势"
data={salesData}
/>
</Col>
<Col sm={8} xs={24}>
<h4>门店销售额排名</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>
</Col>
</Row>
</TabPane>
<TabPane tab="访问量" key="visits">
访问量没有, 因为偷懒了
</TabPane>
</Tabs>
</div>
</Card>
<Row gutter={24}>
<Col lg={12} sm={24} xs={24}>
<Card
bordered={false}
title="线上热门搜索"
extra={iconGroup}
style={{ marginTop: 24 }}
>
<Row gutter={68}>
<Col sm={12} xs={24} style={{ marginBottom: 24 }}>
<NumberInfo
subTitle={<span>搜索用户数量 <Icon style={{ marginLeft: 8 }} type="info-circle-o" /></span>}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
<MiniArea
line
color="#cceafe"
height={45}
data={visitData}
/>
</Col>
<Col sm={12} xs={24} style={{ marginBottom: 24 }}>
<NumberInfo
subTitle="人均搜索次数"
total={2.7}
status="down"
subTotal={26.2}
/>
<MiniArea
line
color="#5dd1dd"
height={45}
data={visitData}
/>
</Col>
</Row>
<Table
Bordered={false}
rowKey={record => record.index}
size="middle"
columns={columns}
dataSource={searchData}
pagination={{
style: { marginBottom: 0 },
showSizeChanger: true,
showQuickJumper: true,
pageSize: 5,
}}
/>
</Card>
</Col>
<Col lg={12} sm={24} xs={24}>
<Card
bordered={false}
title="销售额类别占比"
extra={iconGroup}
style={{ marginTop: 24 }}
>
<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 style={{ marginTop: 32, marginBottom: 67 }}>
<Pie
hasLegend
title="销售额"
subTitle="销售额"
total={yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))}
data={salesPieData}
valueFormat={val => yuan(val)}
height={294}
/>
</div>
</Card>
</Col>
</Row>
<Card
bordered={false}
bodyStyle={{ padding: '0 0 24px 0' }}
style={{ marginTop: 24 }}
>
<Tabs
activeKey={currentTabKey || (offlineData[0] && offlineData[0].name)}
onChange={this.handleTabChange}
>
{
offlineData.map(shop => (
<TabPane
tab={<CustomTab data={shop} currentTabKey={currentTabKey} />}
key={shop.name}
>
<div style={{ padding: '0 24px' }}>
<TimelineChart
data={offlineChartData}
titleMap={{ y1: '客流量', y2: '支付笔数' }}
/>
</div>
</TabPane>)
)
}
</Tabs>
</Card>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.iconGroup {
i {
cursor: pointer;
margin-left: 16px;
}
}
.rankingList {
margin-top: 25px;
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;
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;
}
}
}
.salesCard {
:global {
.ant-tabs-content {
padding: 0 24px 24px 24px;
}
.ant-tabs-bar {
padding-left: 24px;
.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;
}
}
}
@media screen and (max-width: @screen-lg) {
.rankingList {
li {
span:first-child {
margin-right: 8px;
}
}
}
}
@media screen and (max-width: @screen-sm) {
.salesExtra {
display: none;
}
.salesExtraWrap {
position: absolute;
top: 50px;
left: 24px;
}
.salesCard {
:global {
.ant-tabs-content {
padding-top: 30px;
}
}
}
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card } from 'antd';
import numeral from 'numeral';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import { NumberInfo, MiniArea, Pie, WaterWave, Gauge } from '../../components/Charts';
import MapChart from '../../components/MapChart';
import TagCloud from '../../components/TagCloud';
import Countdown from '../../components/Countdown';
import { fixedZero } from '../../utils/utils';
import styles from './Monitor.less';
const activeData = [];
for (let i = 0; i < 24; i += 1) {
activeData.push({
x: `${fixedZero(i)}:00`,
y: (i * 50) + (Math.floor(Math.random() * 200)),
});
}
const MapData = [];
for (let i = 0; i < 50; i += 1) {
MapData.push({
x: Math.floor(Math.random() * 600),
y: Math.floor(Math.random() * 400),
value: Math.floor(Math.random() * 1000) + 500,
});
}
const targetTime = new Date().getTime() + 3900000;
@connect(state => ({
monitor: state.monitor,
}))
export default class Monitor extends PureComponent {
componentDidMount() {
this.props.dispatch({
type: 'monitor/fetchTags',
});
}
render() {
const { monitor } = this.props;
const { tags } = monitor;
return (
<PageHeaderLayout
title="大盘监控"
>
<Row gutter={24}>
<Col lg={16} md={24} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card title="活动实时交易情况">
<Row>
<Col sm={6} xs={12}>
<NumberInfo
subTitle="今日交易总额"
total={numeral(124543233).format('0,0')}
/>
</Col>
<Col sm={6} xs={12}>
<NumberInfo
subTitle="销售目标完成率"
total="92%"
/>
</Col>
<Col sm={6} xs={12}>
<NumberInfo
subTitle="活动剩余时间"
total={<Countdown target={targetTime} />}
/>
</Col>
<Col sm={6} xs={12}>
<NumberInfo
subTitle="每秒交易总额"
total={numeral(234).format('0,0')}
/>
</Col>
</Row>
<div className={styles.mapChart}>
<MapChart
data={MapData}
/>
</div>
</Card>
</Col>
<Col lg={8} md={24} sm={24} xs={24}>
<Card title="活动情况预测" style={{ marginBottom: 24 }}>
<div className={styles.activeChart}>
<NumberInfo
subTitle="目标评估"
total="有望达到预期"
/>
<div style={{ marginTop: 32 }}>
<MiniArea
line
color="#5DD1DD"
height={84}
yAxis={{
tickCount: 3,
tickLine: false,
labels: 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>
</Card>
<Card title="券核效率" style={{ marginBottom: 24 }} bodyStyle={{ textAlign: 'center' }}>
<Gauge
title="跳出率"
height={164}
percent={87}
/>
</Card>
</Col>
</Row>
<Row gutter={24}>
<Col sm={8} xs={24}>
<Card title="各品类占比" style={{ marginBottom: 24 }}>
<Row style={{ padding: '18px 0 19px 0' }}>
<Col span={8}>
<Pie
percent={28}
subTitle="中式快餐"
total="28%"
height={129}
/>
</Col>
<Col span={8}>
<Pie
color="#5DD1DD"
percent={22}
subTitle="西餐"
total="22%"
height={129}
/>
</Col>
<Col span={8}>
<Pie
color="#B5EDC9"
percent={32}
subTitle="火锅"
total="32%"
height={129}
/>
</Col>
</Row>
</Card>
</Col>
<Col sm={8} xs={24} style={{ marginBottom: 24 }}>
<Card title="热门搜索">
<TagCloud
data={tags}
height={161}
/>
</Card>
</Col>
<Col sm={8} xs={24} style={{ marginBottom: 24 }}>
<Card title="资源剩余" bodyStyle={{ textAlign: 'center' }}>
<WaterWave
height={161}
title="补贴资金剩余"
percent={34}
/>
</Card>
</Col>
</Row>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.activeChart {
position: relative;
}
.activeChartGrid {
p {
position: absolute;
top: 80px;
}
p:last-child {
top: 115px;
}
}
.activeChartLegend {
position: relative;
font-size: 0;
margin-top: 8px;
height: 20px;
line-height: 20px;
span {
display: inline-block;
font-size: 12px;
text-align: center;
width: 33.33%;
}
span:first-child {
text-align: left;
}
span:last-child {
text-align: right;
}
}
.mapChart {
padding-top: 46px;
height: 436px;
}
@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, Alert, Icon } 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/CRxBvUggxBYzWBTGmkxF.png',
link: '',
},
{
id: 'members-2',
title: 'Ant Design',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/RBytOnluTcyeyDazAbvs.png',
link: '',
},
{
id: 'members-3',
title: 'DesignLab',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/HQVJYAXtWHEJvLxQjmPa.png',
link: '',
},
{
id: 'members-4',
title: 'Basement',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/HQVJYAXtWHEJvLxQjmPa.png',
link: '',
},
{
id: 'members-5',
title: 'Github',
logo: 'https://gw.alipayobjects.com/zos/rmsportal/RBytOnluTcyeyDazAbvs.png',
link: '',
},
];
@connect(state => ({
project: state.project,
activities: state.activities,
chart: state.chart,
}))
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',
});
}
render() {
const {
project: { loading: projectLoading, notice },
activities: { loading: activitiesLoading, list: activitiesList },
chart: { radarData },
} = this.props;
const pageHeaderContent = (
<Alert
message="平台迁移公告:平台将于本周六 03:00 ~ 04:00 进行迁移,请勿在此期间提交数据。"
type="warning"
showIcon
/>
);
const pageHeaderTitle = (
<div className={styles.pageHeaderTitle}>
<div className={styles.titleAvatar}>
<Avatar size="large" src="https://gw.alipayobjects.com/zos/rmsportal/XertDCubOxUvZbCdgWTW.png" />
</div>
<div className={styles.titleContent}>
<p>早安, 曲丽丽, 祝你开心每一天</p>
<p>交互专家 | 蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED</p>
</div>
</div>
);
const pageHeaderAction = (
<div className={styles.pageHeaderAction}>
<div>
<p><Icon type="appstore-o" /> 项目数</p>
<p>56</p>
<em />
</div>
<div>
<p><Icon type="trophy" /> 团队内排名</p>
<p>8<span> / 24</span></p>
<em />
</div>
<div>
<p><Icon type="eye-o" /> 项目访问</p>
<p>2,223</p>
</div>
</div>
);
return (
<PageHeaderLayout
action={pageHeaderAction}
title={pageHeaderTitle}
content={pageHeaderContent}
>
<Row gutter={24}>
<Col lg={16} md={24} sm={24} xs={24}>
<Card
className={styles.projectList}
style={{ marginBottom: 24 }}
noHovering
title="进行中的项目"
bordered={false}
extra={<Link to="/">全部项目</Link>}
loading={projectLoading}
bodyStyle={{ padding: 0 }}
>
{
!projectLoading && notice.length > 0 && notice.map(item => (
<Card.Grid className={styles.projectGrid} key={item.id}>
<Card noHovering bodyStyle={{ padding: 0 }} bordered={false}>
<Card.Meta
avatar={<Avatar src={item.logo} />}
title={<Link to={item.href}>{item.title}</Link>}
description={item.description}
/>
<div className={styles.projectItemContent}>
<Link to={item.memberLink}>{item.member || ''}</Link>
{
item.updatedAt && <span>{moment(item.updatedAt).fromNow()}</span>
}
</div>
</Card>
</Card.Grid>
))
}
</Card>
<Card
noHovering
style={{ marginBottom: 24 }}
bodyStyle={{ padding: 0 }}
bordered={false}
title="动态"
loading={activitiesLoading}
>
<List loading={activitiesLoading}>
<div className={styles.activitiesList}>
{
activitiesList.map(item => (
<List.Item key={item.id}>
<List.Item.Meta
avatar={<Avatar style={{ marginTop: -12 }} src={item.user.avatar} />}
title={<p>{item.user.name} <a>xx</a> 新建了项目 <a>xxxx</a></p>}
description={moment(item.updatedAt).fromNow()}
/>
</List.Item>
))
}
</div>
</List>
</Card>
</Col>
<Col lg={8} md={24} sm={24} xs={24}>
<Card
style={{ marginBottom: 24 }}
noHovering
title="快速开始 / 便捷导航"
bordered={false}
bodyStyle={{ padding: 0 }}
>
<EditableLinkGroup
onAdd={() => {}}
links={links}
/>
</Card>
<Card
style={{ marginBottom: 24 }}
noHovering
bordered={false}
title="xx 指数"
>
<div className={styles.chart}>
{
<Radar
hasLegend
height={286}
data={radarData}
/>
}
</div>
</Card>
<Card
noHovering
bodyStyle={{ paddingBottom: 0 }}
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}>
<img src={item.logo} alt={item.title} />
<span>{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 24px 24px;
:global {
.ant-list-item-meta-title:hover {
color: @text-color;
}
}
}
.pageHeaderTitle {
display: flex;
.titleAvatar {
flex: 0 1 80px;
& > span {
border-radius: 80px;
display: block;
width: 80px;
height: 80px;
}
}
.titleContent {
position: relative;
top: 8px;
margin-left: 32px;
flex: 1 1 auto;
p {
font-weight: normal;
}
& > p:last-child {
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
margin-top: 12px;
}
}
}
.pageHeaderAction {
float: right;
.clearfix();
& > div {
text-align: right;
padding: 0 24px;
position: relative;
float: left;
& > p:first-child {
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
margin-bottom: 2px;
}
& > p {
color: @text-color;
font-size: 30px;
line-height: 38px;
& > span {
color: @text-color-secondary;
font-size: 20px;
}
}
& > em {
background-color: @border-color-split;
position: absolute;
top: 8px;
right: 0;
width: 1px;
height: 40px;
}
}
& > div:last-child {
padding-right: 0;
}
}
.members {
a {
display: block;
margin-bottom: 24px;
line-height: 24px;
height: 24px;
.textOverflow();
img {
border-radius: 24px;
display: inline;
position: relative;
top: -2px;
width: 24px;
height: 24px;
margin-right: 12px;
vertical-align: middle;
}
span {
font-size: @font-size-base;
color: @text-color;
line-height: 24px;
max-width: 100px;
.textOverflow();
}
&:hover {
span {
color: @primary-color;
}
}
}
}
.projectList {
:global {
.ant-card-meta-title {
font-size: 14px;
a {
color: @heading-color;
&:hover {
color: @primary-color;
}
}
}
.ant-card-meta-description {
font-size: 12px;
min-height: 36px;
}
}
.projectGrid {
width: 33.33%;
}
.projectItemContent {
display: flex;
padding-left: 48px;
margin-top: 12px;
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;
}
}
span {
color: @text-color-secondary;
flex: 0 0 auto;
float: right;
}
}
}
@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) {
.pageHeaderAction {
margin-left: -44px;
& > div {
padding: 0 16px;
}
}
}
@media screen and (max-width: @screen-lg) {
.pageHeaderAction {
margin-left: -64px;
& > div {
padding: 0 16px;
text-align: left;
& > em {
display: none;
}
}
}
}
@media screen and (max-width: @screen-md) {
.projectList {
.projectGrid {
width: 50%;
}
}
}
@media screen and (max-width: @screen-xs) {
.projectList {
.projectGrid {
width: 100%;
}
}
}
import React from 'react';
import Exception from '../../components/Exception';
export default () => <Exception type="403" />;
import React from 'react';
import Exception from '../../components/Exception';
export default () => <Exception type="404" />;
import React from 'react';
import Exception from '../../components/Exception';
export default () => <Exception type="500" />;
import React 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',
}];
function AdvancedForm({ form, dispatch, submitting }) {
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="在后台页面中,大批量的数据修改和提交是很常见的情况。"
>
<Card title="仓库管理" className={styles.card} bordered={false}>
<Form layout="vertical" hideRequiredMark>
<Row gutter={16}>
<Col md={6} sm={24}>
<Form.Item label={fieldLabels.name}>
{getFieldDecorator('name', {
rules: [{ required: true, message: '请输入仓库名称' }],
})(
<Input placeholder="请输入仓库名称" />
)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} md={{ span: 8 }} 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 }} md={{ span: 10 }} 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 md={6} 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 }} md={{ span: 8 }} 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 }} md={{ span: 10 }} 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 md={6} sm={24}>
<Form.Item label={fieldLabels.name2}>
{getFieldDecorator('name2', {
rules: [{ required: true, message: '请输入' }],
})(
<Input placeholder="请输入" />
)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} md={{ span: 8 }} sm={24}>
<Form.Item label={fieldLabels.url2}>
{getFieldDecorator('url2', {
rules: [{ required: true, message: '请选择' }],
})(
<Input placeholder="请输入" />
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} md={{ span: 10 }} 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 md={6} 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 }} md={{ span: 8 }} sm={24}>
<Form.Item label={fieldLabels.dateRange2}>
{getFieldDecorator('dateRange2', {
rules: [{ required: true, message: '请输入' }],
})(
<TimePicker placeholder="提醒时间" style={{ width: '100%' }} />
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} md={{ span: 10 }} 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>
{getErrorInfo()}
<Button size="large">取消</Button>
<Button type="primary" size="large" onClick={validate} loading={submitting}>
提交
</Button>
</FooterToolbar>
</PageHeaderLayout>
);
}
export default connect(state => ({
collapsed: state.global.collapsed,
submitting: state.form.advancedFormSubmitting,
}))(Form.create()(AdvancedForm));
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Form, Input, DatePicker, Select, Button, Card } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
const FormItem = Form.Item;
const Option = Select.Option;
const { RangePicker } = DatePicker;
@connect(state => ({
submitting: state.form.regularFormSubmitting,
}))
@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 } = this.props.form;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 3 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};
const submitFormLayout = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 10, offset: 3 },
},
};
return (
<PageHeaderLayout title="基础表单" content="表单页是向后台提交数据的标准场景。">
<Card bordered={false} noHovering>
<Form onSubmit={this.handleSubmit} hideRequiredMark style={{ marginTop: 24 }}>
<FormItem
{...formItemLayout}
label="应用类型"
hasFeedback
>
{getFieldDecorator('appType', {
rules: [{
required: true, message: '应用类型',
}],
})(
<Select placeholder="请选择应用类型">
<Option value="type1">类型一</Option>
<Option value="type2">类型二</Option>
<Option value="type3">类型三</Option>
</Select>
)}
</FormItem>
<FormItem
{...formItemLayout}
label="产品名"
hasFeedback
>
{getFieldDecorator('productName', {
rules: [{
required: true, message: '请输入产品名',
}],
})(
<Input placeholder="产品名" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="应用名"
hasFeedback
>
{getFieldDecorator('appName', {
rules: [
{ required: true, message: '请输入应用名' },
{ pattern: /^[a-zA-Z0-9-]+$/, message: '只能输入英文、数字、中划线' },
],
})(
<Input placeholder="只能输入英文、数字、中划线" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="中文名"
hasFeedback
>
{getFieldDecorator('appChineseName', {
rules: [
{ required: true, message: '请输入应用中文名' },
{ pattern: /^[\u4e00-\u9fa5]+$/, message: '请输入中文' },
],
})(
<Input placeholder="应用中文名" />
)}
</FormItem>
<FormItem
{...formItemLayout}
label="生效日期"
>
{getFieldDecorator('dateRange', {
rules: [{ type: 'array', required: true, message: '请选择生效日期' }],
})(
<RangePicker
format="YYYY-MM-DD"
placeholder={['开始日期', '结束日期']}
style={{ width: '100%' }}
/>
)}
</FormItem>
<FormItem
{...formItemLayout}
label="域名"
>
{getFieldDecorator('domain', {
rules: [{ required: true, message: '请输入域名' }],
})(
<Input addonBefore="http://" addonAfter=".com" placeholder="facebook" style={{ width: '100%' }} />
)}
</FormItem>
<FormItem {...submitFormLayout} style={{ marginTop: 40 }}>
<Button type="primary" htmlType="submit" loading={submitting}>
新建应用
</Button>
</FormItem>
</Form>
</Card>
</PageHeaderLayout>
);
}
}
import React from 'react';
import { Form, Input, Button, Select, Divider } from 'antd';
import { routerRedux } from 'dva/router';
import styles from './style.less';
const Option = Select.Option;
export default ({ formItemLayout, form, dispatch }) => {
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: 'ant-design@alipay.com',
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" size="large" style={{ width: 80 }}>
<Option value="alipay">支付宝</Option>
<Option value="wepay">微信</Option>
</Select>
{getFieldDecorator('receiverAccount', {
initialValue: 'test@example.com',
rules: [
{ required: true, message: '请输入收款人账户' },
{ type: 'email', message: '账户名应为邮箱格式' },
],
})(
<Input style={{ width: 'calc(100% - 80px)' }} placeholder="test@example.com" />
)}
</Input.Group>
</Form.Item>
<Form.Item
{...formItemLayout}
label="收款人姓名"
>
{getFieldDecorator('receiverName', {
initialValue: 'Alex',
rules: [{ required: true, message: '请输入收款人姓名' }],
})(
<Input placeholder="请输入收款人姓名" />
)}
</Form.Item>
<Form.Item
{...formItemLayout}
label="转账金额"
>
{getFieldDecorator('amount', {
initialValue: '500',
rules: [
{ required: true, message: '请输入转账金额' },
{ pattern: /^(\d+)((?:\.\d+)?)$/, message: '请输入合法金额数字' },
],
})(
<Input prefix="¥" placeholder="请输入金额" />
)}
</Form.Item>
<Form.Item
wrapperCol={{ offset: formItemLayout.labelCol.span }}
label=""
>
<Button type="primary" onClick={onValidateForm}>
下一步
</Button>
</Form.Item>
</Form>
<Divider style={{ margin: '48px 0 16px' }} />
<div className={styles.desc}>
<h3>说明</h3>
<h4>转账到支付宝账户</h4>
<p>如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。</p>
<h4>转账到银行卡</h4>
<p>如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。</p>
</div>
</div>
);
};
import React from 'react';
import { Form, Input, Button, Alert, Divider } from 'antd';
import { routerRedux } from 'dva/router';
import styles from './style.less';
export default ({ formItemLayout, form, data, dispatch, submitting }) => {
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
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>
</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
wrapperCol={{ offset: 5 }}
label=""
>
<Button type="primary" onClick={onValidateForm} loading={submitting}>
提交
</Button>
<Button onClick={onPrev} style={{ marginLeft: 8 }}>
上一步
</Button>
</Form.Item>
</Form>
);
};
import React from 'react';
import { Button, Row, Col } from 'antd';
import { routerRedux } from 'dva/router';
import Result from '../../../components/Result';
import styles from './style.less';
export default ({ dispatch, data }) => {
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" size="large" onClick={onFinish}>
再转一笔
</Button>
<Button size="large">
查看账单
</Button>
</div>
);
return (
<Result
type="success"
title="操作成功"
description="预计两小时内到账"
extra={information}
actions={actions}
className={styles.result}
/>
);
};
import React, { cloneElement, PureComponent } from 'react';
import { connect } from 'dva';
import { Card, Steps, Form } from 'antd';
import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
import Step1 from './Step1';
import styles from '../style.less';
const Step = Steps.Step;
@Form.create()
class StepForm extends PureComponent {
getCurrentStep() {
const { routes } = this.props;
switch (routes[routes.length - 1].path) {
case 'step-form': return 0;
case 'confirm': return 1;
case 'result': return 2;
default: return 0;
}
}
render() {
const { form, stepFormData, submitting, dispatch, children } = this.props;
const formItemLayout = {
labelCol: {
span: 5,
},
wrapperCol: {
span: 19,
},
};
return (
<PageHeaderLayout title="分步表单" content="将表单内容进行分步可以提高用户处理的专注度,降低页面复杂性。">
<Card bordered={false}>
<div>
<Steps current={this.getCurrentStep()} className={styles.steps}>
<Step title="填写转账信息" />
<Step title="确认转账信息" />
<Step title="完成" />
</Steps>
{children ? cloneElement(children, {
form,
formItemLayout,
data: stepFormData,
submitting,
dispatch,
}) : (
<Step1
formItemLayout={formItemLayout}
form={form}
dispatch={dispatch}
/>
)}
</div>
</Card>
</PageHeaderLayout>
);
}
}
export default connect(state => ({
stepFormData: state.form.step,
submitting: state.form.stepFormSubmitting,
}))(StepForm);
@import "~antd/lib/style/themes/default.less";
.stepForm {
margin: 40px auto;
max-width: 500px;
}
.stepFormText {
:global {
.ant-form-item-label,
.ant-form-item-control {
line-height: 22px;
}
}
}
.result {
margin: 0 auto;
max-width: 520px;
padding: 32px 0;
}
.desc {
h3 {
font-size: 14px;
margin: 8px 0;
color: @text-color-secondary;
}
h4 {
margin: 2px 0;
color: @text-color-secondary;
}
p {
margin-bottom: 16px;
}
padding: 0 34px;
color: @text-color-secondary;
font-size: 12px;
}
.information {
line-height: 22px;
:global {
.ant-row:not(:last-child) {
margin-bottom: 24px;
}
}
.label {
font-weight: 500;
text-align: right;
padding-right: 8px;
}
}
.money {
font-weight: 500;
font-size: 20px;
line-height: 22px;
}
import React, { PureComponent } from 'react';
import { Table, Button, Input, message } 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) {
return 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 target = this.getRowByKey(key);
if (target) {
// 进入编辑状态时保存原始数据
if (!target.editable) {
this.cacheOriginData[key] = { ...target };
}
target.editable = !target.editable;
this.setState({ data: [...this.state.data] });
}
}
remove(e, key) {
e.preventDefault();
const newData = this.state.data.filter(item => item.key !== key);
this.setState({ data: newData });
this.props.onChange(newData);
}
newMember = () => {
const newData = [...this.state.data];
newData.push({
key: `NEW_TEMP_ID_${this.index}`,
workId: '',
name: '',
department: '',
editable: true,
});
this.index += 1;
this.setState({ data: newData });
}
handleFieldChange(e, fieldName, key) {
const newData = [...this.state.data];
const target = this.getRowByKey(key);
if (target) {
target[fieldName] = e.target.value;
this.setState({ data: newData });
}
}
saveRow(e, key) {
const target = this.getRowByKey(key);
if (!target.workId || !target.name || !target.department) {
message.error('请填写完整成员信息。');
return;
}
this.toggleEditable(e, key);
this.props.onChange(this.state.data);
}
cancel(e, key) {
e.preventDefault();
const target = this.getRowByKey(key);
if (this.cacheOriginData[key]) {
Object.assign(target, this.cacheOriginData[key]);
target.editable = false;
delete this.cacheOriginData[key];
}
this.setState({ data: [...this.state.data] });
}
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)}
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)}
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)}
placeholder="所属部门"
/>
);
}
return text;
},
}, {
title: '操作',
key: 'action',
render: (text, record) => {
if (record.editable) {
if (record.key.indexOf('NEW_TEMP_ID_') >= 0) {
return (
<span>
<a onClick={e => this.saveRow(e, record.key)}>保存</a>
<span className="ant-divider" />
<a onClick={e => this.remove(e, record.key)}>删除</a>
</span>
);
}
return (
<span>
<a onClick={e => this.saveRow(e, record.key)}>保存</a>
<span className="ant-divider" />
<a onClick={e => this.cancel(e, record.key)}>取消</a>
</span>
);
}
return (
<span>
<a onClick={e => this.toggleEditable(e, record.key)}>编辑</a>
<span className="ant-divider" />
<a onClick={e => this.remove(e, record.key)}>删除</a>
</span>
);
},
}];
return (
<div>
<Table
columns={columns}
dataSource={this.state.data}
pagination={false}
rowClassName={(record) => {
return record.editable ? styles.editable : '';
}}
/>
<Button
style={{ width: '100%', marginTop: 24 }}
type="dashed"
size="large"
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 {
max-width: 750px;
margin: 16px auto;
}
.divider {
border: 0;
border-top: 1px solid @border-color-split;
height: 1px;
margin: 0 0 24px 0;
}
.errorIcon {
cursor: pointer;
color: @error-color;
margin-right: 24px;
i {
margin-right: 4px;
}
}
.errorPopover {
:global {
.ant-popover-inner-content {
padding: 0;
max-height: 400px;
overflow: auto;
min-width: 240px;
}
}
}
.errorListItem {
list-style: none;
border-bottom: 1px solid @border-color-split;
padding: 8px 24px;
cursor: pointer;
transition: all .3s;
&:hover {
background: @primary-1;
}
&:last-child {
border: 0;
}
.errorIcon {
color: @error-color;
float: left;
margin-top: 4px;
margin-right: 8px;
padding-bottom: 22px;
}
.errorField {
font-size: 12px;
color: @text-color-secondary;
margin-top: 4px;
}
}
// 避免表格编辑模式切换时抖动
.editable {
td {
transition: none !important;
padding-top: 12.5px !important;
padding-bottom: 12.5px !important;
}
}
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.Search;
@connect(state => ({
list: state.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>
<span>Owner</span>
<p>{owner}</p>
</div>
<div>
<span>开始时间</span>
<p>{moment(createdAt).format('YYYY-MM-DD hh:mm')}</p>
</div>
<div>
<Progress percent={percent} status={status} strokeWidth={6} />
</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>
<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
title="基础列表"
style={{ marginTop: 16 }}
extra={extraContent}
>
<Button type="dashed" style={{ width: '100%' }}>
<Icon type="plus" /> 添加
</Button>
<List
loading={loading}
pagination={paginationProps}
>
{
list && list.map(item => (
<List.Item
key={item.id}
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>
))
}
</List>
</Card>
</div>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.standardList {
:global {
.ant-list-pagination {
text-align: right;
}
}
.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;
}
& > em {
background-color: @border-color-split;
position: absolute;
height: 56px;
width: 1px;
top: 0;
right: 0;
}
}
.listContent {
margin-left: 24px;
font-size: 0;
& > div {
color: @text-color-secondary;
display: inline-block;
font-size: @font-size-base;
margin-left: 32px;
& > span {
line-height: 20px;
}
& > p {
margin-top: 4px;
line-height: 22px;
}
}
& > div:last-child {
position: relative;
top: -16px;
width: 188px;
}
}
.extraContentSearch {
margin-left: 16px;
width: 272px;
}
}
@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%;
}
}
}
}
@media screen and (max-width: @screen-lg) and (min-width: @screen-md) {
.standardList {
.listContent {
& > div {
display: block;
}
& > div:last-child {
top: 0;
width: 100%;
}
}
}
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Row, Col, Card, Avatar, Spin, Button, Icon } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import styles from './CardList.less';
@connect(state => ({
list: state.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>段落示意:蚂蚁金服务设计平台-design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,
提供跨越设计与开发的体验解决方案。</p>
<div className={styles.contentLink}>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/wUTAfuNZjhmCIxEPxQVY.svg" /> 快速开始
</a>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/qsmGbwvxTAjXfkkrZYov.svg" /> 产品简介
</a>
<a>
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/UGEHGuwlGDalIJlbsNxL.svg" /> 产品文档
</a>
</div>
</div>
);
const extraContent = (
<div className={styles.extraImg}>
<img alt="这是一个标题" src="https://gw.alipayobjects.com/zos/rmsportal/tTIFdlIcZFvvmCROhyBg.png" />
</div>
);
return (
<PageHeaderLayout
title="这是一个标题"
content={content}
extraContent={extraContent}
>
<div className={styles.cardList}>
{
loading ?
<Spin />
:
<Row gutter={16}>
<Col lg={8} md={12} sm={12} xs={24} style={{ marginBottom: 16 }}>
<Button type="dashed" className={styles.newButton}>
<Icon type="plus" /> 新增产品
</Button>
</Col>
{
list && list.map(item => (
<Col lg={8} md={12} sm={12} xs={24} key={item.id} style={{ marginBottom: 16 }}>
<Card
actions={[<a>操作一</a>, <a>操作二</a>]}
>
<Card.Meta
avatar={<Avatar size="large" src={item.avatar} />}
title={item.title}
description={(
<p className={styles.cardDescription}>
<span>{item.description}</span>
</p>
)}
/>
</Card>
</Col>
))
}
</Row>
}
</div>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.cardList {
:global {
.ant-card-meta-content {
margin-top: 0;
}
}
}
.extraImg {
margin-top: -60px;
text-align: center;
}
.newButton {
background-color: transparent;
border-color: @border-color-base;
color: @text-color-secondary;
width: 100%;
height: 178px;
&:hover {
background-color: transparent;
}
}
.cardDescription {
.textOverflowMulti();
}
.pageHeaderContent {
position: relative;
}
.contentLink {
margin-top: 16px;
a {
margin-right: 32px;
}
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-sm) {
.pageHeaderContent {
padding-bottom: 30px;
}
.contentLink {
position: absolute;
left: 0;
bottom: -4px;
width: 1000px;
a {
margin-right: 16px;
}
img {
margin-right: 4px;
}
}
}
import React, { PureComponent } from 'react';
import moment from 'moment';
import { connect } from 'dva';
import { Row, Col, Form, Card, Select, Spin } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import StandardFormRow from '../../components/StandardFormRow';
import TagSelect from '../../components/TagSelect';
import AvatarList from '../../components/AvatarList';
import SearchInput from '../../components/SearchInput';
import styles from './CoverCardList.less';
const Option = Select.Option;
const FormItem = Form.Item;
const TagOption = TagSelect.Option;
const TagExpand = TagSelect.Expand;
/* eslint react/no-array-index-key: 0 */
@Form.create()
@connect(state => ({
list: state.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 ? (
<Row gutter={16} style={{ marginTop: 16 }}>
{
list.map(item => (
<Col lg={6} md={8} sm={12} xs={24} style={{ marginBottom: 16 }} key={item.id}>
<Card
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta
title={item.title}
description={item.subDescription}
/>
<div className={styles.cardItemContent}>
<span>{moment(item.updatedAt).fromNow()}</span>
<div className={styles.avatarList}>
<AvatarList size="small">
{
item.members.map((member, i) => (
<AvatarList.Item
key={`${item.id}-avatar-${i}`}
src={member.avatar}
tips={member.name}
/>
))
}
</AvatarList>
</div>
</div>
</Card>
</Col>
))
}
</Row>
) : null;
const tabList = [
{
key: 'docs',
tab: '文章',
},
{
key: 'app',
tab: '应用',
},
{
key: 'project',
tab: '项目',
},
];
const pageHeaderContent = (
<div style={{ textAlign: 'center' }}>
<SearchInput onSearch={this.handleFormSubmit} />
</div>
);
const formItemLayout = {
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
return (
<PageHeaderLayout
title="带封面的卡片列表"
content={pageHeaderContent}
tabList={tabList}
>
<div className={styles.coverCardList}>
<Card
noHovering
>
<Form
layout="inline"
>
<StandardFormRow title="所属类目" block>
<FormItem>
{getFieldDecorator('category')(
<TagSelect onChange={this.handleFormSubmit}>
<TagOption value="cat1">类目一</TagOption>
<TagOption value="cat2">类目二</TagOption>
<TagOption value="cat3">类目三</TagOption>
<TagOption value="cat4">类目四</TagOption>
<TagExpand>
<TagOption value="cat5">类目五</TagOption>
<TagOption value="cat6">类目六</TagOption>
</TagExpand>
</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>
{
loading && (list.length > 0) && <Spin>
{cardList}
</Spin>
}
{
loading && (list.length < 1) && <div style={{ marginTop: 16 }}><Spin /></div>
}
{
!loading && cardList
}
</div>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.coverCardList {
:global {
.ant-card-meta-description {
font-size: 12px;
}
}
.cardItemContent {
display: flex;
margin-top: 8px;
line-height: 20px;
height: 20px;
span {
flex: 1;
font-size: 12px;
color: @disabled-color;
}
.avatarList {
flex: 0 1 auto;
}
}
}
import React, { PureComponent } from 'react';
import numeral from 'numeral';
import { connect } from 'dva';
import { Row, Col, Form, Card, Select, Spin, Icon, Avatar } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import StandardFormRow from '../../components/StandardFormRow';
import TagSelect from '../../components/TagSelect';
import SearchInput from '../../components/SearchInput';
import styles from './FilterCardList.less';
const Option = Select.Option;
const FormItem = Form.Item;
const TagOption = TagSelect.Option;
const TagExpand = TagSelect.Expand;
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(state => ({
list: state.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 tabList = [
{
key: 'docs',
tab: '文章',
},
{
key: 'apps',
tab: '应用',
default: true,
},
{
key: 'projects',
tab: '项目',
},
];
const CardInfo = ({ activeUser, newUser }) => (
<div className={styles.cardInfo}>
<div>
<p>活跃用户</p>
<p>{activeUser}</p>
<span />
</div>
<div>
<p>新增用户</p>
<p>{newUser}</p>
</div>
</div>
);
const pageHeaderContent = (
<div style={{ textAlign: 'center' }}>
<SearchInput onSearch={this.handleFormSubmit} />
</div>
);
const formItemLayout = {
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
return (
<PageHeaderLayout
title="带筛选卡片列表"
content={pageHeaderContent}
tabList={tabList}
>
<div className={styles.filterCardList}>
<Card
noHovering
>
<Form
layout="inline"
>
<StandardFormRow title="所属类目" block>
<FormItem>
{getFieldDecorator('category')(
<TagSelect onChange={this.handleFormSubmit}>
<TagOption value="cat1">类目一</TagOption>
<TagOption value="cat2">类目二</TagOption>
<TagOption value="cat3">类目三</TagOption>
<TagOption value="cat4">类目四</TagOption>
<TagExpand>
<TagOption value="cat5">类目五</TagOption>
<TagOption value="cat6">类目六</TagOption>
</TagExpand>
</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>
<Row gutter={16} style={{ marginTop: 16 }}>
{
loading && <Spin />
}
{
!loading && list && list.map(item => (
<Col lg={6} md={8} sm={12} xs={24} style={{ marginBottom: 16 }} key={item.id}>
<Card
actions={[<Icon type="copy" />, <Icon type="solution" />, <Icon type="setting" />, <Icon type="delete" />]}
>
<Card.Meta
avatar={<Avatar size="large" src={item.avatar} />}
title={item.title}
/>
<div className={styles.cardItemContent}>
<CardInfo
activeUser={formatWan(item.activeUser)}
newUser={numeral(item.newUser).format('0,0')}
/>
</div>
</Card>
</Col>
))
}
</Row>
</div>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.filterCardList {
:global {
.ant-card-meta-title {
position: relative;
top: 8px;
}
.ant-card-meta-content {
margin-top: 0;
}
}
.cardInfo {
.clearfix();
border: 1px solid @border-color-base;
border-radius: @border-radius-base;
padding: 8px 0;
margin-top: 16px;
width: 100%;
& > div {
position: relative;
text-align: center;
float: left;
width: 50%;
& > span {
background-color: @border-color-split;
position: absolute;
top: 0;
right: 0;
width: 1px;
height: 44px;
}
p {
color: @text-color-secondary;
line-height: 32px;
font-size: 24px;
}
p:first-child {
font-size: 12px;
line-height: 20px;
}
}
}
}
.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 } from 'antd';
import PageHeaderLayout from '../../layouts/PageHeaderLayout';
import StandardFormRow from '../../components/StandardFormRow';
import TagSelect from '../../components/TagSelect';
import SearchInput from '../../components/SearchInput';
import styles from './SearchList.less';
const Option = Select.Option;
const FormItem = Form.Item;
const TagOption = TagSelect.Option;
const TagExpand = TagSelect.Expand;
@Form.create()
@connect(state => ({
list: state.list,
}))
export default class SearchList extends Component {
state = {
count: 3,
showLoadMore: true,
loadingMore: false,
}
componentDidMount() {
const { count } = this.state;
this.props.dispatch({
type: 'list/fetch',
payload: {
count,
},
});
}
setOwner = () => {
const { form } = this.props;
form.setFieldsValue({
owner: ['wzj'],
});
}
handleLoadMore = () => {
const { count } = this.state;
const nextCount = count + 5;
this.setState({
count: nextCount,
loadingMore: true,
});
this.props.dispatch({
type: 'list/fetch',
payload: {
count: nextCount,
},
callback: () => {
this.setState({
loadingMore: false,
});
// fack count
if (nextCount < 10) {
this.setState({
showLoadMore: false,
});
}
},
});
}
render() {
const { showLoadMore, loadingMore } = this.state;
const { form, list: { list } } = this.props;
const { getFieldDecorator } = form;
const owners = [
{
id: 'wzj',
name: '我自己',
},
{
id: 'wjh',
name: '吴家豪',
},
{
id: 'zxx',
name: '周星星',
},
{
id: 'zly',
name: '赵丽颖',
},
{
id: 'ym',
name: '姚明',
},
];
const tabList = [
{
key: 'docs',
tab: '文章',
},
{
key: 'app',
tab: '应用',
},
{
key: 'project',
tab: '项目',
},
];
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}>
<p>{content}</p>
<div>
<Avatar src={avatar} size="small" />{owner} 发布在 <a href={href}>{href}</a>
<em>{moment(updatedAt).format('YYYY-MM-DD hh:mm')}</em>
</div>
</div>
);
const pageHeaderContent = (
<div style={{ textAlign: 'center' }}>
<SearchInput onSearch={this.handleFormSubmit} />
</div>
);
const formItemLayout = {
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
return (
<PageHeaderLayout
title="搜索列表"
content={pageHeaderContent}
tabList={tabList}
>
<div>
<Card noHovering>
<Form layout="inline">
<StandardFormRow title="所属类目" block>
<FormItem>
{getFieldDecorator('category')(
<TagSelect onChange={this.handleFormSubmit}>
<TagOption value="cat1">类目一</TagOption>
<TagOption value="cat2">类目二</TagOption>
<TagOption value="cat3">类目三</TagOption>
<TagOption value="cat4">类目四</TagOption>
<TagExpand>
<TagOption value="cat5">类目五</TagOption>
<TagOption value="cat6">类目六</TagOption>
</TagExpand>
</TagSelect>
)}
</FormItem>
</StandardFormRow>
<StandardFormRow
title="Owner"
grid
>
<Row>
<Col lg={16} md={16} sm={20} xs={20}>
<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 onClick={this.setOwner} style={{ marginLeft: 8 }}>只看自己的</a>
</FormItem>
</Col>
</Row>
</StandardFormRow>
<StandardFormRow
title="其它选项"
grid
last
>
<Row gutter={16}>
<Col lg={8} md={10} sm={10} 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 lg={8} md={10} sm={10} xs={24}>
<FormItem
{...formItemLayout}
label="好评度"
>
{getFieldDecorator('rate', {})(
<FormItem
label="好评度"
>
{getFieldDecorator('rate', {})(
<Select
onChange={this.handleFormSubmit}
placeholder="不限"
style={{ maxWidth: 200, width: '100%' }}
>
<Option value="good">优秀</Option>
</Select>
)}
</FormItem>
)}
</FormItem>
</Col>
</Row>
</StandardFormRow>
</Form>
</Card>
<Card style={{ marginTop: 16 }}>
<List
loadingMore={loadingMore}
showLoadMore={(list.length > 0) && showLoadMore}
onLoadMore={this.handleLoadMore}
itemLayout="vertical"
>
{
list && list.map(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 href={item.href}>{item.title}</a>}
description={<span><Tag>Ant Design</Tag><Tag>设计语言</Tag><Tag>蚂蚁金服</Tag></span>}
/>
<ListContent data={item} />
</List.Item>
))
}
</List>
</Card>
</div>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
@import "../../utils/utils.less";
.listContent {
p {
line-height: 24px;
}
& > div {
color: @text-color-secondary;
margin-top: 16px;
height: 22px;
line-height: 22px;
img {
margin-right: 16px;
}
& > span {
vertical-align: top;
margin-right: 16px;
width: 20px;
height: 20px;
}
& > em {
color: @disabled-color;
font-style: normal;
margin-left: 24px;
}
a {
color: @text-color-secondary;
&:hover {
color: @primary-color;
}
}
}
}
.listItemExtra {
width: 272px;
height: 1px;
}
@media screen and (max-width: @screen-lg) {
.listItemExtra {
width: 0;
height: 1px;
}
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Card, Row, Col, 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.Option;
const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
@connect(state => ({
rule: state.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();
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,
});
}
render() {
const { rule: { loading: ruleLoading, data }, form: { getFieldDecorator } } = this.props;
const { selectedRows, modalVisible, addInputValue } = this.state;
const formItemLayout = {
labelCol: { span: 5 },
wrapperCol: { span: 19 },
};
const menu = (
<Menu onClick={this.handleMenuClick} selectedKeys={[]}>
<Menu.Item key="remove">删除</Menu.Item>
{
selectedRows.length > 1 && <Menu.Item key="approval">批量审批</Menu.Item>
}
</Menu>
);
return (
<PageHeaderLayout title="标准表格">
<Card noHovering>
<div className={styles.tableList}>
<div className={styles.tableListForm}>
<Form onSubmit={this.handleSearch}>
<Row>
<Col md={8} sm={12} xs={24}>
<FormItem {...formItemLayout} label="规则编号">
{getFieldDecorator('no')(
<Input placeholder="请输入" />
)}
</FormItem>
</Col>
<Col md={8} sm={12} xs={24}>
<FormItem {...formItemLayout} label="状态">
{getFieldDecorator('status')(
<Select placeholder="请选择" style={{ width: '100%' }}>
<Option value="0">关闭</Option>
<Option value="1">运行中</Option>
</Select>
)}
</FormItem>
</Col>
<Col md={8} sm={12} xs={24} style={{ marginBottom: 24 }}>
<div className={styles.formButton}>
<Button type="primary" htmlType="submit">查询</Button>
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>重置</Button>
<a style={{ marginLeft: 8, fontSize: 12 }} onClick={this.toggleForm}>
{this.state.expandForm ? '收起' : '展开'} <Icon type={this.state.expandForm ? 'up' : 'down'} />
</a>
</div>
</Col>
</Row>
{
this.state.expandForm && <Row>
<Col md={8} sm={12} xs={24}>
<FormItem {...formItemLayout} label="更新时间">
{getFieldDecorator('updatedAt')(
<DatePicker style={{ width: '100%' }} />
)}
</FormItem>
</Col>
<Col md={8} sm={12} xs={24}>
<FormItem {...formItemLayout} label="调用次数">
{getFieldDecorator('callNo')(
<InputNumber
prefix={<Icon type="right" />}
placeholder="请输入"
/>
)}
</FormItem>
</Col>
</Row>
}
</Form>
</div>
<div className={styles.tableListOperator}>
<Button icon="plus" type="primary" onClick={() => this.handleModalVisible(true)}>新建</Button>
<Button>批量操作</Button>
<Dropdown overlay={menu}>
<Button>
更多操作 <Icon type="down" />
</Button>
</Dropdown>
</div>
<StandardTable
selectedRows={selectedRows}
loading={ruleLoading}
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;
}
}
}
.formButton {
margin-left: 40px;
position: relative;
top: 2px;
}
@media screen and (max-width: @screen-md) {
.formButton {
margin-left: 0;
}
}
import React, { Component } from 'react';
import { connect } from 'dva';
import { Button, Menu, Dropdown, Icon, Row, Col, Steps, Card, Popover, Badge, Table, Tooltip } from 'antd';
import PageHeaderLayout from '../layouts/PageHeaderLayout';
import DescriptionList from '../components/DescriptionList';
import styles from './Profile.less';
const { Step } = Steps;
const { 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>
<Button size="large" type="primary">主操作</Button>
<Button size="large">次操作</Button>
<Dropdown overlay={menu}>
<Button size="large">
更多 <Icon type="down" />
</Button>
</Dropdown>
</div>
);
const extra = (
<Row>
<Col span={12}>
<div className={styles.textSecondary}>状态</div>
<div className={styles.heading}>待审批</div>
</Col>
<Col span={12}>
<div className={styles.textSecondary}>订单金额</div>
<div className={styles.heading}>¥ 568.08</div>
</Col>
</Row>
);
const description = (
<DescriptionList col="2">
<Description term="创建人">曲丽丽</Description>
<Description term="关联单据"><a href="">12421</a></Description>
<Description term="创建时间">2017-07-07</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 style={{ fontSize: 14 }}>
<div style={{ marginTop: 4, marginBottom: 8 }}>
曲丽丽 <Icon type="dingding-o" />
</div>
<div>2016-12-12 12:32</div>
</div>
);
const desc2 = (
<div style={{ fontSize: 14 }}>
<div style={{ marginTop: 4, marginBottom: 8 }}>
周毛毛 <Icon type="dingding-o" style={{ color: '#00A0E9' }} />
</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>
<p className={styles.textSecondary} style={{ marginTop: 8 }} >耗时:2小时25分钟</p>
</div>
);
const customDot = (dot, { status }) => (status === 'process' ?
<Popover 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(state => ({
profile: state.profile,
}))
export default class Profile extends Component {
state = {
operationkey: 'tab1',
}
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'profile/fetch',
});
}
onOperationTabChange = (key) => {
this.setState({ operationkey: key });
}
render() {
const { profile } = this.props;
const { loading, operation1, operation2, operation3 } = profile;
const contentList = {
tab1: <Table
pagination={false}
loading={loading}
dataSource={operation1}
columns={columns}
/>,
tab2: <Table
pagination={false}
loading={loading}
dataSource={operation2}
columns={columns}
/>,
tab3: <Table
pagination={false}
loading={loading}
dataSource={operation3}
columns={columns}
/>,
};
return (
<PageHeaderLayout
title="单号:234231029431"
logo={<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/JcBAEvlHGhVvBekIJCWT.svg" />}
action={action}
content={description}
extraContent={extra}
tabList={tabList}
>
<Card noHovering title="流程进度" style={{ marginBottom: 24 }} bordered={false}>
<Steps progressDot={customDot} current={1}>
<Step title="创建项目" description={desc1} />
<Step title="部门初审" description={desc2} />
<Step title="财务复核" />
<Step title="完成" />
</Steps>
</Card>
<Card noHovering title="用户信息" style={{ marginBottom: 24 }} bordered={false}>
<DescriptionList style={{ marginBottom: 24 }}>
<Description term="用户姓名">付小小</Description>
<Description term="会员卡号">32943898021309809423</Description>
<Description term="身份证">3321944288191034921</Description>
<Description term="联系方式">18322193472</Description>
<Description term="联系地址">曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口</Description>
</DescriptionList>
<DescriptionList style={{ marginBottom: 24 }} title="信息组" col="2">
<Description term="某某数据">725</Description>
<Description term="该数据更新时间">2017-08-08</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>
<Card noHovering type="inner" title="多层级信息组">
<DescriptionList 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>
<div className={styles.divider} />
<DescriptionList style={{ marginBottom: 16 }} title="组名称" col="1">
<Description term="学名">
Citrullus lanatus (Thunb.) Matsum. et Nakai一年生蔓生藤本;茎、枝粗壮,具明显的棱。卷须较粗..
</Description>
</DescriptionList>
<div className={styles.divider} />
<DescriptionList title="组名称">
<Description term="负责人">付小小</Description>
<Description term="角色码">1234568</Description>
</DescriptionList>
</Card>
</Card>
<Card noHovering title="用户近半年来电记录" style={{ marginBottom: 24 }} bordered={false}>
<div className={styles.noData}>
<Icon type="frown-o" /> 暂无数据
</div>
</Card>
<Card
noHovering
className={styles.tabsCard}
bordered={false}
tabList={operationTabList}
onTabChange={this.onOperationTabChange}
>
{contentList[this.state.operationkey]}
</Card>
</PageHeaderLayout>
);
}
}
@import "~antd/lib/style/themes/default.less";
.tabsCard {
margin-bottom: 24px;
}
.noData {
color: @disabled-color;
text-align: center;
line-height: 64px;
}
.heading {
color: @heading-color;
font-size: 20px;
}
.textSecondary {
color: @text-color-secondary;
}
.divider {
border: 0;
border-top: 1px solid @border-color-split;
height: 1px;
margin: 0 0 24px 0;
}
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.65)', fontWeight: '600', marginBottom: 16 }}>
您提交的内容有如下错误:
</div>
<div style={{ marginBottom: 8 }}>
<Icon style={{ color: '#f04134', marginRight: 8 }} type="close-circle-o" />您的账户已被冻结
<a style={{ marginLeft: 24 }}>立即解冻 <Icon type="right" /></a>
</div>
<div>
<Icon style={{ color: '#f04134', marginRight: 8 }} type="close-circle-o" />您的账户还不具备申请资格
<a style={{ marginLeft: 24 }}>立即升级 <Icon type="right" /></a>
</div>
</div>
);
const actions = <Button size="large" type="primary">返回修改</Button>;
export default () => (
<PageHeaderLayout>
<Card bordered={false} noHovering>
<Result
type="error"
title="提交失败"
description="请核对并修改以下信息后,再重新提交。"
extra={extra}
actions={actions}
style={{ margin: '30px 120px' }}
/>
</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.Step;
const desc1 = (
<div style={{ fontSize: 14 }}>
<div style={{ margin: '4px 0' }}>
曲丽丽<Icon style={{ marginLeft: 8 }} type="dingding-o" />
</div>
<div>2016-12-12 12:32</div>
</div>
);
const desc2 = (
<div style={{ fontSize: 14 }}>
<div style={{ margin: '4px 0' }}>
周毛毛<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.65)', fontWeight: '600', marginBottom: 16 }}>
项目名称
</div>
<Row style={{ color: 'rgba(0, 0, 0, 0.65)', marginBottom: 20 }}>
<Col span={6}>项目 ID23421</Col>
<Col span={6}>负责人:曲丽丽</Col>
<Col span={12}>生效时间: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 size="large" type="primary">返回列表</Button>
<Button size="large">查看项目</Button>
<Button size="large"> </Button>
</div>
);
export default () => (
<PageHeaderLayout>
<Card bordered={false} noHovering>
<Result
type="success"
title="提交成功"
description="提交结果页用于反馈一系列操作任务的处理结果,
如果仅是简单操作,使用 Message 全局提示反馈即可。
本文字区域可以展示简单的补充说明,如果有类似展示
“单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。"
extra={extra}
actions={actions}
style={{ margin: '30px 120px' }}
/>
</Card>
</PageHeaderLayout>
);
import React, { Component } from 'react';
import { connect } from 'dva';
import { routerRedux, Link } from 'dva/router';
import { Form, Input, Tabs, Button, Icon, Checkbox, Row, Col, Alert } from 'antd';
import styles from './Login.less';
const FormItem = Form.Item;
const TabPane = Tabs.TabPane;
@connect(state => ({
login: state.login,
}))
@Form.create()
export default class Login extends Component {
state = {
count: 0,
type: 'account',
}
componentWillReceiveProps(nextProps) {
if (nextProps.login.status === 'ok') {
this.props.dispatch(routerRedux.push('/'));
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
onSwitch = (key) => {
this.setState({
type: key,
});
}
onGetCaptcha = () => {
let count = 59;
this.setState({ count });
this.interval = setInterval(() => {
count -= 1;
this.setState({ count });
if (count === 0) {
clearInterval(this.interval);
}
}, 1000);
}
handleSubmit = (e) => {
e.preventDefault();
const { type } = this.state;
this.props.form.validateFields({ force: true },
(err, values) => {
if (!err) {
this.props.dispatch({
type: `login/${type}Submit`,
payload: values,
});
}
}
);
}
msg = (message) => {
return <Alert style={{ marginBottom: 16 }} message={message} type="error" showIcon closable />;
}
render() {
const { form, login } = this.props;
const { getFieldDecorator } = form;
const { count, type } = this.state;
return (
<div className={styles.main}>
<Form onSubmit={this.handleSubmit}>
<Tabs activeKey={type} onChange={this.onSwitch}>
<TabPane tab="账户密码登录" key="account">
{login.status === 'error' && login.type === 'account' && this.msg('账户或密码错误')}
<FormItem>
{getFieldDecorator('userName', {
rules: [{
required: type === 'account', message: '请输入账户名!',
}],
})(
<Input
prefix={<Icon type="user" style={{ fontSize: 14, color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder="账户"
/>
)}
</FormItem>
<FormItem>
{getFieldDecorator('password', {
rules: [{
required: type === 'account', message: '请输入密码!',
}],
})(
<Input
prefix={<Icon type="lock" style={{ fontSize: 14, color: 'rgba(0, 0, 0, 0.25)' }} />}
type="password"
placeholder="密码"
/>
)}
</FormItem>
</TabPane>
<TabPane tab="手机号登录" key="mobile">
{login.status === 'error' && login.type === 'mobile' && this.msg('验证码错误')}
<FormItem>
{getFieldDecorator('mobile', {
rules: [{
required: type === 'mobile', message: '请输入手机号!',
}, {
pattern: /^1\d{10}$/, message: '手机号格式错误!',
}],
})(
<Input
prefix={<Icon type="mobile" style={{ fontSize: 14, color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder="手机号"
/>
)}
</FormItem>
<FormItem>
<Row gutter={8}>
<Col span={16}>
{getFieldDecorator('captcha', {
rules: [{
required: type === 'mobile', message: '请输入验证码!',
}],
})(
<Input
size="large"
prefix={<Icon type="mail" style={{ fontSize: 14, color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder="验证码"
/>
)}
</Col>
<Col span={8}>
<Button
disabled={count}
className={styles.getCaptcha}
size="large"
onClick={this.onGetCaptcha}
>
{count ? `${count} s` : '获取验证码'}
</Button>
</Col>
</Row>
</FormItem>
</TabPane>
</Tabs>
<FormItem className={styles.additional}>
{getFieldDecorator('remember', {
valuePropName: 'checked',
initialValue: true,
})(
<Checkbox>自动登录</Checkbox>
)}
<a className={styles.forgot} href="">忘记密码</a>
<Button loading={login.submitting} className={styles.submit} type="primary" htmlType="submit">
登录
</Button>
</FormItem>
</Form>
<div className={styles.other}>
其他登录方式
{/* 需要加到 Icon 中 */}
<span className={styles.iconAlipay} />
<span className={styles.iconTaobao} />
<span className={styles.iconWeibo} />
<Link className={styles.register} to="/user/register">注册账户</Link>
</div>
</div>
);
}
}
@import "~antd/lib/style/themes/default.less";
.main {
width: 368px;
margin: 0 auto;
:global {
.ant-tabs .ant-tabs-bar {
border-bottom: 0;
margin-bottom: 24px;
text-align: center;
}
.ant-form-item {
margin-bottom: 16px;
}
}
.getCaptcha {
display: block;
width: 100%;
}
.additional {
text-align: left;
.forgot {
float: right;
}
.submit {
width: 100%;
margin-top: 16px;
}
}
.iconAlipay, .iconTaobao, .iconWeibo {
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;
}
.iconAlipay {
background-position: -24px 0;
&:hover {
background-position: 0 0;
}
}
.iconTaobao {
background-position: -24px -24px;
&:hover {
background-position: 0 -24px;
}
}
.iconWeibo {
background-position: -24px -48px;
&:hover {
background-position: 0 -48px;
}
}
.other {
text-align: left;
margin-top: 32px;
.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.Option;
const InputGroup = Input.Group;
const passwordStatusMap = {
ok: <p className={styles.success}>强度:强</p>,
pass: <p className={styles.warning}>强度:中</p>,
pool: <p className={styles.error}>强度:太短</p>,
};
const passwordProgressMap = {
ok: 'success',
pass: 'normal',
pool: 'exception',
};
@connect(state => ({
register: state.register,
}))
@Form.create()
export default class Register extends Component {
state = {
count: 0,
confirmDirty: false,
visible: false,
help: '',
}
componentWillReceiveProps(nextProps) {
if (nextProps.register.status === 'ok') {
this.props.dispatch(routerRedux.push('/'));
}
}
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.form;
const value = form.getFieldValue('password');
if (value && value.length > 9) {
return 'ok';
}
if (value && value.length > 5) {
return 'pass';
}
return 'pool';
}
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields({ force: true },
(err, values) => {
if (!err) {
this.props.dispatch({
type: 'register/submit',
payload: values,
});
}
}
);
}
handleConfirmBlur = (e) => {
const value = e.target.value;
this.setState({ confirmDirty: this.state.confirmDirty || !!value });
}
checkConfirm = (rule, value, callback) => {
const form = this.props.form;
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.form;
if (value && this.state.confirmDirty) {
form.validateFields(['confirm'], { force: true });
}
callback();
}
}
}
renderPasswordProgress = () => {
const form = this.props.form;
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, register } = this.props;
const { getFieldDecorator } = form;
const { count } = this.state;
return (
<div className={styles.main}>
<h3>注册</h3>
<Form onSubmit={this.handleSubmit}>
<FormItem>
{getFieldDecorator('mail', {
rules: [{
required: true, message: '请输入邮箱地址!',
}, {
type: 'email', message: '邮箱地址格式错误!',
}],
})(
<Input placeholder="邮箱" />
)}
</FormItem>
<FormItem help={this.state.help}>
<Popover
content={
<div>
{passwordStatusMap[this.getPasswordStatus()]}
{this.renderPasswordProgress()}
<p style={{ marginTop: 10 }}>请至少输入 6 个字符。请不要使用容易被猜到的密码。</p>
</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
type="password"
placeholder="确认密码"
/>
)}
</FormItem>
<FormItem>
<InputGroup className={styles.mobileGroup} compact>
<FormItem>
{getFieldDecorator('prefix', {
initialValue: '86',
})(
<Select size="large">
<Option value="86">+86</Option>
<Option value="87">+87</Option>
</Select>
)}
</FormItem>
<FormItem>
{getFieldDecorator('mobile', {
rules: [{
required: true, message: '请输入手机号!',
}, {
pattern: /^1\d{10}$/, message: '手机号格式错误!',
}],
})(
<Input placeholder="11位手机号" />
)}
</FormItem>
</InputGroup>
</FormItem>
<FormItem>
<Row gutter={8}>
<Col span={16}>
{getFieldDecorator('captcha', {
rules: [{
required: true, message: '请输入验证码!',
}],
})(
<Input
size="large"
placeholder="验证码"
/>
)}
</Col>
<Col span={8}>
<Button
disabled={count}
className={styles.getCaptcha}
size="large"
onClick={this.onGetCaptcha}
>
{count ? `${count} s` : '获取验证码'}
</Button>
</Col>
</Row>
</FormItem>
<FormItem>
<Button loading={register.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: 16px;
}
}
h3 {
font-size: 16px;
margin-bottom: 16px;
}
.mobileGroup {
:global {
.ant-form-item {
margin-bottom: 0;
display: table-cell;
vertical-align: top;
&:first-child {
width: 20%;
.ant-select-selection {
border-right-width: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
&:last-child {
width: 80%;
.ant-input {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
}
}
.getCaptcha {
display: block;
width: 100%;
}
.submit {
width: 50%;
}
.login {
float: right;
}
}
.success, .warning, .error {
transition: color .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';
const actions = (
<div>
<Button size="large" type="primary"><a href="">查看邮箱</a></Button>
<Button size="large"><Link to="/">返回首页</Link></Button>
</div>
);
export default () => (
<div style={{ width: 520, margin: '0 auto' }}>
<Result
type="success"
title="你的账户:AntDesign@example.com 注册成功"
description="激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。"
actions={actions}
style={{ marginTop: 64 }}
/>
</div>
);
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 queryProfile() {
return request('/api/profile');
}
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 fakeMobileLogin(params) {
return request('/api/login/mobile', {
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 query() {
return request('/api/users');
}
export async function queryCurrent() {
return request('/api/currentUser');
}
import fetch from 'dva/fetch';
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
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 => response.json())
.catch(err => ({ err }));
}
import moment from 'moment';
function fixedZero(val) {
return val * 1 < 10 ? `0${val}` : val;
}
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(new Date(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).getTime() - 1000)];
}
if (type === 'year') {
const year = now.getFullYear();
return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)];
}
}
export default {
fixedZero,
getTimeDistance,
};
.textOverflow() {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.textOverflowMulti(@line: 3) {
overflow: hidden;
position: relative;
line-height: 1.5em;
max-height: @line * 1.5em;
text-align: justify;
margin-right: -1em;
padding-right: 1em;
&:before {
content: '...';
position: absolute;
right: 0;
bottom: 0;
}
&:after {
background: white;
content: '';
margin-top: 0.2em;
position: absolute;
right: 0;
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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment