committed by
Yu
216 changed files with 43 additions and 8301 deletions
@ -1,8 +0,0 @@ |
|||
import CheckPermissions from './CheckPermissions'; |
|||
|
|||
const Authorized = ({ children, authority, noMatch = null }) => { |
|||
const childrenRender = typeof children === 'undefined' ? null : children; |
|||
return CheckPermissions(authority, childrenRender, noMatch); |
|||
}; |
|||
|
|||
export default Authorized; |
|||
@ -1,13 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import { RouteProps } from 'react-router'; |
|||
|
|||
type authorityFN = (currentAuthority?: string) => boolean; |
|||
|
|||
type authority = string | string[] | authorityFN | Promise<any>; |
|||
|
|||
export interface IAuthorizedRouteProps extends RouteProps { |
|||
authority: authority; |
|||
} |
|||
export { authority }; |
|||
|
|||
export class AuthorizedRoute extends React.Component<IAuthorizedRouteProps, any> {} |
|||
@ -1,15 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Route, Redirect } from 'react-router-dom'; |
|||
import Authorized from './Authorized'; |
|||
|
|||
// TODO: umi只会返回render和rest
|
|||
const AuthorizedRoute = ({ component: Component, render, authority, redirectPath, ...rest }) => ( |
|||
<Authorized |
|||
authority={authority} |
|||
noMatch={<Route {...rest} render={() => <Redirect to={{ pathname: redirectPath }} />} />} |
|||
> |
|||
<Route {...rest} render={props => (Component ? <Component {...props} /> : render(props))} /> |
|||
</Authorized> |
|||
); |
|||
|
|||
export default AuthorizedRoute; |
|||
@ -1,88 +0,0 @@ |
|||
import React from 'react'; |
|||
import PromiseRender from './PromiseRender'; |
|||
import { CURRENT } from './renderAuthorize'; |
|||
|
|||
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; |
|||
} |
|||
if (Array.isArray(currentAuthority)) { |
|||
for (let i = 0; i < currentAuthority.length; i += 1) { |
|||
const element = currentAuthority[i]; |
|||
if (authority.indexOf(element) >= 0) { |
|||
return target; |
|||
} |
|||
} |
|||
} |
|||
return Exception; |
|||
} |
|||
|
|||
// string 处理
|
|||
if (typeof authority === 'string') { |
|||
if (authority === currentAuthority) { |
|||
return target; |
|||
} |
|||
if (Array.isArray(currentAuthority)) { |
|||
for (let i = 0; i < currentAuthority.length; i += 1) { |
|||
const element = currentAuthority[i]; |
|||
if (authority === element) { |
|||
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); |
|||
// 函数执行后返回值是 Promise
|
|||
if (isPromise(bool)) { |
|||
return <PromiseRender ok={target} error={Exception} promise={bool} />; |
|||
} |
|||
if (bool) { |
|||
return target; |
|||
} |
|||
return Exception; |
|||
} catch (error) { |
|||
throw error; |
|||
} |
|||
} |
|||
throw new Error('unsupported parameters'); |
|||
}; |
|||
|
|||
export { checkPermissions }; |
|||
|
|||
const check = (authority, target, Exception) => |
|||
checkPermissions(authority, CURRENT, target, Exception); |
|||
|
|||
export default check; |
|||
@ -1,55 +0,0 @@ |
|||
import { checkPermissions } from './CheckPermissions'; |
|||
|
|||
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'); |
|||
}); |
|||
it('authority is string, currentAuthority is array, return ok', () => { |
|||
expect(checkPermissions('user', ['user'], target, error)).toEqual('ok'); |
|||
}); |
|||
it('authority is string, currentAuthority is array, return ok', () => { |
|||
expect(checkPermissions('user', ['user', 'admin'], target, error)).toEqual('ok'); |
|||
}); |
|||
it('authority is array, currentAuthority is array, return ok', () => { |
|||
expect(checkPermissions(['user', 'admin'], ['user', 'admin'], target, error)).toEqual('ok'); |
|||
}); |
|||
it('Wrong Function permission authentication', () => { |
|||
expect(checkPermissions(() => false, ['user'], target, error)).toEqual('error'); |
|||
}); |
|||
it('Correct Function permission authentication', () => { |
|||
expect(checkPermissions(() => true, ['user'], target, error)).toEqual('ok'); |
|||
}); |
|||
it('authority is undefined , return ok', () => { |
|||
expect(checkPermissions(null, ['user'], target, error)).toEqual('ok'); |
|||
}); |
|||
}); |
|||
@ -1,65 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Spin } from 'antd'; |
|||
|
|||
export default class PromiseRender extends React.PureComponent { |
|||
state = { |
|||
component: null, |
|||
}; |
|||
|
|||
componentDidMount() { |
|||
this.setRenderComponent(this.props); |
|||
} |
|||
|
|||
componentDidUpdate(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: Component } = this.state; |
|||
const { ok, error, promise, ...rest } = this.props; |
|||
return Component ? ( |
|||
<Component {...rest} /> |
|||
) : ( |
|||
<div |
|||
style={{ |
|||
width: '100%', |
|||
height: '100%', |
|||
margin: 'auto', |
|||
paddingTop: 50, |
|||
textAlign: 'center', |
|||
}} |
|||
> |
|||
<Spin size="large" /> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
@ -1,55 +0,0 @@ |
|||
import React from 'react'; |
|||
import Exception from '../Exception'; |
|||
import CheckPermissions from './CheckPermissions'; |
|||
/** |
|||
* 默认不能访问任何页面 |
|||
* default is "NULL" |
|||
*/ |
|||
const Exception403 = () => <Exception type="403" />; |
|||
|
|||
// 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, function:()=>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, function: () => 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(target) { |
|||
const component = CheckPermissions(authority, target, classError || Exception403); |
|||
return checkIsInstantiation(component); |
|||
}; |
|||
}; |
|||
|
|||
export default authorize; |
|||
@ -1,23 +0,0 @@ |
|||
--- |
|||
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, |
|||
); |
|||
``` |
|||
@ -1,31 +0,0 @@ |
|||
--- |
|||
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, |
|||
); |
|||
``` |
|||
@ -1,25 +0,0 @@ |
|||
--- |
|||
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, |
|||
); |
|||
``` |
|||
@ -1,28 +0,0 @@ |
|||
--- |
|||
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, |
|||
); |
|||
``` |
|||
@ -1,32 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import AuthorizedRoute, { authority } from './AuthorizedRoute'; |
|||
export type IReactComponent<P = any> = |
|||
| React.StatelessComponent<P> |
|||
| React.ComponentClass<P> |
|||
| React.ClassicComponentClass<P>; |
|||
|
|||
type Secured = ( |
|||
authority: authority, |
|||
error?: React.ReactNode |
|||
) => <T extends IReactComponent>(target: T) => T; |
|||
|
|||
type check = <T extends IReactComponent, S extends IReactComponent>( |
|||
authority: authority, |
|||
target: T, |
|||
Exception: S |
|||
) => T | S; |
|||
|
|||
export interface IAuthorizedProps { |
|||
authority: authority; |
|||
noMatch?: React.ReactNode; |
|||
} |
|||
|
|||
export class Authorized extends React.Component<IAuthorizedProps, any> { |
|||
public static Secured: Secured; |
|||
public static AuthorizedRoute: typeof AuthorizedRoute; |
|||
public static check: check; |
|||
} |
|||
|
|||
declare function renderAuthorize(currentAuthority: string): typeof Authorized; |
|||
|
|||
export default renderAuthorize; |
|||
@ -1,11 +0,0 @@ |
|||
import Authorized from './Authorized'; |
|||
import AuthorizedRoute from './AuthorizedRoute'; |
|||
import Secured from './Secured'; |
|||
import check from './CheckPermissions'; |
|||
import renderAuthorize from './renderAuthorize'; |
|||
|
|||
Authorized.Secured = Secured; |
|||
Authorized.AuthorizedRoute = AuthorizedRoute; |
|||
Authorized.check = check; |
|||
|
|||
export default renderAuthorize(Authorized); |
|||
@ -1,58 +0,0 @@ |
|||
--- |
|||
title: |
|||
en-US: Authorized |
|||
zh-CN: Authorized |
|||
subtitle: 权限 |
|||
cols: 1 |
|||
order: 15 |
|||
--- |
|||
|
|||
权限组件,通过比对现有权限与准入权限,决定相关元素的展示。 |
|||
|
|||
## API |
|||
|
|||
### RenderAuthorized |
|||
|
|||
`RenderAuthorized: (currentAuthority: string | () => string) => Authorized` |
|||
|
|||
权限组件默认 export RenderAuthorized 函数,它接收当前权限作为参数,返回一个权限对象,该对象提供以下几种使用方式。 |
|||
|
|||
|
|||
### Authorized |
|||
|
|||
最基础的权限控制。 |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| children | 正常渲染的元素,权限判断通过时展示 | ReactNode | - | |
|||
| authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean | Promise` | - | |
|||
| noMatch | 权限异常渲染元素,权限判断不通过时展示 | ReactNode | - | |
|||
|
|||
### Authorized.AuthorizedRoute |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean | Promise` | - | |
|||
| redirectPath | 权限异常时重定向的页面路由 | string | - | |
|||
|
|||
其余参数与 `Route` 相同。 |
|||
|
|||
### Authorized.Secured |
|||
|
|||
注解方式,`@Authorized.Secured(authority, error)` |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean | Promise` | - | |
|||
| error | 权限异常时渲染元素 | ReactNode | <Exception type="403" /> | |
|||
|
|||
### Authorized.check |
|||
|
|||
函数形式的 Authorized,用于某些不能被 HOC 包裹的组件。 `Authorized.check(authority, target, Exception)` |
|||
注意:传入一个 Promise 时,无论正确还是错误返回的都是一个 ReactClass。 |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean | Promise` | - | |
|||
| target | 权限判断通过时渲染的元素 | ReactNode | - | |
|||
| Exception | 权限异常时渲染元素 | ReactNode | - | |
|||
@ -1,25 +0,0 @@ |
|||
/* eslint-disable import/no-mutable-exports */ |
|||
let CURRENT = 'NULL'; |
|||
/** |
|||
* use authority or getAuthority |
|||
* @param {string|()=>String} currentAuthority |
|||
*/ |
|||
const renderAuthorize = Authorized => currentAuthority => { |
|||
if (currentAuthority) { |
|||
if (typeof currentAuthority === 'function') { |
|||
CURRENT = currentAuthority(); |
|||
} |
|||
if ( |
|||
Object.prototype.toString.call(currentAuthority) === '[object String]' || |
|||
Array.isArray(currentAuthority) |
|||
) { |
|||
CURRENT = currentAuthority; |
|||
} |
|||
} else { |
|||
CURRENT = 'NULL'; |
|||
} |
|||
return Authorized; |
|||
}; |
|||
|
|||
export { CURRENT }; |
|||
export default Authorized => renderAuthorize(Authorized); |
|||
@ -1,10 +0,0 @@ |
|||
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); |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
--- |
|||
order: 0 |
|||
title: |
|||
zh-CN: 要显示的最大项目 |
|||
en-US: Max Items to Show |
|||
--- |
|||
|
|||
`maxLength` attribute specifies the maximum number of items to show while `excessItemsStyle` style the excess |
|||
item component. |
|||
|
|||
````jsx |
|||
import AvatarList from 'ant-design-pro/lib/AvatarList'; |
|||
|
|||
ReactDOM.render( |
|||
<AvatarList size="mini" maxLength={3} excessItemsStyle={{ color: '#f56a00', backgroundColor: '#fde3cf' }}> |
|||
<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.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" /> |
|||
<AvatarList.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" /> |
|||
<AvatarList.Item tips="Niko" src="https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png" /> |
|||
</AvatarList> |
|||
, mountNode); |
|||
```` |
|||
@ -1,20 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,14 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import AvatarItem from './AvatarItem'; |
|||
|
|||
export interface IAvatarListProps { |
|||
size?: 'large' | 'small' | 'mini' | 'default'; |
|||
maxLength?: number; |
|||
excessItemsStyle?: React.CSSProperties; |
|||
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; |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
--- |
|||
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` | |
|||
| maxLength | max items to show | number | - | |
|||
| excessItemsStyle | the excess item style | CSSProperties | - | |
|||
|
|||
### AvatarList.Item |
|||
|
|||
| Property | Description | Type | Default | |
|||
| -------- | -------------------------------------------- | --------- | ------- | |
|||
| tips | title tips for avatar item | ReactNode | - | |
|||
| src | the address of the image for an image avatar | string | - | |
|||
@ -1,61 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Tooltip, Avatar } from 'antd'; |
|||
import classNames from 'classnames'; |
|||
|
|||
import styles from './index.less'; |
|||
|
|||
const avatarSizeToClassName = size => |
|||
classNames(styles.avatarItem, { |
|||
[styles.avatarItemLarge]: size === 'large', |
|||
[styles.avatarItemSmall]: size === 'small', |
|||
[styles.avatarItemMini]: size === 'mini', |
|||
}); |
|||
|
|||
const AvatarList = ({ children, size, maxLength, excessItemsStyle, ...other }) => { |
|||
const numOfChildren = React.Children.count(children); |
|||
const numToShow = maxLength >= numOfChildren ? numOfChildren : maxLength; |
|||
|
|||
const childrenWithProps = React.Children.toArray(children) |
|||
.slice(0, numToShow) |
|||
.map(child => |
|||
React.cloneElement(child, { |
|||
size, |
|||
}) |
|||
); |
|||
|
|||
if (numToShow < numOfChildren) { |
|||
const cls = avatarSizeToClassName(size); |
|||
|
|||
childrenWithProps.push( |
|||
<li key="exceed" className={cls}> |
|||
<Avatar size={size} style={excessItemsStyle}>{`+${numOfChildren - maxLength}`}</Avatar> |
|||
</li> |
|||
); |
|||
} |
|||
|
|||
return ( |
|||
<div {...other} className={styles.avatarList}> |
|||
<ul> {childrenWithProps} </ul> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
const Item = ({ src, size, tips, onClick = () => {} }) => { |
|||
const cls = avatarSizeToClassName(size); |
|||
|
|||
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; |
|||
@ -1,50 +0,0 @@ |
|||
@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; |
|||
|
|||
.ant-avatar-string { |
|||
font-size: 12px; |
|||
line-height: 18px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
import React from 'react'; |
|||
import range from 'lodash/range'; |
|||
import { mount } from 'enzyme'; |
|||
import AvatarList from './index'; |
|||
|
|||
const renderItems = numItems => |
|||
range(numItems).map(i => ( |
|||
<AvatarList.Item |
|||
key={i} |
|||
tips="Jake" |
|||
src="https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png" |
|||
/> |
|||
)); |
|||
|
|||
describe('AvatarList', () => { |
|||
it('renders all items', () => { |
|||
const wrapper = mount(<AvatarList>{renderItems(4)}</AvatarList>); |
|||
expect(wrapper.find('AvatarList').length).toBe(1); |
|||
expect(wrapper.find('Item').length).toBe(4); |
|||
expect(wrapper.findWhere(node => node.key() === 'exceed').length).toBe(0); |
|||
}); |
|||
|
|||
it('renders max of 3 items', () => { |
|||
const wrapper = mount(<AvatarList maxLength={3}>{renderItems(4)}</AvatarList>); |
|||
expect(wrapper.find('AvatarList').length).toBe(1); |
|||
expect(wrapper.find('Item').length).toBe(3); |
|||
expect(wrapper.findWhere(node => node.key() === 'exceed').length).toBe(1); |
|||
}); |
|||
}); |
|||
@ -1,25 +0,0 @@ |
|||
--- |
|||
title: AvatarList |
|||
subtitle: 用户头像列表 |
|||
order: 1 |
|||
cols: 1 |
|||
--- |
|||
|
|||
一组用户头像,常用在项目/团队成员列表。可通过设置 `size` 属性来指定头像大小。 |
|||
|
|||
## API |
|||
|
|||
### AvatarList |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
| ---------------- | -------- | ---------------------------------- | --------- | |
|||
| size | 头像大小 | `large`、`small` 、`mini`, `default` | `default` | |
|||
| maxLength | 要显示的最大项目 | number | - | |
|||
| excessItemsStyle | 多余的项目风格 | CSSProperties | - | |
|||
|
|||
### AvatarList.Item |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
| ---- | ------ | --------- | --- | |
|||
| tips | 头像展示文案 | ReactNode | - | |
|||
| src | 头像图片连接 | string | - | |
|||
@ -1,15 +0,0 @@ |
|||
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> {} |
|||
@ -1,113 +0,0 @@ |
|||
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, { passive: true }); |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
window.removeEventListener('resize', this.resize); |
|||
} |
|||
|
|||
handleRoot = n => { |
|||
this.root = n; |
|||
}; |
|||
|
|||
handleRef = n => { |
|||
this.node = n; |
|||
}; |
|||
|
|||
@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, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
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; |
|||
@ -1,14 +0,0 @@ |
|||
import { CardProps } from 'antd/lib/card'; |
|||
import * as React from 'react'; |
|||
|
|||
export interface IChartCardProps extends CardProps { |
|||
title: React.ReactNode; |
|||
action?: React.ReactNode; |
|||
total?: React.ReactNode | number | (() => React.ReactNode | number); |
|||
footer?: React.ReactNode; |
|||
contentHeight?: number; |
|||
avatar?: React.ReactNode; |
|||
style?: React.CSSProperties; |
|||
} |
|||
|
|||
export default class ChartCard extends React.Component<IChartCardProps, any> {} |
|||
@ -1,82 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Card } 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; |
|||
}; |
|||
|
|||
class ChartCard extends React.PureComponent { |
|||
renderConnet = () => { |
|||
const { contentHeight, title, avatar, action, total, footer, children, loading } = this.props; |
|||
if (loading) { |
|||
return false; |
|||
} |
|||
return ( |
|||
<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> |
|||
); |
|||
}; |
|||
|
|||
render() { |
|||
const { |
|||
loading = false, |
|||
contentHeight, |
|||
title, |
|||
avatar, |
|||
action, |
|||
total, |
|||
footer, |
|||
children, |
|||
...rest |
|||
} = this.props; |
|||
return ( |
|||
<Card loading={loading} bodyStyle={{ padding: '20px 24px 8px 24px' }} {...rest}> |
|||
{this.renderConnet()} |
|||
</Card> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default ChartCard; |
|||
@ -1,75 +0,0 @@ |
|||
@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: 4px; |
|||
right: 0; |
|||
line-height: 1; |
|||
} |
|||
.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; |
|||
} |
|||
} |
|||
@ -1,8 +0,0 @@ |
|||
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> {} |
|||
@ -1,12 +0,0 @@ |
|||
import React from 'react'; |
|||
|
|||
import styles from './index.less'; |
|||
|
|||
const Field = ({ label, value, ...rest }) => ( |
|||
<div className={styles.field} {...rest}> |
|||
<span className={styles.label}>{label}</span> |
|||
<span className={styles.number}>{value}</span> |
|||
</div> |
|||
); |
|||
|
|||
export default Field; |
|||
@ -1,17 +0,0 @@ |
|||
@import '~antd/lib/style/themes/default.less'; |
|||
|
|||
.field { |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
margin: 0; |
|||
.label, |
|||
.number { |
|||
font-size: @font-size-base; |
|||
line-height: 22px; |
|||
} |
|||
.number { |
|||
color: @heading-color; |
|||
margin-left: 8px; |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
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> {} |
|||
@ -1,167 +0,0 @@ |
|||
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() |
|||
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={() => ` |
|||
<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> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default Gauge; |
|||
@ -1,29 +0,0 @@ |
|||
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 | string; |
|||
y: number; |
|||
}>; |
|||
} |
|||
|
|||
export default class MiniArea extends React.Component<IMiniAreaProps, any> {} |
|||
@ -1,108 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Chart, Axis, Tooltip, Geom } from 'bizcharts'; |
|||
import autoHeight from '../autoHeight'; |
|||
import styles from '../index.less'; |
|||
|
|||
@autoHeight() |
|||
class MiniArea extends React.PureComponent { |
|||
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> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default MiniArea; |
|||
@ -1,12 +0,0 @@ |
|||
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> {} |
|||
@ -1,51 +0,0 @@ |
|||
import React from 'react'; |
|||
import { Chart, Tooltip, Geom } from 'bizcharts'; |
|||
import autoHeight from '../autoHeight'; |
|||
import styles from '../index.less'; |
|||
|
|||
@autoHeight() |
|||
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> |
|||
); |
|||
} |
|||
} |
|||
export default MiniBar; |
|||
@ -1,10 +0,0 @@ |
|||
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> {} |
|||
@ -1,27 +0,0 @@ |
|||
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; |
|||
@ -1,35 +0,0 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,21 +0,0 @@ |
|||
import * as React from 'react'; |
|||
export interface IPieProps { |
|||
animate?: boolean; |
|||
color?: string; |
|||
colors?: string[]; |
|||
height: number; |
|||
hasLegend?: boolean; |
|||
padding?: [number, number, number, number]; |
|||
percent?: number; |
|||
data?: Array<{ |
|||
x: string | string; |
|||
y: number; |
|||
}>; |
|||
total?: React.ReactNode | number | (() => React.ReactNode | number); |
|||
title?: React.ReactNode; |
|||
tooltip?: boolean; |
|||
valueFormat?: (value: string) => string | React.ReactNode; |
|||
subTitle?: React.ReactNode; |
|||
} |
|||
|
|||
export default class Pie extends React.Component<IPieProps, any> {} |
|||
@ -1,271 +0,0 @@ |
|||
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() |
|||
class Pie extends Component { |
|||
state = { |
|||
legendData: [], |
|||
legendBlock: false, |
|||
}; |
|||
|
|||
componentDidMount() { |
|||
window.addEventListener( |
|||
'resize', |
|||
() => { |
|||
this.requestRef = requestAnimationFrame(() => this.resize()); |
|||
}, |
|||
{ passive: true } |
|||
); |
|||
} |
|||
|
|||
componentDidUpdate(preProps) { |
|||
const { data } = this.props; |
|||
if (data !== preProps.data) { |
|||
// because of charts data create when rendered
|
|||
// so there is a trick for get rendered time
|
|||
this.getLegendData(); |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
window.cancelAnimationFrame(this.requestRef); |
|||
window.removeEventListener('resize', this.resize); |
|||
this.resize.cancel(); |
|||
} |
|||
|
|||
getG2Instance = chart => { |
|||
this.chart = chart; |
|||
requestAnimationFrame(() => { |
|||
this.getLegendData(); |
|||
this.resize(); |
|||
}); |
|||
}; |
|||
|
|||
// for custom lengend view
|
|||
getLegendData = () => { |
|||
if (!this.chart) return; |
|||
const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
|
|||
if (!geom) return; |
|||
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, |
|||
}); |
|||
}; |
|||
|
|||
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, |
|||
}); |
|||
}; |
|||
|
|||
// for window resize auto responsive legend
|
|||
@Bind() |
|||
@Debounce(300) |
|||
resize() { |
|||
const { hasLegend } = this.props; |
|||
const { legendBlock } = this.state; |
|||
if (!hasLegend || !this.root) { |
|||
window.removeEventListener('resize', this.resize); |
|||
return; |
|||
} |
|||
if (this.root.parentNode.clientWidth <= 380) { |
|||
if (!legendBlock) { |
|||
this.setState({ |
|||
legendBlock: true, |
|||
}); |
|||
} |
|||
} else if (legendBlock) { |
|||
this.setState({ |
|||
legendBlock: false, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
valueFormat, |
|||
subTitle, |
|||
total, |
|||
hasLegend = false, |
|||
className, |
|||
style, |
|||
height, |
|||
forceFit = true, |
|||
percent, |
|||
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 { |
|||
data: propsData, |
|||
selected: propsSelected = true, |
|||
tooltip: propsTooltip = true, |
|||
} = this.props; |
|||
|
|||
let data = propsData || []; |
|||
let selected = propsSelected; |
|||
let tooltip = propsTooltip; |
|||
|
|||
const defaultColors = colors; |
|||
data = data || []; |
|||
selected = selected || true; |
|||
tooltip = tooltip || true; |
|||
let formatColor; |
|||
|
|||
const scale = { |
|||
x: { |
|||
type: 'cat', |
|||
range: [0, 1], |
|||
}, |
|||
y: { |
|||
min: 0, |
|||
}, |
|||
}; |
|||
|
|||
if (percent || percent === 0) { |
|||
selected = false; |
|||
tooltip = false; |
|||
formatColor = value => { |
|||
if (value === '占比') { |
|||
return color || 'rgba(24, 144, 255, 0.85)'; |
|||
} |
|||
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 || percent === 0 ? 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}> |
|||
{`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`} |
|||
</span> |
|||
<span className={styles.value}>{valueFormat ? valueFormat(item.y) : item.y}</span> |
|||
</li> |
|||
))} |
|||
</ul> |
|||
)} |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default Pie; |
|||
@ -1,94 +0,0 @@ |
|||
@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; |
|||
max-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; |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
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> {} |
|||
@ -1,184 +0,0 @@ |
|||
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() |
|||
class Radar extends Component { |
|||
state = { |
|||
legendData: [], |
|||
}; |
|||
|
|||
componentDidMount() { |
|||
this.getLegendData(); |
|||
} |
|||
|
|||
componentDidUpdate(preProps) { |
|||
const { data } = this.props; |
|||
if (data !== preProps.data) { |
|||
this.getLegendData(); |
|||
} |
|||
} |
|||
|
|||
getG2Instance = chart => { |
|||
this.chart = chart; |
|||
}; |
|||
|
|||
// for custom lengend view
|
|||
getLegendData = () => { |
|||
if (!this.chart) return; |
|||
const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
|
|||
if (!geom) return; |
|||
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 = 5, |
|||
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> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default Radar; |
|||
@ -1,46 +0,0 @@ |
|||
@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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
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> {} |
|||
@ -1,182 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
import { Chart, Geom, Coord, Shape, Tooltip } 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() { |
|||
requestAnimationFrame(() => { |
|||
this.initTagCloud(); |
|||
this.renderChart(); |
|||
}); |
|||
window.addEventListener('resize', this.resize, { passive: true }); |
|||
} |
|||
|
|||
componentDidUpdate(preProps) { |
|||
const { data } = this.props; |
|||
if (JSON.stringify(preProps.data) !== JSON.stringify(data)) { |
|||
this.renderChart(this.props); |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
this.isUnmount = true; |
|||
window.cancelAnimationFrame(this.requestRef); |
|||
window.removeEventListener('resize', this.resize); |
|||
} |
|||
|
|||
resize = () => { |
|||
this.requestRef = requestAnimationFrame(() => { |
|||
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; |
|||
const w = this.root.offsetWidth; |
|||
|
|||
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: 0, |
|||
timeInterval: 5000, // max execute time
|
|||
rotate() { |
|||
return 0; |
|||
}, |
|||
fontSize(d) { |
|||
// eslint-disable-next-line
|
|||
return Math.pow((d.value - min) / (max - min), 2) * (17.5 - 5) + 5; |
|||
}, |
|||
}); |
|||
|
|||
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 }, |
|||
}} |
|||
> |
|||
<Tooltip showTitle={false} /> |
|||
<Coord reflect="y" /> |
|||
<Geom |
|||
type="point" |
|||
position="x*y" |
|||
color="text" |
|||
shape="cloud" |
|||
tooltip={[ |
|||
'text*value', |
|||
function trans(text, value) { |
|||
return { name: text, value }; |
|||
}, |
|||
]} |
|||
/> |
|||
</Chart> |
|||
)} |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default TagCloud; |
|||
@ -1,6 +0,0 @@ |
|||
.tagCloud { |
|||
overflow: hidden; |
|||
canvas { |
|||
transform-origin: 0 0; |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
import * as React from 'react'; |
|||
export interface ITimelineChartProps { |
|||
data: Array<{ |
|||
x: number; |
|||
y1: number; |
|||
y2?: number; |
|||
}>; |
|||
titleMap: { y1: string; y2?: string }; |
|||
padding?: [number, number, number, number]; |
|||
height?: number; |
|||
style?: React.CSSProperties; |
|||
} |
|||
|
|||
export default class TimelineChart extends React.Component<ITimelineChartProps, any> {} |
|||
@ -1,120 +0,0 @@ |
|||
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() |
|||
class TimelineChart extends React.Component { |
|||
render() { |
|||
const { |
|||
title, |
|||
height = 400, |
|||
padding = [60, 20, 40, 40], |
|||
titleMap = { |
|||
y1: 'y1', |
|||
y2: 'y2', |
|||
}, |
|||
borderWidth = 2, |
|||
data: sourceData, |
|||
} = this.props; |
|||
|
|||
const data = Array.isArray(sourceData) ? sourceData : [{ x: 0, y1: 0, y2: 0 }]; |
|||
|
|||
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> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default TimelineChart; |
|||
@ -1,3 +0,0 @@ |
|||
.timelineChart { |
|||
background: #fff; |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
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> {} |
|||
@ -1,213 +0,0 @@ |
|||
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() |
|||
class WaterWave extends PureComponent { |
|||
state = { |
|||
radio: 1, |
|||
}; |
|||
|
|||
componentDidMount() { |
|||
this.renderChart(); |
|||
this.resize(); |
|||
window.addEventListener( |
|||
'resize', |
|||
() => { |
|||
requestAnimationFrame(() => this.resize()); |
|||
}, |
|||
{ passive: true } |
|||
); |
|||
} |
|||
|
|||
componentDidUpdate(props) { |
|||
const { percent } = this.props; |
|||
if (props.percent !== percent) { |
|||
// 不加这个会造成绘制缓慢
|
|||
this.renderChart('update'); |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
cancelAnimationFrame(this.timer); |
|||
if (this.node) { |
|||
this.node.innerHTML = ''; |
|||
} |
|||
window.removeEventListener('resize', this.resize); |
|||
} |
|||
|
|||
resize = () => { |
|||
if (this.root) { |
|||
const { height } = this.props; |
|||
const { offsetWidth } = this.root.parentNode; |
|||
this.setState({ |
|||
radio: offsetWidth < height ? offsetWidth / height : 1, |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
renderChart(type) { |
|||
const { percent, color = '#1890FF' } = this.props; |
|||
const data = percent / 100; |
|||
const self = this; |
|||
cancelAnimationFrame(this.timer); |
|||
|
|||
if (!this.node || (data !== 0 && !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, color); |
|||
ctx.fillStyle = gradient; |
|||
ctx.fill(); |
|||
ctx.restore(); |
|||
} |
|||
|
|||
function render() { |
|||
ctx.clearRect(0, 0, canvasWidth, canvasHeight); |
|||
if (circleLock && type !== 'update') { |
|||
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 = color; |
|||
} |
|||
} 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> |
|||
); |
|||
} |
|||
} |
|||
|
|||
export default WaterWave; |
|||
@ -1,28 +0,0 @@ |
|||
@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; |
|||
} |
|||
} |
|||
@ -1,62 +0,0 @@ |
|||
/* eslint eqeqeq: 0 */ |
|||
import React from 'react'; |
|||
|
|||
function computeHeight(node) { |
|||
const totalHeight = parseInt(getComputedStyle(node).height, 10); |
|||
const padding = |
|||
parseInt(getComputedStyle(node).paddingTop, 10) + |
|||
parseInt(getComputedStyle(node).paddingBottom, 10); |
|||
return totalHeight - padding; |
|||
} |
|||
|
|||
function getAutoHeight(n) { |
|||
if (!n) { |
|||
return 0; |
|||
} |
|||
|
|||
let node = n; |
|||
|
|||
let height = computeHeight(node); |
|||
|
|||
while (!height) { |
|||
node = node.parentNode; |
|||
if (node) { |
|||
height = computeHeight(node); |
|||
} else { |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return height; |
|||
} |
|||
|
|||
const autoHeight = () => WrappedComponent => |
|||
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; |
|||
@ -1,3 +0,0 @@ |
|||
import * as BizChart from 'bizcharts'; |
|||
|
|||
export = BizChart; |
|||
@ -1,3 +0,0 @@ |
|||
import * as BizChart from 'bizcharts'; |
|||
|
|||
export default BizChart; |
|||
@ -1,26 +0,0 @@ |
|||
--- |
|||
order: 4 |
|||
title: 柱状图 |
|||
--- |
|||
|
|||
通过设置 `x`,`y` 属性,可以快速的构建出一个漂亮的柱状图,各种纬度的关系则是通过自定义的数据展现。 |
|||
|
|||
````jsx |
|||
import { Bar } from 'ant-design-pro/lib/Charts'; |
|||
|
|||
const salesData = []; |
|||
for (let i = 0; i < 12; i += 1) { |
|||
salesData.push({ |
|||
x: `${i + 1}月`, |
|||
y: Math.floor(Math.random() * 1000) + 200, |
|||
}); |
|||
} |
|||
|
|||
ReactDOM.render( |
|||
<Bar |
|||
height={200} |
|||
title="销售额趋势" |
|||
data={salesData} |
|||
/> |
|||
, mountNode); |
|||
```` |
|||
@ -1,95 +0,0 @@ |
|||
--- |
|||
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, |
|||
); |
|||
``` |
|||
@ -1,18 +0,0 @@ |
|||
--- |
|||
order: 7 |
|||
title: 仪表盘 |
|||
--- |
|||
|
|||
仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。 |
|||
|
|||
````jsx |
|||
import { Gauge } from 'ant-design-pro/lib/Charts'; |
|||
|
|||
ReactDOM.render( |
|||
<Gauge |
|||
title="核销率" |
|||
height={164} |
|||
percent={87} |
|||
/> |
|||
, mountNode); |
|||
```` |
|||
@ -1,28 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,28 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,16 +0,0 @@ |
|||
--- |
|||
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 |
|||
); |
|||
``` |
|||
@ -1,12 +0,0 @@ |
|||
--- |
|||
order: 3 |
|||
title: 迷你进度条 |
|||
--- |
|||
|
|||
````jsx |
|||
import { MiniProgress } from 'ant-design-pro/lib/Charts'; |
|||
|
|||
ReactDOM.render( |
|||
<MiniProgress percent={78} strokeWidth={8} target={80} /> |
|||
, mountNode); |
|||
```` |
|||
@ -1,84 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,54 +0,0 @@ |
|||
--- |
|||
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, |
|||
); |
|||
``` |
|||
@ -1,64 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,25 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,27 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,20 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,15 +0,0 @@ |
|||
// 全局 G2 设置
|
|||
import { track, setTheme } from 'bizcharts'; |
|||
|
|||
track(false); |
|||
|
|||
const config = { |
|||
defaultColor: '#1089ff', |
|||
shape: { |
|||
interval: { |
|||
fillOpacity: 1, |
|||
}, |
|||
}, |
|||
}; |
|||
|
|||
setTheme(config); |
|||
@ -1,17 +0,0 @@ |
|||
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 }; |
|||
@ -1,49 +0,0 @@ |
|||
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, |
|||
}; |
|||
@ -1,19 +0,0 @@ |
|||
.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; |
|||
} |
|||
} |
|||
@ -1,132 +0,0 @@ |
|||
--- |
|||
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 | - | |
|||
@ -1,24 +0,0 @@ |
|||
--- |
|||
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); |
|||
```` |
|||
@ -1,9 +0,0 @@ |
|||
import * as React from 'react'; |
|||
export interface ICountDownProps { |
|||
format?: (time: number) => void; |
|||
target: Date | number; |
|||
onEnd?: () => void; |
|||
style?: React.CSSProperties; |
|||
} |
|||
|
|||
export default class CountDown extends React.Component<ICountDownProps, any> {} |
|||
@ -1,15 +0,0 @@ |
|||
--- |
|||
title: CountDown |
|||
cols: 1 |
|||
order: 3 |
|||
--- |
|||
|
|||
Simple CountDown Component. |
|||
|
|||
## API |
|||
|
|||
| Property | Description | Type | Default | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| format | Formatter of time | Function(time) | | |
|||
| target | Target time | Date | - | |
|||
| onEnd | Countdown to the end callback | funtion | -| |
|||
@ -1,121 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
|
|||
function fixedZero(val) { |
|||
return val * 1 < 10 ? `0${val}` : val; |
|||
} |
|||
const initTime = props => { |
|||
let lastTime = 0; |
|||
let targetTime = 0; |
|||
try { |
|||
if (Object.prototype.toString.call(props.target) === '[object Date]') { |
|||
targetTime = props.target.getTime(); |
|||
} else { |
|||
targetTime = new Date(props.target).getTime(); |
|||
} |
|||
} catch (e) { |
|||
throw new Error('invalid target prop', e); |
|||
} |
|||
|
|||
lastTime = targetTime - new Date().getTime(); |
|||
return { |
|||
lastTime: lastTime < 0 ? 0 : lastTime, |
|||
}; |
|||
}; |
|||
|
|||
class CountDown extends Component { |
|||
timer = 0; |
|||
|
|||
interval = 1000; |
|||
|
|||
constructor(props) { |
|||
super(props); |
|||
const { lastTime } = initTime(props); |
|||
this.state = { |
|||
lastTime, |
|||
}; |
|||
} |
|||
|
|||
static getDerivedStateFromProps(nextProps, preState) { |
|||
const { lastTime } = initTime(nextProps); |
|||
if (preState.lastTime !== lastTime) { |
|||
return { |
|||
lastTime, |
|||
}; |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
componentDidMount() { |
|||
this.tick(); |
|||
} |
|||
|
|||
componentDidUpdate(prevProps) { |
|||
const { target } = this.props; |
|||
if (target !== prevProps.target) { |
|||
clearTimeout(this.timer); |
|||
this.tick(); |
|||
} |
|||
} |
|||
|
|||
componentWillUnmount() { |
|||
clearTimeout(this.timer); |
|||
} |
|||
|
|||
// defaultFormat = time => (
|
|||
// <span>{moment(time).format('hh:mm:ss')}</span>
|
|||
// );
|
|||
defaultFormat = time => { |
|||
const hours = 60 * 60 * 1000; |
|||
const minutes = 60 * 1000; |
|||
|
|||
const h = Math.floor(time / hours); |
|||
const m = Math.floor((time - h * hours) / minutes); |
|||
const s = Math.floor((time - h * hours - m * minutes) / 1000); |
|||
return ( |
|||
<span> |
|||
{fixedZero(h)}:{fixedZero(m)}:{fixedZero(s)} |
|||
</span> |
|||
); |
|||
}; |
|||
|
|||
tick = () => { |
|||
const { onEnd } = this.props; |
|||
let { lastTime } = this.state; |
|||
|
|||
this.timer = setTimeout(() => { |
|||
if (lastTime < this.interval) { |
|||
clearTimeout(this.timer); |
|||
this.setState( |
|||
{ |
|||
lastTime: 0, |
|||
}, |
|||
() => { |
|||
if (onEnd) { |
|||
onEnd(); |
|||
} |
|||
} |
|||
); |
|||
} else { |
|||
lastTime -= this.interval; |
|||
this.setState( |
|||
{ |
|||
lastTime, |
|||
}, |
|||
() => { |
|||
this.tick(); |
|||
} |
|||
); |
|||
} |
|||
}, this.interval); |
|||
}; |
|||
|
|||
render() { |
|||
const { format = this.defaultFormat, onEnd, ...rest } = this.props; |
|||
const { lastTime } = this.state; |
|||
const result = format(lastTime); |
|||
|
|||
return <span {...rest}>{result}</span>; |
|||
} |
|||
} |
|||
|
|||
export default CountDown; |
|||
@ -1,16 +0,0 @@ |
|||
--- |
|||
title: CountDown |
|||
subtitle: 倒计时 |
|||
cols: 1 |
|||
order: 3 |
|||
--- |
|||
|
|||
倒计时组件。 |
|||
|
|||
## API |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| format | 时间格式化显示 | Function(time) | | |
|||
| target | 目标时间 | Date | - | |
|||
| onEnd | 倒计时结束回调 | funtion | -| |
|||
@ -1,9 +0,0 @@ |
|||
import * as React from 'react'; |
|||
|
|||
export default class Description extends React.Component< |
|||
{ |
|||
term: React.ReactNode; |
|||
style?: React.CSSProperties; |
|||
}, |
|||
any |
|||
> {} |
|||
@ -1,22 +0,0 @@ |
|||
import React from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { Col } from 'antd'; |
|||
import styles from './index.less'; |
|||
import responsive from './responsive'; |
|||
|
|||
const Description = ({ term, column, children, ...restProps }) => ( |
|||
<Col {...responsive[column]} {...restProps}> |
|||
{term && <div className={styles.term}>{term}</div>} |
|||
{children !== null && children !== undefined && <div className={styles.detail}>{children}</div>} |
|||
</Col> |
|||
); |
|||
|
|||
Description.defaultProps = { |
|||
term: '', |
|||
}; |
|||
|
|||
Description.propTypes = { |
|||
term: PropTypes.node, |
|||
}; |
|||
|
|||
export default Description; |
|||
@ -1,33 +0,0 @@ |
|||
import React from 'react'; |
|||
import classNames from 'classnames'; |
|||
import { Row } from 'antd'; |
|||
import styles from './index.less'; |
|||
|
|||
const DescriptionList = ({ |
|||
className, |
|||
title, |
|||
col = 3, |
|||
layout = 'horizontal', |
|||
gutter = 32, |
|||
children, |
|||
size, |
|||
...restProps |
|||
}) => { |
|||
const clsString = classNames(styles.descriptionList, styles[layout], className, { |
|||
[styles.small]: size === 'small', |
|||
[styles.large]: size === 'large', |
|||
}); |
|||
const column = col > 4 ? 4 : col; |
|||
return ( |
|||
<div className={clsString} {...restProps}> |
|||
{title ? <div className={styles.title}>{title}</div> : null} |
|||
<Row gutter={gutter}> |
|||
{React.Children.map(children, child => |
|||
child ? React.cloneElement(child, { column }) : child |
|||
)} |
|||
</Row> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default DescriptionList; |
|||
@ -1,43 +0,0 @@ |
|||
--- |
|||
order: 0 |
|||
title: |
|||
zh-CN: 基本 |
|||
en-US: Basic |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
基本描述列表。 |
|||
|
|||
## en-US |
|||
|
|||
Basic DescriptionList. |
|||
|
|||
````jsx |
|||
import DescriptionList from 'ant-design-pro/lib/DescriptionList'; |
|||
|
|||
const { Description } = DescriptionList; |
|||
|
|||
ReactDOM.render( |
|||
<DescriptionList size="large" title="title"> |
|||
<Description term="Firefox"> |
|||
A free, open source, cross-platform, |
|||
graphical web browser developed by the |
|||
Mozilla Corporation and hundreds of |
|||
volunteers. |
|||
</Description> |
|||
<Description term="Firefox"> |
|||
A free, open source, cross-platform, |
|||
graphical web browser developed by the |
|||
Mozilla Corporation and hundreds of |
|||
volunteers. |
|||
</Description> |
|||
<Description term="Firefox"> |
|||
A free, open source, cross-platform, |
|||
graphical web browser developed by the |
|||
Mozilla Corporation and hundreds of |
|||
volunteers. |
|||
</Description> |
|||
</DescriptionList> |
|||
, mountNode); |
|||
```` |
|||
@ -1,43 +0,0 @@ |
|||
--- |
|||
order: 1 |
|||
title: |
|||
zh-CN: 垂直型 |
|||
en-US: Vertical |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
垂直布局。 |
|||
|
|||
## en-US |
|||
|
|||
Vertical layout. |
|||
|
|||
````jsx |
|||
import DescriptionList from 'ant-design-pro/lib/DescriptionList'; |
|||
|
|||
const { Description } = DescriptionList; |
|||
|
|||
ReactDOM.render( |
|||
<DescriptionList size="large" title="title" layout="vertical"> |
|||
<Description term="Firefox"> |
|||
A free, open source, cross-platform, |
|||
graphical web browser developed by the |
|||
Mozilla Corporation and hundreds of |
|||
volunteers. |
|||
</Description> |
|||
<Description term="Firefox"> |
|||
A free, open source, cross-platform, |
|||
graphical web browser developed by the |
|||
Mozilla Corporation and hundreds of |
|||
volunteers. |
|||
</Description> |
|||
<Description term="Firefox"> |
|||
A free, open source, cross-platform, |
|||
graphical web browser developed by the |
|||
Mozilla Corporation and hundreds of |
|||
volunteers. |
|||
</Description> |
|||
</DescriptionList> |
|||
, mountNode); |
|||
```` |
|||
@ -1,15 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import Description from './Description'; |
|||
|
|||
export interface IDescriptionListProps { |
|||
layout?: 'horizontal' | 'vertical'; |
|||
col?: number; |
|||
title: React.ReactNode; |
|||
gutter?: number; |
|||
size?: 'large' | 'small'; |
|||
style?: React.CSSProperties; |
|||
} |
|||
|
|||
export default class DescriptionList extends React.Component<IDescriptionListProps, any> { |
|||
public static Description: typeof Description; |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
--- |
|||
title: DescriptionList |
|||
cols: 1 |
|||
order: 4 |
|||
--- |
|||
|
|||
Groups display multiple read-only fields, which are common to informational displays on detail pages. |
|||
|
|||
## API |
|||
|
|||
### DescriptionList |
|||
|
|||
| Property | Description | Type | Default | |
|||
|----------|------------------------------------------|-------------|---------| |
|||
| layout | type of layout | Enum{'horizontal', 'vertical'} | 'horizontal' | |
|||
| col | specify the maximum number of columns to display, the final columns number is determined by col setting combined with [Responsive Rules](/components/DescriptionList#Responsive-Rules) | number(0 < col <= 4) | 3 | |
|||
| title | title | ReactNode | - | |
|||
| gutter | specify the distance between two items, unit is `px` | number | 32 | |
|||
| size | size of list | Enum{'large', 'small'} | - | |
|||
|
|||
#### Responsive Rules |
|||
|
|||
| Window Width | Columns Number | |
|||
|---------------------|---------------------------------------------| |
|||
| `≥768px` | `col` | |
|||
| `≥576px` | `col < 2 ? col : 2` | |
|||
| `<576px` | `1` | |
|||
|
|||
### DescriptionList.Description |
|||
|
|||
| Property | Description | Type | Default | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| term | item title | ReactNode | - | |
|||
@ -1,5 +0,0 @@ |
|||
import DescriptionList from './DescriptionList'; |
|||
import Description from './Description'; |
|||
|
|||
DescriptionList.Description = Description; |
|||
export default DescriptionList; |
|||
@ -1,77 +0,0 @@ |
|||
@import '~antd/lib/style/themes/default.less'; |
|||
|
|||
.descriptionList { |
|||
// offset the padding-bottom of last row |
|||
:global { |
|||
.ant-row { |
|||
margin-bottom: -16px; |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
|
|||
.title { |
|||
font-size: 14px; |
|||
color: @heading-color; |
|||
font-weight: 500; |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
.term { |
|||
// Line-height is 22px IE dom height will calculate error |
|||
line-height: 20px; |
|||
padding-bottom: 16px; |
|||
margin-right: 8px; |
|||
color: @heading-color; |
|||
white-space: nowrap; |
|||
display: table-cell; |
|||
|
|||
&:after { |
|||
content: ':'; |
|||
margin: 0 8px 0 2px; |
|||
position: relative; |
|||
top: -0.5px; |
|||
} |
|||
} |
|||
|
|||
.detail { |
|||
line-height: 20px; |
|||
width: 100%; |
|||
padding-bottom: 16px; |
|||
color: @text-color; |
|||
display: table-cell; |
|||
} |
|||
|
|||
&.small { |
|||
// offset the padding-bottom of last row |
|||
:global { |
|||
.ant-row { |
|||
margin-bottom: -8px; |
|||
} |
|||
} |
|||
.title { |
|||
margin-bottom: 12px; |
|||
color: @text-color; |
|||
} |
|||
.term, |
|||
.detail { |
|||
padding-bottom: 8px; |
|||
} |
|||
} |
|||
|
|||
&.large { |
|||
.title { |
|||
font-size: 16px; |
|||
} |
|||
} |
|||
|
|||
&.vertical { |
|||
.term { |
|||
padding-bottom: 8px; |
|||
display: block; |
|||
} |
|||
|
|||
.detail { |
|||
display: block; |
|||
} |
|||
} |
|||
} |
|||
@ -1,37 +0,0 @@ |
|||
--- |
|||
title: DescriptionList |
|||
subtitle: 描述列表 |
|||
cols: 1 |
|||
order: 4 |
|||
--- |
|||
|
|||
成组展示多个只读字段,常见于详情页的信息展示。 |
|||
|
|||
## API |
|||
|
|||
### DescriptionList |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| layout | 布局方式 | Enum{'horizontal', 'vertical'} | 'horizontal' | |
|||
| col | 指定信息最多分几列展示,最终一行几列由 col 配置结合[响应式规则](/components/DescriptionList#响应式规则)决定 | number(0 < col <= 4) | 3 | |
|||
| title | 列表标题 | ReactNode | - | |
|||
| gutter | 列表项间距,单位为 `px` | number | 32 | |
|||
| size | 列表型号 | Enum{'large', 'small'} | - | |
|||
|
|||
#### 响应式规则 |
|||
|
|||
| 窗口宽度 | 展示列数 | |
|||
|---------------------|---------------------------------------------| |
|||
| `≥768px` | `col` | |
|||
| `≥576px` | `col < 2 ? col : 2` | |
|||
| `<576px` | `1` | |
|||
|
|||
### DescriptionList.Description |
|||
|
|||
| 参数 | 说明 | 类型 | 默认值 | |
|||
|----------|------------------------------------------|-------------|-------| |
|||
| term | 列表项标题 | ReactNode | - | |
|||
|
|||
|
|||
|
|||
@ -1,6 +0,0 @@ |
|||
export default { |
|||
1: { xs: 24 }, |
|||
2: { xs: 24, sm: 12 }, |
|||
3: { xs: 24, sm: 12, md: 8 }, |
|||
4: { xs: 24, sm: 12, md: 6 }, |
|||
}; |
|||
@ -1,31 +0,0 @@ |
|||
--- |
|||
order: 1 |
|||
title: |
|||
zh-CN: 按照行数省略 |
|||
en-US: Truncate according to the number of rows |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。但是在这种模式下所有 `children` 将会被转换成纯文本。 |
|||
|
|||
并且注意在这种模式下,外容器需要有指定的宽度(或设置自身宽度)。 |
|||
|
|||
## en-US |
|||
|
|||
`lines` attribute specifies the maximum number of rows where the text will automatically be truncated when exceeded. In this mode, all children will be converted to plain text. |
|||
|
|||
Also note that, in this mode, the outer container needs to have a specified width (or set its own width). |
|||
|
|||
|
|||
````jsx |
|||
import Ellipsis from 'ant-design-pro/lib/Ellipsis'; |
|||
|
|||
const article = <p>There were injuries alleged in three <a href="#cover">cases in 2015</a>, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.</p>; |
|||
|
|||
ReactDOM.render( |
|||
<div style={{ width: 200 }}> |
|||
<Ellipsis tooltip lines={3}>{article}</Ellipsis> |
|||
</div> |
|||
, mountNode); |
|||
```` |
|||
@ -1,28 +0,0 @@ |
|||
--- |
|||
order: 0 |
|||
title: |
|||
zh-CN: 按照字符数省略 |
|||
en-US: Truncate according to the number of character |
|||
--- |
|||
|
|||
## zh-CN |
|||
|
|||
通过设置 `length` 属性指定文本最长长度,如果超过这个长度会自动截取。 |
|||
|
|||
## en-US |
|||
|
|||
`length` attribute specifies the maximum length where the text will automatically be truncated when exceeded. |
|||
|
|||
````jsx |
|||
import Ellipsis from 'ant-design-pro/lib/Ellipsis'; |
|||
|
|||
const article = 'There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.'; |
|||
|
|||
ReactDOM.render( |
|||
<div> |
|||
<Ellipsis length={100}>{article}</Ellipsis> |
|||
<h4 style={{ marginTop: 24 }}>Show Tooltip</h4> |
|||
<Ellipsis length={100} tooltip>{article}</Ellipsis> |
|||
</div> |
|||
, mountNode); |
|||
```` |
|||
@ -1,21 +0,0 @@ |
|||
import * as React from 'react'; |
|||
import { TooltipProps } from 'antd/lib/tooltip'; |
|||
|
|||
export interface IEllipsisTooltipProps extends TooltipProps { |
|||
title?: undefined; |
|||
overlayStyle?: undefined; |
|||
} |
|||
|
|||
export interface IEllipsisProps { |
|||
tooltip?: boolean | IEllipsisTooltipProps; |
|||
length?: number; |
|||
lines?: number; |
|||
style?: React.CSSProperties; |
|||
className?: string; |
|||
fullWidthRecognition?: boolean; |
|||
} |
|||
|
|||
export function getStrFullLength(str: string): number; |
|||
export function cutStrByFullLength(str: string, maxLength: number): string; |
|||
|
|||
export default class Ellipsis extends React.Component<IEllipsisProps, any> {} |
|||
@ -1,16 +0,0 @@ |
|||
--- |
|||
title: Ellipsis |
|||
cols: 1 |
|||
order: 10 |
|||
--- |
|||
|
|||
When the text is too long, the Ellipsis automatically shortens it according to its length or the maximum number of lines. |
|||
|
|||
## API |
|||
|
|||
Property | Description | Type | Default |
|||
----|------|-----|------ |
|||
tooltip | tooltip for showing the full text content when hovering over | boolean | - |
|||
length | maximum number of characters in the text before being truncated | number | - |
|||
lines | maximum number of rows in the text before being truncated | number | `1` |
|||
fullWidthRecognition | whether consider full-width character length as 2 when calculate string length | boolean | - |
|||
@ -1,270 +0,0 @@ |
|||
import React, { Component } from 'react'; |
|||
import { Tooltip } from 'antd'; |
|||
import classNames from 'classnames'; |
|||
import styles from './index.less'; |
|||
|
|||
/* eslint react/no-did-mount-set-state: 0 */ |
|||
/* eslint no-param-reassign: 0 */ |
|||
|
|||
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined; |
|||
|
|||
const TooltipOverlayStyle = { |
|||
overflowWrap: 'break-word', |
|||
wordWrap: 'break-word', |
|||
}; |
|||
|
|||
export const getStrFullLength = (str = '') => |
|||
str.split('').reduce((pre, cur) => { |
|||
const charCode = cur.charCodeAt(0); |
|||
if (charCode >= 0 && charCode <= 128) { |
|||
return pre + 1; |
|||
} |
|||
return pre + 2; |
|||
}, 0); |
|||
|
|||
export const cutStrByFullLength = (str = '', maxLength) => { |
|||
let showLength = 0; |
|||
return str.split('').reduce((pre, cur) => { |
|||
const charCode = cur.charCodeAt(0); |
|||
if (charCode >= 0 && charCode <= 128) { |
|||
showLength += 1; |
|||
} else { |
|||
showLength += 2; |
|||
} |
|||
if (showLength <= maxLength) { |
|||
return pre + cur; |
|||
} |
|||
return pre; |
|||
}, ''); |
|||
}; |
|||
|
|||
const getTooltip = ({ tooltip, overlayStyle, title, children }) => { |
|||
if (tooltip) { |
|||
const props = tooltip === true ? { overlayStyle, title } : { ...tooltip, overlayStyle, title }; |
|||
return <Tooltip {...props}>{children}</Tooltip>; |
|||
} |
|||
return children; |
|||
}; |
|||
|
|||
const EllipsisText = ({ text, length, tooltip, fullWidthRecognition, ...other }) => { |
|||
if (typeof text !== 'string') { |
|||
throw new Error('Ellipsis children must be string.'); |
|||
} |
|||
const textLength = fullWidthRecognition ? getStrFullLength(text) : text.length; |
|||
if (textLength <= length || length < 0) { |
|||
return <span {...other}>{text}</span>; |
|||
} |
|||
const tail = '...'; |
|||
let displayText; |
|||
if (length - tail.length <= 0) { |
|||
displayText = ''; |
|||
} else { |
|||
displayText = fullWidthRecognition ? cutStrByFullLength(text, length) : text.slice(0, length); |
|||
} |
|||
|
|||
const spanAttrs = tooltip ? {} : { ...other }; |
|||
return getTooltip({ |
|||
tooltip, |
|||
overlayStyle: TooltipOverlayStyle, |
|||
title: text, |
|||
children: ( |
|||
<span {...spanAttrs}> |
|||
{displayText} |
|||
{tail} |
|||
</span> |
|||
), |
|||
}); |
|||
}; |
|||
|
|||
export default class Ellipsis extends Component { |
|||
state = { |
|||
text: '', |
|||
targetCount: 0, |
|||
}; |
|||
|
|||
componentDidMount() { |
|||
if (this.node) { |
|||
this.computeLine(); |
|||
} |
|||
} |
|||
|
|||
componentDidUpdate(perProps) { |
|||
const { lines } = this.props; |
|||
if (lines !== perProps.lines) { |
|||
this.computeLine(); |
|||
} |
|||
} |
|||
|
|||
computeLine = () => { |
|||
const { lines } = this.props; |
|||
if (lines && !isSupportLineClamp) { |
|||
const text = this.shadowChildren.innerText || this.shadowChildren.textContent; |
|||
const lineHeight = parseInt(getComputedStyle(this.root).lineHeight, 10); |
|||
const targetHeight = lines * lineHeight; |
|||
this.content.style.height = `${targetHeight}px`; |
|||
const totalHeight = this.shadowChildren.offsetHeight; |
|||
const shadowNode = this.shadow.firstChild; |
|||
|
|||
if (totalHeight <= targetHeight) { |
|||
this.setState({ |
|||
text, |
|||
targetCount: text.length, |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
// bisection
|
|||
const len = text.length; |
|||
const mid = Math.ceil(len / 2); |
|||
|
|||
const count = this.bisection(targetHeight, mid, 0, len, text, shadowNode); |
|||
|
|||
this.setState({ |
|||
text, |
|||
targetCount: count, |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
bisection = (th, m, b, e, text, shadowNode) => { |
|||
const suffix = '...'; |
|||
let mid = m; |
|||
let end = e; |
|||
let begin = b; |
|||
shadowNode.innerHTML = text.substring(0, mid) + suffix; |
|||
let sh = shadowNode.offsetHeight; |
|||
|
|||
if (sh <= th) { |
|||
shadowNode.innerHTML = text.substring(0, mid + 1) + suffix; |
|||
sh = shadowNode.offsetHeight; |
|||
if (sh > th || mid === begin) { |
|||
return mid; |
|||
} |
|||
begin = mid; |
|||
if (end - begin === 1) { |
|||
mid = 1 + begin; |
|||
} else { |
|||
mid = Math.floor((end - begin) / 2) + begin; |
|||
} |
|||
return this.bisection(th, mid, begin, end, text, shadowNode); |
|||
} |
|||
if (mid - 1 < 0) { |
|||
return mid; |
|||
} |
|||
shadowNode.innerHTML = text.substring(0, mid - 1) + suffix; |
|||
sh = shadowNode.offsetHeight; |
|||
if (sh <= th) { |
|||
return mid - 1; |
|||
} |
|||
end = mid; |
|||
mid = Math.floor((end - begin) / 2) + begin; |
|||
return this.bisection(th, mid, begin, end, text, shadowNode); |
|||
}; |
|||
|
|||
handleRoot = n => { |
|||
this.root = n; |
|||
}; |
|||
|
|||
handleContent = n => { |
|||
this.content = n; |
|||
}; |
|||
|
|||
handleNode = n => { |
|||
this.node = n; |
|||
}; |
|||
|
|||
handleShadow = n => { |
|||
this.shadow = n; |
|||
}; |
|||
|
|||
handleShadowChildren = n => { |
|||
this.shadowChildren = n; |
|||
}; |
|||
|
|||
render() { |
|||
const { text, targetCount } = this.state; |
|||
const { |
|||
children, |
|||
lines, |
|||
length, |
|||
className, |
|||
tooltip, |
|||
fullWidthRecognition, |
|||
...restProps |
|||
} = this.props; |
|||
|
|||
const cls = classNames(styles.ellipsis, className, { |
|||
[styles.lines]: lines && !isSupportLineClamp, |
|||
[styles.lineClamp]: lines && isSupportLineClamp, |
|||
}); |
|||
|
|||
if (!lines && !length) { |
|||
return ( |
|||
<span className={cls} {...restProps}> |
|||
{children} |
|||
</span> |
|||
); |
|||
} |
|||
|
|||
// length
|
|||
if (!lines) { |
|||
return ( |
|||
<EllipsisText |
|||
className={cls} |
|||
length={length} |
|||
text={children || ''} |
|||
tooltip={tooltip} |
|||
fullWidthRecognition={fullWidthRecognition} |
|||
{...restProps} |
|||
/> |
|||
); |
|||
} |
|||
|
|||
const id = `antd-pro-ellipsis-${`${new Date().getTime()}${Math.floor(Math.random() * 100)}`}`; |
|||
|
|||
// support document.body.style.webkitLineClamp
|
|||
if (isSupportLineClamp) { |
|||
const style = `#${id}{-webkit-line-clamp:${lines};-webkit-box-orient: vertical;}`; |
|||
|
|||
const node = ( |
|||
<div id={id} className={cls} {...restProps}> |
|||
<style>{style}</style> |
|||
{children} |
|||
</div> |
|||
); |
|||
|
|||
return getTooltip({ |
|||
tooltip, |
|||
overlayStyle: TooltipOverlayStyle, |
|||
title: children, |
|||
children: node, |
|||
}); |
|||
} |
|||
|
|||
const childNode = ( |
|||
<span ref={this.handleNode}> |
|||
{targetCount > 0 && text.substring(0, targetCount)} |
|||
{targetCount > 0 && targetCount < text.length && '...'} |
|||
</span> |
|||
); |
|||
|
|||
return ( |
|||
<div {...restProps} ref={this.handleRoot} className={cls}> |
|||
<div ref={this.handleContent}> |
|||
{getTooltip({ |
|||
tooltip, |
|||
overlayStyle: TooltipOverlayStyle, |
|||
title: text, |
|||
children: childNode, |
|||
})} |
|||
<div className={styles.shadow} ref={this.handleShadowChildren}> |
|||
{children} |
|||
</div> |
|||
<div className={styles.shadow} ref={this.handleShadow}> |
|||
<span>{text}</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
.ellipsis { |
|||
overflow: hidden; |
|||
display: inline-block; |
|||
word-break: break-all; |
|||
width: 100%; |
|||
} |
|||
|
|||
.lines { |
|||
position: relative; |
|||
.shadow { |
|||
display: block; |
|||
position: absolute; |
|||
color: transparent; |
|||
opacity: 0; |
|||
z-index: -999; |
|||
} |
|||
} |
|||
|
|||
.lineClamp { |
|||
position: relative; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
display: -webkit-box; |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue