173 changed files with 11798 additions and 0 deletions
@ -0,0 +1,16 @@ |
|||
# http://editorconfig.org |
|||
root = true |
|||
|
|||
[*] |
|||
indent_style = space |
|||
indent_size = 2 |
|||
end_of_line = lf |
|||
charset = utf-8 |
|||
trim_trailing_whitespace = true |
|||
insert_final_newline = true |
|||
|
|||
[*.md] |
|||
trim_trailing_whitespace = false |
|||
|
|||
[Makefile] |
|||
indent_style = tab |
|||
@ -0,0 +1,40 @@ |
|||
{ |
|||
"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 |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
|||
|
|||
# dependencies |
|||
/node_modules |
|||
|
|||
# production |
|||
/dist |
|||
|
|||
# misc |
|||
.DS_Store |
|||
npm-debug.log* |
|||
@ -0,0 +1,26 @@ |
|||
{ |
|||
"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" |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
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); |
|||
@ -0,0 +1,205 @@ |
|||
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, |
|||
}; |
|||
@ -0,0 +1,184 @@ |
|||
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, |
|||
}; |
|||
@ -0,0 +1,85 @@ |
|||
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: '待办', |
|||
}]); |
|||
}, |
|||
}; |
|||
@ -0,0 +1,74 @@ |
|||
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, |
|||
}; |
|||
@ -0,0 +1,128 @@ |
|||
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, |
|||
}; |
|||
@ -0,0 +1,45 @@ |
|||
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, |
|||
}; |
|||
@ -0,0 +1,47 @@ |
|||
{ |
|||
"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" |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
<!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> |
|||
|
After Width: | Height: | Size: 177 KiB |
@ -0,0 +1,202 @@ |
|||
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, |
|||
}]; |
|||
@ -0,0 +1,31 @@ |
|||
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> |
|||
); |
|||
@ -0,0 +1,41 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
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; |
|||
@ -0,0 +1,29 @@ |
|||
@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; |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
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; |
|||
@ -0,0 +1,34 @@ |
|||
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; |
|||
@ -0,0 +1,45 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
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; |
|||
@ -0,0 +1,17 @@ |
|||
@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; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,195 @@ |
|||
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; |
|||
@ -0,0 +1,29 @@ |
|||
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, |
|||
}; |
|||
@ -0,0 +1,95 @@ |
|||
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; |
|||
@ -0,0 +1,78 @@ |
|||
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; |
|||
@ -0,0 +1,27 @@ |
|||
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; |
|||
@ -0,0 +1,37 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,32 @@ |
|||
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> |
|||
); |
|||
@ -0,0 +1,46 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,224 @@ |
|||
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; |
|||
@ -0,0 +1,70 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
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; |
|||
@ -0,0 +1,38 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
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; |
|||
@ -0,0 +1,49 @@ |
|||
@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; |
|||
} |
|||
} |
|||
@ -0,0 +1,189 @@ |
|||
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; |
|||
@ -0,0 +1,25 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
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 => `¥ ${numeral(val).format('0,0')}`; |
|||
|
|||
export default { |
|||
IconUp, |
|||
IconDown, |
|||
yuan, |
|||
Bar, |
|||
Pie, |
|||
Gauge, |
|||
Radar, |
|||
MiniBar, |
|||
MiniArea, |
|||
MiniProgress, |
|||
ChartCard, |
|||
Trend, |
|||
Field, |
|||
NumberInfo, |
|||
WaterWave, |
|||
}; |
|||
@ -0,0 +1,9 @@ |
|||
.miniChart { |
|||
position: relative; |
|||
width: 100%; |
|||
& > div { |
|||
position: absolute; |
|||
bottom: -34px; |
|||
width: 100%; |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
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; |
|||
@ -0,0 +1,17 @@ |
|||
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; |
|||
@ -0,0 +1,18 @@ |
|||
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> |
|||
); |
|||
}; |
|||
@ -0,0 +1,35 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,35 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,5 @@ |
|||
import DescriptionList from './DescriptionList'; |
|||
import Description from './Description'; |
|||
|
|||
DescriptionList.Description = Description; |
|||
export default DescriptionList; |
|||
@ -0,0 +1,50 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
--- |
|||
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 | - | |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,6 @@ |
|||
export default { |
|||
1: { xs: 24 }, |
|||
2: { xs: 24, sm: 12 }, |
|||
3: { xs: 24, sm: 12, md: 8 }, |
|||
4: { xs: 24, sm: 12, md: 6 }, |
|||
}; |
|||
@ -0,0 +1,46 @@ |
|||
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; |
|||
@ -0,0 +1,29 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,14 @@ |
|||
--- |
|||
order: 0 |
|||
title: 404 |
|||
--- |
|||
|
|||
404 页面。 |
|||
|
|||
````jsx |
|||
import { Exception } from 'ant-design-pro'; |
|||
|
|||
ReactDOM.render( |
|||
<Exception type="404" /> |
|||
, mountNode); |
|||
```` |
|||
@ -0,0 +1,14 @@ |
|||
--- |
|||
order: 1 |
|||
title: 500 |
|||
--- |
|||
|
|||
500 页面。 |
|||
|
|||
````jsx |
|||
import { Exception } from 'ant-design-pro'; |
|||
|
|||
ReactDOM.render( |
|||
<Exception type="500" /> |
|||
, mountNode); |
|||
```` |
|||
@ -0,0 +1,26 @@ |
|||
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> |
|||
); |
|||
}; |
|||
@ -0,0 +1,37 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
--- |
|||
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 | - | |
|||
@ -0,0 +1,19 @@ |
|||
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; |
|||
@ -0,0 +1,44 @@ |
|||
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> |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
@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; |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
--- |
|||
category: Components |
|||
type: General |
|||
title: FooterToolbar |
|||
subtitle: 底部固定工具栏 |
|||
cols: 1 |
|||
--- |
|||
|
|||
## API |
|||
@ -0,0 +1,29 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,18 @@ |
|||
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> |
|||
); |
|||
}; |
|||
@ -0,0 +1,23 @@ |
|||
@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; |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
--- |
|||
category: Components |
|||
type: General |
|||
title: GlobalFooter |
|||
subtitle: 全局页脚 |
|||
cols: 1 |
|||
--- |
|||
|
|||
页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。 |
|||
|
|||
## API |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - | |
|||
| copyright | 版权信息 | ReactNode | - | |
|||
@ -0,0 +1,65 @@ |
|||
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> |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
.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; |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
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; |
|||
@ -0,0 +1,10 @@ |
|||
.mapChart { |
|||
background-color: #fff; |
|||
position: relative; |
|||
.canvas { |
|||
width: 100%; |
|||
& > img { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
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> |
|||
); |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
@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; |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
--- |
|||
order: 1 |
|||
title: 通知图标 |
|||
--- |
|||
|
|||
通常用在全局导航上。 |
|||
|
|||
````jsx |
|||
import { NoticeIcon } from 'ant-design-pro'; |
|||
|
|||
ReactDOM.render(<NoticeIcon count={5} />, mountNode); |
|||
```` |
|||
@ -0,0 +1,41 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,93 @@ |
|||
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> |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
--- |
|||
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 | - |
|||
@ -0,0 +1,71 @@ |
|||
--- |
|||
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> |
|||
@ -0,0 +1,26 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,81 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,67 @@ |
|||
--- |
|||
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> |
|||
@ -0,0 +1,98 @@ |
|||
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> |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,95 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
--- |
|||
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` 组件会自动获取。 |
|||
@ -0,0 +1,10 @@ |
|||
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>); |
|||
@ -0,0 +1,12 @@ |
|||
@import "~antd/lib/style/themes/default.less"; |
|||
@import "../../utils/utils.less"; |
|||
|
|||
.radioText { |
|||
display: inline; |
|||
:global { |
|||
.ant-radio-button-wrapper { |
|||
border: none; |
|||
padding: 0 12px; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,39 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,20 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -0,0 +1,21 @@ |
|||
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> |
|||
); |
|||
}; |
|||
@ -0,0 +1,45 @@ |
|||
@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; |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
--- |
|||
category: Components |
|||
type: General |
|||
title: Result |
|||
subtitle: 处理结果 |
|||
cols: 1 |
|||
--- |
|||
|
|||
结果页用于对用户进行的一系列任务处理结果进行反馈。 |
|||
|
|||
## API |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| type | 类型,不同类型自带对应的图标 | Enum {'success', 'error'} | - | |
|||
| title | 标题 | ReactNode | - | |
|||
| description | 结果描述 | ReactNode | - | |
|||
| extra | 补充信息,有默认的灰色背景 | ReactNode | - | |
|||
| actions | 操作建议,推荐放置跳转链接,按钮组等 | ReactNode | - | |
|||
@ -0,0 +1,15 @@ |
|||
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> |
|||
); |
|||
@ -0,0 +1,45 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
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> |
|||
); |
|||
}; |
|||
@ -0,0 +1,68 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,149 @@ |
|||
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> 项 |
|||
服务调用总计 <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; |
|||
@ -0,0 +1,22 @@ |
|||
@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; |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue