commit
55498a2a92
@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
'babel-plugin-module-resolver',
|
||||
{
|
||||
alias: {
|
||||
components: './src/components',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
@ -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,71 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": ["airbnb", "prettier"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"mocha": true,
|
||||
"jest": true,
|
||||
"jasmine": true
|
||||
},
|
||||
"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],
|
||||
"react/jsx-wrap-multilines": [
|
||||
"error",
|
||||
{
|
||||
"declaration": "parens-new-line",
|
||||
"assignment": "parens-new-line",
|
||||
"return": "parens-new-line",
|
||||
"arrow": "parens-new-line",
|
||||
"condition": "parens-new-line",
|
||||
"logical": "parens-new-line",
|
||||
"prop": "ignore"
|
||||
}
|
||||
],
|
||||
"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],
|
||||
"jsx-a11y/click-events-have-key-events": [0],
|
||||
"jsx-a11y/anchor-is-valid": [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"
|
||||
}
|
||||
],
|
||||
"object-curly-newline": [0],
|
||||
"function-paren-newline": [0],
|
||||
"no-restricted-globals": [0],
|
||||
"require-yield": [1]
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"polyfills": ["fetch", "promises"]
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
# roadhog-api-doc ignore
|
||||
/src/utils/request-temp.js
|
||||
_roadhog-api-doc
|
||||
|
||||
# production
|
||||
/dist
|
||||
/vscode
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
yarn-error.log
|
||||
|
||||
/coverage
|
||||
.idea
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
*bak
|
||||
jsconfig.json
|
@ -0,0 +1,3 @@
|
||||
**/*.md
|
||||
**/*.svg
|
||||
package.json
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".prettierrc",
|
||||
"options": { "parser": "json" }
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
import mockjs from 'mockjs';
|
||||
import { getRule, postRule } from './mock/rule';
|
||||
import { getActivities, getNotice, getFakeList } from './mock/api';
|
||||
import { getFakeChartData } from './mock/chart';
|
||||
import { getProfileBasicData } from './mock/profile';
|
||||
import { getProfileAdvancedData } from './mock/profile';
|
||||
import { getNotices } from './mock/notices';
|
||||
import { format, delay } from 'roadhog-api-doc';
|
||||
|
||||
// 是否禁用代理
|
||||
const noProxy = process.env.NO_PROXY === 'true';
|
||||
|
||||
// 代码中会兼容本地 service mock 以及部署站点的静态数据
|
||||
const proxy = {
|
||||
// 支持值为 Object 和 Array
|
||||
'GET /api/currentUser': {
|
||||
$desc: '获取当前用户接口',
|
||||
$params: {
|
||||
pageSize: {
|
||||
desc: '分页',
|
||||
exp: 2,
|
||||
},
|
||||
},
|
||||
$body: {
|
||||
name: 'Serati Ma',
|
||||
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
|
||||
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({ message: 'Ok' });
|
||||
},
|
||||
'GET /api/tags': mockjs.mock({
|
||||
'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }],
|
||||
}),
|
||||
'GET /api/fake_list': getFakeList,
|
||||
'GET /api/fake_chart_data': getFakeChartData,
|
||||
'GET /api/profile/basic': getProfileBasicData,
|
||||
'GET /api/profile/advanced': getProfileAdvancedData,
|
||||
'POST /api/login/account': (req, res) => {
|
||||
const { password, userName, type } = req.body;
|
||||
if (password === '888888' && userName === 'admin') {
|
||||
res.send({
|
||||
status: 'ok',
|
||||
type,
|
||||
currentAuthority: 'admin',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (password === '123456' && userName === 'user') {
|
||||
res.send({
|
||||
status: 'ok',
|
||||
type,
|
||||
currentAuthority: 'user',
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.send({
|
||||
status: 'error',
|
||||
type,
|
||||
currentAuthority: 'guest',
|
||||
});
|
||||
},
|
||||
'POST /api/register': (req, res) => {
|
||||
res.send({ status: 'ok', currentAuthority: 'user' });
|
||||
},
|
||||
'GET /api/notices': getNotices,
|
||||
'GET /api/500': (req, res) => {
|
||||
res.status(500).send({
|
||||
timestamp: 1513932555104,
|
||||
status: 500,
|
||||
error: 'error',
|
||||
message: 'error',
|
||||
path: '/base/category/list',
|
||||
});
|
||||
},
|
||||
'GET /api/404': (req, res) => {
|
||||
res.status(404).send({
|
||||
timestamp: 1513932643431,
|
||||
status: 404,
|
||||
error: 'Not Found',
|
||||
message: 'No message available',
|
||||
path: '/base/category/list/2121212',
|
||||
});
|
||||
},
|
||||
'GET /api/403': (req, res) => {
|
||||
res.status(403).send({
|
||||
timestamp: 1513932555104,
|
||||
status: 403,
|
||||
error: 'Unauthorized',
|
||||
message: 'Unauthorized',
|
||||
path: '/base/category/list',
|
||||
});
|
||||
},
|
||||
'GET /api/401': (req, res) => {
|
||||
res.status(401).send({
|
||||
timestamp: 1513932555104,
|
||||
status: 401,
|
||||
error: 'Unauthorized',
|
||||
message: 'Unauthorized',
|
||||
path: '/base/category/list',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default (noProxy ? {} : delay(proxy, 1000));
|
@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
|
||||
"rules": {
|
||||
"selector-pseudo-class-no-unknown": null,
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"at-rule-empty-line-before": null,
|
||||
"at-rule-name-space-after": null,
|
||||
"comment-empty-line-before": null,
|
||||
"declaration-bang-space-before": null,
|
||||
"declaration-empty-line-before": null,
|
||||
"function-comma-newline-after": null,
|
||||
"function-name-case": null,
|
||||
"function-parentheses-newline-inside": null,
|
||||
"function-max-empty-lines": null,
|
||||
"function-whitespace-after": null,
|
||||
"number-leading-zero": null,
|
||||
"number-no-trailing-zeros": null,
|
||||
"rule-empty-line-before": null,
|
||||
"selector-combinator-space-after": null,
|
||||
"selector-descendant-combinator-no-non-space": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
"selector-pseudo-element-colon-notation": null,
|
||||
"unit-no-unknown": null,
|
||||
"no-descending-specificity": null,
|
||||
"value-list-max-empty-lines": null
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "8"
|
||||
|
||||
env:
|
||||
matrix:
|
||||
- TEST_TYPE=lint
|
||||
- TEST_TYPE=build
|
||||
- TEST_TYPE=test-all
|
||||
- TEST_TYPE=test-dist
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- xvfb
|
||||
|
||||
install:
|
||||
- export DISPLAY=':99.0'
|
||||
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
- npm install
|
||||
|
||||
script:
|
||||
- |
|
||||
if [ "$TEST_TYPE" = lint ]; then
|
||||
npm run lint
|
||||
elif [ "$TEST_TYPE" = build ]; then
|
||||
npm run build
|
||||
elif [ "$TEST_TYPE" = test-all ]; then
|
||||
npm run test:all
|
||||
elif [ "$TEST_TYPE" = test-dist ]; then
|
||||
npm run site
|
||||
mv dist/* ./
|
||||
php -S localhost:8000 &
|
||||
DEBUG=* npm test .e2e.js
|
||||
fi
|
@ -0,0 +1,22 @@
|
||||
const path = require('path');
|
||||
|
||||
export default {
|
||||
entry: 'src/index.js',
|
||||
extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]],
|
||||
env: {
|
||||
development: {
|
||||
extraBabelPlugins: ['dva-hmr'],
|
||||
},
|
||||
},
|
||||
alias: {
|
||||
components: path.resolve(__dirname, 'src/components/'),
|
||||
},
|
||||
ignoreMomentLocale: true,
|
||||
theme: './src/theme.js',
|
||||
html: {
|
||||
template: './src/index.ejs',
|
||||
},
|
||||
disableDynamicImport: true,
|
||||
publicPath: '/',
|
||||
hash: true,
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at afc163@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Alipay.inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,26 @@
|
||||
# Test against the latest version of this Node.js version
|
||||
environment:
|
||||
nodejs_version: "8"
|
||||
|
||||
# this is how to allow failing jobs in the matrix
|
||||
matrix:
|
||||
fast_finish: true # set this flag to immediately finish build once one of the jobs fails.
|
||||
|
||||
# Install scripts. (runs after repo cloning)
|
||||
install:
|
||||
# Get the latest stable version of Node.js or io.js
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
# install modules
|
||||
- npm install
|
||||
# Output useful info for debugging.
|
||||
- node --version
|
||||
- npm --version
|
||||
|
||||
# Post-install test scripts.
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm run test:all
|
||||
- npm run build
|
||||
|
||||
# Don't actually build.
|
||||
build: off
|
@ -0,0 +1,197 @@
|
||||
import moment from 'moment';
|
||||
|
||||
// mock data
|
||||
const visitData = [];
|
||||
const beginDay = new Date().getTime();
|
||||
|
||||
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
|
||||
for (let i = 0; i < fakeY.length; i += 1) {
|
||||
visitData.push({
|
||||
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
|
||||
y: fakeY[i],
|
||||
});
|
||||
}
|
||||
|
||||
const visitData2 = [];
|
||||
const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
|
||||
for (let i = 0; i < fakeY2.length; i += 1) {
|
||||
visitData2.push({
|
||||
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
|
||||
y: fakeY2[i],
|
||||
});
|
||||
}
|
||||
|
||||
const salesData = [];
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
salesData.push({
|
||||
x: `${i + 1}月`,
|
||||
y: Math.floor(Math.random() * 1000) + 200,
|
||||
});
|
||||
}
|
||||
const searchData = [];
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
searchData.push({
|
||||
index: i + 1,
|
||||
keyword: `搜索关键词-${i}`,
|
||||
count: Math.floor(Math.random() * 1000),
|
||||
range: Math.floor(Math.random() * 100),
|
||||
status: Math.floor((Math.random() * 10) % 2),
|
||||
});
|
||||
}
|
||||
const salesTypeData = [
|
||||
{
|
||||
x: '家用电器',
|
||||
y: 4544,
|
||||
},
|
||||
{
|
||||
x: '食用酒水',
|
||||
y: 3321,
|
||||
},
|
||||
{
|
||||
x: '个护健康',
|
||||
y: 3113,
|
||||
},
|
||||
{
|
||||
x: '服饰箱包',
|
||||
y: 2341,
|
||||
},
|
||||
{
|
||||
x: '母婴产品',
|
||||
y: 1231,
|
||||
},
|
||||
{
|
||||
x: '其他',
|
||||
y: 1231,
|
||||
},
|
||||
];
|
||||
|
||||
const salesTypeDataOnline = [
|
||||
{
|
||||
x: '家用电器',
|
||||
y: 244,
|
||||
},
|
||||
{
|
||||
x: '食用酒水',
|
||||
y: 321,
|
||||
},
|
||||
{
|
||||
x: '个护健康',
|
||||
y: 311,
|
||||
},
|
||||
{
|
||||
x: '服饰箱包',
|
||||
y: 41,
|
||||
},
|
||||
{
|
||||
x: '母婴产品',
|
||||
y: 121,
|
||||
},
|
||||
{
|
||||
x: '其他',
|
||||
y: 111,
|
||||
},
|
||||
];
|
||||
|
||||
const salesTypeDataOffline = [
|
||||
{
|
||||
x: '家用电器',
|
||||
y: 99,
|
||||
},
|
||||
{
|
||||
x: '个护健康',
|
||||
y: 188,
|
||||
},
|
||||
{
|
||||
x: '服饰箱包',
|
||||
y: 344,
|
||||
},
|
||||
{
|
||||
x: '母婴产品',
|
||||
y: 255,
|
||||
},
|
||||
{
|
||||
x: '其他',
|
||||
y: 65,
|
||||
},
|
||||
];
|
||||
|
||||
const offlineData = [];
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
offlineData.push({
|
||||
name: `门店${i}`,
|
||||
cvr: Math.ceil(Math.random() * 9) / 10,
|
||||
});
|
||||
}
|
||||
const offlineChartData = [];
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
offlineChartData.push({
|
||||
x: new Date().getTime() + 1000 * 60 * 30 * i,
|
||||
y1: Math.floor(Math.random() * 100) + 10,
|
||||
y2: Math.floor(Math.random() * 100) + 10,
|
||||
});
|
||||
}
|
||||
|
||||
const radarOriginData = [
|
||||
{
|
||||
name: '个人',
|
||||
ref: 10,
|
||||
koubei: 8,
|
||||
output: 4,
|
||||
contribute: 5,
|
||||
hot: 7,
|
||||
},
|
||||
{
|
||||
name: '团队',
|
||||
ref: 3,
|
||||
koubei: 9,
|
||||
output: 6,
|
||||
contribute: 3,
|
||||
hot: 1,
|
||||
},
|
||||
{
|
||||
name: '部门',
|
||||
ref: 4,
|
||||
koubei: 1,
|
||||
output: 6,
|
||||
contribute: 5,
|
||||
hot: 7,
|
||||
},
|
||||
];
|
||||
|
||||
//
|
||||
const radarData = [];
|
||||
const radarTitleMap = {
|
||||
ref: '引用',
|
||||
koubei: '口碑',
|
||||
output: '产量',
|
||||
contribute: '贡献',
|
||||
hot: '热度',
|
||||
};
|
||||
radarOriginData.forEach(item => {
|
||||
Object.keys(item).forEach(key => {
|
||||
if (key !== 'name') {
|
||||
radarData.push({
|
||||
name: item.name,
|
||||
label: radarTitleMap[key],
|
||||
value: item[key],
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const getFakeChartData = {
|
||||
visitData,
|
||||
visitData2,
|
||||
salesData,
|
||||
searchData,
|
||||
offlineData,
|
||||
offlineChartData,
|
||||
salesTypeData,
|
||||
salesTypeDataOnline,
|
||||
salesTypeDataOffline,
|
||||
radarData,
|
||||
};
|
||||
|
||||
export default {
|
||||
getFakeChartData,
|
||||
};
|
@ -0,0 +1,158 @@
|
||||
const basicGoods = [
|
||||
{
|
||||
id: '1234561',
|
||||
name: '矿泉水 550ml',
|
||||
barcode: '12421432143214321',
|
||||
price: '2.00',
|
||||
num: '1',
|
||||
amount: '2.00',
|
||||
},
|
||||
{
|
||||
id: '1234562',
|
||||
name: '凉茶 300ml',
|
||||
barcode: '12421432143214322',
|
||||
price: '3.00',
|
||||
num: '2',
|
||||
amount: '6.00',
|
||||
},
|
||||
{
|
||||
id: '1234563',
|
||||
name: '好吃的薯片',
|
||||
barcode: '12421432143214323',
|
||||
price: '7.00',
|
||||
num: '4',
|
||||
amount: '28.00',
|
||||
},
|
||||
{
|
||||
id: '1234564',
|
||||
name: '特别好吃的蛋卷',
|
||||
barcode: '12421432143214324',
|
||||
price: '8.50',
|
||||
num: '3',
|
||||
amount: '25.50',
|
||||
},
|
||||
];
|
||||
|
||||
const basicProgress = [
|
||||
{
|
||||
key: '1',
|
||||
time: '2017-10-01 14:10',
|
||||
rate: '联系客户',
|
||||
status: 'processing',
|
||||
operator: '取货员 ID1234',
|
||||
cost: '5mins',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
time: '2017-10-01 14:05',
|
||||
rate: '取货员出发',
|
||||
status: 'success',
|
||||
operator: '取货员 ID1234',
|
||||
cost: '1h',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
time: '2017-10-01 13:05',
|
||||
rate: '取货员接单',
|
||||
status: 'success',
|
||||
operator: '取货员 ID1234',
|
||||
cost: '5mins',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
time: '2017-10-01 13:00',
|
||||
rate: '申请审批通过',
|
||||
status: 'success',
|
||||
operator: '系统',
|
||||
cost: '1h',
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
time: '2017-10-01 12:00',
|
||||
rate: '发起退货申请',
|
||||
status: 'success',
|
||||
operator: '用户',
|
||||
cost: '5mins',
|
||||
},
|
||||
];
|
||||
|
||||
const advancedOperation1 = [
|
||||
{
|
||||
key: 'op1',
|
||||
type: '订购关系生效',
|
||||
name: '曲丽丽',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
{
|
||||
key: 'op2',
|
||||
type: '财务复审',
|
||||
name: '付小小',
|
||||
status: 'reject',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '不通过原因',
|
||||
},
|
||||
{
|
||||
key: 'op3',
|
||||
type: '部门初审',
|
||||
name: '周毛毛',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
{
|
||||
key: 'op4',
|
||||
type: '提交订单',
|
||||
name: '林东东',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '很棒',
|
||||
},
|
||||
{
|
||||
key: 'op5',
|
||||
type: '创建订单',
|
||||
name: '汗牙牙',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
];
|
||||
|
||||
const advancedOperation2 = [
|
||||
{
|
||||
key: 'op1',
|
||||
type: '订购关系生效',
|
||||
name: '曲丽丽',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
];
|
||||
|
||||
const advancedOperation3 = [
|
||||
{
|
||||
key: 'op1',
|
||||
type: '创建订单',
|
||||
name: '汗牙牙',
|
||||
status: 'agree',
|
||||
updatedAt: '2017-10-03 19:23:12',
|
||||
memo: '-',
|
||||
},
|
||||
];
|
||||
|
||||
export const getProfileBasicData = {
|
||||
basicGoods,
|
||||
basicProgress,
|
||||
};
|
||||
|
||||
export const getProfileAdvancedData = {
|
||||
advancedOperation1,
|
||||
advancedOperation2,
|
||||
advancedOperation3,
|
||||
};
|
||||
|
||||
export default {
|
||||
getProfileBasicData,
|
||||
getProfileAdvancedData,
|
||||
};
|
@ -0,0 +1,137 @@
|
||||
import { parse } from 'url';
|
||||
|
||||
// mock tableListDataSource
|
||||
let tableListDataSource = [];
|
||||
for (let i = 0; i < 46; i += 1) {
|
||||
tableListDataSource.push({
|
||||
key: i,
|
||||
disabled: i % 6 === 0,
|
||||
href: 'https://ant.design',
|
||||
avatar: [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
||||
][i % 2],
|
||||
no: `TradeCode ${i}`,
|
||||
title: `一个任务名称 ${i}`,
|
||||
owner: '曲丽丽',
|
||||
description: '这是一段描述',
|
||||
callNo: Math.floor(Math.random() * 1000),
|
||||
status: Math.floor(Math.random() * 10) % 4,
|
||||
updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
|
||||
createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1}`),
|
||||
progress: Math.ceil(Math.random() * 100),
|
||||
});
|
||||
}
|
||||
|
||||
export function getRule(req, res, u) {
|
||||
let url = u;
|
||||
if (!url || Object.prototype.toString.call(url) !== '[object String]') {
|
||||
url = req.url; // eslint-disable-line
|
||||
}
|
||||
|
||||
const params = parse(url, true).query;
|
||||
|
||||
let dataSource = [...tableListDataSource];
|
||||
|
||||
if (params.sorter) {
|
||||
const s = params.sorter.split('_');
|
||||
dataSource = dataSource.sort((prev, next) => {
|
||||
if (s[1] === 'descend') {
|
||||
return next[s[0]] - prev[s[0]];
|
||||
}
|
||||
return prev[s[0]] - next[s[0]];
|
||||
});
|
||||
}
|
||||
|
||||
if (params.status) {
|
||||
const status = params.status.split(',');
|
||||
let filterDataSource = [];
|
||||
status.forEach(s => {
|
||||
filterDataSource = filterDataSource.concat(
|
||||
[...dataSource].filter(data => parseInt(data.status, 10) === parseInt(s[0], 10))
|
||||
);
|
||||
});
|
||||
dataSource = filterDataSource;
|
||||
}
|
||||
|
||||
if (params.no) {
|
||||
dataSource = dataSource.filter(data => data.no.indexOf(params.no) > -1);
|
||||
}
|
||||
|
||||
let pageSize = 10;
|
||||
if (params.pageSize) {
|
||||
pageSize = params.pageSize * 1;
|
||||
}
|
||||
|
||||
const result = {
|
||||
list: dataSource,
|
||||
pagination: {
|
||||
total: dataSource.length,
|
||||
pageSize,
|
||||
current: parseInt(params.currentPage, 10) || 1,
|
||||
},
|
||||
};
|
||||
|
||||
if (res && res.json) {
|
||||
res.json(result);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function postRule(req, res, u, b) {
|
||||
let url = u;
|
||||
if (!url || Object.prototype.toString.call(url) !== '[object String]') {
|
||||
url = req.url; // eslint-disable-line
|
||||
}
|
||||
|
||||
const body = (b && b.body) || req.body;
|
||||
const { method, no, description } = body;
|
||||
|
||||
switch (method) {
|
||||
/* eslint no-case-declarations:0 */
|
||||
case 'delete':
|
||||
tableListDataSource = tableListDataSource.filter(item => no.indexOf(item.no) === -1);
|
||||
break;
|
||||
case 'post':
|
||||
const i = Math.ceil(Math.random() * 10000);
|
||||
tableListDataSource.unshift({
|
||||
key: i,
|
||||
href: 'https://ant.design',
|
||||
avatar: [
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png',
|
||||
'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png',
|
||||
][i % 2],
|
||||
no: `TradeCode ${i}`,
|
||||
title: `一个任务名称 ${i}`,
|
||||
owner: '曲丽丽',
|
||||
description,
|
||||
callNo: Math.floor(Math.random() * 1000),
|
||||
status: Math.floor(Math.random() * 10) % 2,
|
||||
updatedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
progress: Math.ceil(Math.random() * 100),
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const result = {
|
||||
list: tableListDataSource,
|
||||
pagination: {
|
||||
total: tableListDataSource.length,
|
||||
},
|
||||
};
|
||||
|
||||
if (res && res.json) {
|
||||
res.json(result);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getRule,
|
||||
postRule,
|
||||
};
|
@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "ant-design-pro",
|
||||
"version": "1.3.0",
|
||||
"description": "An out-of-box UI solution for enterprise applications",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"precommit": "npm run lint-staged",
|
||||
"start": "cross-env ESLINT=none roadhog dev",
|
||||
"start:no-proxy": "cross-env NO_PROXY=true ESLINT=none roadhog dev",
|
||||
"build": "cross-env ESLINT=none roadhog build",
|
||||
"site": "roadhog-api-doc static && gh-pages -d dist",
|
||||
"analyze": "cross-env ANALYZE=true roadhog build",
|
||||
"lint:style": "stylelint \"src/**/*.less\" --syntax less",
|
||||
"lint": "eslint --ext .js src mock tests && npm run lint:style",
|
||||
"lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style",
|
||||
"lint-staged": "lint-staged",
|
||||
"lint-staged:js": "eslint --ext .js",
|
||||
"test": "roadhog test",
|
||||
"test:component": "roadhog test ./src/components",
|
||||
"test:all": "node ./tests/run-tests.js",
|
||||
"prettier": "prettier --write ./src/**/**/**/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@antv/data-set": "^0.8.0",
|
||||
"@babel/polyfill": "^7.0.0-beta.36",
|
||||
"antd": "^3.3.0",
|
||||
"babel-runtime": "^6.9.2",
|
||||
"bizcharts": "^3.1.3-beta.1",
|
||||
"bizcharts-plugin-slider": "^2.0.1",
|
||||
"classnames": "^2.2.5",
|
||||
"dva": "^2.1.0",
|
||||
"dva-loading": "^1.0.4",
|
||||
"enquire-js": "^0.2.1",
|
||||
"lodash": "^4.17.4",
|
||||
"lodash-decorators": "^4.4.1",
|
||||
"moment": "^2.19.1",
|
||||
"numeral": "^2.0.6",
|
||||
"omit.js": "^1.0.0",
|
||||
"path-to-regexp": "^2.1.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"qs": "^6.5.0",
|
||||
"rc-drawer-menu": "^0.5.0",
|
||||
"react": "^16.2.0",
|
||||
"react-container-query": "^0.9.1",
|
||||
"react-document-title": "^2.0.3",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-fittext": "^1.0.0",
|
||||
"rollbar": "^2.3.4",
|
||||
"url-polyfill": "^1.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^8.1.2",
|
||||
"babel-plugin-dva-hmr": "^0.4.1",
|
||||
"babel-plugin-import": "^1.6.7",
|
||||
"babel-plugin-module-resolver": "^3.1.1",
|
||||
"cross-env": "^5.1.1",
|
||||
"cross-port-killer": "^1.0.1",
|
||||
"enzyme": "^3.1.0",
|
||||
"eslint": "^4.14.0",
|
||||
"eslint-config-airbnb": "^16.0.0",
|
||||
"eslint-plugin-babel": "^4.0.0",
|
||||
"eslint-plugin-compat": "^2.1.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-markdown": "^1.0.0-beta.6",
|
||||
"eslint-plugin-react": "^7.0.1",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"gh-pages": "^1.0.0",
|
||||
"husky": "^0.14.3",
|
||||
"lint-staged": "^6.0.0",
|
||||
"mockjs": "^1.0.1-beta3",
|
||||
"prettier": "1.11.1",
|
||||
"pro-download": "^1.0.1",
|
||||
"redbox-react": "^1.5.0",
|
||||
"regenerator-runtime": "^0.11.1",
|
||||
"roadhog": "^2.3.0",
|
||||
"roadhog-api-doc": "^0.3.4",
|
||||
"stylelint": "^8.4.0",
|
||||
"stylelint-config-prettier": "^3.0.4",
|
||||
"stylelint-config-standard": "^18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"puppeteer": "^1.1.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.{js,jsx,less}": [
|
||||
"prettier --wirter",
|
||||
"git add"
|
||||
],
|
||||
"**/*.{js,jsx}": "lint-staged:js",
|
||||
"**/*.less": "stylelint --syntax less"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 10"
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 4.6 KiB |
@ -0,0 +1,177 @@
|
||||
import { isUrl } from '../utils/utils';
|
||||
|
||||
const menuData = [
|
||||
{
|
||||
name: 'dashboard',
|
||||
icon: 'dashboard',
|
||||
path: 'dashboard',
|
||||
children: [
|
||||
{
|
||||
name: '分析页',
|
||||
path: 'analysis',
|
||||
},
|
||||
{
|
||||
name: '监控页',
|
||||
path: 'monitor',
|
||||
},
|
||||
{
|
||||
name: '工作台',
|
||||
path: 'workplace',
|
||||
// hideInBreadcrumb: true,
|
||||
// hideInMenu: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '表单页',
|
||||
icon: 'form',
|
||||
path: 'form',
|
||||
children: [
|
||||
{
|
||||
name: '基础表单',
|
||||
path: 'basic-form',
|
||||
},
|
||||
{
|
||||
name: '分步表单',
|
||||
path: 'step-form',
|
||||
},
|
||||
{
|
||||
name: '高级表单',
|
||||
authority: 'admin',
|
||||
path: 'advanced-form',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '列表页',
|
||||
icon: 'table',
|
||||
path: 'list',
|
||||
children: [
|
||||
{
|
||||
name: '查询表格',
|
||||
path: 'table-list',
|
||||
},
|
||||
{
|
||||
name: '标准列表',
|
||||
path: 'basic-list',
|
||||
},
|
||||
{
|
||||
name: '卡片列表',
|
||||
path: 'card-list',
|
||||
},
|
||||
{
|
||||
name: '搜索列表',
|
||||
path: 'search',
|
||||
children: [
|
||||
{
|
||||
name: '搜索列表(文章)',
|
||||
path: 'articles',
|
||||
},
|
||||
{
|
||||
name: '搜索列表(项目)',
|
||||
path: 'projects',
|
||||
},
|
||||
{
|
||||
name: '搜索列表(应用)',
|
||||
path: 'applications',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '详情页',
|
||||
icon: 'profile',
|
||||
path: 'profile',
|
||||
children: [
|
||||
{
|
||||
name: '基础详情页',
|
||||
path: 'basic',
|
||||
},
|
||||
{
|
||||
name: '高级详情页',
|
||||
path: 'advanced',
|
||||
authority: 'admin',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '结果页',
|
||||
icon: 'check-circle-o',
|
||||
path: 'result',
|
||||
children: [
|
||||
{
|
||||
name: '成功',
|
||||
path: 'success',
|
||||
},
|
||||
{
|
||||
name: '失败',
|
||||
path: 'fail',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '异常页',
|
||||
icon: 'warning',
|
||||
path: 'exception',
|
||||
children: [
|
||||
{
|
||||
name: '403',
|
||||
path: '403',
|
||||
},
|
||||
{
|
||||
name: '404',
|
||||
path: '404',
|
||||
},
|
||||
{
|
||||
name: '500',
|
||||
path: '500',
|
||||
},
|
||||
{
|
||||
name: '触发异常',
|
||||
path: 'trigger',
|
||||
hideInMenu: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '账户',
|
||||
icon: 'user',
|
||||
path: 'user',
|
||||
authority: 'guest',
|
||||
children: [
|
||||
{
|
||||
name: '登录',
|
||||
path: 'login',
|
||||
},
|
||||
{
|
||||
name: '注册',
|
||||
path: 'register',
|
||||
},
|
||||
{
|
||||
name: '注册结果',
|
||||
path: 'register-result',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function formatter(data, parentPath = '/', parentAuthority) {
|
||||
return data.map(item => {
|
||||
let { path } = item;
|
||||
if (!isUrl(path)) {
|
||||
path = parentPath + item.path;
|
||||
}
|
||||
const result = {
|
||||
...item,
|
||||
path,
|
||||
authority: item.authority || parentAuthority,
|
||||
};
|
||||
if (item.children) {
|
||||
result.children = formatter(item.children, `${parentPath}${item.path}/`, item.authority);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
export const getMenuData = () => formatter(menuData);
|
@ -0,0 +1,205 @@
|
||||
import { createElement } from 'react';
|
||||
import dynamic from 'dva/dynamic';
|
||||
import pathToRegexp from 'path-to-regexp';
|
||||
import { getMenuData } from './menu';
|
||||
|
||||
let routerDataCache;
|
||||
|
||||
const modelNotExisted = (app, model) =>
|
||||
// eslint-disable-next-line
|
||||
!app._models.some(({ namespace }) => {
|
||||
return namespace === model.substring(model.lastIndexOf('/') + 1);
|
||||
});
|
||||
|
||||
// wrapper of dynamic
|
||||
const dynamicWrapper = (app, models, component) => {
|
||||
// () => require('module')
|
||||
// transformed by babel-plugin-dynamic-import-node-sync
|
||||
if (component.toString().indexOf('.then(') < 0) {
|
||||
models.forEach(model => {
|
||||
if (modelNotExisted(app, model)) {
|
||||
// eslint-disable-next-line
|
||||
app.model(require(`../models/${model}`).default);
|
||||
}
|
||||
});
|
||||
return props => {
|
||||
if (!routerDataCache) {
|
||||
routerDataCache = getRouterData(app);
|
||||
}
|
||||
return createElement(component().default, {
|
||||
...props,
|
||||
routerData: routerDataCache,
|
||||
});
|
||||
};
|
||||
}
|
||||
// () => import('module')
|
||||
return dynamic({
|
||||
app,
|
||||
models: () =>
|
||||
models.filter(model => modelNotExisted(app, model)).map(m => import(`../models/${m}.js`)),
|
||||
// add routerData prop
|
||||
component: () => {
|
||||
if (!routerDataCache) {
|
||||
routerDataCache = getRouterData(app);
|
||||
}
|
||||
return component().then(raw => {
|
||||
const Component = raw.default || raw;
|
||||
return props =>
|
||||
createElement(Component, {
|
||||
...props,
|
||||
routerData: routerDataCache,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function getFlatMenuData(menus) {
|
||||
let keys = {};
|
||||
menus.forEach(item => {
|
||||
if (item.children) {
|
||||
keys[item.path] = { ...item };
|
||||
keys = { ...keys, ...getFlatMenuData(item.children) };
|
||||
} else {
|
||||
keys[item.path] = { ...item };
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
export const getRouterData = app => {
|
||||
const routerConfig = {
|
||||
'/': {
|
||||
component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')),
|
||||
},
|
||||
'/dashboard/analysis': {
|
||||
component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')),
|
||||
},
|
||||
'/dashboard/monitor': {
|
||||
component: dynamicWrapper(app, ['monitor'], () => import('../routes/Dashboard/Monitor')),
|
||||
},
|
||||
'/dashboard/workplace': {
|
||||
component: dynamicWrapper(app, ['project', 'activities', 'chart'], () =>
|
||||
import('../routes/Dashboard/Workplace')
|
||||
),
|
||||
// hideInBreadcrumb: true,
|
||||
// name: '工作台',
|
||||
// authority: 'admin',
|
||||
},
|
||||
'/form/basic-form': {
|
||||
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/BasicForm')),
|
||||
},
|
||||
'/form/step-form': {
|
||||
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm')),
|
||||
},
|
||||
'/form/step-form/info': {
|
||||
name: '分步表单(填写转账信息)',
|
||||
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step1')),
|
||||
},
|
||||
'/form/step-form/confirm': {
|
||||
name: '分步表单(确认转账信息)',
|
||||
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step2')),
|
||||
},
|
||||
'/form/step-form/result': {
|
||||
name: '分步表单(完成)',
|
||||
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/StepForm/Step3')),
|
||||
},
|
||||
'/form/advanced-form': {
|
||||
component: dynamicWrapper(app, ['form'], () => import('../routes/Forms/AdvancedForm')),
|
||||
},
|
||||
'/list/table-list': {
|
||||
component: dynamicWrapper(app, ['rule'], () => import('../routes/List/TableList')),
|
||||
},
|
||||
'/list/basic-list': {
|
||||
component: dynamicWrapper(app, ['list'], () => import('../routes/List/BasicList')),
|
||||
},
|
||||
'/list/card-list': {
|
||||
component: dynamicWrapper(app, ['list'], () => import('../routes/List/CardList')),
|
||||
},
|
||||
'/list/search': {
|
||||
component: dynamicWrapper(app, ['list'], () => import('../routes/List/List')),
|
||||
},
|
||||
'/list/search/projects': {
|
||||
component: dynamicWrapper(app, ['list'], () => import('../routes/List/Projects')),
|
||||
},
|
||||
'/list/search/applications': {
|
||||
component: dynamicWrapper(app, ['list'], () => import('../routes/List/Applications')),
|
||||
},
|
||||
'/list/search/articles': {
|
||||
component: dynamicWrapper(app, ['list'], () => import('../routes/List/Articles')),
|
||||
},
|
||||
'/profile/basic': {
|
||||
component: dynamicWrapper(app, ['profile'], () => import('../routes/Profile/BasicProfile')),
|
||||
},
|
||||
'/profile/advanced': {
|
||||
component: dynamicWrapper(app, ['profile'], () =>
|
||||
import('../routes/Profile/AdvancedProfile')
|
||||
),
|
||||
},
|
||||
'/result/success': {
|
||||
component: dynamicWrapper(app, [], () => import('../routes/Result/Success')),
|
||||
},
|
||||
'/result/fail': {
|
||||
component: dynamicWrapper(app, [], () => import('../routes/Result/Error')),
|
||||
},
|
||||
'/exception/403': {
|
||||
component: dynamicWrapper(app, [], () => import('../routes/Exception/403')),
|
||||
},
|
||||
'/exception/404': {
|
||||
component: dynamicWrapper(app, [], () => import('../routes/Exception/404')),
|
||||
},
|
||||
'/exception/500': {
|
||||
component: dynamicWrapper(app, [], () => import('../routes/Exception/500')),
|
||||
},
|
||||
'/exception/trigger': {
|
||||
component: dynamicWrapper(app, ['error'], () =>
|
||||
import('../routes/Exception/triggerException')
|
||||
),
|
||||
},
|
||||
'/user': {
|
||||
component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')),
|
||||
},
|
||||
'/user/login': {
|
||||
component: dynamicWrapper(app, ['login'], () => import('../routes/User/Login')),
|
||||
},
|
||||
'/user/register': {
|
||||
component: dynamicWrapper(app, ['register'], () => import('../routes/User/Register')),
|
||||
},
|
||||
'/user/register-result': {
|
||||
component: dynamicWrapper(app, [], () => import('../routes/User/RegisterResult')),
|
||||
},
|
||||
// '/user/:id': {
|
||||
// component: dynamicWrapper(app, [], () => import('../routes/User/SomeComponent')),
|
||||
// },
|
||||
};
|
||||
// Get name from ./menu.js or just set it in the router data.
|
||||
const menuData = getFlatMenuData(getMenuData());
|
||||
|
||||
// Route configuration data
|
||||
// eg. {name,authority ...routerConfig }
|
||||
const routerData = {};
|
||||
// The route matches the menu
|
||||
Object.keys(routerConfig).forEach(path => {
|
||||
// Regular match item name
|
||||
// eg. router /user/:id === /user/chen
|
||||
const pathRegexp = pathToRegexp(path);
|
||||
const menuKey = Object.keys(menuData).find(key => pathRegexp.test(`${key}`));
|
||||
let menuItem = {};
|
||||
// If menuKey is not empty
|
||||
if (menuKey) {
|
||||
menuItem = menuData[menuKey];
|
||||
}
|
||||
let router = routerConfig[path];
|
||||
// If you need to configure complex parameter routing,
|
||||
// https://github.com/ant-design/ant-design-pro-site/blob/master/docs/router-and-nav.md#%E5%B8%A6%E5%8F%82%E6%95%B0%E7%9A%84%E8%B7%AF%E7%94%B1%E8%8F%9C%E5%8D%95
|
||||
// eg . /list/:type/user/info/:id
|
||||
router = {
|
||||
...router,
|
||||
name: router.name || menuItem.name,
|
||||
authority: router.authority || menuItem.authority,
|
||||
hideInBreadcrumb: router.hideInBreadcrumb || menuItem.hideInBreadcrumb,
|
||||
};
|
||||
routerData[path] = router;
|
||||
});
|
||||
return routerData;
|
||||
};
|
@ -0,0 +1,82 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { MiniArea } from '../Charts';
|
||||
import NumberInfo from '../NumberInfo';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
function fixedZero(val) {
|
||||
return val * 1 < 10 ? `0${val}` : val;
|
||||
}
|
||||
|
||||
function getActiveData() {
|
||||
const activeData = [];
|
||||
for (let i = 0; i < 24; i += 1) {
|
||||
activeData.push({
|
||||
x: `${fixedZero(i)}:00`,
|
||||
y: Math.floor(Math.random() * 200) + i * 50,
|
||||
});
|
||||
}
|
||||
return activeData;
|
||||
}
|
||||
|
||||
export default class ActiveChart extends Component {
|
||||
state = {
|
||||
activeData: getActiveData(),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.timer = setInterval(() => {
|
||||
this.setState({
|
||||
activeData: getActiveData(),
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activeData = [] } = this.state;
|
||||
|
||||
return (
|
||||
<div className={styles.activeChart}>
|
||||
<NumberInfo subTitle="目标评估" total="有望达到预期" />
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<MiniArea
|
||||
animate={false}
|
||||
line
|
||||
borderWidth={2}
|
||||
height={84}
|
||||
scale={{
|
||||
y: {
|
||||
tickCount: 3,
|
||||
},
|
||||
}}
|
||||
yAxis={{
|
||||
tickLine: false,
|
||||
label: false,
|
||||
title: false,
|
||||
line: false,
|
||||
}}
|
||||
data={activeData}
|
||||
/>
|
||||
</div>
|
||||
{activeData && (
|
||||
<div className={styles.activeChartGrid}>
|
||||
<p>{[...activeData].sort()[activeData.length - 1].y + 200} 亿元</p>
|
||||
<p>{[...activeData].sort()[Math.floor(activeData.length / 2)].y} 亿元</p>
|
||||
</div>
|
||||
)}
|
||||
{activeData && (
|
||||
<div className={styles.activeChartLegend}>
|
||||
<span>00:00</span>
|
||||
<span>{activeData[Math.floor(activeData.length / 2)].x}</span>
|
||||
<span>{activeData[activeData.length - 1].x}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
.activeChart {
|
||||
position: relative;
|
||||
}
|
||||
.activeChartGrid {
|
||||
p {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
}
|
||||
p:last-child {
|
||||
top: 115px;
|
||||
}
|
||||
}
|
||||
.activeChartLegend {
|
||||
position: relative;
|
||||
font-size: 0;
|
||||
margin-top: 8px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
width: 33.33%;
|
||||
}
|
||||
span:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
span:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import CheckPermissions from './CheckPermissions';
|
||||
|
||||
class Authorized extends React.Component {
|
||||
render() {
|
||||
const { children, authority, noMatch = null } = this.props;
|
||||
const childrenRender = typeof children === 'undefined' ? null : children;
|
||||
return CheckPermissions(authority, childrenRender, noMatch);
|
||||
}
|
||||
}
|
||||
|
||||
export default Authorized;
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Route, Redirect } from 'react-router-dom';
|
||||
import Authorized from './Authorized';
|
||||
|
||||
class AuthorizedRoute extends React.Component {
|
||||
render() {
|
||||
const { component: Component, render, authority, redirectPath, ...rest } = this.props;
|
||||
return (
|
||||
<Authorized
|
||||
authority={authority}
|
||||
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />}
|
||||
>
|
||||
<Route {...rest} render={props => (Component ? <Component {...props} /> : render(props))} />
|
||||
</Authorized>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthorizedRoute;
|
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import PromiseRender from './PromiseRender';
|
||||
import { CURRENT } from './index';
|
||||
|
||||
function isPromise(obj) {
|
||||
return (
|
||||
!!obj &&
|
||||
(typeof obj === 'object' || typeof obj === 'function') &&
|
||||
typeof obj.then === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用权限检查方法
|
||||
* Common check permissions method
|
||||
* @param { 权限判定 Permission judgment type string |array | Promise | Function } authority
|
||||
* @param { 你的权限 Your permission description type:string} currentAuthority
|
||||
* @param { 通过的组件 Passing components } target
|
||||
* @param { 未通过的组件 no pass components } Exception
|
||||
*/
|
||||
const checkPermissions = (authority, currentAuthority, target, Exception) => {
|
||||
// 没有判定权限.默认查看所有
|
||||
// Retirement authority, return target;
|
||||
if (!authority) {
|
||||
return target;
|
||||
}
|
||||
// 数组处理
|
||||
if (Array.isArray(authority)) {
|
||||
if (authority.indexOf(currentAuthority) >= 0) {
|
||||
return target;
|
||||
}
|
||||
return Exception;
|
||||
}
|
||||
|
||||
// string 处理
|
||||
if (typeof authority === 'string') {
|
||||
if (authority === currentAuthority) {
|
||||
return target;
|
||||
}
|
||||
return Exception;
|
||||
}
|
||||
|
||||
// Promise 处理
|
||||
if (isPromise(authority)) {
|
||||
return <PromiseRender ok={target} error={Exception} promise={authority} />;
|
||||
}
|
||||
|
||||
// Function 处理
|
||||
if (typeof authority === 'function') {
|
||||
try {
|
||||
const bool = authority(currentAuthority);
|
||||
if (bool) {
|
||||
return target;
|
||||
}
|
||||
return Exception;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
throw new Error('unsupported parameters');
|
||||
};
|
||||
|
||||
export { checkPermissions };
|
||||
|
||||
const check = (authority, target, Exception) => {
|
||||
return checkPermissions(authority, CURRENT, target, Exception);
|
||||
};
|
||||
|
||||
export default check;
|
@ -0,0 +1,37 @@
|
||||
import { checkPermissions } from './CheckPermissions.js';
|
||||
|
||||
const target = 'ok';
|
||||
const error = 'error';
|
||||
|
||||
describe('test CheckPermissions', () => {
|
||||
it('Correct string permission authentication', () => {
|
||||
expect(checkPermissions('user', 'user', target, error)).toEqual('ok');
|
||||
});
|
||||
it('Correct string permission authentication', () => {
|
||||
expect(checkPermissions('user', 'NULL', target, error)).toEqual('error');
|
||||
});
|
||||
it('authority is undefined , return ok', () => {
|
||||
expect(checkPermissions(null, 'NULL', target, error)).toEqual('ok');
|
||||
});
|
||||
it('currentAuthority is undefined , return error', () => {
|
||||
expect(checkPermissions('admin', null, target, error)).toEqual('error');
|
||||
});
|
||||
it('Wrong string permission authentication', () => {
|
||||
expect(checkPermissions('admin', 'user', target, error)).toEqual('error');
|
||||
});
|
||||
it('Correct Array permission authentication', () => {
|
||||
expect(checkPermissions(['user', 'admin'], 'user', target, error)).toEqual('ok');
|
||||
});
|
||||
it('Wrong Array permission authentication,currentAuthority error', () => {
|
||||
expect(checkPermissions(['user', 'admin'], 'user,admin', target, error)).toEqual('error');
|
||||
});
|
||||
it('Wrong Array permission authentication', () => {
|
||||
expect(checkPermissions(['user', 'admin'], 'guest', target, error)).toEqual('error');
|
||||
});
|
||||
it('Wrong Function permission authentication', () => {
|
||||
expect(checkPermissions(() => false, 'guest', target, error)).toEqual('error');
|
||||
});
|
||||
it('Correct Function permission authentication', () => {
|
||||
expect(checkPermissions(() => true, 'guest', target, error)).toEqual('ok');
|
||||
});
|
||||
});
|
@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { Spin } from 'antd';
|
||||
|
||||
export default class PromiseRender extends React.PureComponent {
|
||||
state = {
|
||||
component: null,
|
||||
};
|
||||
componentDidMount() {
|
||||
this.setRenderComponent(this.props);
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// new Props enter
|
||||
this.setRenderComponent(nextProps);
|
||||
}
|
||||
// set render Component : ok or error
|
||||
setRenderComponent(props) {
|
||||
const ok = this.checkIsInstantiation(props.ok);
|
||||
const error = this.checkIsInstantiation(props.error);
|
||||
props.promise
|
||||
.then(() => {
|
||||
this.setState({
|
||||
component: ok,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
component: error,
|
||||
});
|
||||
});
|
||||
}
|
||||
// Determine whether the incoming component has been instantiated
|
||||
// AuthorizedRoute is already instantiated
|
||||
// Authorized render is already instantiated, children is no instantiated
|
||||
// Secured is not instantiated
|
||||
checkIsInstantiation = target => {
|
||||
if (!React.isValidElement(target)) {
|
||||
return target;
|
||||
}
|
||||
return () => target;
|
||||
};
|
||||
render() {
|
||||
const Component = this.state.component;
|
||||
return Component ? (
|
||||
<Component {...this.props} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
margin: 'auto',
|
||||
paddingTop: 50,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import Exception from '../Exception/index';
|
||||
import CheckPermissions from './CheckPermissions';
|
||||
/**
|
||||
* 默认不能访问任何页面
|
||||
* default is "NULL"
|
||||
*/
|
||||
const Exception403 = () => <Exception type="403" style={{ minHeight: 500, height: '80%' }} />;
|
||||
|
||||
// Determine whether the incoming component has been instantiated
|
||||
// AuthorizedRoute is already instantiated
|
||||
// Authorized render is already instantiated, children is no instantiated
|
||||
// Secured is not instantiated
|
||||
const checkIsInstantiation = target => {
|
||||
if (!React.isValidElement(target)) {
|
||||
return target;
|
||||
}
|
||||
return () => target;
|
||||
};
|
||||
|
||||
/**
|
||||
* 用于判断是否拥有权限访问此view权限
|
||||
* authority 支持传入 string ,funtion:()=>boolean|Promise
|
||||
* e.g. 'user' 只有user用户能访问
|
||||
* e.g. 'user,admin' user和 admin 都能访问
|
||||
* e.g. ()=>boolean 返回true能访问,返回false不能访问
|
||||
* e.g. Promise then 能访问 catch不能访问
|
||||
* e.g. authority support incoming string, funtion: () => boolean | Promise
|
||||
* e.g. 'user' only user user can access
|
||||
* e.g. 'user, admin' user and admin can access
|
||||
* e.g. () => boolean true to be able to visit, return false can not be accessed
|
||||
* e.g. Promise then can not access the visit to catch
|
||||
* @param {string | function | Promise} authority
|
||||
* @param {ReactNode} error 非必需参数
|
||||
*/
|
||||
const authorize = (authority, error) => {
|
||||
/**
|
||||
* conversion into a class
|
||||
* 防止传入字符串时找不到staticContext造成报错
|
||||
* String parameters can cause staticContext not found error
|
||||
*/
|
||||
let classError = false;
|
||||
if (error) {
|
||||
classError = () => error;
|
||||
}
|
||||
if (!authority) {
|
||||
throw new Error('authority is required');
|
||||
}
|
||||
return function decideAuthority(targer) {
|
||||
const component = CheckPermissions(authority, targer, classError || Exception403);
|
||||
return checkIsInstantiation(component);
|
||||
};
|
||||
};
|
||||
|
||||
export default authorize;
|
@ -0,0 +1,23 @@
|
||||
---
|
||||
order: 1
|
||||
title:
|
||||
zh-CN: 使用数组作为参数
|
||||
en-US: Use Array as a parameter
|
||||
---
|
||||
|
||||
Use Array as a parameter
|
||||
|
||||
```jsx
|
||||
import RenderAuthorized from 'ant-design-pro/lib/Authorized';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
const Authorized = RenderAuthorized('user');
|
||||
const noMatch = <Alert message="No permission." type="error" showIcon />;
|
||||
|
||||
ReactDOM.render(
|
||||
<Authorized authority={['user', 'admin']} noMatch={noMatch}>
|
||||
<Alert message="Use Array as a parameter passed!" type="success" showIcon />
|
||||
</Authorized>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -0,0 +1,31 @@
|
||||
---
|
||||
order: 2
|
||||
title:
|
||||
zh-CN: 使用方法作为参数
|
||||
en-US: Use function as a parameter
|
||||
---
|
||||
|
||||
Use Function as a parameter
|
||||
|
||||
```jsx
|
||||
import RenderAuthorized from 'ant-design-pro/lib/Authorized';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
const Authorized = RenderAuthorized('user');
|
||||
const noMatch = <Alert message="No permission." type="error" showIcon />;
|
||||
|
||||
const havePermission = () => {
|
||||
return false;
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Authorized authority={havePermission} noMatch={noMatch}>
|
||||
<Alert
|
||||
message="Use Function as a parameter passed!"
|
||||
type="success"
|
||||
showIcon
|
||||
/>
|
||||
</Authorized>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -0,0 +1,25 @@
|
||||
---
|
||||
order: 0
|
||||
title:
|
||||
zh-CN: 基本使用
|
||||
en-US: Basic use
|
||||
---
|
||||
|
||||
Basic use
|
||||
|
||||
```jsx
|
||||
import RenderAuthorized from 'ant-design-pro/lib/Authorized';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
const Authorized = RenderAuthorized('user');
|
||||
const noMatch = <Alert message="No permission." type="error" showIcon />;
|
||||
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<Authorized authority="admin" noMatch={noMatch}>
|
||||
<Alert message="user Passed!" type="success" showIcon />
|
||||
</Authorized>
|
||||
</div>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -0,0 +1,28 @@
|
||||
---
|
||||
order: 3
|
||||
title:
|
||||
zh-CN: 注解基本使用
|
||||
en-US: Basic use secured
|
||||
---
|
||||
|
||||
secured demo used
|
||||
|
||||
```jsx
|
||||
import RenderAuthorized from 'ant-design-pro/lib/Authorized';
|
||||
import { Alert } from 'antd';
|
||||
|
||||
const { Secured } = RenderAuthorized('user');
|
||||
|
||||
@Secured('admin')
|
||||
class TestSecuredString extends React.Component {
|
||||
render() {
|
||||
<Alert message="user Passed!" type="success" showIcon />;
|
||||
}
|
||||
}
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<TestSecuredString />
|
||||
</div>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
import { RouteProps } from 'react-router';
|
||||
|
||||
type authorityFN = (currentAuthority?: string) => boolean;
|
||||
|
||||
type authority = string | Array<string> | authorityFN | Promise<any>;
|
||||
|
||||
export type IReactComponent<P = any> =
|
||||
| React.StatelessComponent<P>
|
||||
| React.ComponentClass<P>
|
||||
| React.ClassicComponentClass<P>;
|
||||
|
||||
interface Secured {
|
||||
(authority: authority, error?: React.ReactNode): <T extends IReactComponent>(target: T) => T;
|
||||
}
|
||||
|
||||
export interface AuthorizedRouteProps extends RouteProps {
|
||||
authority: authority;
|
||||
}
|
||||
export class AuthorizedRoute extends React.Component<AuthorizedRouteProps, any> {}
|
||||
|
||||
interface check {
|
||||
<T extends IReactComponent, S extends IReactComponent>(
|
||||
authority: authority,
|
||||
target: T,
|
||||
Exception: S
|
||||
): T | S;
|
||||
}
|
||||
|
||||
interface AuthorizedProps {
|
||||
authority: authority;
|
||||
noMatch?: React.ReactNode;
|
||||
}
|
||||
|
||||
export class Authorized extends React.Component<AuthorizedProps, any> {
|
||||
static Secured: Secured;
|
||||
static AuthorizedRoute: typeof AuthorizedRoute;
|
||||
static check: check;
|
||||
}
|
||||
|
||||
declare function renderAuthorize(currentAuthority: string): typeof Authorized;
|
||||
|
||||
export default renderAuthorize;
|
@ -0,0 +1,32 @@
|
||||
import Authorized from './Authorized';
|
||||
import AuthorizedRoute from './AuthorizedRoute';
|
||||
import Secured from './Secured';
|
||||
import check from './CheckPermissions.js';
|
||||
|
||||
/* eslint-disable import/no-mutable-exports */
|
||||
let CURRENT = 'NULL';
|
||||
|
||||
Authorized.Secured = Secured;
|
||||
Authorized.AuthorizedRoute = AuthorizedRoute;
|
||||
Authorized.check = check;
|
||||
|
||||
/**
|
||||
* use authority or getAuthority
|
||||
* @param {string|()=>String} currentAuthority
|
||||
*/
|
||||
const renderAuthorize = currentAuthority => {
|
||||
if (currentAuthority) {
|
||||
if (currentAuthority.constructor.name === 'Function') {
|
||||
CURRENT = currentAuthority();
|
||||
}
|
||||
if (currentAuthority.constructor.name === 'String') {
|
||||
CURRENT = currentAuthority;
|
||||
}
|
||||
} else {
|
||||
CURRENT = 'NULL';
|
||||
}
|
||||
return Authorized;
|
||||
};
|
||||
|
||||
export { CURRENT };
|
||||
export default renderAuthorize;
|
@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
export interface IAvatarItemProps {
|
||||
tips: React.ReactNode;
|
||||
src: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class AvatarItem extends React.Component<IAvatarItemProps, any> {
|
||||
constructor(props: IAvatarItemProps);
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
---
|
||||
order: 0
|
||||
title:
|
||||
zh-CN: 基础样例
|
||||
en-US: Basic Usage
|
||||
---
|
||||
|
||||
Simplest of usage.
|
||||
|
||||
````jsx
|
||||
import AvatarList from 'ant-design-pro/lib/AvatarList';
|
||||
|
||||
ReactDOM.render(
|
||||
<AvatarList size="mini">
|
||||
<AvatarList.Item tips="Jake" src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" />
|
||||
<AvatarList.Item tips="Andy" src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png" />
|
||||
<AvatarList.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" />
|
||||
</AvatarList>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import AvatarItem from './AvatarItem';
|
||||
|
||||
export interface IAvatarListProps {
|
||||
size?: 'large' | 'small' | 'mini' | 'default';
|
||||
style?: React.CSSProperties;
|
||||
children: React.ReactElement<AvatarItem> | Array<React.ReactElement<AvatarItem>>;
|
||||
}
|
||||
|
||||
export default class AvatarList extends React.Component<IAvatarListProps, any> {
|
||||
public static Item: typeof AvatarItem;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
---
|
||||
title: AvatarList
|
||||
order: 1
|
||||
cols: 1
|
||||
---
|
||||
|
||||
A list of user's avatar for project or group member list frequently. If a large or small AvatarList is desired, set the `size` property to either `large` or `small` and `mini` respectively. Omit the `size` property for a AvatarList with the default size.
|
||||
|
||||
## API
|
||||
|
||||
### AvatarList
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| size | size of list | `large`、`small` 、`mini`, `default` | `default` |
|
||||
|
||||
### AvatarList.Item
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| tips | title tips for avatar item | ReactNode\/string | - |
|
||||
| src | the address of the image for an image avatar | string | - |
|
@ -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',
|
||||
[styles.avatarItemMini]: size === 'mini',
|
||||
});
|
||||
|
||||
return (
|
||||
<li className={cls} onClick={onClick}>
|
||||
{tips ? (
|
||||
<Tooltip title={tips}>
|
||||
<Avatar src={src} size={size} style={{ cursor: 'pointer' }} />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Avatar src={src} size={size} />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
AvatarList.Item = Item;
|
||||
|
||||
export default AvatarList;
|
@ -0,0 +1,45 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.avatarList {
|
||||
display: inline-block;
|
||||
ul {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
font-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatarItem {
|
||||
display: inline-block;
|
||||
font-size: @font-size-base;
|
||||
margin-left: -8px;
|
||||
width: @avatar-size-base;
|
||||
height: @avatar-size-base;
|
||||
:global {
|
||||
.ant-avatar {
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatarItemLarge {
|
||||
width: @avatar-size-lg;
|
||||
height: @avatar-size-lg;
|
||||
}
|
||||
|
||||
.avatarItemSmall {
|
||||
width: @avatar-size-sm;
|
||||
height: @avatar-size-sm;
|
||||
}
|
||||
|
||||
.avatarItemMini {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
:global {
|
||||
.ant-avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: AvatarList
|
||||
subtitle: 用户头像列表
|
||||
order: 1
|
||||
cols: 1
|
||||
---
|
||||
|
||||
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。
|
||||
|
||||
## API
|
||||
|
||||
### AvatarList
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` |
|
||||
|
||||
### AvatarList.Item
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| tips | 头像展示文案 | ReactNode\/string | - |
|
||||
| src | 头像图片连接 | string | - |
|
@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
export interface IBarProps {
|
||||
title: React.ReactNode;
|
||||
color?: string;
|
||||
padding?: [number, number, number, number];
|
||||
height: number;
|
||||
data: Array<{
|
||||
x: string;
|
||||
y: number;
|
||||
}>;
|
||||
autoLabel?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class Bar extends React.Component<IBarProps, any> {}
|
@ -0,0 +1,113 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Chart, Axis, Tooltip, Geom } from 'bizcharts';
|
||||
import Debounce from 'lodash-decorators/debounce';
|
||||
import Bind from 'lodash-decorators/bind';
|
||||
import autoHeight from '../autoHeight';
|
||||
import styles from '../index.less';
|
||||
|
||||
@autoHeight()
|
||||
class Bar extends Component {
|
||||
state = {
|
||||
autoHideXLabels: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
@Bind()
|
||||
@Debounce(400)
|
||||
resize() {
|
||||
if (!this.node) {
|
||||
return;
|
||||
}
|
||||
const canvasWidth = this.node.parentNode.clientWidth;
|
||||
const { data = [], autoLabel = true } = this.props;
|
||||
if (!autoLabel) {
|
||||
return;
|
||||
}
|
||||
const minWidth = data.length * 30;
|
||||
const { autoHideXLabels } = this.state;
|
||||
|
||||
if (canvasWidth <= minWidth) {
|
||||
if (!autoHideXLabels) {
|
||||
this.setState({
|
||||
autoHideXLabels: true,
|
||||
});
|
||||
}
|
||||
} else if (autoHideXLabels) {
|
||||
this.setState({
|
||||
autoHideXLabels: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleRoot = n => {
|
||||
this.root = n;
|
||||
};
|
||||
|
||||
handleRef = n => {
|
||||
this.node = n;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
height,
|
||||
title,
|
||||
forceFit = true,
|
||||
data,
|
||||
color = 'rgba(24, 144, 255, 0.85)',
|
||||
padding,
|
||||
} = this.props;
|
||||
|
||||
const { autoHideXLabels } = this.state;
|
||||
|
||||
const scale = {
|
||||
x: {
|
||||
type: 'cat',
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const tooltip = [
|
||||
'x*y',
|
||||
(x, y) => ({
|
||||
name: x,
|
||||
value: y,
|
||||
}),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.chart} style={{ height }} ref={this.handleRoot}>
|
||||
<div ref={this.handleRef}>
|
||||
{title && <h4 style={{ marginBottom: 20 }}>{title}</h4>}
|
||||
<Chart
|
||||
scale={scale}
|
||||
height={title ? height - 41 : height}
|
||||
forceFit={forceFit}
|
||||
data={data}
|
||||
padding={padding || 'auto'}
|
||||
>
|
||||
<Axis
|
||||
name="x"
|
||||
title={false}
|
||||
label={autoHideXLabels ? false : {}}
|
||||
tickLine={autoHideXLabels ? false : {}}
|
||||
/>
|
||||
<Axis name="y" min={0} />
|
||||
<Tooltip showTitle={false} crosshairs={false} />
|
||||
<Geom type="interval" position="x*y" color={color} tooltip={tooltip} />
|
||||
</Chart>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Bar;
|
@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
export interface IChartCardProps {
|
||||
title: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
total?: React.ReactNode | function | number;
|
||||
footer?: React.ReactNode;
|
||||
contentHeight?: number;
|
||||
avatar?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class ChartCard extends React.Component<IChartCardProps, any> {}
|
@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { Card, Spin } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const renderTotal = total => {
|
||||
let totalDom;
|
||||
switch (typeof total) {
|
||||
case 'undefined':
|
||||
totalDom = null;
|
||||
break;
|
||||
case 'function':
|
||||
totalDom = <div className={styles.total}>{total()}</div>;
|
||||
break;
|
||||
default:
|
||||
totalDom = <div className={styles.total}>{total}</div>;
|
||||
}
|
||||
return totalDom;
|
||||
};
|
||||
|
||||
const ChartCard = ({
|
||||
loading = false,
|
||||
contentHeight,
|
||||
title,
|
||||
avatar,
|
||||
action,
|
||||
total,
|
||||
footer,
|
||||
children,
|
||||
...rest
|
||||
}) => {
|
||||
const content = (
|
||||
<div className={styles.chartCard}>
|
||||
<div
|
||||
className={classNames(styles.chartTop, {
|
||||
[styles.chartTopMargin]: !children && !footer,
|
||||
})}
|
||||
>
|
||||
<div className={styles.avatar}>{avatar}</div>
|
||||
<div className={styles.metaWrap}>
|
||||
<div className={styles.meta}>
|
||||
<span className={styles.title}>{title}</span>
|
||||
<span className={styles.action}>{action}</span>
|
||||
</div>
|
||||
{renderTotal(total)}
|
||||
</div>
|
||||
</div>
|
||||
{children && (
|
||||
<div className={styles.content} style={{ height: contentHeight || 'auto' }}>
|
||||
<div className={contentHeight && styles.contentFixed}>{children}</div>
|
||||
</div>
|
||||
)}
|
||||
{footer && (
|
||||
<div
|
||||
className={classNames(styles.footer, {
|
||||
[styles.footerMargin]: !children,
|
||||
})}
|
||||
>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card bodyStyle={{ padding: '20px 24px 8px 24px' }} {...rest}>
|
||||
{
|
||||
<Spin spinning={loading} wrapperClassName={styles.spin}>
|
||||
{content}
|
||||
</Spin>
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChartCard;
|
@ -0,0 +1,78 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.chartCard {
|
||||
position: relative;
|
||||
.chartTop {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.chartTopMargin {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.chartTopHasMargin {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.metaWrap {
|
||||
float: left;
|
||||
}
|
||||
.avatar {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
float: left;
|
||||
margin-right: 20px;
|
||||
img {
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
.meta {
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
line-height: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.action {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
.total {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
color: @heading-color;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
font-size: 30px;
|
||||
line-height: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
.content {
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.contentFixed {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.footer {
|
||||
border-top: 1px solid @border-color-split;
|
||||
padding-top: 9px;
|
||||
margin-top: 8px;
|
||||
& > * {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.footerMargin {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.spin :global(.ant-spin-container) {
|
||||
overflow: visible;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import * as React from 'react';
|
||||
export interface IFieldProps {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class Field extends React.Component<IFieldProps, any> {}
|
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const Field = ({ label, value, ...rest }) => (
|
||||
<div className={styles.field} {...rest}>
|
||||
<span>{label}</span>
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Field;
|
@ -0,0 +1,16 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.field {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
span {
|
||||
font-size: @font-size-base;
|
||||
line-height: 22px;
|
||||
}
|
||||
span:last-child {
|
||||
margin-left: 8px;
|
||||
color: @heading-color;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
export interface IGaugeProps {
|
||||
title: React.ReactNode;
|
||||
color?: string;
|
||||
height: number;
|
||||
bgColor?: number;
|
||||
percent: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class Gauge extends React.Component<IGaugeProps, any> {}
|
@ -0,0 +1,167 @@
|
||||
import React from 'react';
|
||||
import { Chart, Geom, Axis, Coord, Guide, Shape } from 'bizcharts';
|
||||
import autoHeight from '../autoHeight';
|
||||
|
||||
const { Arc, Html, Line } = Guide;
|
||||
|
||||
const defaultFormatter = val => {
|
||||
switch (val) {
|
||||
case '2':
|
||||
return '差';
|
||||
case '4':
|
||||
return '中';
|
||||
case '6':
|
||||
return '良';
|
||||
case '8':
|
||||
return '优';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
Shape.registerShape('point', 'pointer', {
|
||||
drawShape(cfg, group) {
|
||||
let point = cfg.points[0];
|
||||
point = this.parsePoint(point);
|
||||
const center = this.parsePoint({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
group.addShape('line', {
|
||||
attrs: {
|
||||
x1: center.x,
|
||||
y1: center.y,
|
||||
x2: point.x,
|
||||
y2: point.y,
|
||||
stroke: cfg.color,
|
||||
lineWidth: 2,
|
||||
lineCap: 'round',
|
||||
},
|
||||
});
|
||||
return group.addShape('circle', {
|
||||
attrs: {
|
||||
x: center.x,
|
||||
y: center.y,
|
||||
r: 6,
|
||||
stroke: cfg.color,
|
||||
lineWidth: 3,
|
||||
fill: '#fff',
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@autoHeight()
|
||||
export default class Gauge extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
height,
|
||||
percent,
|
||||
forceFit = true,
|
||||
formatter = defaultFormatter,
|
||||
color = '#2F9CFF',
|
||||
bgColor = '#F0F2F5',
|
||||
} = this.props;
|
||||
const cols = {
|
||||
value: {
|
||||
type: 'linear',
|
||||
min: 0,
|
||||
max: 10,
|
||||
tickCount: 6,
|
||||
nice: true,
|
||||
},
|
||||
};
|
||||
const data = [{ value: percent / 10 }];
|
||||
return (
|
||||
<Chart height={height} data={data} scale={cols} padding={[-16, 0, 16, 0]} forceFit={forceFit}>
|
||||
<Coord type="polar" startAngle={-1.25 * Math.PI} endAngle={0.25 * Math.PI} radius={0.8} />
|
||||
<Axis name="1" line={null} />
|
||||
<Axis
|
||||
line={null}
|
||||
tickLine={null}
|
||||
subTickLine={null}
|
||||
name="value"
|
||||
zIndex={2}
|
||||
gird={null}
|
||||
label={{
|
||||
offset: -12,
|
||||
formatter,
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
fill: 'rgba(0, 0, 0, 0.65)',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Guide>
|
||||
<Line
|
||||
start={[3, 0.905]}
|
||||
end={[3, 0.85]}
|
||||
lineStyle={{
|
||||
stroke: color,
|
||||
lineDash: null,
|
||||
lineWidth: 2,
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
start={[5, 0.905]}
|
||||
end={[5, 0.85]}
|
||||
lineStyle={{
|
||||
stroke: color,
|
||||
lineDash: null,
|
||||
lineWidth: 3,
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
start={[7, 0.905]}
|
||||
end={[7, 0.85]}
|
||||
lineStyle={{
|
||||
stroke: color,
|
||||
lineDash: null,
|
||||
lineWidth: 3,
|
||||
}}
|
||||
/>
|
||||
<Arc
|
||||
zIndex={0}
|
||||
start={[0, 0.965]}
|
||||
end={[10, 0.965]}
|
||||
style={{
|
||||
stroke: bgColor,
|
||||
lineWidth: 10,
|
||||
}}
|
||||
/>
|
||||
<Arc
|
||||
zIndex={1}
|
||||
start={[0, 0.965]}
|
||||
end={[data[0].value, 0.965]}
|
||||
style={{
|
||||
stroke: color,
|
||||
lineWidth: 10,
|
||||
}}
|
||||
/>
|
||||
<Html
|
||||
position={['50%', '95%']}
|
||||
html={() => {
|
||||
return `
|
||||
<div style="width: 300px;text-align: center;font-size: 12px!important;">
|
||||
<p style="font-size: 14px; color: rgba(0,0,0,0.43);margin: 0;">${title}</p>
|
||||
<p style="font-size: 24px;color: rgba(0,0,0,0.85);margin: 0;">
|
||||
${data[0].value * 10}%
|
||||
</p>
|
||||
</div>`;
|
||||
}}
|
||||
/>
|
||||
</Guide>
|
||||
<Geom
|
||||
line={false}
|
||||
type="point"
|
||||
position="value*1"
|
||||
shape="pointer"
|
||||
color={color}
|
||||
active={false}
|
||||
/>
|
||||
</Chart>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import * as React from 'react';
|
||||
|
||||
// g2已经更新到3.0
|
||||
// 不带的写了
|
||||
|
||||
export interface IAxis {
|
||||
title: any;
|
||||
line: any;
|
||||
gridAlign: any;
|
||||
labels: any;
|
||||
tickLine: any;
|
||||
grid: any;
|
||||
}
|
||||
|
||||
export interface IMiniAreaProps {
|
||||
color?: string;
|
||||
height: number;
|
||||
borderColor?: string;
|
||||
line?: boolean;
|
||||
animate?: boolean;
|
||||
xAxis?: IAxis;
|
||||
yAxis?: IAxis;
|
||||
data: Array<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default class MiniArea extends React.Component<IMiniAreaProps, any> {}
|
@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { Chart, Axis, Tooltip, Geom } from 'bizcharts';
|
||||
import autoHeight from '../autoHeight';
|
||||
import styles from '../index.less';
|
||||
|
||||
@autoHeight()
|
||||
export default class MiniArea extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
height,
|
||||
data = [],
|
||||
forceFit = true,
|
||||
color = 'rgba(24, 144, 255, 0.2)',
|
||||
borderColor = '#1089ff',
|
||||
scale = {},
|
||||
borderWidth = 2,
|
||||
line,
|
||||
xAxis,
|
||||
yAxis,
|
||||
animate = true,
|
||||
} = this.props;
|
||||
|
||||
const padding = [36, 5, 30, 5];
|
||||
|
||||
const scaleProps = {
|
||||
x: {
|
||||
type: 'cat',
|
||||
range: [0, 1],
|
||||
...scale.x,
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
...scale.y,
|
||||
},
|
||||
};
|
||||
|
||||
const tooltip = [
|
||||
'x*y',
|
||||
(x, y) => ({
|
||||
name: x,
|
||||
value: y,
|
||||
}),
|
||||
];
|
||||
|
||||
const chartHeight = height + 54;
|
||||
|
||||
return (
|
||||
<div className={styles.miniChart} style={{ height }}>
|
||||
<div className={styles.chartContent}>
|
||||
{height > 0 && (
|
||||
<Chart
|
||||
animate={animate}
|
||||
scale={scaleProps}
|
||||
height={chartHeight}
|
||||
forceFit={forceFit}
|
||||
data={data}
|
||||
padding={padding}
|
||||
>
|
||||
<Axis
|
||||
key="axis-x"
|
||||
name="x"
|
||||
label={false}
|
||||
line={false}
|
||||
tickLine={false}
|
||||
grid={false}
|
||||
{...xAxis}
|
||||
/>
|
||||
<Axis
|
||||
key="axis-y"
|
||||
name="y"
|
||||
label={false}
|
||||
line={false}
|
||||
tickLine={false}
|
||||
grid={false}
|
||||
{...yAxis}
|
||||
/>
|
||||
<Tooltip showTitle={false} crosshairs={false} />
|
||||
<Geom
|
||||
type="area"
|
||||
position="x*y"
|
||||
color={color}
|
||||
tooltip={tooltip}
|
||||
shape="smooth"
|
||||
style={{
|
||||
fillOpacity: 1,
|
||||
}}
|
||||
/>
|
||||
{line ? (
|
||||
<Geom
|
||||
type="line"
|
||||
position="x*y"
|
||||
shape="smooth"
|
||||
color={borderColor}
|
||||
size={borderWidth}
|
||||
tooltip={false}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ display: 'none' }} />
|
||||
)}
|
||||
</Chart>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
export interface IMiniBarProps {
|
||||
color?: string;
|
||||
height: number;
|
||||
data: Array<{
|
||||
x: number | string;
|
||||
y: number;
|
||||
}>;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class MiniBar extends React.Component<IMiniBarProps, any> {}
|
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { Chart, Tooltip, Geom } from 'bizcharts';
|
||||
import autoHeight from '../autoHeight';
|
||||
import styles from '../index.less';
|
||||
|
||||
@autoHeight()
|
||||
export default class MiniBar extends React.Component {
|
||||
render() {
|
||||
const { height, forceFit = true, color = '#1890FF', data = [] } = this.props;
|
||||
|
||||
const scale = {
|
||||
x: {
|
||||
type: 'cat',
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const padding = [36, 5, 30, 5];
|
||||
|
||||
const tooltip = [
|
||||
'x*y',
|
||||
(x, y) => ({
|
||||
name: x,
|
||||
value: y,
|
||||
}),
|
||||
];
|
||||
|
||||
// for tooltip not to be hide
|
||||
const chartHeight = height + 54;
|
||||
|
||||
return (
|
||||
<div className={styles.miniChart} style={{ height }}>
|
||||
<div className={styles.chartContent}>
|
||||
<Chart
|
||||
scale={scale}
|
||||
height={chartHeight}
|
||||
forceFit={forceFit}
|
||||
data={data}
|
||||
padding={padding}
|
||||
>
|
||||
<Tooltip showTitle={false} crosshairs={false} />
|
||||
<Geom type="interval" position="x*y" color={color} tooltip={tooltip} />
|
||||
</Chart>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
export interface IMiniProgressProps {
|
||||
target: number;
|
||||
color?: string;
|
||||
strokeWidth?: number;
|
||||
percent?: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class MiniProgress extends React.Component<IMiniProgressProps, any> {}
|
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => (
|
||||
<div className={styles.miniProgress}>
|
||||
<Tooltip title={`目标值: ${target}%`}>
|
||||
<div className={styles.target} style={{ left: target ? `${target}%` : null }}>
|
||||
<span style={{ backgroundColor: color || null }} />
|
||||
<span style={{ backgroundColor: color || null }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className={styles.progressWrap}>
|
||||
<div
|
||||
className={styles.progress}
|
||||
style={{
|
||||
backgroundColor: color || null,
|
||||
width: percent ? `${percent}%` : null,
|
||||
height: strokeWidth || null,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MiniProgress;
|
@ -0,0 +1,35 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.miniProgress {
|
||||
padding: 5px 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.progressWrap {
|
||||
background-color: @background-color-base;
|
||||
position: relative;
|
||||
}
|
||||
.progress {
|
||||
transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
|
||||
border-radius: 1px 0 0 1px;
|
||||
background-color: @primary-color;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.target {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
span {
|
||||
border-radius: 100px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
width: 2px;
|
||||
}
|
||||
span:last-child {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
export interface IPieProps {
|
||||
animate?: boolean;
|
||||
color?: string;
|
||||
height: number;
|
||||
hasLegend?: boolean;
|
||||
padding?: [number, number, number, number];
|
||||
percent?: number;
|
||||
data?: Array<{
|
||||
x: string | string;
|
||||
y: number;
|
||||
}>;
|
||||
total?: string | function;
|
||||
title?: React.ReactNode;
|
||||
tooltip?: boolean;
|
||||
valueFormat?: (value: string) => string | React.ReactNode;
|
||||
subTitle?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default class Pie extends React.Component<IPieProps, any> {}
|
@ -0,0 +1,255 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Chart, Tooltip, Geom, Coord } from 'bizcharts';
|
||||
import { DataView } from '@antv/data-set';
|
||||
import { Divider } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import ReactFitText from 'react-fittext';
|
||||
import Debounce from 'lodash-decorators/debounce';
|
||||
import Bind from 'lodash-decorators/bind';
|
||||
import autoHeight from '../autoHeight';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
/* eslint react/no-danger:0 */
|
||||
@autoHeight()
|
||||
export default class Pie extends Component {
|
||||
state = {
|
||||
legendData: [],
|
||||
legendBlock: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getLegendData();
|
||||
this.resize();
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.data !== nextProps.data) {
|
||||
// because of charts data create when rendered
|
||||
// so there is a trick for get rendered time
|
||||
this.setState(
|
||||
{
|
||||
legendData: [...this.state.legendData],
|
||||
},
|
||||
() => {
|
||||
this.getLegendData();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
this.resize.cancel();
|
||||
}
|
||||
|
||||
getG2Instance = chart => {
|
||||
this.chart = chart;
|
||||
};
|
||||
|
||||
// for custom lengend view
|
||||
getLegendData = () => {
|
||||
if (!this.chart) return;
|
||||
const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
|
||||
const items = geom.get('dataArray') || []; // 获取图形对应的
|
||||
|
||||
const legendData = items.map(item => {
|
||||
/* eslint no-underscore-dangle:0 */
|
||||
const origin = item[0]._origin;
|
||||
origin.color = item[0].color;
|
||||
origin.checked = true;
|
||||
return origin;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
legendData,
|
||||
});
|
||||
};
|
||||
|
||||
// for window resize auto responsive legend
|
||||
@Bind()
|
||||
@Debounce(300)
|
||||
resize() {
|
||||
const { hasLegend } = this.props;
|
||||
if (!hasLegend || !this.root) {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
return;
|
||||
}
|
||||
if (this.root.parentNode.clientWidth <= 380) {
|
||||
if (!this.state.legendBlock) {
|
||||
this.setState({
|
||||
legendBlock: true,
|
||||
});
|
||||
}
|
||||
} else if (this.state.legendBlock) {
|
||||
this.setState({
|
||||
legendBlock: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleRoot = n => {
|
||||
this.root = n;
|
||||
};
|
||||
|
||||
handleLegendClick = (item, i) => {
|
||||
const newItem = item;
|
||||
newItem.checked = !newItem.checked;
|
||||
|
||||
const { legendData } = this.state;
|
||||
legendData[i] = newItem;
|
||||
|
||||
const filteredLegendData = legendData.filter(l => l.checked).map(l => l.x);
|
||||
|
||||
if (this.chart) {
|
||||
this.chart.filter('x', val => filteredLegendData.indexOf(val) > -1);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
legendData,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
valueFormat,
|
||||
subTitle,
|
||||
total,
|
||||
hasLegend = false,
|
||||
className,
|
||||
style,
|
||||
height,
|
||||
forceFit = true,
|
||||
percent = 0,
|
||||
color,
|
||||
inner = 0.75,
|
||||
animate = true,
|
||||
colors,
|
||||
lineWidth = 1,
|
||||
} = this.props;
|
||||
|
||||
const { legendData, legendBlock } = this.state;
|
||||
const pieClassName = classNames(styles.pie, className, {
|
||||
[styles.hasLegend]: !!hasLegend,
|
||||
[styles.legendBlock]: legendBlock,
|
||||
});
|
||||
|
||||
const defaultColors = colors;
|
||||
let data = this.props.data || [];
|
||||
let selected = this.props.selected || true;
|
||||
let tooltip = this.props.tooltip || true;
|
||||
let formatColor;
|
||||
|
||||
const scale = {
|
||||
x: {
|
||||
type: 'cat',
|
||||
range: [0, 1],
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
};
|
||||
|
||||
if (percent) {
|
||||
selected = false;
|
||||
tooltip = false;
|
||||
formatColor = value => {
|
||||
if (value === '占比') {
|
||||
return color || 'rgba(24, 144, 255, 0.85)';
|
||||
} else {
|
||||
return '#F0F2F5';
|
||||
}
|
||||
};
|
||||
|
||||
data = [
|
||||
{
|
||||
x: '占比',
|
||||
y: parseFloat(percent),
|
||||
},
|
||||
{
|
||||
x: '反比',
|
||||
y: 100 - parseFloat(percent),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const tooltipFormat = [
|
||||
'x*percent',
|
||||
(x, p) => ({
|
||||
name: x,
|
||||
value: `${(p * 100).toFixed(2)}%`,
|
||||
}),
|
||||
];
|
||||
|
||||
const padding = [12, 0, 12, 0];
|
||||
|
||||
const dv = new DataView();
|
||||
dv.source(data).transform({
|
||||
type: 'percent',
|
||||
field: 'y',
|
||||
dimension: 'x',
|
||||
as: 'percent',
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={this.handleRoot} className={pieClassName} style={style}>
|
||||
<ReactFitText maxFontSize={25}>
|
||||
<div className={styles.chart}>
|
||||
<Chart
|
||||
scale={scale}
|
||||
height={height}
|
||||
forceFit={forceFit}
|
||||
data={dv}
|
||||
padding={padding}
|
||||
animate={animate}
|
||||
onGetG2Instance={this.getG2Instance}
|
||||
>
|
||||
{!!tooltip && <Tooltip showTitle={false} />}
|
||||
<Coord type="theta" innerRadius={inner} />
|
||||
<Geom
|
||||
style={{ lineWidth, stroke: '#fff' }}
|
||||
tooltip={tooltip && tooltipFormat}
|
||||
type="intervalStack"
|
||||
position="percent"
|
||||
color={['x', percent ? formatColor : defaultColors]}
|
||||
selected={selected}
|
||||
/>
|
||||
</Chart>
|
||||
|
||||
{(subTitle || total) && (
|
||||
<div className={styles.total}>
|
||||
{subTitle && <h4 className="pie-sub-title">{subTitle}</h4>}
|
||||
{/* eslint-disable-next-line */}
|
||||
{total && (
|
||||
<div className="pie-stat">{typeof total === 'function' ? total() : total}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ReactFitText>
|
||||
|
||||
{hasLegend && (
|
||||
<ul className={styles.legend}>
|
||||
{legendData.map((item, i) => (
|
||||
<li key={item.x} onClick={() => this.handleLegendClick(item, i)}>
|
||||
<span
|
||||
className={styles.dot}
|
||||
style={{
|
||||
backgroundColor: !item.checked ? '#aaa' : item.color,
|
||||
}}
|
||||
/>
|
||||
<span className={styles.legendTitle}>{item.x}</span>
|
||||
<Divider type="vertical" />
|
||||
<span className={styles.percent}>
|
||||
{`${(isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}
|
||||
</span>
|
||||
<span className={styles.value}>{valueFormat ? valueFormat(item.y) : item.y}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.pie {
|
||||
position: relative;
|
||||
.chart {
|
||||
position: relative;
|
||||
}
|
||||
&.hasLegend .chart {
|
||||
width: ~'calc(100% - 240px)';
|
||||
}
|
||||
.legend {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
min-width: 200px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin: 0 20px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
li {
|
||||
cursor: pointer;
|
||||
margin-bottom: 16px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dot {
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
.line {
|
||||
background-color: @border-color-split;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
}
|
||||
.legendTitle {
|
||||
color: @text-color;
|
||||
}
|
||||
.percent {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
.value {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.total {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
text-align: center;
|
||||
height: 62px;
|
||||
transform: translate(-50%, -50%);
|
||||
& > h4 {
|
||||
color: @text-color-secondary;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
height: 22px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
& > p {
|
||||
color: @heading-color;
|
||||
display: block;
|
||||
font-size: 1.2em;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.legendBlock {
|
||||
&.hasLegend .chart {
|
||||
width: 100%;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
.legend {
|
||||
position: relative;
|
||||
transform: none;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
export interface IRadarProps {
|
||||
title?: React.ReactNode;
|
||||
height: number;
|
||||
padding?: [number, number, number, number];
|
||||
hasLegend?: boolean;
|
||||
data: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}>;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class Radar extends React.Component<IRadarProps, any> {}
|
@ -0,0 +1,180 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Chart, Tooltip, Geom, Coord, Axis } from 'bizcharts';
|
||||
import { Row, Col } from 'antd';
|
||||
import autoHeight from '../autoHeight';
|
||||
import styles from './index.less';
|
||||
|
||||
/* eslint react/no-danger:0 */
|
||||
@autoHeight()
|
||||
export default class Radar extends Component {
|
||||
state = {
|
||||
legendData: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.getLengendData();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.data !== nextProps.data) {
|
||||
this.getLengendData();
|
||||
}
|
||||
}
|
||||
|
||||
getG2Instance = chart => {
|
||||
this.chart = chart;
|
||||
};
|
||||
|
||||
// for custom lengend view
|
||||
getLengendData = () => {
|
||||
if (!this.chart) return;
|
||||
const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
|
||||
const items = geom.get('dataArray') || []; // 获取图形对应的
|
||||
|
||||
const legendData = items.map(item => {
|
||||
// eslint-disable-next-line
|
||||
const origins = item.map(t => t._origin);
|
||||
const result = {
|
||||
name: origins[0].name,
|
||||
color: item[0].color,
|
||||
checked: true,
|
||||
value: origins.reduce((p, n) => p + n.value, 0),
|
||||
};
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
legendData,
|
||||
});
|
||||
};
|
||||
|
||||
handleRef = n => {
|
||||
this.node = n;
|
||||
};
|
||||
|
||||
handleLegendClick = (item, i) => {
|
||||
const newItem = item;
|
||||
newItem.checked = !newItem.checked;
|
||||
|
||||
const { legendData } = this.state;
|
||||
legendData[i] = newItem;
|
||||
|
||||
const filteredLegendData = legendData.filter(l => l.checked).map(l => l.name);
|
||||
|
||||
if (this.chart) {
|
||||
this.chart.filter('name', val => filteredLegendData.indexOf(val) > -1);
|
||||
this.chart.repaint();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
legendData,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const defaultColors = [
|
||||
'#1890FF',
|
||||
'#FACC14',
|
||||
'#2FC25B',
|
||||
'#8543E0',
|
||||
'#F04864',
|
||||
'#13C2C2',
|
||||
'#fa8c16',
|
||||
'#a0d911',
|
||||
];
|
||||
|
||||
const {
|
||||
data = [],
|
||||
height = 0,
|
||||
title,
|
||||
hasLegend = false,
|
||||
forceFit = true,
|
||||
tickCount = 4,
|
||||
padding = [35, 30, 16, 30],
|
||||
animate = true,
|
||||
colors = defaultColors,
|
||||
} = this.props;
|
||||
|
||||
const { legendData } = this.state;
|
||||
|
||||
const scale = {
|
||||
value: {
|
||||
min: 0,
|
||||
tickCount,
|
||||
},
|
||||
};
|
||||
|
||||
const chartHeight = height - (hasLegend ? 80 : 22);
|
||||
|
||||
return (
|
||||
<div className={styles.radar} style={{ height }}>
|
||||
{title && <h4>{title}</h4>}
|
||||
<Chart
|
||||
scale={scale}
|
||||
height={chartHeight}
|
||||
forceFit={forceFit}
|
||||
data={data}
|
||||
padding={padding}
|
||||
animate={animate}
|
||||
onGetG2Instance={this.getG2Instance}
|
||||
>
|
||||
<Tooltip />
|
||||
<Coord type="polar" />
|
||||
<Axis
|
||||
name="label"
|
||||
line={null}
|
||||
tickLine={null}
|
||||
grid={{
|
||||
lineStyle: {
|
||||
lineDash: null,
|
||||
},
|
||||
hideFirstLine: false,
|
||||
}}
|
||||
/>
|
||||
<Axis
|
||||
name="value"
|
||||
grid={{
|
||||
type: 'polygon',
|
||||
lineStyle: {
|
||||
lineDash: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Geom type="line" position="label*value" color={['name', colors]} size={1} />
|
||||
<Geom
|
||||
type="point"
|
||||
position="label*value"
|
||||
color={['name', colors]}
|
||||
shape="circle"
|
||||
size={3}
|
||||
/>
|
||||
</Chart>
|
||||
{hasLegend && (
|
||||
<Row className={styles.legend}>
|
||||
{legendData.map((item, i) => (
|
||||
<Col
|
||||
span={24 / legendData.length}
|
||||
key={item.name}
|
||||
onClick={() => this.handleLegendClick(item, i)}
|
||||
>
|
||||
<div className={styles.legendItem}>
|
||||
<p>
|
||||
<span
|
||||
className={styles.dot}
|
||||
style={{
|
||||
backgroundColor: !item.checked ? '#aaa' : item.color,
|
||||
}}
|
||||
/>
|
||||
<span>{item.name}</span>
|
||||
</p>
|
||||
<h6>{item.value}</h6>
|
||||
</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.radar {
|
||||
.legend {
|
||||
margin-top: 16px;
|
||||
.legendItem {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: @text-color-secondary;
|
||||
line-height: 22px;
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
h6 {
|
||||
color: @heading-color;
|
||||
padding-left: 16px;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:after {
|
||||
background-color: @border-color-split;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
width: 1px;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
> :last-child .legendItem:after {
|
||||
display: none;
|
||||
}
|
||||
.dot {
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
export interface ITagCloudProps {
|
||||
data: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
}>;
|
||||
height: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class TagCloud extends React.Component<ITagCloudProps, any> {}
|
@ -0,0 +1,164 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Chart, Geom, Coord, Shape } from 'bizcharts';
|
||||
import DataSet from '@antv/data-set';
|
||||
import Debounce from 'lodash-decorators/debounce';
|
||||
import Bind from 'lodash-decorators/bind';
|
||||
import classNames from 'classnames';
|
||||
import autoHeight from '../autoHeight';
|
||||
import styles from './index.less';
|
||||
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
/* eslint no-param-reassign: 0 */
|
||||
|
||||
const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png';
|
||||
|
||||
@autoHeight()
|
||||
class TagCloud extends Component {
|
||||
state = {
|
||||
dv: null,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.initTagCloud();
|
||||
this.renderChart();
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (JSON.stringify(nextProps.data) !== JSON.stringify(this.props.data)) {
|
||||
this.renderChart(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isUnmount = true;
|
||||
window.removeEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
resize = () => {
|
||||
this.renderChart();
|
||||
};
|
||||
|
||||
saveRootRef = node => {
|
||||
this.root = node;
|
||||
};
|
||||
|
||||
initTagCloud = () => {
|
||||
function getTextAttrs(cfg) {
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
fillOpacity: cfg.opacity,
|
||||
fontSize: cfg.origin._origin.size,
|
||||
rotate: cfg.origin._origin.rotate,
|
||||
text: cfg.origin._origin.text,
|
||||
textAlign: 'center',
|
||||
fontFamily: cfg.origin._origin.font,
|
||||
fill: cfg.color,
|
||||
textBaseline: 'Alphabetic',
|
||||
},
|
||||
cfg.style
|
||||
);
|
||||
}
|
||||
|
||||
// 给point注册一个词云的shape
|
||||
Shape.registerShape('point', 'cloud', {
|
||||
drawShape(cfg, container) {
|
||||
const attrs = getTextAttrs(cfg);
|
||||
return container.addShape('text', {
|
||||
attrs: Object.assign(attrs, {
|
||||
x: cfg.x,
|
||||
y: cfg.y,
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@Bind()
|
||||
@Debounce(500)
|
||||
renderChart(nextProps) {
|
||||
// const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
|
||||
const { data, height } = nextProps || this.props;
|
||||
|
||||
if (data.length < 1 || !this.root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const h = height * 4;
|
||||
const w = this.root.offsetWidth * 4;
|
||||
|
||||
const onload = () => {
|
||||
const dv = new DataSet.View().source(data);
|
||||
const range = dv.range('value');
|
||||
const [min, max] = range;
|
||||
dv.transform({
|
||||
type: 'tag-cloud',
|
||||
fields: ['name', 'value'],
|
||||
imageMask: this.imageMask,
|
||||
font: 'Verdana',
|
||||
size: [w, h], // 宽高设置最好根据 imageMask 做调整
|
||||
padding: 5,
|
||||
timeInterval: 5000, // max execute time
|
||||
rotate() {
|
||||
return 0;
|
||||
},
|
||||
fontSize(d) {
|
||||
// eslint-disable-next-line
|
||||
return Math.pow((d.value - min) / (max - min), 2) * (70 - 20) + 20;
|
||||
},
|
||||
});
|
||||
|
||||
if (this.isUnmount) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dv,
|
||||
w,
|
||||
h,
|
||||
});
|
||||
};
|
||||
|
||||
if (!this.imageMask) {
|
||||
this.imageMask = new Image();
|
||||
this.imageMask.crossOrigin = '';
|
||||
this.imageMask.src = imgUrl;
|
||||
|
||||
this.imageMask.onload = onload;
|
||||
} else {
|
||||
onload();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className, height } = this.props;
|
||||
const { dv, w, h } = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.tagCloud, className)}
|
||||
style={{ width: '100%', height }}
|
||||
ref={this.saveRootRef}
|
||||
>
|
||||
{dv && (
|
||||
<Chart
|
||||
width={w}
|
||||
height={h}
|
||||
data={dv}
|
||||
padding={0}
|
||||
scale={{
|
||||
x: { nice: false },
|
||||
y: { nice: false },
|
||||
}}
|
||||
>
|
||||
<Coord reflect="y" />
|
||||
<Geom type="point" position="x*y" color="text" shape="cloud" />
|
||||
</Chart>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TagCloud;
|
@ -0,0 +1,7 @@
|
||||
.tagCloud {
|
||||
overflow: hidden;
|
||||
canvas {
|
||||
transform: scale(0.25);
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
export interface ITimelineChartProps {
|
||||
data: Array<{
|
||||
x: string;
|
||||
y1: string;
|
||||
y2: string;
|
||||
}>;
|
||||
titleMap: { y1: string; y2: string };
|
||||
padding?: [number, number, number, number];
|
||||
height?: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class TimelineChart extends React.Component<ITimelineChartProps, any> {}
|
@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { Chart, Tooltip, Geom, Legend, Axis } from 'bizcharts';
|
||||
import DataSet from '@antv/data-set';
|
||||
import Slider from 'bizcharts-plugin-slider';
|
||||
import autoHeight from '../autoHeight';
|
||||
import styles from './index.less';
|
||||
|
||||
@autoHeight()
|
||||
export default class TimelineChart extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
height = 400,
|
||||
padding = [60, 20, 40, 40],
|
||||
titleMap = {
|
||||
y1: 'y1',
|
||||
y2: 'y2',
|
||||
},
|
||||
borderWidth = 2,
|
||||
data = [
|
||||
{
|
||||
x: 0,
|
||||
y1: 0,
|
||||
y2: 0,
|
||||
},
|
||||
],
|
||||
} = this.props;
|
||||
|
||||
data.sort((a, b) => a.x - b.x);
|
||||
|
||||
let max;
|
||||
if (data[0] && data[0].y1 && data[0].y2) {
|
||||
max = Math.max(
|
||||
[...data].sort((a, b) => b.y1 - a.y1)[0].y1,
|
||||
[...data].sort((a, b) => b.y2 - a.y2)[0].y2
|
||||
);
|
||||
}
|
||||
|
||||
const ds = new DataSet({
|
||||
state: {
|
||||
start: data[0].x,
|
||||
end: data[data.length - 1].x,
|
||||
},
|
||||
});
|
||||
|
||||
const dv = ds.createView();
|
||||
dv
|
||||
.source(data)
|
||||
.transform({
|
||||
type: 'filter',
|
||||
callback: obj => {
|
||||
const date = obj.x;
|
||||
return date <= ds.state.end && date >= ds.state.start;
|
||||
},
|
||||
})
|
||||
.transform({
|
||||
type: 'map',
|
||||
callback(row) {
|
||||
const newRow = { ...row };
|
||||
newRow[titleMap.y1] = row.y1;
|
||||
newRow[titleMap.y2] = row.y2;
|
||||
return newRow;
|
||||
},
|
||||
})
|
||||
.transform({
|
||||
type: 'fold',
|
||||
fields: [titleMap.y1, titleMap.y2], // 展开字段集
|
||||
key: 'key', // key字段
|
||||
value: 'value', // value字段
|
||||
});
|
||||
|
||||
const timeScale = {
|
||||
type: 'time',
|
||||
tickInterval: 60 * 60 * 1000,
|
||||
mask: 'HH:mm',
|
||||
range: [0, 1],
|
||||
};
|
||||
|
||||
const cols = {
|
||||
x: timeScale,
|
||||
value: {
|
||||
max,
|
||||
min: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const SliderGen = () => (
|
||||
<Slider
|
||||
padding={[0, padding[1] + 20, 0, padding[3]]}
|
||||
width="auto"
|
||||
height={26}
|
||||
xAxis="x"
|
||||
yAxis="y1"
|
||||
scales={{ x: timeScale }}
|
||||
data={data}
|
||||
start={ds.state.start}
|
||||
end={ds.state.end}
|
||||
backgroundChart={{ type: 'line' }}
|
||||
onChange={({ startValue, endValue }) => {
|
||||
ds.setState('start', startValue);
|
||||
ds.setState('end', endValue);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.timelineChart} style={{ height: height + 30 }}>
|
||||
<div>
|
||||
{title && <h4>{title}</h4>}
|
||||
<Chart height={height} padding={padding} data={dv} scale={cols} forceFit>
|
||||
<Axis name="x" />
|
||||
<Tooltip />
|
||||
<Legend name="key" position="top" />
|
||||
<Geom type="line" position="x*value" size={borderWidth} color="key" />
|
||||
</Chart>
|
||||
<div style={{ marginRight: -20 }}>
|
||||
<SliderGen />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
.timelineChart {
|
||||
background: #fff;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
export interface IWaterWaveProps {
|
||||
title: React.ReactNode;
|
||||
color?: string;
|
||||
height: number;
|
||||
percent: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export default class WaterWave extends React.Component<IWaterWaveProps, any> {}
|
@ -0,0 +1,197 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import autoHeight from '../autoHeight';
|
||||
import styles from './index.less';
|
||||
|
||||
/* eslint no-return-assign: 0 */
|
||||
/* eslint no-mixed-operators: 0 */
|
||||
// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
|
||||
|
||||
@autoHeight()
|
||||
export default class WaterWave extends PureComponent {
|
||||
state = {
|
||||
radio: 1,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.renderChart();
|
||||
this.resize();
|
||||
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
cancelAnimationFrame(this.timer);
|
||||
if (this.node) {
|
||||
this.node.innerHTML = '';
|
||||
}
|
||||
window.removeEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
resize = () => {
|
||||
const { height } = this.props;
|
||||
const { offsetWidth } = this.root.parentNode;
|
||||
this.setState({
|
||||
radio: offsetWidth < height ? offsetWidth / height : 1,
|
||||
});
|
||||
};
|
||||
|
||||
renderChart() {
|
||||
const { percent, color = '#1890FF' } = this.props;
|
||||
const data = percent / 100;
|
||||
const self = this;
|
||||
|
||||
if (!this.node || !data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = this.node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const canvasWidth = canvas.width;
|
||||
const canvasHeight = canvas.height;
|
||||
const radius = canvasWidth / 2;
|
||||
const lineWidth = 2;
|
||||
const cR = radius - lineWidth;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = lineWidth * 2;
|
||||
|
||||
const axisLength = canvasWidth - lineWidth;
|
||||
const unit = axisLength / 8;
|
||||
const range = 0.2; // 振幅
|
||||
let currRange = range;
|
||||
const xOffset = lineWidth;
|
||||
let sp = 0; // 周期偏移量
|
||||
let currData = 0;
|
||||
const waveupsp = 0.005; // 水波上涨速度
|
||||
|
||||
let arcStack = [];
|
||||
const bR = radius - lineWidth;
|
||||
const circleOffset = -(Math.PI / 2);
|
||||
let circleLock = true;
|
||||
|
||||
for (let i = circleOffset; i < circleOffset + 2 * Math.PI; i += 1 / (8 * Math.PI)) {
|
||||
arcStack.push([radius + bR * Math.cos(i), radius + bR * Math.sin(i)]);
|
||||
}
|
||||
|
||||
const cStartPoint = arcStack.shift();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.moveTo(cStartPoint[0], cStartPoint[1]);
|
||||
|
||||
function drawSin() {
|
||||
ctx.beginPath();
|
||||
ctx.save();
|
||||
|
||||
const sinStack = [];
|
||||
for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
|
||||
const x = sp + (xOffset + i) / unit;
|
||||
const y = Math.sin(x) * currRange;
|
||||
const dx = i;
|
||||
const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y;
|
||||
|
||||
ctx.lineTo(dx, dy);
|
||||
sinStack.push([dx, dy]);
|
||||
}
|
||||
|
||||
const startPoint = sinStack.shift();
|
||||
|
||||
ctx.lineTo(xOffset + axisLength, canvasHeight);
|
||||
ctx.lineTo(xOffset, canvasHeight);
|
||||
ctx.lineTo(startPoint[0], startPoint[1]);
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
|
||||
gradient.addColorStop(0, '#ffffff');
|
||||
gradient.addColorStop(1, '#1890FF');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function render() {
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
if (circleLock) {
|
||||
if (arcStack.length) {
|
||||
const temp = arcStack.shift();
|
||||
ctx.lineTo(temp[0], temp[1]);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
circleLock = false;
|
||||
ctx.lineTo(cStartPoint[0], cStartPoint[1]);
|
||||
ctx.stroke();
|
||||
arcStack = null;
|
||||
|
||||
ctx.globalCompositeOperation = 'destination-over';
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.save();
|
||||
ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, 1);
|
||||
|
||||
ctx.restore();
|
||||
ctx.clip();
|
||||
ctx.fillStyle = '#1890FF';
|
||||
}
|
||||
} else {
|
||||
if (data >= 0.85) {
|
||||
if (currRange > range / 4) {
|
||||
const t = range * 0.01;
|
||||
currRange -= t;
|
||||
}
|
||||
} else if (data <= 0.1) {
|
||||
if (currRange < range * 1.5) {
|
||||
const t = range * 0.01;
|
||||
currRange += t;
|
||||
}
|
||||
} else {
|
||||
if (currRange <= range) {
|
||||
const t = range * 0.01;
|
||||
currRange += t;
|
||||
}
|
||||
if (currRange >= range) {
|
||||
const t = range * 0.01;
|
||||
currRange -= t;
|
||||
}
|
||||
}
|
||||
if (data - currData > 0) {
|
||||
currData += waveupsp;
|
||||
}
|
||||
if (data - currData < 0) {
|
||||
currData -= waveupsp;
|
||||
}
|
||||
|
||||
sp += 0.07;
|
||||
drawSin();
|
||||
}
|
||||
self.timer = requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { radio } = this.state;
|
||||
const { percent, title, height } = this.props;
|
||||
return (
|
||||
<div
|
||||
className={styles.waterWave}
|
||||
ref={n => (this.root = n)}
|
||||
style={{ transform: `scale(${radio})` }}
|
||||
>
|
||||
<div style={{ width: height, height, overflow: 'hidden' }}>
|
||||
<canvas
|
||||
className={styles.waterWaveCanvasWrapper}
|
||||
ref={n => (this.node = n)}
|
||||
width={height * 2}
|
||||
height={height * 2}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.text} style={{ width: height }}>
|
||||
{title && <span>{title}</span>}
|
||||
<h4>{percent}%</h4>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.waterWave {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
transform-origin: left;
|
||||
.text {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 32px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
span {
|
||||
color: @text-color-secondary;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
h4 {
|
||||
color: @heading-color;
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
.waterWaveCanvasWrapper {
|
||||
transform: scale(0.5);
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/* eslint eqeqeq: 0 */
|
||||
import React from 'react';
|
||||
|
||||
function computeHeight(node) {
|
||||
const totalHeight = parseInt(getComputedStyle(node).height, 10);
|
||||
const padding =
|
||||
parseInt(getComputedStyle(node).paddingTop, 10) +
|
||||
parseInt(getComputedStyle(node).paddingBottom, 10);
|
||||
return totalHeight - padding;
|
||||
}
|
||||
|
||||
function getAutoHeight(n) {
|
||||
if (!n) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let node = n;
|
||||
|
||||
let height = computeHeight(node);
|
||||
|
||||
while (!height) {
|
||||
node = node.parentNode;
|
||||
if (node) {
|
||||
height = computeHeight(node);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
const autoHeight = () => WrappedComponent => {
|
||||
return class extends React.Component {
|
||||
state = {
|
||||
computedHeight: 0,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { height } = this.props;
|
||||
if (!height) {
|
||||
const h = getAutoHeight(this.root);
|
||||
// eslint-disable-next-line
|
||||
this.setState({ computedHeight: h });
|
||||
}
|
||||
}
|
||||
|
||||
handleRoot = node => {
|
||||
this.root = node;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { height } = this.props;
|
||||
const { computedHeight } = this.state;
|
||||
const h = height || computedHeight;
|
||||
return (
|
||||
<div ref={this.handleRoot}>{h > 0 && <WrappedComponent {...this.props} height={h} />}</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default autoHeight;
|
@ -0,0 +1,95 @@
|
||||
---
|
||||
order: 1
|
||||
title: 图表卡片
|
||||
---
|
||||
|
||||
用于展示图表的卡片容器,可以方便的配合其它图表套件展示丰富信息。
|
||||
|
||||
```jsx
|
||||
import { ChartCard, yuan, Field } from 'ant-design-pro/lib/Charts';
|
||||
import Trend from 'ant-design-pro/lib/Trend';
|
||||
import { Row, Col, Icon, Tooltip } from 'antd';
|
||||
import numeral from 'numeral';
|
||||
|
||||
ReactDOM.render(
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<ChartCard
|
||||
title="销售额"
|
||||
action={
|
||||
<Tooltip title="指标说明">
|
||||
<Icon type="info-circle-o" />
|
||||
</Tooltip>
|
||||
}
|
||||
total={() => (
|
||||
<span dangerouslySetInnerHTML={{ __html: yuan(126560) }} />
|
||||
)}
|
||||
footer={
|
||||
<Field label="日均销售额" value={numeral(12423).format("0,0")} />
|
||||
}
|
||||
contentHeight={46}
|
||||
>
|
||||
<span>
|
||||
周同比
|
||||
<Trend flag="up" style={{ marginLeft: 8, color: "rgba(0,0,0,.85)" }}>
|
||||
12%
|
||||
</Trend>
|
||||
</span>
|
||||
<span style={{ marginLeft: 16 }}>
|
||||
日环比
|
||||
<Trend
|
||||
flag="down"
|
||||
style={{ marginLeft: 8, color: "rgba(0,0,0,.85)" }}
|
||||
>
|
||||
11%
|
||||
</Trend>
|
||||
</span>
|
||||
</ChartCard>
|
||||
</Col>
|
||||
<Col span={24} style={{ marginTop: 24 }}>
|
||||
<ChartCard
|
||||
title="移动指标"
|
||||
avatar={
|
||||
<img
|
||||
style={{ width: 56, height: 56 }}
|
||||
src="https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png"
|
||||
alt="indicator"
|
||||
/>
|
||||
}
|
||||
action={
|
||||
<Tooltip title="指标说明">
|
||||
<Icon type="info-circle-o" />
|
||||
</Tooltip>
|
||||
}
|
||||
total={() => (
|
||||
<span dangerouslySetInnerHTML={{ __html: yuan(126560) }} />
|
||||
)}
|
||||
footer={
|
||||
<Field label="日均销售额" value={numeral(12423).format("0,0")} />
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={24} style={{ marginTop: 24 }}>
|
||||
<ChartCard
|
||||
title="移动指标"
|
||||
avatar={
|
||||
<img
|
||||
alt="indicator"
|
||||
style={{ width: 56, height: 56 }}
|
||||
src="https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png"
|
||||
/>
|
||||
}
|
||||
action={
|
||||
<Tooltip title="指标说明">
|
||||
<Icon type="info-circle-o" />
|
||||
</Tooltip>
|
||||
}
|
||||
total={() => (
|
||||
<span dangerouslySetInnerHTML={{ __html: yuan(126560) }} />
|
||||
)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -0,0 +1,18 @@
|
||||
---
|
||||
order: 7
|
||||
title: 仪表盘
|
||||
---
|
||||
|
||||
仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。
|
||||
|
||||
````jsx
|
||||
import { Gauge } from 'ant-design-pro/lib/Charts';
|
||||
|
||||
ReactDOM.render(
|
||||
<Gauge
|
||||
title="核销率"
|
||||
height={164}
|
||||
percent={87}
|
||||
/>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,28 @@
|
||||
---
|
||||
order: 2
|
||||
col: 2
|
||||
title: 迷你区域图
|
||||
---
|
||||
|
||||
````jsx
|
||||
import { MiniArea } from 'ant-design-pro/lib/Charts';
|
||||
import moment from 'moment';
|
||||
|
||||
const visitData = [];
|
||||
const beginDay = new Date().getTime();
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
visitData.push({
|
||||
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
|
||||
y: Math.floor(Math.random() * 100) + 10,
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<MiniArea
|
||||
line
|
||||
color="#cceafe"
|
||||
height={45}
|
||||
data={visitData}
|
||||
/>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,28 @@
|
||||
---
|
||||
order: 2
|
||||
col: 2
|
||||
title: 迷你柱状图
|
||||
---
|
||||
|
||||
迷你柱状图更适合展示简单的区间数据,简洁的表现方式可以很好的减少大数据量的视觉展现压力。
|
||||
|
||||
````jsx
|
||||
import { MiniBar } from 'ant-design-pro/lib/Charts';
|
||||
import moment from 'moment';
|
||||
|
||||
const visitData = [];
|
||||
const beginDay = new Date().getTime();
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
visitData.push({
|
||||
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
|
||||
y: Math.floor(Math.random() * 100) + 10,
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<MiniBar
|
||||
height={45}
|
||||
data={visitData}
|
||||
/>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,16 @@
|
||||
---
|
||||
order: 6
|
||||
title: 迷你饼状图
|
||||
---
|
||||
|
||||
通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展
|
||||
现更多业务场景。
|
||||
|
||||
```jsx
|
||||
import { Pie } from 'ant-design-pro/lib/Charts';
|
||||
|
||||
ReactDOM.render(
|
||||
<Pie percent={28} subTitle="中式快餐" total="28%" height={140} />,
|
||||
mountNode
|
||||
);
|
||||
```
|
@ -0,0 +1,12 @@
|
||||
---
|
||||
order: 3
|
||||
title: 迷你进度条
|
||||
---
|
||||
|
||||
````jsx
|
||||
import { MiniProgress } from 'ant-design-pro/lib/Charts';
|
||||
|
||||
ReactDOM.render(
|
||||
<MiniProgress percent={78} strokeWidth={8} target={80} />
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,84 @@
|
||||
---
|
||||
order: 0
|
||||
title: 图表套件组合展示
|
||||
---
|
||||
|
||||
利用 Ant Design Pro 提供的图表套件,可以灵活组合符合设计规范的图表来满足复杂的业务需求。
|
||||
|
||||
````jsx
|
||||
import { ChartCard, Field, MiniArea, MiniBar, MiniProgress } from 'ant-design-pro/lib/Charts';
|
||||
import Trend from 'ant-design-pro/lib/Trend';
|
||||
import NumberInfo from 'ant-design-pro/lib/NumberInfo';
|
||||
import { Row, Col, Icon, Tooltip } from 'antd';
|
||||
import numeral from 'numeral';
|
||||
import moment from 'moment';
|
||||
|
||||
const visitData = [];
|
||||
const beginDay = new Date().getTime();
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
visitData.push({
|
||||
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
|
||||
y: Math.floor(Math.random() * 100) + 10,
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Row>
|
||||
<Col span={24}>
|
||||
<ChartCard
|
||||
title="搜索用户数量"
|
||||
total={numeral(8846).format('0,0')}
|
||||
contentHeight={134}
|
||||
>
|
||||
<NumberInfo
|
||||
subTitle={<span>本周访问</span>}
|
||||
total={numeral(12321).format('0,0')}
|
||||
status="up"
|
||||
subTotal={17.1}
|
||||
/>
|
||||
<MiniArea
|
||||
line
|
||||
height={45}
|
||||
data={visitData}
|
||||
/>
|
||||
</ChartCard>
|
||||
</Col>
|
||||
<Col span={24} style={{ marginTop: 24 }}>
|
||||
<ChartCard
|
||||
title="访问量"
|
||||
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
|
||||
total={numeral(8846).format('0,0')}
|
||||
footer={<Field label="日访问量" value={numeral(1234).format('0,0')} />}
|
||||
contentHeight={46}
|
||||
>
|
||||
<MiniBar
|
||||
height={46}
|
||||
data={visitData}
|
||||
/>
|
||||
</ChartCard>
|
||||
</Col>
|
||||
<Col span={24} style={{ marginTop: 24 }}>
|
||||
<ChartCard
|
||||
title="线上购物转化率"
|
||||
action={<Tooltip title="指标说明"><Icon type="info-circle-o" /></Tooltip>}
|
||||
total="78%"
|
||||
footer={
|
||||
<div>
|
||||
<span>
|
||||
周同比
|
||||
<Trend flag="up" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>12%</Trend>
|
||||
</span>
|
||||
<span style={{ marginLeft: 16 }}>
|
||||
日环比
|
||||
<Trend flag="down" style={{ marginLeft: 8, color: 'rgba(0,0,0,.85)' }}>11%</Trend>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
contentHeight={46}
|
||||
>
|
||||
<MiniProgress percent={78} strokeWidth={8} target={80} />
|
||||
</ChartCard>
|
||||
</Col>
|
||||
</Row>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,54 @@
|
||||
---
|
||||
order: 5
|
||||
title: 饼状图
|
||||
---
|
||||
|
||||
```jsx
|
||||
import { Pie, yuan } from 'ant-design-pro/lib/Charts';
|
||||
|
||||
const salesPieData = [
|
||||
{
|
||||
x: '家用电器',
|
||||
y: 4544,
|
||||
},
|
||||
{
|
||||
x: '食用酒水',
|
||||
y: 3321,
|
||||
},
|
||||
{
|
||||
x: '个护健康',
|
||||
y: 3113,
|
||||
},
|
||||
{
|
||||
x: '服饰箱包',
|
||||
y: 2341,
|
||||
},
|
||||
{
|
||||
x: '母婴产品',
|
||||
y: 1231,
|
||||
},
|
||||
{
|
||||
x: '其他',
|
||||
y: 1231,
|
||||
},
|
||||
];
|
||||
|
||||
ReactDOM.render(
|
||||
<Pie
|
||||
hasLegend
|
||||
title="销售额"
|
||||
subTitle="销售额"
|
||||
total={() => (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: yuan(salesPieData.reduce((pre, now) => now.y + pre, 0))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
data={salesPieData}
|
||||
valueFormat={val => <span dangerouslySetInnerHTML={{ __html: yuan(val) }} />}
|
||||
height={294}
|
||||
/>,
|
||||
mountNode,
|
||||
);
|
||||
```
|
@ -0,0 +1,64 @@
|
||||
---
|
||||
order: 7
|
||||
title: 雷达图
|
||||
---
|
||||
|
||||
````jsx
|
||||
import { Radar, ChartCard } from 'ant-design-pro/lib/Charts';
|
||||
|
||||
const radarOriginData = [
|
||||
{
|
||||
name: '个人',
|
||||
ref: 10,
|
||||
koubei: 8,
|
||||
output: 4,
|
||||
contribute: 5,
|
||||
hot: 7,
|
||||
},
|
||||
{
|
||||
name: '团队',
|
||||
ref: 3,
|
||||
koubei: 9,
|
||||
output: 6,
|
||||
contribute: 3,
|
||||
hot: 1,
|
||||
},
|
||||
{
|
||||
name: '部门',
|
||||
ref: 4,
|
||||
koubei: 1,
|
||||
output: 6,
|
||||
contribute: 5,
|
||||
hot: 7,
|
||||
},
|
||||
];
|
||||
const radarData = [];
|
||||
const radarTitleMap = {
|
||||
ref: '引用',
|
||||
koubei: '口碑',
|
||||
output: '产量',
|
||||
contribute: '贡献',
|
||||
hot: '热度',
|
||||
};
|
||||
radarOriginData.forEach((item) => {
|
||||
Object.keys(item).forEach((key) => {
|
||||
if (key !== 'name') {
|
||||
radarData.push({
|
||||
name: item.name,
|
||||
label: radarTitleMap[key],
|
||||
value: item[key],
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<ChartCard title="数据比例">
|
||||
<Radar
|
||||
hasLegend
|
||||
height={286}
|
||||
data={radarData}
|
||||
/>
|
||||
</ChartCard>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,25 @@
|
||||
---
|
||||
order: 9
|
||||
title: 标签云
|
||||
---
|
||||
|
||||
标签云是一套相关的标签以及与此相应的权重展示方式,一般典型的标签云有 30 至 150 个标签,而权重影响使用的字体大小或其他视觉效果。
|
||||
|
||||
````jsx
|
||||
import { TagCloud } from 'ant-design-pro/lib/Charts';
|
||||
|
||||
const tags = [];
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
tags.push({
|
||||
name: `TagClout-Title-${i}`,
|
||||
value: Math.floor((Math.random() * 50)) + 20,
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<TagCloud
|
||||
data={tags}
|
||||
height={200}
|
||||
/>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,27 @@
|
||||
---
|
||||
order: 9
|
||||
title: 带有时间轴的图表
|
||||
---
|
||||
|
||||
使用 `TimelineChart` 组件可以实现带有时间轴的柱状图展现,而其中的 `x` 属性,则是时间值的指向,默认最多支持同时展现两个指标,分别是 `y1` 和 `y2`。
|
||||
|
||||
````jsx
|
||||
import { TimelineChart } from 'ant-design-pro/lib/Charts';
|
||||
|
||||
const chartData = [];
|
||||
for (let i = 0; i < 20; i += 1) {
|
||||
chartData.push({
|
||||
x: (new Date().getTime()) + (1000 * 60 * 30 * i),
|
||||
y1: Math.floor(Math.random() * 100) + 1000,
|
||||
y2: Math.floor(Math.random() * 100) + 10,
|
||||
});
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<TimelineChart
|
||||
height={200}
|
||||
data={chartData}
|
||||
titleMap={{ y1: '客流量', y2: '支付笔数' }}
|
||||
/>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,20 @@
|
||||
---
|
||||
order: 8
|
||||
title: 水波图
|
||||
---
|
||||
|
||||
水波图是一种比例的展示方式,可以更直观的展示关键值的占比。
|
||||
|
||||
````jsx
|
||||
import { WaterWave } from 'ant-design-pro/lib/Charts';
|
||||
|
||||
ReactDOM.render(
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<WaterWave
|
||||
height={161}
|
||||
title="补贴资金剩余"
|
||||
percent={34}
|
||||
/>
|
||||
</div>
|
||||
, mountNode);
|
||||
````
|
@ -0,0 +1,15 @@
|
||||
// 全局 G2 设置
|
||||
import { track, setTheme } from 'bizcharts';
|
||||
|
||||
track(false);
|
||||
|
||||
const config = {
|
||||
defaultColor: '#1089ff',
|
||||
shape: {
|
||||
interval: {
|
||||
fillOpacity: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setTheme(config);
|
@ -0,0 +1,17 @@
|
||||
import * as numeral from 'numeral';
|
||||
export { default as ChartCard } from './ChartCard';
|
||||
export { default as Bar } from './Bar';
|
||||
export { default as Pie } from './Pie';
|
||||
export { default as Radar } from './Radar';
|
||||
export { default as Gauge } from './Gauge';
|
||||
export { default as MiniArea } from './MiniArea';
|
||||
export { default as MiniBar } from './MiniBar';
|
||||
export { default as MiniProgress } from './MiniProgress';
|
||||
export { default as Field } from './Field';
|
||||
export { default as WaterWave } from './WaterWave';
|
||||
export { default as TagCloud } from './TagCloud';
|
||||
export { default as TimelineChart } from './TimelineChart';
|
||||
|
||||
declare const yuan: (value: number | string) => string;
|
||||
|
||||
export { yuan };
|
@ -0,0 +1,49 @@
|
||||
import numeral from 'numeral';
|
||||
import './g2';
|
||||
import ChartCard from './ChartCard';
|
||||
import Bar from './Bar';
|
||||
import Pie from './Pie';
|
||||
import Radar from './Radar';
|
||||
import Gauge from './Gauge';
|
||||
import MiniArea from './MiniArea';
|
||||
import MiniBar from './MiniBar';
|
||||
import MiniProgress from './MiniProgress';
|
||||
import Field from './Field';
|
||||
import WaterWave from './WaterWave';
|
||||
import TagCloud from './TagCloud';
|
||||
import TimelineChart from './TimelineChart';
|
||||
|
||||
const yuan = val => `¥ ${numeral(val).format('0,0')}`;
|
||||
|
||||
const Charts = {
|
||||
yuan,
|
||||
Bar,
|
||||
Pie,
|
||||
Gauge,
|
||||
Radar,
|
||||
MiniBar,
|
||||
MiniArea,
|
||||
MiniProgress,
|
||||
ChartCard,
|
||||
Field,
|
||||
WaterWave,
|
||||
TagCloud,
|
||||
TimelineChart,
|
||||
};
|
||||
|
||||
export {
|
||||
Charts as default,
|
||||
yuan,
|
||||
Bar,
|
||||
Pie,
|
||||
Gauge,
|
||||
Radar,
|
||||
MiniBar,
|
||||
MiniArea,
|
||||
MiniProgress,
|
||||
ChartCard,
|
||||
Field,
|
||||
WaterWave,
|
||||
TagCloud,
|
||||
TimelineChart,
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
.miniChart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
.chartContent {
|
||||
position: absolute;
|
||||
bottom: -28px;
|
||||
width: 100%;
|
||||
> div {
|
||||
margin: 0 -5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
.chartLoading {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
margin-left: -7px;
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
---
|
||||
title:
|
||||
en-US: Charts
|
||||
zh-CN: Charts
|
||||
subtitle: 图表
|
||||
order: 2
|
||||
cols: 2
|
||||
---
|
||||
|
||||
Ant Design Pro 提供的业务中常用的图表类型,都是基于 [G2](https://antv.alipay.com/g2/doc/index.html) 按照 Ant Design 图表规范封装,需要注意的是 Ant Design Pro 的图表组件以套件形式提供,可以任意组合实现复杂的业务需求。
|
||||
|
||||
因为结合了 Ant Design 的标准设计,本着极简的设计思想以及开箱即用的理念,简化了大量 API 配置,所以如果需要灵活定制图表,可以参考 Ant Design Pro 图表实现,自行基于 [G2](https://antv.alipay.com/g2/doc/index.html) 封装图表组件使用。
|
||||
|
||||
## API
|
||||
|
||||
### ChartCard
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 卡片标题 | ReactNode\|string | - |
|
||||
| action | 卡片操作 | ReactNode | - |
|
||||
| total | 数据总量 | ReactNode \| number \| function | - |
|
||||
| footer | 卡片底部 | ReactNode | - |
|
||||
| contentHeight | 内容区域高度 | number | - |
|
||||
| avatar | 右侧图标 | React.ReactNode | - |
|
||||
### MiniBar
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| color | 图表颜色 | string | `#1890FF` |
|
||||
| height | 图表高度 | number | - |
|
||||
| data | 数据 | array<{x, y}> | - |
|
||||
|
||||
### MiniArea
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.2)` |
|
||||
| borderColor | 图表边颜色 | string | `#1890FF` |
|
||||
| height | 图表高度 | number | - |
|
||||
| line | 是否显示描边 | boolean | false |
|
||||
| animate | 是否显示动画 | boolean | true |
|
||||
| xAxis | [x 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
|
||||
| yAxis | [y 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
|
||||
| data | 数据 | array<{x, y}> | - |
|
||||
|
||||
### MiniProgress
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| target | 目标比例 | number | - |
|
||||
| color | 进度条颜色 | string | - |
|
||||
| strokeWidth | 进度条高度 | number | - |
|
||||
| percent | 进度比例 | number | - |
|
||||
|
||||
### Bar
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
|
||||
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
|
||||
| height | 图表高度 | number | - |
|
||||
| data | 数据 | array<{x, y}> | - |
|
||||
| autoLabel | 在宽度不足时,自动隐藏 x 轴的 label | boolean | `true` |
|
||||
|
||||
### Pie
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| animate | 是否显示动画 | boolean | true |
|
||||
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
|
||||
| height | 图表高度 | number | - |
|
||||
| hasLegend | 是否显示 legend | boolean | `false` |
|
||||
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
|
||||
| percent | 占比 | number | - |
|
||||
| tooltip | 是否显示 tooltip | boolean | true |
|
||||
| valueFormat | 显示值的格式化函数 | function | - |
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| subTitle | 图表子标题 | ReactNode\|string | - |
|
||||
| total | 图标中央的总数 | string | function | - |
|
||||
|
||||
### Radar
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| height | 图表高度 | number | - |
|
||||
| hasLegend | 是否显示 legend | boolean | `false` |
|
||||
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
|
||||
| data | 图标数据 | array<{name,label,value}> | - |
|
||||
|
||||
### Gauge
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| height | 图表高度 | number | - |
|
||||
| color | 图表颜色 | string | `#2F9CFF` |
|
||||
| bgColor | 图表背景颜色 | string | `#F0F2F5` |
|
||||
| percent | 进度比例 | number | - |
|
||||
|
||||
### WaterWave
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| title | 图表标题 | ReactNode\|string | - |
|
||||
| height | 图表高度 | number | - |
|
||||
| color | 图表颜色 | string | `#1890FF` |
|
||||
| percent | 进度比例 | number | - |
|
||||
|
||||
### TagCloud
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| data | 标题 | Array<name, value\> | - |
|
||||
| height | 高度值 | number | - |
|
||||
|
||||
### TimelineChart
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| data | 标题 | Array<x, y1, y2\> | - |
|
||||
| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - |
|
||||
| height | 高度值 | number | 400 |
|
||||
|
||||
### Field
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
|----------|------------------------------------------|-------------|-------|
|
||||
| label | 标题 | ReactNode\|string | - |
|
||||
| value | 值 | ReactNode\|string | - |
|
@ -0,0 +1,24 @@
|
||||
---
|
||||
order: 0
|
||||
title:
|
||||
zh-CN: 基本
|
||||
en-US: Basic
|
||||
---
|
||||
|
||||
## zh-CN
|
||||
|
||||
简单的倒计时组件使用。
|
||||
|
||||
## en-US
|
||||
|
||||
The simplest usage.
|
||||
|
||||
````jsx
|
||||
import CountDown from 'ant-design-pro/lib/CountDown';
|
||||
|
||||
const targetTime = new Date().getTime() + 3900000;
|
||||
|
||||
ReactDOM.render(
|
||||
<CountDown style={{ fontSize: 20 }} target={targetTime} />
|
||||
, mountNode);
|
||||
````
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue