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;
}
}
}
This diff is collapsed.
@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;
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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