;
7 |
8 | export type IReactComponent =
9 | | React.StatelessComponent
10 | | React.ComponentClass
11 | | React.ClassicComponentClass
;
12 |
13 | interface Secured {
14 | (authority: authority, error?: React.ReactNode): (
15 | target: T,
16 | ) => T;
17 | }
18 |
19 | export interface AuthorizedRouteProps extends RouteProps {
20 | authority: authority;
21 | }
22 | export class AuthorizedRoute extends React.Component<
23 | AuthorizedRouteProps,
24 | any
25 | > {}
26 |
27 | interface check {
28 | (
29 | authority: authority,
30 | target: T,
31 | Exception: S,
32 | ): T | S;
33 | }
34 |
35 | interface AuthorizedProps {
36 | authority: authority;
37 | noMatch?: React.ReactNode;
38 | }
39 |
40 | export class Authorized extends React.Component {
41 | static Secured: Secured;
42 | static AuthorizedRoute: typeof AuthorizedRoute;
43 | static check: check;
44 | }
45 |
46 | declare function renderAuthorize(currentAuthority: string): typeof Authorized;
47 |
48 | export default renderAuthorize;
49 |
--------------------------------------------------------------------------------
/src/components/Charts/Bar/shape/baseLine.js:
--------------------------------------------------------------------------------
1 | const baseLine = (cfg, container, shape, transpose) => {
2 | const points = cfg.points;
3 | let path = [];
4 |
5 | if(transpose){
6 | path.push(['M', points[0].x, points[0].y]);
7 | path.push(['L', points[1].x, 1]);
8 | path.push(['L', points[2].x, 1]);
9 | path.push(['L', points[3].x, points[3].y]);
10 | }else{
11 | path.push(['M', points[0].x, 1]);
12 | path.push(['L', points[1].x, points[1].y]);
13 | path.push(['L', points[2].x, points[2].y]);
14 | path.push(['L', points[3].x, 1]);
15 | }
16 | path.push('Z');
17 | path = shape.parsePath(path); // 将 0 - 1 转化为画布坐标
18 | const setting = transpose ? {
19 | x: path[0][1],
20 | y: path[2][2],
21 | width: path[2][1] - path[0][1],
22 | height: path[0][2] - path[2][2],
23 | fill: cfg.color,
24 | radius: path[2][1] - path[0][1] > path[0][2] - path[2][2] ? (path[0][2] - path[2][2]) / 2:0,
25 | opacity: 0.3,
26 | } : {
27 | x: path[1][1],
28 | y: path[1][2],
29 | width: path[2][1] - path[1][1],
30 | height: path[0][2] - path[1][2],
31 | fill: cfg.color,
32 | radius: path[0][2] - path[1][2] > path[2][1] - path[1][1] ? (path[2][1] - path[1][1]) / 2 : 0,
33 | opacity: 0.3,
34 | };
35 | return setting;
36 | };
37 |
38 | export default baseLine;
39 |
--------------------------------------------------------------------------------
/src/models/tab.js:
--------------------------------------------------------------------------------
1 | const panes = JSON.parse(sessionStorage.getItem('panes'));
2 |
3 | export default {
4 | namespace: 'tab',
5 |
6 | state: {
7 | panes: panes ? panes.panes : [
8 | {
9 | title: '今日动态',
10 | key: '/dashboard/report',
11 | content: ''
12 | }
13 | ],
14 | activeKey: panes ? panes.activeKey : '/dashboard/report'
15 | },
16 |
17 | reducers: {
18 | update(state, { payload }) {
19 | const findIndex = state.panes.find(item => item.title === payload.title)
20 | const panes = findIndex === undefined ? [...state.panes, payload] : state.panes
21 | sessionStorage.setItem('panes', JSON.stringify({
22 | panes,
23 | activeKey: payload.key,
24 | }));
25 | return {
26 | panes,
27 | activeKey: payload.key,
28 | }
29 | },
30 | check(state, { activeKey }) {
31 | sessionStorage.setItem('panes', JSON.stringify({
32 | ...state,
33 | activeKey
34 | }));
35 | return {
36 | ...state,
37 | activeKey
38 | }
39 | },
40 | delete(state, { payload }) {
41 | const { findIndex, lastKey } = payload;
42 | let panes = state.panes.concat();
43 | panes.splice(findIndex, 1);
44 | sessionStorage.setItem('panes', JSON.stringify({
45 | panes,
46 | activeKey: lastKey,
47 | }));
48 | return {
49 | panes,
50 | activeKey: lastKey
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/components/Login/map.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Input, Icon } from 'antd';
3 | import styles from './index.less';
4 |
5 | const map = {
6 | UserName: {
7 | component: Input,
8 | props: {
9 | size: 'large',
10 | prefix: ,
11 | placeholder: 'admin',
12 | },
13 | rules: [{
14 | required: true, message: 'Please enter username!',
15 | }],
16 | },
17 | Password: {
18 | component: Input,
19 | props: {
20 | size: 'large',
21 | prefix: ,
22 | type: 'password',
23 | placeholder: '888888',
24 | },
25 | rules: [{
26 | required: true, message: 'Please enter password!',
27 | }],
28 | },
29 | Mobile: {
30 | component: Input,
31 | props: {
32 | size: 'large',
33 | prefix: ,
34 | placeholder: 'mobile number',
35 | },
36 | rules: [{
37 | required: true, message: 'Please enter mobile number!',
38 | }, {
39 | pattern: /^1\d{10}$/, message: 'Wrong mobile number format!',
40 | }],
41 | },
42 | Captcha: {
43 | component: Input,
44 | props: {
45 | size: 'large',
46 | prefix: ,
47 | placeholder: 'captcha',
48 | },
49 | rules: [{
50 | required: true, message: 'Please enter Captcha!',
51 | }],
52 | },
53 | };
54 |
55 | export default map;
56 |
--------------------------------------------------------------------------------
/src/components/Charts/autoHeight.js:
--------------------------------------------------------------------------------
1 | /* eslint eqeqeq: 0 */
2 | import React from 'react';
3 |
4 | function computeHeight(node) {
5 | const totalHeight = parseInt(getComputedStyle(node).height, 10);
6 | const padding =
7 | parseInt(getComputedStyle(node).paddingTop, 10) +
8 | parseInt(getComputedStyle(node).paddingBottom, 10);
9 | return totalHeight - padding;
10 | }
11 |
12 | function getAutoHeight(n) {
13 | if (!n) {
14 | return 0;
15 | }
16 |
17 | let node = n;
18 |
19 | let height = computeHeight(node);
20 |
21 | while (!height) {
22 | node = node.parentNode;
23 | if (node) {
24 | height = computeHeight(node);
25 | } else {
26 | break;
27 | }
28 | }
29 |
30 | return height;
31 | }
32 |
33 | const autoHeight = () => (WrappedComponent) => {
34 | return class extends React.Component {
35 | state = {
36 | computedHeight: 0,
37 | };
38 |
39 | componentDidMount() {
40 | const { height } = this.props;
41 | if (!height) {
42 | const h = getAutoHeight(this.root);
43 | // eslint-disable-next-line
44 | this.setState({ computedHeight: h });
45 | }
46 | }
47 |
48 | handleRoot = (node) => {
49 | this.root = node;
50 | };
51 |
52 | render() {
53 | const { height } = this.props;
54 | const { computedHeight } = this.state;
55 | const h = height || computedHeight;
56 | return (
57 | {h > 0 && }
58 | );
59 | }
60 | };
61 | };
62 |
63 | export default autoHeight;
64 |
--------------------------------------------------------------------------------
/src/components/PageHeader/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | en-US: PageHeader
4 | zh-CN: PageHeader
5 | subtitle: 页头
6 | cols: 1
7 | order: 11
8 | ---
9 |
10 | 页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。
11 |
12 | ## API
13 |
14 | | 参数 | 说明 | 类型 | 默认值 |
15 | |----------|------------------------------------------|-------------|-------|
16 | | title | title 区域 | ReactNode | - |
17 | | logo | logo区域 | ReactNode | - |
18 | | action | 操作区,位于 title 行的行尾 | ReactNode | - |
19 | | content | 内容区 | ReactNode | - |
20 | | extraContent | 额外内容区,位于content的右侧 | ReactNode | - |
21 | | breadcrumbList | 面包屑数据,配置了此属性时 `routes` `params` `location` `breadcrumbNameMap` 无效 | array<{title: ReactNode, href?: string}> | - |
22 | | routes | 面包屑相关属性,router 的路由栈信息 | object[] | - |
23 | | params | 面包屑相关属性,路由的参数 | object | - |
24 | | location | 面包屑相关属性,当前的路由信息 | object | - |
25 | | breadcrumbNameMap | 面包屑相关属性,路由的地址-名称映射表 | object | - |
26 | | tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | - |
27 | | tabActiveKey | 当前高亮的 tab 项 | string | - |
28 | | tabDefaultActiveKey | 默认高亮的 tab 项 | string | 第一项 |
29 | | onTabChange | 切换面板的回调 | (key) => void | - |
30 | | linkElement | 定义链接的元素,默认为 `a`,可传入 react-router 的 Link | string\|ReactElement | - |
31 |
32 | > 面包屑的配置方式有三种,一是直接配置 `breadcrumbList`,二是结合 `react-router@2` `react-router@3`,配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router),三是结合 `react-router@4`,配置 `location` `breadcrumbNameMap`,优先级依次递减,脚手架中使用最后一种。 对于后两种用法,你也可以将 `routes` `params` 及 `location` `breadcrumbNameMap` 放到 context 中,组件会自动获取。
33 |
--------------------------------------------------------------------------------
/src/components/PageHeader/index.test.js:
--------------------------------------------------------------------------------
1 | import { getBreadcrumb } from './index';
2 | import { urlToList } from '../utils/pathTools';
3 |
4 | const routerData = {
5 | '/dashboard/analysis': {
6 | name: '分析页',
7 | },
8 | '/userinfo': {
9 | name: '用户列表',
10 | },
11 | '/userinfo/:id': {
12 | name: '用户信息',
13 | },
14 | '/userinfo/:id/addr': {
15 | name: '收货订单',
16 | },
17 | };
18 | describe('test getBreadcrumb', () => {
19 | it('Simple url', () => {
20 | expect(getBreadcrumb(routerData, '/dashboard/analysis').name).toEqual(
21 | '分析页',
22 | );
23 | });
24 | it('Parameters url', () => {
25 | expect(getBreadcrumb(routerData, '/userinfo/2144').name).toEqual(
26 | '用户信息',
27 | );
28 | });
29 | it('The middle parameter url', () => {
30 | expect(getBreadcrumb(routerData, '/userinfo/2144/addr').name).toEqual(
31 | '收货订单',
32 | );
33 | });
34 | it('Loop through the parameters', () => {
35 | const urlNameList = urlToList('/userinfo/2144/addr').map((url) => {
36 | return getBreadcrumb(routerData, url).name;
37 | });
38 | expect(urlNameList).toEqual(['用户列表', '用户信息', '收货订单']);
39 | });
40 |
41 | it('a path', () => {
42 | const urlNameList = urlToList('/userinfo').map((url) => {
43 | return getBreadcrumb(routerData, url).name;
44 | });
45 | expect(urlNameList).toEqual(['用户列表']);
46 | });
47 | it('Secondary path', () => {
48 | const urlNameList = urlToList('/userinfo/2144').map((url) => {
49 | return getBreadcrumb(routerData, url).name;
50 | });
51 | expect(urlNameList).toEqual(['用户列表', '用户信息']);
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/src/components/NumberInfo/index.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .numberInfo {
4 | .suffix {
5 | color: @text-color;
6 | font-size: 16px;
7 | font-style: normal;
8 | margin-left: 4px;
9 | }
10 | .numberInfoTitle {
11 | color: @text-color;
12 | font-size: @font-size-lg;
13 | margin-bottom: 16px;
14 | transition: all .3s;
15 | }
16 | .numberInfoSubTitle {
17 | color: @text-color-secondary;
18 | font-size: @font-size-base;
19 | height: 22px;
20 | line-height: 22px;
21 | overflow: hidden;
22 | text-overflow: ellipsis;
23 | word-break: break-all;
24 | white-space: nowrap;
25 | }
26 | .numberInfoValue {
27 | margin-top: 4px;
28 | font-size: 0;
29 | overflow: hidden;
30 | text-overflow: ellipsis;
31 | word-break: break-all;
32 | white-space: nowrap;
33 | & > span {
34 | color: @heading-color;
35 | display: inline-block;
36 | line-height: 32px;
37 | height: 32px;
38 | font-size: 24px;
39 | margin-right: 32px;
40 | }
41 | .subTotal {
42 | color: @text-color-secondary;
43 | font-size: @font-size-lg;
44 | vertical-align: top;
45 | margin-right: 0;
46 | i {
47 | font-size: 12px;
48 | transform: scale(0.82);
49 | margin-left: 4px;
50 | }
51 | :global {
52 | .anticon-caret-up {
53 | color: @red-6;
54 | }
55 | .anticon-caret-down {
56 | color: @green-6;
57 | }
58 | }
59 | }
60 | }
61 | }
62 | .numberInfolight {
63 | .numberInfoValue {
64 | & > span {
65 | color: @text-color;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/NoticeList.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .list {
4 | max-height: 400px;
5 | overflow: auto;
6 | .item {
7 | transition: all .3s;
8 | overflow: hidden;
9 | cursor: pointer;
10 | padding-left: 24px;
11 | padding-right: 24px;
12 |
13 | .meta {
14 | width: 100%;
15 | }
16 |
17 | .avatar {
18 | background: #fff;
19 | margin-top: 4px;
20 | }
21 |
22 | &.read {
23 | opacity: .4;
24 | }
25 | &:last-child {
26 | border-bottom: 0;
27 | }
28 | &:hover {
29 | background: @primary-1;
30 | }
31 | .title {
32 | font-weight: normal;
33 | margin-bottom: 8px;
34 | }
35 | .description {
36 | font-size: 12px;
37 | line-height: @line-height-base;
38 | }
39 | .datetime {
40 | font-size: 12px;
41 | margin-top: 4px;
42 | line-height: @line-height-base;
43 | }
44 | .extra {
45 | float: right;
46 | color: @text-color-secondary;
47 | font-weight: normal;
48 | margin-right: 0;
49 | margin-top: -1.5px;
50 | }
51 | }
52 | }
53 |
54 | .notFound {
55 | text-align: center;
56 | padding: 73px 0 88px 0;
57 | color: @text-color-secondary;
58 | img {
59 | display: inline-block;
60 | margin-bottom: 16px;
61 | height: 76px;
62 | }
63 | }
64 |
65 | .clear {
66 | height: 46px;
67 | line-height: 46px;
68 | text-align: center;
69 | color: @text-color;
70 | border-radius: 0 0 @border-radius-base @border-radius-base;
71 | border-top: 1px solid @border-color-split;
72 | transition: all .3s;
73 | cursor: pointer;
74 |
75 | &:hover {
76 | color: @heading-color;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/Authorized/PromiseRender.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Spin } from 'antd';
3 |
4 | export default class PromiseRender extends React.PureComponent {
5 | state = {
6 | component: null,
7 | };
8 | componentDidMount() {
9 | this.setRenderComponent(this.props);
10 | }
11 | componentWillReceiveProps(nextProps) {
12 | // new Props enter
13 | this.setRenderComponent(nextProps);
14 | }
15 | // set render Component : ok or error
16 | setRenderComponent(props) {
17 | const ok = this.checkIsInstantiation(props.ok);
18 | const error = this.checkIsInstantiation(props.error);
19 | props.promise
20 | .then(() => {
21 | this.setState({
22 | component: ok,
23 | });
24 | })
25 | .catch(() => {
26 | this.setState({
27 | component: error,
28 | });
29 | });
30 | }
31 | // Determine whether the incoming component has been instantiated
32 | // AuthorizedRoute is already instantiated
33 | // Authorized render is already instantiated, children is no instantiated
34 | // Secured is not instantiated
35 | checkIsInstantiation = (target) => {
36 | if (!React.isValidElement(target)) {
37 | return target;
38 | }
39 | return () => target;
40 | };
41 | render() {
42 | const Component = this.state.component;
43 | return Component ? (
44 |
45 | ) : (
46 |
55 |
56 |
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/Login/index.en-US.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Login
3 | cols: 1
4 | order: 15
5 | ---
6 |
7 | Support multiple common ways of login with built-in controls. You can choose your own combinations and use with your custom controls.
8 |
9 | ## API
10 |
11 | ### Login
12 |
13 | Property | Description | Type | Default
14 | ----|------|-----|------
15 | defaultActiveKey | default key to activate the tab panel | String | -
16 | onTabChange | callback on changing tabs | (key) => void | -
17 | onSubmit | callback on submit | (err, values) => void | -
18 |
19 | ### Login.Tab
20 |
21 | Property | Description | Type | Default
22 | ----|------|-----|------
23 | key | key of the tab | String | -
24 | tab | displayed text of the tab | ReactNode | -
25 |
26 | ### Login.UserName
27 |
28 | Property | Description | Type | Default
29 | ----|------|-----|------
30 | name | name of the control, also the key of the submitted data | String | -
31 | rules | validation rules, same with [option.rules](getFieldDecorator(id, options)) in Form getFieldDecorator(id, options) | object[] | -
32 |
33 | Apart from the above properties, Login.Username also support all properties of antd.Input, together with the default values of basic settings, such as _placeholder_, _size_ and _prefix_. All of these default values can be over-written.
34 |
35 | ### Login.Password, Login.Mobile are the same as Login.UserName
36 |
37 | ### Login.Captcha
38 |
39 | Property | Description | Type | Default
40 | ----|------|-----|------
41 | onGetCaptcha | callback on getting a new Captcha | () => void | -
42 |
43 | Apart from the above properties, _Login.Captcha_ support the same properties with _Login.UserName_.
44 |
45 | ### Login.Submit
46 |
47 | Support all properties of _antd.Button_.
--------------------------------------------------------------------------------
/src/components/PageHeader/demo/structure.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title: Structure
4 | ---
5 |
6 | 基本结构,具备响应式布局功能,主要断点为 768px 和 576px,拖动窗口改变大小试试看。
7 |
8 | ````jsx
9 | import PageHeader from 'ant-design-pro/lib/PageHeader';
10 |
11 | const breadcrumbList = [{
12 | title: '面包屑',
13 | }];
14 |
15 | const tabList = [{
16 | key: '1',
17 | tab: '页签一',
18 | }, {
19 | key: '2',
20 | tab: '页签二',
21 | }, {
22 | key: '3',
23 | tab: '页签三',
24 | }];
25 |
26 | ReactDOM.render(
27 | }
31 | logo={logo
}
32 | action={action
}
33 | content={content
}
34 | extraContent={extraContent
}
35 | breadcrumbList={breadcrumbList}
36 | tabList={tabList}
37 | tabActiveKey="1"
38 | />
39 |
40 | , mountNode);
41 | ````
42 |
43 |
69 |
--------------------------------------------------------------------------------
/src/routes/Dashboard/Indicator/Appointment.less:
--------------------------------------------------------------------------------
1 | .legendWrap {
2 | display: flex;
3 | flex-wrap: wrap;
4 | flex-direction: row;
5 | width: 200px;
6 | .legend {
7 | width: 100px;
8 | margin: 15px 0;
9 | .mark {
10 | display: inline-block;
11 | width: 10px;
12 | height: 10px;
13 | border-radius: 50%;
14 | margin-right: 5px;
15 | }
16 | .count {
17 | font-size: 20px;
18 | color: #333;
19 | padding-left: 15px;
20 | }
21 | }
22 | }
23 | .appointmentCountWrap {
24 | padding: 0 10px;
25 | width: 33.33%;
26 | height: 100px;
27 | margin-bottom: 20px;
28 | display: inline-block;
29 | vertical-align: top;
30 | .appointmentCount {
31 | border: 1px solid #E8E8E8;
32 | border-radius: 2px;
33 | width: 100%;
34 | height: 100%;
35 | display: flex;
36 | align-items: center;
37 | padding: 0 15px;
38 | .left {
39 | flex: 1;
40 | display: flex;
41 | align-items: center;
42 | justify-content: center;
43 | color: #666;
44 | text-align: center;
45 | }
46 | .right {
47 | flex: 2.5;
48 | padding-left: 10px;
49 | .between {
50 | display: flex;
51 | justify-content: space-between;
52 | align-items: center;
53 | }
54 | .number {
55 | font-size: 18px;
56 | color: #15984B;
57 | }
58 | .title {
59 | color: #333;
60 | }
61 | }
62 | }
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/src/routes/Exception/triggerException.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Button, Spin, Card } from 'antd';
3 | import { connect } from 'dva';
4 | import styles from './style.less';
5 |
6 | @connect(state => ({
7 | isloading: state.error.isloading,
8 | }))
9 | export default class TriggerException extends PureComponent {
10 | state={
11 | isloading: false,
12 | }
13 | trigger401 = () => {
14 | this.setState({
15 | isloading: true,
16 | });
17 | this.props.dispatch({
18 | type: 'error/query401',
19 | });
20 | };
21 | trigger403 = () => {
22 | this.setState({
23 | isloading: true,
24 | });
25 | this.props.dispatch({
26 | type: 'error/query403',
27 | });
28 | };
29 | trigger500 = () => {
30 | this.setState({
31 | isloading: true,
32 | });
33 | this.props.dispatch({
34 | type: 'error/query500',
35 | });
36 | };
37 | trigger404 = () => {
38 | this.setState({
39 | isloading: true,
40 | });
41 | this.props.dispatch({
42 | type: 'error/query404',
43 | });
44 | };
45 | render() {
46 | return (
47 |
48 |
49 |
52 |
55 |
58 |
61 |
62 |
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/models/date.js:
--------------------------------------------------------------------------------
1 | import { getDate, computeDays, getRangePickerValue } from '../utils/utils';
2 |
3 | const date = JSON.parse(sessionStorage.getItem('date'));
4 |
5 | const reportBeginDate = date ? date.report.beginDate : getDate(new Date().getTime()-24*60*60*1000);
6 | const reportEndDate = date ? date.report.endDate : getDate(new Date().getTime()-24*60*60*1000);
7 |
8 | const indicatorBeginDate = date ? date.indicator.beginDate : getDate(new Date().getTime()-24*60*60*1000);
9 | const indicatorEndDate = date ? date.indicator.endDate : getDate(new Date().getTime()-24*60*60*1000);
10 |
11 | export default {
12 | namespace: 'date',
13 |
14 | state: {
15 | report: {
16 | beginDate: reportBeginDate,
17 | endDate: reportEndDate,
18 | rangePickerValue: getRangePickerValue(reportBeginDate, reportEndDate),
19 | rangeDateType: computeDays(reportBeginDate, reportEndDate) < 14 ? 'daily' : 'monthly',
20 | rangeDate: computeDays(reportBeginDate, reportEndDate) + 1,
21 | isOneDay: computeDays(reportBeginDate, reportEndDate) === 0
22 | },
23 | indicator: {
24 | beginDate: indicatorBeginDate,
25 | endDate: indicatorEndDate,
26 | rangePickerValue: getRangePickerValue(indicatorBeginDate, indicatorEndDate),
27 | rangeDateType: computeDays(indicatorBeginDate, indicatorEndDate) < 14 ? 'daily' : 'monthly',
28 | rangeDate: computeDays(indicatorBeginDate, indicatorEndDate) + 1,
29 | isOneDay: computeDays(indicatorBeginDate, indicatorEndDate) === 0
30 | }
31 | },
32 |
33 | reducers: {
34 | //第二个参数为dispatch的对象
35 | update(state, { payload }) {
36 | sessionStorage.setItem('date', JSON.stringify({
37 | ...state,
38 | ...payload,
39 | }));
40 | return {
41 | ...state,
42 | ...payload
43 | }
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/models/global.js:
--------------------------------------------------------------------------------
1 | import { queryNotices } from '../services/api';
2 |
3 | export default {
4 | namespace: 'global',
5 |
6 | state: {
7 | collapsed: false,
8 | notices: [],
9 | },
10 |
11 | effects: {
12 | *fetchNotices(_, { call, put }) {
13 | const data = yield call(queryNotices);
14 | yield put({
15 | type: 'saveNotices',
16 | payload: data,
17 | });
18 | yield put({
19 | type: 'user/changeNotifyCount',
20 | payload: data.length,
21 | });
22 | },
23 | *clearNotices({ payload }, { put, select }) {
24 | yield put({
25 | type: 'saveClearedNotices',
26 | payload,
27 | });
28 | const count = yield select(state => state.global.notices.length);
29 | yield put({
30 | type: 'user/changeNotifyCount',
31 | payload: count,
32 | });
33 | },
34 | },
35 |
36 | reducers: {
37 | changeLayoutCollapsed(state, { payload }) {
38 | return {
39 | ...state,
40 | collapsed: payload,
41 | };
42 | },
43 | saveNotices(state, { payload }) {
44 | return {
45 | ...state,
46 | notices: payload,
47 | };
48 | },
49 | saveClearedNotices(state, { payload }) {
50 | return {
51 | ...state,
52 | notices: state.notices.filter(item => item.type !== payload),
53 | };
54 | },
55 | },
56 |
57 | subscriptions: {
58 | setup({ history }) {
59 | // Subscribe history(url) change, trigger `load` action if pathname is `/`
60 | return history.listen(({ pathname, search }) => {
61 | if (typeof window.ga !== 'undefined') {
62 | window.ga('send', 'pageview', pathname + search);
63 | }
64 | });
65 | },
66 | },
67 | };
68 |
--------------------------------------------------------------------------------
/src/routes/Dashboard/Report/Dynamic.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .wrapCard {
4 | display: inline-block;
5 | padding: 0 10px;
6 | margin-bottom: 24px;
7 | }
8 | .wrapPrice {
9 | border-bottom: 1px solid #e8e8e8;
10 | padding-bottom: 20px;
11 | margin-bottom: 32px;
12 | .price {
13 | width: 100%;
14 | text-align: center;
15 | font-size: 48px;
16 | color: #15984B;
17 | }
18 | .compare {
19 | text-align: center;
20 | }
21 | }
22 | .vertical {
23 | position: absolute;
24 | top: 46%;
25 | transform: translateY(-50%);
26 | width: 1px;
27 | height: 50px;
28 | background: #e8e8e8;
29 | }
30 | .monthlyInpatientCardTitle {
31 | height: 40px;
32 | color: #fff;
33 | padding: 10px 20px;
34 | font-weight: 500;
35 | background: #FEA101;
36 | }
37 | .monthlyInpatientCardContent {
38 | position: relative;
39 | display: flex;
40 | .part {
41 | flex: 1;
42 | text-align: center;
43 | .partOne {
44 | color: #999;
45 | padding: 10px 0;
46 | }
47 | .partTwo {
48 | color: #FEA101;
49 | font-size: 36px;
50 | margin-bottom: 5px;
51 | }
52 | .partThree {
53 | display: inline-block;
54 | padding-left: 14px;
55 | }
56 | }
57 | }
58 | .burdenCard {
59 | background-color: #FAFFF9;
60 | border-color: #B4D9C4;
61 | border-radius: 8px;
62 | .item {
63 | font-size: 15px;
64 | text-align: center;
65 | color: #666;
66 | height: 40px;
67 | display: flex;
68 | justify-content: center;
69 | align-items: center;
70 | }
71 | .count {
72 | text-align: center;
73 | color: #333;
74 | font-size: 24px;
75 | }
76 | .compare {
77 | border-top: 1px solid #B4D9C4;
78 | text-align: center;
79 | padding-top: 10px;
80 | margin-top: 10px;
81 | }
82 | }
--------------------------------------------------------------------------------
/src/components/Exception/index.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .exception {
4 | display: flex;
5 | align-items: center;
6 | height: 100%;
7 |
8 | .imgBlock {
9 | flex: 0 0 62.5%;
10 | width: 62.5%;
11 | padding-right: 152px;
12 | zoom: 1;
13 | &:before,
14 | &:after {
15 | content: " ";
16 | display: table;
17 | }
18 | &:after {
19 | clear: both;
20 | visibility: hidden;
21 | font-size: 0;
22 | height: 0;
23 | }
24 | }
25 |
26 | .imgEle {
27 | height: 360px;
28 | width: 100%;
29 | max-width: 430px;
30 | float: right;
31 | background-repeat: no-repeat;
32 | background-position: 50% 50%;
33 | background-size: contain;
34 | }
35 |
36 | .content {
37 | flex: auto;
38 |
39 | h1 {
40 | color: #434e59;
41 | font-size: 72px;
42 | font-weight: 600;
43 | line-height: 72px;
44 | margin-bottom: 24px;
45 | }
46 |
47 | .desc {
48 | color: @text-color-secondary;
49 | font-size: 20px;
50 | line-height: 28px;
51 | margin-bottom: 16px;
52 | }
53 |
54 | .actions {
55 | button:not(:last-child) {
56 | margin-right: 8px;
57 | }
58 | }
59 | }
60 | }
61 |
62 | @media screen and (max-width: @screen-xl) {
63 | .exception {
64 | .imgBlock {
65 | padding-right: 88px;
66 | }
67 | }
68 | }
69 |
70 | @media screen and (max-width: @screen-sm) {
71 | .exception {
72 | display: block;
73 | text-align: center;
74 | .imgBlock {
75 | padding-right: 0;
76 | margin: 0 auto 24px;
77 | }
78 | }
79 | }
80 |
81 | @media screen and (max-width: @screen-xs) {
82 | .exception {
83 | .imgBlock {
84 | margin-bottom: -24px;
85 | overflow: hidden;
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/components/Authorized/CheckPermissions.test.js:
--------------------------------------------------------------------------------
1 | import { checkPermissions } from './CheckPermissions.js';
2 |
3 | const target = 'ok';
4 | const error = 'error';
5 |
6 | describe('test CheckPermissions', () => {
7 | it('Correct string permission authentication', () => {
8 | expect(checkPermissions('user', 'user', target, error)).toEqual('ok');
9 | });
10 | it('Correct string permission authentication', () => {
11 | expect(checkPermissions('user', 'NULL', target, error)).toEqual('error');
12 | });
13 | it('authority is undefined , return ok', () => {
14 | expect(checkPermissions(null, 'NULL', target, error)).toEqual('ok');
15 | });
16 | it('currentAuthority is undefined , return error', () => {
17 | expect(checkPermissions('admin', null, target, error)).toEqual('error');
18 | });
19 | it('Wrong string permission authentication', () => {
20 | expect(checkPermissions('admin', 'user', target, error)).toEqual('error');
21 | });
22 | it('Correct Array permission authentication', () => {
23 | expect(checkPermissions(['user', 'admin'], 'user', target, error)).toEqual(
24 | 'ok'
25 | );
26 | });
27 | it('Wrong Array permission authentication,currentAuthority error', () => {
28 | expect(
29 | checkPermissions(['user', 'admin'], 'user,admin', target, error)
30 | ).toEqual('error');
31 | });
32 | it('Wrong Array permission authentication', () => {
33 | expect(checkPermissions(['user', 'admin'], 'guest', target, error)).toEqual(
34 | 'error'
35 | );
36 | });
37 | it('Wrong Function permission authentication', () => {
38 | expect(checkPermissions(() => false, 'guest', target, error)).toEqual(
39 | 'error'
40 | );
41 | });
42 | it('Correct Function permission authentication', () => {
43 | expect(checkPermissions(() => true, 'guest', target, error)).toEqual('ok');
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/src/components/PageHeader/demo/image.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 2
3 | title: With Image
4 | ---
5 |
6 | 带图片的页头。
7 |
8 | ````jsx
9 | import PageHeader from 'ant-design-pro/lib/PageHeader';
10 |
11 | const content = (
12 |
13 |
段落示意:蚂蚁金服务设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。
14 |
25 |
26 | );
27 |
28 | const extra = (
29 |
30 |

31 |
32 | );
33 |
34 | const breadcrumbList = [{
35 | title: '一级菜单',
36 | href: '/',
37 | }, {
38 | title: '二级菜单',
39 | href: '/',
40 | }, {
41 | title: '三级菜单',
42 | }];
43 |
44 | ReactDOM.render(
45 |
53 | , mountNode);
54 | ````
55 |
56 |
76 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/NoticeList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Avatar, List } from 'antd';
3 | import classNames from 'classnames';
4 | import styles from './NoticeList.less';
5 |
6 | export default function NoticeList({
7 | data = [], onClick, onClear, title, locale, emptyText, emptyImage,
8 | }) {
9 | if (data.length === 0) {
10 | return (
11 |
12 | {emptyImage ? (
13 |

14 | ) : null}
15 |
{emptyText || locale.emptyText}
16 |
17 | );
18 | }
19 | return (
20 |
21 |
22 | {data.map((item, i) => {
23 | const itemCls = classNames(styles.item, {
24 | [styles.read]: item.read,
25 | });
26 | return (
27 | onClick(item)}>
28 | : null}
31 | title={
32 |
33 | {item.title}
34 |
{item.extra}
35 |
36 | }
37 | description={
38 |
39 |
40 | {item.description}
41 |
42 |
{item.datetime}
43 |
44 | }
45 | />
46 |
47 | );
48 | })}
49 |
50 |
51 | {locale.clear}{title}
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Authorized/CheckPermissions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PromiseRender from './PromiseRender';
3 | import { CURRENT } from './index';
4 |
5 | function isPromise(obj) {
6 | return (
7 | !!obj &&
8 | (typeof obj === 'object' || typeof obj === 'function') &&
9 | typeof obj.then === 'function'
10 | );
11 | }
12 |
13 | /**
14 | * 通用权限检查方法
15 | * Common check permissions method
16 | * @param { 权限判定 Permission judgment type string |array | Promise | Function } authority
17 | * @param { 你的权限 Your permission description type:string} currentAuthority
18 | * @param { 通过的组件 Passing components } target
19 | * @param { 未通过的组件 no pass components } Exception
20 | */
21 | const checkPermissions = (authority, currentAuthority, target, Exception) => {
22 | // 没有判定权限.默认查看所有
23 | // Retirement authority, return target;
24 | if (!authority) {
25 | return target;
26 | }
27 | // 数组处理
28 | if (Array.isArray(authority)) {
29 | if (authority.indexOf(currentAuthority) >= 0) {
30 | return target;
31 | }
32 | return Exception;
33 | }
34 |
35 | // string 处理
36 | if (typeof authority === 'string') {
37 | if (authority === currentAuthority) {
38 | return target;
39 | }
40 | return Exception;
41 | }
42 |
43 | // Promise 处理
44 | if (isPromise(authority)) {
45 | return ;
46 | }
47 |
48 | // Function 处理
49 | if (typeof authority === 'function') {
50 | try {
51 | const bool = authority(currentAuthority);
52 | if (bool) {
53 | return target;
54 | }
55 | return Exception;
56 | } catch (error) {
57 | throw error;
58 | }
59 | }
60 | throw new Error('unsupported parameters');
61 | };
62 |
63 | export { checkPermissions };
64 |
65 | const check = (authority, target, Exception) => {
66 | return checkPermissions(authority, CURRENT, target, Exception);
67 | };
68 |
69 | export default check;
70 |
--------------------------------------------------------------------------------
/src/components/Authorized/Secured.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Exception from '../Exception/index';
3 | import CheckPermissions from './CheckPermissions';
4 | /**
5 | * 默认不能访问任何页面
6 | * default is "NULL"
7 | */
8 | const Exception403 = () => (
9 |
10 | );
11 |
12 | // Determine whether the incoming component has been instantiated
13 | // AuthorizedRoute is already instantiated
14 | // Authorized render is already instantiated, children is no instantiated
15 | // Secured is not instantiated
16 | const checkIsInstantiation = (target) => {
17 | if (!React.isValidElement(target)) {
18 | return target;
19 | }
20 | return () => target;
21 | };
22 |
23 | /**
24 | * 用于判断是否拥有权限访问此view权限
25 | * authority 支持传入 string ,funtion:()=>boolean|Promise
26 | * e.g. 'user' 只有user用户能访问
27 | * e.g. 'user,admin' user和 admin 都能访问
28 | * e.g. ()=>boolean 返回true能访问,返回false不能访问
29 | * e.g. Promise then 能访问 catch不能访问
30 | * e.g. authority support incoming string, funtion: () => boolean | Promise
31 | * e.g. 'user' only user user can access
32 | * e.g. 'user, admin' user and admin can access
33 | * e.g. () => boolean true to be able to visit, return false can not be accessed
34 | * e.g. Promise then can not access the visit to catch
35 | * @param {string | function | Promise} authority
36 | * @param {ReactNode} error 非必需参数
37 | */
38 | const authorize = (authority, error) => {
39 | /**
40 | * conversion into a class
41 | * 防止传入字符串时找不到staticContext造成报错
42 | * String parameters can cause staticContext not found error
43 | */
44 | let classError = false;
45 | if (error) {
46 | classError = () => error;
47 | }
48 | if (!authority) {
49 | throw new Error('authority is required');
50 | }
51 | return function decideAuthority(targer) {
52 | const component = CheckPermissions(authority, targer, classError || Exception403);
53 | return checkIsInstantiation(component);
54 | };
55 | };
56 |
57 | export default authorize;
58 |
--------------------------------------------------------------------------------
/.roadhogrc.mock.js:
--------------------------------------------------------------------------------
1 | import { delay } from 'roadhog-api-doc';
2 | import mock_dynamic from './mock/Report/dynamic';
3 | import mock_clinic from './mock/Report/clinic';
4 | import mock_prescription from './mock/Report/prescription';
5 | import mock_inpatient from './mock/Report/inpatient';
6 | import mock_nonDrug from './mock/Report/nonDrug';
7 | import mock_income from './mock/Report/income';
8 |
9 | import mock_appointment from './mock/Indicator/appointment';
10 | import { getMockBedSpace } from './mock/Indicator/bedSpace';
11 | import mock_consumable from './mock/Indicator/consumable';
12 | import { getMockEmphasis } from './mock/Indicator/emphasis';
13 | import { getMockQuality } from './mock/Indicator/quality';
14 | import { getMockSurgery } from './mock/Indicator/surgery';
15 | // 是否禁用mock
16 | const noProxy = process.env.NO_PROXY === 'true';
17 | console.log('proxy:' + noProxy);
18 | // 代码中会兼容本地 service mock 以及部署站点的静态数据
19 | const proxy = {
20 | 'GET /api/currentUser': {
21 | $desc: "获取当前用户接口",
22 | $params: {
23 | pageSize: {
24 | desc: '分页',
25 | exp: 2,
26 | },
27 | },
28 | $body: {
29 | name: '沈浩',
30 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
31 | userid: '00000001',
32 | notifyCount: 12,
33 | },
34 | },
35 | 'GET /api/today_dynamic_info': mock_dynamic,
36 | 'GET /api/total_outemer_info': mock_clinic,
37 | 'GET /api/tcm_prescription_info': mock_prescription,
38 | 'GET /api/non_drug_info': mock_nonDrug,
39 | 'GET /api/in_out_hospital': mock_inpatient,
40 | 'GET /api/income': mock_income,
41 |
42 | 'GET /api/consumable_info': mock_consumable,
43 | 'GET /api/reservation_info': mock_appointment,
44 | 'GET /api/surgery_info': getMockSurgery,
45 | 'GET /api/bed_info': getMockBedSpace,
46 | 'GET /api/medical_treat_quality': getMockQuality,
47 | 'GET /api/data_operation_info': getMockEmphasis,
48 | };
49 |
50 | export default noProxy ? {
51 | 'GET /api/(.*)': 'http://msodc.xinhua.com/',
52 | } : delay(proxy, 500);
53 |
--------------------------------------------------------------------------------
/src/components/Charts/Pie/index.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .pie {
4 | position: relative;
5 | display: flex;
6 | justify-content: space-around;
7 | align-items: center;
8 | .chart {
9 | position: relative;
10 | }
11 | &.hasLegend .chart {
12 | // width: ~"calc(65% - 100px)";
13 | width: 55%;
14 | }
15 | .legend {
16 | position: relative;
17 | // position: absolute;
18 | // right: 0;
19 | // top: 50%;
20 | // transform: translateY(-50%);
21 | margin: 0;
22 | list-style: none;
23 | padding: 0;
24 | li {
25 | cursor: pointer;
26 | margin-bottom: 16px;
27 | height: 22px;
28 | line-height: 22px;
29 | &:last-child {
30 | margin-bottom: 0;
31 | }
32 | }
33 | }
34 | .dot {
35 | border-radius: 8px;
36 | display: inline-block;
37 | margin-right: 8px;
38 | position: relative;
39 | top: -1px;
40 | height: 8px;
41 | width: 8px;
42 | }
43 | .line {
44 | background-color: @border-color-split;
45 | display: inline-block;
46 | margin-right: 8px;
47 | width: 1px;
48 | height: 16px;
49 | }
50 | .legendTitle {
51 | color: #333;
52 | }
53 | .percent {
54 | color: @text-color-secondary;
55 | }
56 | .value {
57 | position: absolute;
58 | right: 0;
59 | }
60 | .title {
61 | margin-bottom: 8px;
62 | }
63 | .total {
64 | position: absolute;
65 | left: 50%;
66 | top: 50%;
67 | text-align: center;
68 | height: 62px;
69 | transform: translate(-50%, -50%);
70 | & > h4 {
71 | color: @text-color-secondary;
72 | font-size: 14px;
73 |
74 | margin-top: 5px;
75 | font-weight: normal;
76 | }
77 | & > p {
78 | color: @heading-color;
79 | display: block;
80 |
81 | white-space: nowrap;
82 | }
83 | }
84 | }
85 |
86 | .legendBlock {
87 | &.hasLegend .chart {
88 | width: 100%;
89 | margin: 0 0 32px 0;
90 | }
91 | .legend {
92 | position: relative;
93 | transform: none;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "plugins": ["compat"],
5 | "env": {
6 | "browser": true,
7 | "node": true,
8 | "es6": true,
9 | "mocha": true,
10 | "jest": true,
11 | "jasmine": true
12 | },
13 | "rules": {
14 | "generator-star-spacing": [0],
15 | "consistent-return": [0],
16 | "react/forbid-prop-types": [0],
17 | "react/jsx-filename-extension": [1, { "extensions": [".js"] }],
18 | "global-require": [1],
19 | "import/prefer-default-export": [0],
20 | "react/jsx-no-bind": [0],
21 | "react/prop-types": [0],
22 | "react/prefer-stateless-function": [0],
23 | "react/jsx-wrap-multilines": ["error", {
24 | "declaration": "parens-new-line",
25 | "assignment": "parens-new-line",
26 | "return": "parens-new-line",
27 | "arrow": "parens-new-line",
28 | "condition": "parens-new-line",
29 | "logical": "parens-new-line",
30 | "prop": "ignore"
31 | }],
32 | "no-else-return": [0],
33 | "no-restricted-syntax": [0],
34 | "import/no-extraneous-dependencies": [0],
35 | "no-use-before-define": [0],
36 | "jsx-a11y/no-static-element-interactions": [0],
37 | "jsx-a11y/no-noninteractive-element-interactions": [0],
38 | "jsx-a11y/click-events-have-key-events": [0],
39 | "jsx-a11y/anchor-is-valid": [0],
40 | "no-nested-ternary": [0],
41 | "arrow-body-style": [0],
42 | "import/extensions": [0],
43 | "no-bitwise": [0],
44 | "no-cond-assign": [0],
45 | "import/no-unresolved": [0],
46 | "comma-dangle": ["error", {
47 | "arrays": "always-multiline",
48 | "objects": "always-multiline",
49 | "imports": "always-multiline",
50 | "exports": "always-multiline",
51 | "functions": "ignore"
52 | }],
53 | "object-curly-newline": [0],
54 | "function-paren-newline": [0],
55 | "no-restricted-globals": [0],
56 | "require-yield": [1],
57 | "compat/compat": "error"
58 | },
59 | "parserOptions": {
60 | "ecmaFeatures": {
61 | "experimentalObjectRestSpread": true
62 | }
63 | },
64 | "settings": {
65 | "polyfills": ["fetch", "promises"]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/TabNav/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Tabs, Button } from 'antd';
3 | import { routerRedux } from 'dva/router';
4 | import { Link } from 'dva/router';
5 | import { connect } from 'dva';
6 | import styles from './index.less';
7 | const TabPane = Tabs.TabPane;
8 |
9 | @connect(({ tab }) => ({
10 | tab,
11 | }))
12 |
13 | class TabNav extends React.Component {
14 | constructor(props) {
15 | super(props);
16 | }
17 |
18 | onChange = (activeKey) => {
19 | const { search } = this.props;
20 | let path = {
21 | pathname: activeKey,
22 | search
23 | };
24 | this.props.dispatch({
25 | type: 'tab/check',
26 | activeKey
27 | })
28 | this.props.dispatch(routerRedux.push(path));
29 | }
30 | //其实操控dispatch无异于操控state,区别在于将state的公有化,供其他模块使用
31 | onEdit = (targetKey, action) => {
32 | this[action](targetKey);
33 | }
34 | remove = (targetKey) => {
35 | const { tab } = this.props;
36 | const { panes, activeKey } = tab;
37 | const panesLength = panes.length;
38 | let lastIndex = 0;
39 | const findIndex = panes.findIndex(item => item.key === targetKey);
40 | findIndex === panesLength - 1 ? lastIndex = panesLength - 2 : lastIndex = findIndex + 1;
41 | const lastKey = lastIndex >= 0 ? panes[lastIndex].key : 'empty';
42 | let path = {
43 | pathname: lastKey,
44 | };
45 | this.props.dispatch(routerRedux.push(path));
46 | this.props.dispatch({
47 | type: 'tab/delete',
48 | payload: {
49 | findIndex,
50 | lastKey
51 | }
52 | })
53 | }
54 | render() {
55 | const { tab } = this.props;
56 | const { panes, activeKey } = tab;
57 | return (
58 |
59 |
69 | {panes.map(pane => )}
70 |
71 |
72 | );
73 | }
74 | }
75 |
76 | export default TabNav;
--------------------------------------------------------------------------------
/src/components/Authorized/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title:
3 | en-US: Authorized
4 | zh-CN: Authorized
5 | subtitle: 权限
6 | cols: 1
7 | order: 15
8 | ---
9 |
10 | 权限组件,通过比对现有权限与准入权限,决定相关元素的展示。
11 |
12 | ## API
13 |
14 | ### RenderAuthorized
15 |
16 | `RenderAuthorized: (currentAuthority: string | () => string) => Authorized`
17 |
18 | 权限组件默认 export RenderAuthorized 函数,它接收当前权限作为参数,返回一个权限对象,该对象提供以下几种使用方式。
19 |
20 |
21 | ### Authorized
22 |
23 | 最基础的权限控制。
24 |
25 | | 参数 | 说明 | 类型 | 默认值 |
26 | |----------|------------------------------------------|-------------|-------|
27 | | children | 正常渲染的元素,权限判断通过时展示 | ReactNode | - |
28 | | authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean` | - |
29 | | noMatch | 权限异常渲染元素,权限判断不通过时展示 | ReactNode | - |
30 |
31 | ### Authorized.AuthorizedRoute
32 |
33 | | 参数 | 说明 | 类型 | 默认值 |
34 | |----------|------------------------------------------|-------------|-------|
35 | | authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean` | - |
36 | | redirectPath | 权限异常时重定向的页面路由 | string | - |
37 |
38 | 其余参数与 `Route` 相同。
39 |
40 | ### Authorized.Secured
41 |
42 | 注解方式,`@Authorized.Secured(authority, error)`
43 |
44 | | 参数 | 说明 | 类型 | 默认值 |
45 | |----------|------------------------------------------|-------------|-------|
46 | | authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean` | - |
47 | | error | 权限异常时渲染元素 | ReactNode | |
48 |
49 | ### Authorized.check
50 |
51 | 函数形式的 Authorized,用于某些不能被 HOC 包裹的组件。 `Authorized.check(authority, target, Exception)`
52 | 注意:传入一个 Promise 时,无论正确还是错误返回的都是一个 ReactClass。
53 |
54 | | 参数 | 说明 | 类型 | 默认值 |
55 | |----------|------------------------------------------|-------------|-------|
56 | | authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean` | - |
57 | | target | 权限判断通过时渲染的元素 | ReactNode | - |
58 | | Exception | 权限异常时渲染元素 | ReactNode | - |
59 |
--------------------------------------------------------------------------------
/src/routes/Dashboard/Report/NonDrug.less:
--------------------------------------------------------------------------------
1 | .columnRank {
2 | display: flex;
3 | flex-wrap: wrap;
4 | flex-direction: column;
5 | height: 764px;
6 | }
7 | .wrapCard {
8 | display: inline-block;
9 | width: 50%;
10 | padding: 0 10px;
11 | margin-bottom: 20px;
12 | .countCard {
13 | background: #FFF8F6;
14 | border: 1px solid #FAE0DA;
15 | border-radius: 2px;
16 | :global {
17 | .ant-card-body:before, .ant-card-body:after {
18 | content: none;
19 | }
20 | }
21 | }
22 |
23 | }
24 | .dailyCard {
25 | .part1 {
26 | flex: 2;
27 | color: #333;
28 | padding-right: 20px;
29 | }
30 | .part2 {
31 | flex: 1;
32 | display: flex;
33 | // justify-content: center;
34 | align-items: center;
35 | padding-left: 20px;
36 | border-left: 1px solid #ccc;
37 | height: 22px;
38 | text-align: center;
39 | font-size: 24px;
40 | color: #333;
41 | }
42 | }
43 | .monthlyCard {
44 | .part1 {
45 | padding-left: 38px;
46 | color: #666;
47 | height: 42px;
48 | display: flex;
49 | align-items: center;
50 | // justify-content: flex-end;
51 | margin-bottom: 4px;
52 | }
53 | .part2 {
54 | display: flex;
55 | align-items: center;
56 | .left {
57 | flex: 1;
58 | font-size: 24px;
59 | text-align: right;
60 | padding-right: 7%;
61 | color: #333;
62 | }
63 | .right {
64 | width: 85px;
65 | text-align: right;
66 | padding-left: 5%;
67 | color: #4A4A4A;
68 | }
69 | }
70 | }
71 | .one {
72 | border-top: 30px solid #FECD01;
73 | border-left: 30px solid #FECD01;
74 | }
75 | .two {
76 | border-top: 30px solid #D8D8D8;
77 | border-left: 30px solid #D8D8D8;
78 | }
79 | .three {
80 | border-top: 30px solid #F0D49B;
81 | border-left: 30px solid #F0D49B;
82 | }
83 | .arrow {
84 | position: absolute;
85 | top: 0;
86 | left: 0;
87 | border-bottom: 30px solid transparent;
88 | border-right: 30px solid transparent;
89 | .num {
90 | position: absolute;
91 | top: -29px;
92 | left: -18px;
93 | color: #fff;
94 | font-size: 20px;
95 | }
96 | }
97 | .square {
98 | width: 46px;
99 | height: 30px;
100 | background: #FFC4B5;
101 | border-radius: 2px;
102 | position: absolute;
103 | top: 0;
104 | left: 0;
105 | line-height: 30px;
106 | text-align: center;
107 | color: #fff;
108 | font-size: 20px;
109 | }
--------------------------------------------------------------------------------
/src/components/Result/demo/classic.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 1
3 | title: Classic
4 | ---
5 |
6 | 典型结果页面。
7 |
8 | ````jsx
9 | import Result from 'ant-design-pro/lib/Result';
10 | import { Button, Row, Col, Icon, Steps } from 'antd';
11 |
12 | const { Step } = Steps;
13 |
14 | const desc1 = (
15 |
16 |
17 | 曲丽丽
18 |
19 |
20 |
2016-12-12 12:32
21 |
22 | );
23 |
24 | const desc2 = (
25 |
26 |
27 | 周毛毛
28 |
29 |
30 |
31 |
32 | );
33 |
34 | const extra = (
35 |
36 |
37 | 项目名称
38 |
39 |
40 |
41 | 项目 ID:
42 | 23421
43 |
44 |
45 | 负责人:
46 | 曲丽丽
47 |
48 |
49 | 生效时间:
50 | 2016-12-12 ~ 2017-12-12
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 |
62 | const actions = (
63 |
64 |
65 |
66 |
67 |
68 | );
69 |
70 | ReactDOM.render(
71 |
79 | , mountNode);
80 | ````
81 |
--------------------------------------------------------------------------------
/src/components/GlobalHeader/index.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .header {
4 | height: 64px;
5 | padding: 0 12px 0 0;
6 | background: #fff;
7 | box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
8 | position: relative;
9 | }
10 |
11 | :global {
12 | .ant-layout {
13 | min-height: 100vh;
14 | overflow-x: hidden;
15 | }
16 | }
17 |
18 | .logo {
19 | height: 64px;
20 | line-height: 58px;
21 | vertical-align: top;
22 | display: inline-block;
23 | padding: 0 0 0 24px;
24 | cursor: pointer;
25 | font-size: 20px;
26 | img {
27 | display: inline-block;
28 | vertical-align: middle;
29 | }
30 | }
31 |
32 | .menu {
33 | :global(.anticon) {
34 | margin-right: 8px;
35 | }
36 | :global(.ant-dropdown-menu-item) {
37 | width: 160px;
38 | }
39 | }
40 |
41 | i.trigger {
42 | font-size: 20px;
43 | line-height: 64px;
44 | cursor: pointer;
45 | transition: all .3s, padding 0s;
46 | padding: 0 24px;
47 | &:hover {
48 | background: @primary-1;
49 | }
50 | }
51 |
52 | .right {
53 | float: right;
54 | height: 100%;
55 | .action {
56 | cursor: pointer;
57 | padding: 0 12px;
58 | display: inline-block;
59 | transition: all .3s;
60 | height: 100%;
61 | > i {
62 | font-size: 16px;
63 | vertical-align: middle;
64 | color: @text-color;
65 | }
66 | &:hover,
67 | &:global(.ant-popover-open) {
68 | background: @primary-1;
69 | }
70 | }
71 | .search {
72 | padding: 0;
73 | margin: 0 12px;
74 | &:hover {
75 | background: transparent;
76 | }
77 | }
78 | .account {
79 | .avatar {
80 | margin: 20px 8px 20px 0;
81 | color: @primary-color;
82 | background: rgba(255, 255, 255, .85);
83 | vertical-align: middle;
84 | }
85 | }
86 | }
87 |
88 | @media only screen and (max-width: @screen-md) {
89 | .header {
90 | :global(.ant-divider-vertical) {
91 | vertical-align: unset;
92 | }
93 | .name {
94 | display: none;
95 | }
96 | i.trigger {
97 | padding: 0 12px;
98 | }
99 | .logo {
100 | padding-right: 12px;
101 | position: relative;
102 | }
103 | .right {
104 | position: absolute;
105 | right: 12px;
106 | top: 0;
107 | background: #fff;
108 | .account {
109 | .avatar {
110 | margin-right: 0;
111 | }
112 | }
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/HeaderSearch/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Input, Icon, AutoComplete } from 'antd';
4 | import classNames from 'classnames';
5 | import styles from './index.less';
6 |
7 | export default class HeaderSearch extends PureComponent {
8 | static defaultProps = {
9 | defaultActiveFirstOption: false,
10 | onPressEnter: () => {},
11 | onSearch: () => {},
12 | className: '',
13 | placeholder: '',
14 | dataSource: [],
15 | };
16 | static propTypes = {
17 | className: PropTypes.string,
18 | placeholder: PropTypes.string,
19 | onSearch: PropTypes.func,
20 | onPressEnter: PropTypes.func,
21 | defaultActiveFirstOption: PropTypes.bool,
22 | dataSource: PropTypes.array,
23 | };
24 | state = {
25 | searchMode: false,
26 | value: '',
27 | };
28 | componentWillUnmount() {
29 | clearTimeout(this.timeout);
30 | }
31 | onKeyDown = (e) => {
32 | if (e.key === 'Enter') {
33 | this.timeout = setTimeout(() => {
34 | this.props.onPressEnter(this.state.value); // Fix duplicate onPressEnter
35 | }, 0);
36 | }
37 | }
38 | onChange = (value) => {
39 | this.setState({ value });
40 | if (this.props.onChange) {
41 | this.props.onChange();
42 | }
43 | }
44 | enterSearchMode = () => {
45 | this.setState({ searchMode: true }, () => {
46 | if (this.state.searchMode) {
47 | this.input.focus();
48 | }
49 | });
50 | }
51 | leaveSearchMode = () => {
52 | this.setState({
53 | searchMode: false,
54 | value: '',
55 | });
56 | }
57 | render() {
58 | const { className, placeholder, ...restProps } = this.props;
59 | const inputClass = classNames(styles.input, {
60 | [styles.show]: this.state.searchMode,
61 | });
62 | return (
63 |
67 |
68 |
75 | { this.input = node; }}
78 | onKeyDown={this.onKeyDown}
79 | onBlur={this.leaveSearchMode}
80 | />
81 |
82 |
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/PageHeader/demo/standard.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 1
3 | title: Standard
4 | ---
5 |
6 | 标准页头。
7 |
8 | ````jsx
9 | import PageHeader from 'ant-design-pro/lib/PageHeader';
10 | import DescriptionList from 'ant-design-pro/lib/DescriptionList';
11 | import { Button, Menu, Dropdown, Icon, Row, Col } from 'antd';
12 |
13 | const { Description } = DescriptionList;
14 | const ButtonGroup = Button.Group;
15 |
16 | const description = (
17 |
18 | 曲丽丽
19 | XX 服务
20 | 2017-07-07
21 | 12421
22 |
23 | );
24 |
25 | const menu = (
26 |
31 | );
32 |
33 | const action = (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 |
46 | const extra = (
47 |
48 |
49 | 状态
50 | 待审批
51 |
52 |
53 | 订单金额
54 | ¥ 568.08
55 |
56 |
57 | );
58 |
59 | const breadcrumbList = [{
60 | title: '一级菜单',
61 | href: '/',
62 | }, {
63 | title: '二级菜单',
64 | href: '/',
65 | }, {
66 | title: '三级菜单',
67 | }];
68 |
69 | const tabList = [{
70 | key: 'detail',
71 | tab: '详情',
72 | }, {
73 | key: 'rule',
74 | tab: '规则',
75 | }];
76 |
77 | function onTabChange(key) {
78 | console.log(key);
79 | }
80 |
81 | ReactDOM.render(
82 |
83 |
}
86 | action={action}
87 | content={description}
88 | extraContent={extra}
89 | breadcrumbList={breadcrumbList}
90 | tabList={tabList}
91 | tabActiveKey="detail"
92 | onTabChange={onTabChange}
93 | />
94 |
95 | , mountNode);
96 | ````
97 |
98 |
103 |
--------------------------------------------------------------------------------
/src/components/GlobalHeader/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Menu, Icon, Spin, Tag, Dropdown, Avatar, Divider, Tooltip } from 'antd';
3 | import moment from 'moment';
4 | import groupBy from 'lodash/groupBy';
5 | import Debounce from 'lodash-decorators/debounce';
6 | import { Link } from 'dva/router';
7 | import NoticeIcon from '../NoticeIcon';
8 | import HeaderSearch from '../HeaderSearch';
9 | import styles from './index.less';
10 |
11 | export default class GlobalHeader extends PureComponent {
12 | state = {
13 | token: window._csrf ? window._csrf.token : 'token'
14 | }
15 | componentWillUnmount() {
16 | this.triggerResizeEvent.cancel();
17 | }
18 | toggle = () => {
19 | const { collapsed, onCollapse } = this.props;
20 | onCollapse(!collapsed);
21 | this.triggerResizeEvent();
22 | }
23 | @Debounce(600)
24 | triggerResizeEvent() { // eslint-disable-line
25 | const event = document.createEvent('HTMLEvents');
26 | event.initEvent('resize', true, false);
27 | window.dispatchEvent(event);
28 | }
29 | logoOut = () => {
30 | document.querySelector('#logoutForm').submit();
31 | }
32 | render() {
33 | const {
34 | currentUser, collapsed, fetchingNotices, isMobile, logo,
35 | onNoticeVisibleChange, onMenuClick, onNoticeClear,
36 | } = this.props;
37 | const { token } = this.state
38 | const menu = (
39 |
42 | );
43 | return (
44 |
45 | {isMobile && (
46 | [
47 | (
48 |
49 |

50 |
51 | ),
52 |
,
53 | ]
54 | )}
55 |
60 |
61 | {currentUser ? (
62 |
63 |
64 | {currentUser.name}
65 |
66 |
67 | ) :
}
68 |
69 |
70 |
71 | 运营数据中心
75 |
76 |
77 |
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/components/PageHeader/index.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .pageHeader {
4 | background: @component-background;
5 | padding: 16px 32px 0 32px;
6 | border-bottom: @border-width-base @border-style-base @border-color-split;
7 |
8 | .detail {
9 | display: flex;
10 | }
11 |
12 | .row {
13 | display: flex;
14 | }
15 |
16 | .breadcrumb {
17 | margin-bottom: 16px;
18 | }
19 |
20 | .tabs {
21 | margin: 0 0 -17px -8px;
22 |
23 | :global {
24 | .ant-tabs-bar {
25 | border-bottom: @border-width-base @border-style-base @border-color-split;
26 | }
27 | }
28 | }
29 |
30 | .logo {
31 | flex: 0 1 auto;
32 | margin-right: 16px;
33 | padding-top: 1px;
34 | > img {
35 | width: 28px;
36 | height: 28px;
37 | border-radius: @border-radius-base;
38 | display: block;
39 | }
40 | }
41 |
42 | .title {
43 | font-size: 20px;
44 | font-weight: 500;
45 | color: @heading-color;
46 | }
47 |
48 | .action {
49 | margin-left: 56px;
50 | min-width: 266px;
51 |
52 | :global {
53 | .ant-btn-group:not(:last-child),
54 | .ant-btn:not(:last-child) {
55 | margin-right: 8px;
56 | }
57 |
58 | .ant-btn-group > .ant-btn {
59 | margin-right: 0;
60 | }
61 | }
62 | }
63 |
64 | .title, .action, .content, .extraContent, .main {
65 | flex: auto;
66 | }
67 |
68 | .title, .action {
69 | margin-bottom: 16px;
70 | }
71 |
72 | .logo, .content, .extraContent {
73 | margin-bottom: 16px;
74 | }
75 |
76 | .action, .extraContent {
77 | text-align: right;
78 | }
79 |
80 | .extraContent {
81 | margin-left: 88px;
82 | min-width: 242px;
83 | }
84 | }
85 |
86 | @media screen and (max-width: @screen-xl) {
87 | .pageHeader {
88 | .extraContent {
89 | margin-left: 44px;
90 | }
91 | }
92 | }
93 |
94 | @media screen and (max-width: @screen-lg) {
95 | .pageHeader {
96 | .extraContent {
97 | margin-left: 20px;
98 | }
99 | }
100 | }
101 |
102 | @media screen and (max-width: @screen-md) {
103 | .pageHeader {
104 | .row {
105 | display: block;
106 | }
107 |
108 | .action, .extraContent {
109 | margin-left: 0;
110 | text-align: left;
111 | }
112 | }
113 | }
114 |
115 | @media screen and (max-width: @screen-sm) {
116 | .pageHeader {
117 | .detail {
118 | display: block;
119 | }
120 | }
121 | }
122 |
123 | @media screen and (max-width: @screen-xs) {
124 | .pageHeader {
125 | .action {
126 | :global {
127 | .ant-btn-group, .ant-btn {
128 | display: block;
129 | margin-bottom: 8px;
130 | }
131 | .ant-btn-group > .ant-btn {
132 | display: inline-block;
133 | margin-bottom: 0;
134 | }
135 | }
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/Progress/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { formatPercent } from '../../utils/utils';
3 |
4 | import styles from './index.less';
5 |
6 | const Progress = ({
7 | width = 0,
8 | vertical= false,
9 | percent = 0,
10 | radius = 0,
11 | color = '#3AC9A8',
12 | background = false,
13 | textSize = 14
14 | }) => {
15 | var progressStyle = {
16 | height: 0,
17 | width: 0
18 | }
19 | //有背景且水平
20 | if(background && !vertical) {
21 | var progressStyle = {
22 | width: '95%',
23 | alignItems: 'center'
24 | }
25 | var progressOuter = {
26 | background,
27 | borderRadius: radius,
28 | width: '100%',
29 | height: width,
30 | marginRight: 10,
31 | flex: 1
32 | }
33 | var progressInner = {
34 | height: width,
35 | width: formatPercent(percent),
36 | borderRadius: radius,
37 | background: color,
38 | }
39 | }
40 | //没有背景且水平
41 | if(!background && !vertical) {
42 | var progressStyle = {
43 | width: '100%'
44 | }
45 | var progressOuter = {
46 | width: formatPercent(percent),
47 | marginRight: 10,
48 | }
49 | var progressInner = {
50 | width: '100%',
51 | height: width,
52 | borderRadius: radius,
53 | background: color,
54 | }
55 | }
56 | //有背景且垂直
57 | if(background && vertical) {
58 | var progressStyle = {
59 | height: '100%',
60 | flexDirection: 'column-reverse'
61 | }
62 | var progressOuter = {
63 | background,
64 | borderRadius: radius,
65 | height: '100%',
66 | width,
67 | marginTop: 5,
68 | display: 'flex',
69 | flexDirection: 'column-reverse',
70 | }
71 | var progressInner = {
72 | width,
73 | height: formatPercent(percent),
74 | display: 'inline-block',
75 | borderRadius: radius,
76 | background: color,
77 | }
78 | }
79 | //无背景且垂直
80 | if(!background && vertical) {
81 | var progressStyle = {
82 | height: '100%',
83 | flexDirection: 'column-reverse',
84 | alignItems: 'center'
85 | }
86 | var progressOuter = {
87 | borderRadius: radius,
88 | height: formatPercent(percent),
89 | width,
90 | marginTop: 5,
91 | display: 'flex',
92 | flexDirection: 'column-reverse',
93 | }
94 | var progressInner = {
95 | width,
96 | height: '100%',
97 | display: 'inline-block',
98 | borderRadius: radius,
99 | background: color,
100 | }
101 | }
102 | return (
103 |
104 |
107 |
108 | {formatPercent(percent)}
109 |
110 |
111 | )
112 | }
113 |
114 | export default Progress;
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | import fetch from 'dva/fetch';
2 | import { notification } from 'antd';
3 | import { routerRedux } from 'dva/router';
4 | import store from '../index';
5 |
6 | const codeMessage = {
7 | 200: '服务器成功返回请求的数据。',
8 | 201: '新建或修改数据成功。',
9 | 202: '一个请求已经进入后台排队(异步任务)。',
10 | 204: '删除数据成功。',
11 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
12 | 401: '用户没有权限(令牌、用户名、密码错误)。',
13 | 403: '用户得到授权,但是访问是被禁止的。',
14 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
15 | 406: '请求的格式不可得。',
16 | 410: '请求的资源被永久删除,且不会再得到的。',
17 | 422: '当创建一个对象时,发生一个验证错误。',
18 | 500: '服务器发生错误,请检查服务器。',
19 | 502: '网关错误。',
20 | 503: '服务不可用,服务器暂时过载或维护。',
21 | 504: '网关超时。',
22 | };
23 | function checkStatus(response) {
24 | if (response.status >= 200 && response.status < 300) {
25 | return response;
26 | }
27 | const errortext = codeMessage[response.status] || response.statusText;
28 | notification.error({
29 | message: `请求错误 ${response.status}: ${response.url}`,
30 | description: errortext,
31 | });
32 | const error = new Error(errortext);
33 | error.name = response.status;
34 | error.response = response;
35 | throw error;
36 | }
37 |
38 | /**
39 | * Requests a URL, returning a promise.
40 | *
41 | * @param {string} url The URL we want to request
42 | * @param {object} [options] The options we want to pass to "fetch"
43 | * @return {object} An object containing either "data" or "err"
44 | */
45 | export default function request(url, options) {
46 | const defaultOptions = {
47 | credentials: 'include',
48 | };
49 | const newOptions = { ...defaultOptions, ...options };
50 | if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
51 | if (!(newOptions.body instanceof FormData)) {
52 | newOptions.headers = {
53 | Accept: 'application/json',
54 | 'Content-Type': 'application/json; charset=utf-8',
55 | ...newOptions.headers,
56 | };
57 | newOptions.body = JSON.stringify(newOptions.body);
58 | } else {
59 | // newOptions.body is FormData
60 | newOptions.headers = {
61 | Accept: 'application/json',
62 | 'Content-Type': 'multipart/form-data',
63 | ...newOptions.headers,
64 | };
65 | }
66 | }
67 |
68 | return fetch(url, newOptions)
69 | .then(checkStatus)
70 | .then((response) => {
71 | if (newOptions.method === 'DELETE' || response.status === 204) {
72 | return response.text();
73 | }
74 | return response.json();
75 | })
76 | .catch((e) => {
77 | const { dispatch } = store;
78 | const status = e.name;
79 | if (status === 401) {
80 | dispatch({
81 | type: 'login/logout',
82 | });
83 | return;
84 | }
85 | if (status === 403) {
86 | dispatch(routerRedux.push('/exception/403'));
87 | return;
88 | }
89 | if (status <= 504 && status >= 500) {
90 | dispatch(routerRedux.push('/exception/500'));
91 | return;
92 | }
93 | if (status >= 404 && status < 422) {
94 | dispatch(routerRedux.push('/exception/404'));
95 | }
96 | });
97 | }
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "operation-data-center",
3 | "version": "1.1.0",
4 | "description": "An out-of-box UI solution for enterprise applications",
5 | "private": true,
6 | "scripts": {
7 | "mock": "cross-env DISABLE_ESLINT=true roadhog dev",
8 | "start": "cross-env NO_PROXY=true DISABLE_ESLINT=true roadhog dev",
9 | "build": "cross-env DISABLE_ESLINT=true roadhog build",
10 | "site": "roadhog-api-doc static && gh-pages -d dist",
11 | "analyze": "cross-env ANALYZE=true roadhog build",
12 | "lint:style": "stylelint \"src/**/*.less\" --syntax less",
13 | "lint": "eslint --ext .js src mock tests && npm run lint:style",
14 | "lint:fix": "eslint --fix --ext .js src mock tests && npm run lint:style",
15 | "lint-staged": "lint-staged",
16 | "lint-staged:js": "eslint --ext .js",
17 | "test": "roadhog test",
18 | "test:component": "roadhog test ./src/components",
19 | "test:all": "node ./tests/run-tests.js"
20 | },
21 | "dependencies": {
22 | "@antv/data-set": "^0.8.0",
23 | "@babel/polyfill": "^7.0.0-beta.36",
24 | "antd": "^3.1.0",
25 | "babel-runtime": "^6.9.2",
26 | "bizcharts": "^3.1.3-beta.1",
27 | "bizcharts-plugin-slider": "^2.0.1",
28 | "classnames": "^2.2.5",
29 | "dva": "^2.1.0",
30 | "dva-loading": "^1.0.4",
31 | "enquire-js": "^0.1.1",
32 | "lodash": "^4.17.4",
33 | "lodash-decorators": "^4.4.1",
34 | "moment": "^2.19.1",
35 | "numeral": "^2.0.6",
36 | "omit.js": "^1.0.0",
37 | "path-to-regexp": "^2.1.0",
38 | "prop-types": "^15.5.10",
39 | "qs": "^6.5.0",
40 | "rc-drawer-menu": "^0.5.0",
41 | "react": "^16.2.0",
42 | "react-container-query": "^0.9.1",
43 | "react-document-title": "^2.0.3",
44 | "react-dom": "^16.2.0",
45 | "react-fittext": "^1.0.0",
46 | "rollbar": "^2.3.4",
47 | "url-polyfill": "^1.0.10"
48 | },
49 | "devDependencies": {
50 | "babel-eslint": "^8.1.2",
51 | "babel-plugin-dva-hmr": "^0.4.1",
52 | "babel-plugin-import": "^1.6.3",
53 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
54 | "cross-env": "^5.1.1",
55 | "cross-port-killer": "^1.0.1",
56 | "enzyme": "^3.1.0",
57 | "eslint": "^4.14.0",
58 | "eslint-config-airbnb": "^16.0.0",
59 | "eslint-plugin-babel": "^4.0.0",
60 | "eslint-plugin-compat": "^2.1.0",
61 | "eslint-plugin-import": "^2.8.0",
62 | "eslint-plugin-jsx-a11y": "^6.0.3",
63 | "eslint-plugin-markdown": "^1.0.0-beta.6",
64 | "eslint-plugin-react": "^7.0.1",
65 | "gh-pages": "^1.0.0",
66 | "husky": "^0.14.3",
67 | "lint-staged": "^6.0.0",
68 | "mockjs": "^1.0.1-beta3",
69 | "pro-download": "^1.0.1",
70 | "redbox-react": "^1.5.0",
71 | "regenerator-runtime": "^0.11.1",
72 | "roadhog": "^2.1.0",
73 | "roadhog-api-doc": "^0.3.4",
74 | "stylelint": "^8.4.0",
75 | "stylelint-config-standard": "^18.0.0"
76 | },
77 | "optionalDependencies": {
78 | "puppeteer": "^1.1.1"
79 | },
80 | "lint-staged": {
81 | "**/*.{js,jsx}": "lint-staged:js",
82 | "**/*.less": "stylelint --syntax less"
83 | },
84 | "browserslist": [
85 | "> 1%",
86 | "last 2 versions",
87 | "not ie <= 10"
88 | ]
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/NoticeIcon/index.js:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { Popover, Icon, Tabs, Badge, Spin } from 'antd';
3 | import classNames from 'classnames';
4 | import List from './NoticeList';
5 | import styles from './index.less';
6 |
7 | const { TabPane } = Tabs;
8 |
9 | export default class NoticeIcon extends PureComponent {
10 | static defaultProps = {
11 | onItemClick: () => {},
12 | onPopupVisibleChange: () => {},
13 | onTabChange: () => {},
14 | onClear: () => {},
15 | loading: false,
16 | locale: {
17 | emptyText: '暂无数据',
18 | clear: '清空',
19 | },
20 | emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
21 | };
22 | static Tab = TabPane;
23 | constructor(props) {
24 | super(props);
25 | this.state = {};
26 | if (props.children && props.children[0]) {
27 | this.state.tabType = props.children[0].props.title;
28 | }
29 | }
30 | onItemClick = (item, tabProps) => {
31 | const { onItemClick } = this.props;
32 | onItemClick(item, tabProps);
33 | }
34 | onTabChange = (tabType) => {
35 | this.setState({ tabType });
36 | this.props.onTabChange(tabType);
37 | }
38 | getNotificationBox() {
39 | const { children, loading, locale } = this.props;
40 | if (!children) {
41 | return null;
42 | }
43 | const panes = React.Children.map(children, (child) => {
44 | const title = child.props.list && child.props.list.length > 0
45 | ? `${child.props.title} (${child.props.list.length})` : child.props.title;
46 | return (
47 |
48 | this.onItemClick(item, child.props)}
52 | onClear={() => this.props.onClear(child.props.title)}
53 | title={child.props.title}
54 | locale={locale}
55 | />
56 |
57 | );
58 | });
59 | return (
60 |
61 |
62 | {panes}
63 |
64 |
65 | );
66 | }
67 | render() {
68 | const { className, count, popupAlign, onPopupVisibleChange } = this.props;
69 | const noticeButtonClass = classNames(className, styles.noticeButton);
70 | const notificationBox = this.getNotificationBox();
71 | const trigger = (
72 |
73 |
74 |
75 |
76 |
77 | );
78 | if (!notificationBox) {
79 | return trigger;
80 | }
81 | const popoverProps = {};
82 | if ('popupVisible' in this.props) {
83 | popoverProps.visible = this.props.popupVisible;
84 | }
85 | return (
86 |
96 | {trigger}
97 |
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/mock/Report/dynamic.js:
--------------------------------------------------------------------------------
1 | const mock_dynamic = {
2 | "result": {
3 | //总门急诊人次
4 | "totalEmergencyVisitsModule": {
5 | "totalEmergencyVisitsMom": 0.1653,
6 | "totalEmergencyVisits": 52553,
7 | "totalEmergencyVisitsYoy": -0.1404
8 | },
9 | //中医处方数量
10 | "chinaPrescriptionsModule": {
11 | "chinaPrescriptionsMom": 0.338,
12 | "chinaPrescriptions": 19897,
13 | "chinaPrescriptionsYoy": 0.069
14 | },
15 | //非药物中治率(门诊)
16 | "nondrugTreatmentRateModule": {
17 | "nondrugTreatmentRate": 0.0321,
18 | "nondrugTreatmentRateYoy": 0.0073,
19 | "nondrugTreatmentRateMom": 0.006
20 | },
21 | //住院
22 | "inHospitalModule": [
23 | {
24 | "recover": 1968,
25 | "recoverYoy": 0.105,
26 | "recoverMom": 0.2495
27 | },
28 | {
29 | "admissionsYoy": 0.1275,
30 | "admissions": 1972,
31 | "admissionsMom": 0.3406
32 | },
33 | {
34 | "inHospitalMom": 0.2862,
35 | "inHospital": 25772,
36 | "inHospitalYoy": -0.0103
37 | }
38 | ],
39 | //总收入
40 | "totalIncomeModule": [
41 | {
42 | "inHospitalIncome": 37621717.1,
43 | "inHospitalIncomeMom": 0.3073,
44 | "inHospitalIncomeYoy": 0.1069
45 | },
46 | {
47 | "drugIncomeMom": 0.2381,
48 | "drugIncomeYoy": 0.0855,
49 | "drugIncome": 23913973.43
50 | },
51 | {
52 | "outpatientEmergencyIncomeYoy": 0.0312,
53 | "outpatientEmergencyIncomeMom": 0.3109,
54 | "outpatientEmergencyIncome": 16997740.74
55 | },
56 | {
57 | "totalIncome": 54619457.84,
58 | "totalIncomeMom": 0.3084,
59 | "totalIncomeYoy": 0.0822
60 | },
61 | {
62 | "nonDrugIncomeYoy": 0.0796,
63 | "nonDrugIncomeMom": 0.369,
64 | "nonDrugIncome": 30705484.41
65 | }
66 | ],
67 | //患者负担
68 | "patientBurdenModule": [
69 | {
70 | "perOutpatientTreatFees": 73.76,
71 | "perOutpatientTreatFeesYoy": 0.074,
72 | "perOutpatientTreatFeesMom": 0.0186
73 | },
74 | {
75 | "perMedicalExaminationFeesMom": -0.6146,
76 | "perMedicalExaminationFees": 52.91,
77 | "perMedicalExaminationFeesYoy": -0.5761
78 | },
79 | {
80 | "perPrescriptionFeesMom": -0.0065,
81 | "perPrescriptionFees": 202.14,
82 | "perPrescriptionFeesYoy": -0.0044
83 | },
84 | {
85 | "perDaysOfRecoverYoy": -0.104,
86 | "perDaysOfRecover": 13.1,
87 | "perDaysOfRecoverMom": 0.0299
88 | },
89 | {
90 | "perRecoverDrugFeesYoy": -0.0816,
91 | "perRecoverDrugFees": 17930.96,
92 | "perRecoverDrugFeesMom": -0.0794
93 | },
94 | {
95 | "perOutpatientEmergencyFeesYoy": 0.1996,
96 | "perOutpatientEmergencyFees": 323.44,
97 | "perOutpatientEmergencyFeesMom": 0.125
98 | },
99 | {
100 | "perOutpatientDrugFeesYoy": 0.0538,
101 | "perOutpatientDrugFees": 195.81,
102 | "perOutpatientDrugFeesMom": -0.0884
103 | },
104 | {
105 | "perInHospitalFeesMom": -0.1094,
106 | "perInHospitalFeesYoy": -0.0672,
107 | "perInHospitalFees": 6424.63
108 | }
109 | ]
110 | }
111 | }
112 |
113 | export default mock_dynamic;
--------------------------------------------------------------------------------
/src/components/Login/LoginItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Form, Button, Row, Col } from 'antd';
4 | import omit from 'omit.js';
5 | import styles from './index.less';
6 | import map from './map';
7 |
8 | const FormItem = Form.Item;
9 |
10 | function generator({ defaultProps, defaultRules, type }) {
11 | return (WrappedComponent) => {
12 | return class BasicComponent extends Component {
13 | static contextTypes = {
14 | form: PropTypes.object,
15 | updateActive: PropTypes.func,
16 | };
17 | constructor(props) {
18 | super(props);
19 | this.state = {
20 | count: 0,
21 | };
22 | }
23 | componentDidMount() {
24 | if (this.context.updateActive) {
25 | this.context.updateActive(this.props.name);
26 | }
27 | }
28 | componentWillUnmount() {
29 | clearInterval(this.interval);
30 | }
31 | onGetCaptcha = () => {
32 | let count = 59;
33 | this.setState({ count });
34 | if (this.props.onGetCaptcha) {
35 | this.props.onGetCaptcha();
36 | }
37 | this.interval = setInterval(() => {
38 | count -= 1;
39 | this.setState({ count });
40 | if (count === 0) {
41 | clearInterval(this.interval);
42 | }
43 | }, 1000);
44 | }
45 | render() {
46 | const { getFieldDecorator } = this.context.form;
47 | const options = {};
48 | let otherProps = {};
49 | const { onChange, defaultValue, rules, name, ...restProps } = this.props;
50 | const { count } = this.state;
51 | options.rules = rules || defaultRules;
52 | if (onChange) {
53 | options.onChange = onChange;
54 | }
55 | if (defaultValue) {
56 | options.initialValue = defaultValue;
57 | }
58 | otherProps = restProps || otherProps;
59 | if (type === 'Captcha') {
60 | const inputProps = omit(otherProps, ['onGetCaptcha']);
61 | return (
62 |
63 |
64 |
65 | {getFieldDecorator(name, options)(
66 |
67 | )}
68 |
69 |
70 |
78 |
79 |
80 |
81 | );
82 | }
83 | return (
84 |
85 | {getFieldDecorator(name, options)(
86 |
87 | )}
88 |
89 | );
90 | }
91 | };
92 | };
93 | }
94 |
95 | const LoginItem = {};
96 | Object.keys(map).forEach((item) => {
97 | LoginItem[item] = generator({
98 | defaultProps: map[item].props,
99 | defaultRules: map[item].rules,
100 | type: item,
101 | })(map[item].component);
102 | });
103 |
104 | export default LoginItem;
105 |
--------------------------------------------------------------------------------
/src/components/Login/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Form, Tabs } from 'antd';
4 | import classNames from 'classnames';
5 | import LoginItem from './LoginItem';
6 | import LoginTab from './LoginTab';
7 | import LoginSubmit from './LoginSubmit';
8 | import styles from './index.less';
9 |
10 | @Form.create()
11 | class Login extends Component {
12 | static defaultProps = {
13 | className: '',
14 | defaultActiveKey: '',
15 | onTabChange: () => {},
16 | onSubmit: () => {},
17 | };
18 | static propTypes = {
19 | className: PropTypes.string,
20 | defaultActiveKey: PropTypes.string,
21 | onTabChange: PropTypes.func,
22 | onSubmit: PropTypes.func,
23 | };
24 | static childContextTypes = {
25 | tabUtil: PropTypes.object,
26 | form: PropTypes.object,
27 | updateActive: PropTypes.func,
28 | };
29 | state = {
30 | type: this.props.defaultActiveKey,
31 | tabs: [],
32 | active: {},
33 | };
34 | getChildContext() {
35 | return {
36 | tabUtil: {
37 | addTab: (id) => {
38 | this.setState({
39 | tabs: [...this.state.tabs, id],
40 | });
41 | },
42 | removeTab: (id) => {
43 | this.setState({
44 | tabs: this.state.tabs.filter(currentId => currentId !== id),
45 | });
46 | },
47 | },
48 | form: this.props.form,
49 | updateActive: (activeItem) => {
50 | const { type, active } = this.state;
51 | if (active[type]) {
52 | active[type].push(activeItem);
53 | } else {
54 | active[type] = [activeItem];
55 | }
56 | this.setState({
57 | active,
58 | });
59 | },
60 | };
61 | }
62 | onSwitch = (type) => {
63 | this.setState({
64 | type,
65 | });
66 | this.props.onTabChange(type);
67 | }
68 | handleSubmit = (e) => {
69 | e.preventDefault();
70 | const { active, type } = this.state;
71 | const activeFileds = active[type];
72 | this.props.form.validateFields(activeFileds, { force: true },
73 | (err, values) => {
74 | this.props.onSubmit(err, values);
75 | }
76 | );
77 | }
78 | render() {
79 | const { className, children } = this.props;
80 | const { type, tabs } = this.state;
81 | const TabChildren = [];
82 | const otherChildren = [];
83 | React.Children.forEach(children, (item) => {
84 | if (!item) {
85 | return;
86 | }
87 | // eslint-disable-next-line
88 | if (item.type.__ANT_PRO_LOGIN_TAB) {
89 | TabChildren.push(item);
90 | } else {
91 | otherChildren.push(item);
92 | }
93 | });
94 | return (
95 |
114 | );
115 | }
116 | }
117 |
118 | Login.Tab = LoginTab;
119 | Login.Submit = LoginSubmit;
120 | Object.keys(LoginItem).forEach((item) => {
121 | Login[item] = LoginItem[item];
122 | });
123 |
124 | export default Login;
125 |
--------------------------------------------------------------------------------
/src/components/Login/demo/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | order: 0
3 | title:
4 | zh-CN: 标准登录
5 | en-US: Standard Login
6 | ---
7 |
8 | Support login with account and mobile number.
9 |
10 | ````jsx
11 | import Login from 'ant-design-pro/lib/Login';
12 | import { Alert, Checkbox } from 'antd';
13 |
14 | const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
15 |
16 | class LoginDemo extends React.Component {
17 | state = {
18 | notice: '',
19 | type: 'tab2',
20 | autoLogin: true,
21 | }
22 | onSubmit = (err, values) => {
23 | console.log('value collected ->', { ...values, autoLogin: this.state.autoLogin });
24 | if (this.state.type === 'tab1') {
25 | this.setState({
26 | notice: '',
27 | }, () => {
28 | if (!err && (values.username !== 'admin' || values.password !== '888888')) {
29 | setTimeout(() => {
30 | this.setState({
31 | notice: 'The combination of username and password is incorrect!',
32 | });
33 | }, 500);
34 | }
35 | });
36 | }
37 | }
38 | onTabChange = (key) => {
39 | this.setState({
40 | type: key,
41 | });
42 | }
43 | changeAutoLogin = (e) => {
44 | this.setState({
45 | autoLogin: e.target.checked,
46 | });
47 | }
48 | render() {
49 | return (
50 |
55 |
56 | {
57 | this.state.notice &&
58 |
59 | }
60 |
61 |
62 |
63 |
64 |
65 | console.log('Get captcha!')} name="captcha" />
66 |
67 |
71 | Login
72 |
73 | Other login methods
74 |
75 |
76 |
77 |
Register
78 |
79 |
80 | );
81 | }
82 | }
83 |
84 | ReactDOM.render(, mountNode);
85 | ````
86 |
87 |
116 |
--------------------------------------------------------------------------------
/src/components/Charts/LineOrArea/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Chart, Tooltip, Geom, Legend, Axis } from 'bizcharts';
3 | import DataSet from '@antv/data-set';
4 | import autoHeight from '../autoHeight';
5 | import styles from './index.less';
6 |
7 | @autoHeight()
8 | export default class LineOrArea extends React.Component {
9 | render() {
10 | const {
11 | title,
12 | area = false,
13 | point = false,
14 | line = false,
15 | bar = false,
16 | barWidth = 10,
17 | height = 400,
18 | titleMap = {},
19 | borderWidth = 1,
20 | data = [],
21 | padding,
22 | lineColor,
23 | areaColor,
24 | lineWidth = 1,
25 | pointSize = 4,
26 | opacity = [1],
27 | legend = true,
28 | type = 'line',
29 | shape,
30 | xAxisRotate,
31 | scale,
32 | GeomConfig,
33 | LegendSetting
34 | } = this.props;
35 |
36 | const position = `${titleMap.x}*value`
37 | const ds = new DataSet();
38 | const dv = ds.createView();
39 | dv.source(data)
40 | //重命名
41 | .transform({
42 | type: 'rename',
43 | map: titleMap.filedsMap,
44 | })
45 | //将fileds中对应字段的内容转换成key:value的形式
46 | .transform({
47 | type: 'fold',
48 | fields: Object.values(titleMap.filedsMap), // 展开字段集
49 | key: 'key', // key字段
50 | value: 'value', // value字段
51 | });
52 | const xlabel = {
53 | offset: 15, // 数值,设置坐标轴文本 label 距离坐标轴线的距离
54 | // 设置文本的显示样式,还可以是个回调函数,回调函数的参数为该坐标轴对应字段的数值
55 | textStyle: {
56 | textAlign: 'start', // 文本对齐方向,可取值为: start center end
57 | fill: '#333', // 文本的颜色
58 | fontSize: '12', // 文本大小
59 | //fontWeight: 'bold', // 文本粗细
60 | rotate: xAxisRotate,
61 | //textBaseline: 'middle' // 文本基准线,可取 top middle bottom,默认为middle
62 | },
63 | autoRotate: xAxisRotate ? false : true, // 文本是否需要自动旋转,默认为 true
64 | }
65 | //设置刻度线样式
66 | const axisLine = {
67 | stroke: '#e8e8e8',
68 | lineWidth: 1
69 | };
70 |
71 | return (
72 |
73 |
74 |
75 |
81 |
86 |
87 | {legend && ()}
88 | {area && (
89 |
98 | )}
99 | {line && (
100 | {
107 | return {
108 | name: key,
109 | value: value
110 | }
111 | }]}
112 | {...GeomConfig?GeomConfig.line:{}}
113 | />
114 | )}
115 | {point && (
116 |
125 | )}
126 |
127 |
128 |
129 | );
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/routes/Dashboard/Indicator/Emphasis.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 |
3 | .dailyConsumable {
4 | display: flex;
5 | position: relative;
6 | height: 84px;
7 | margin-top: 10px;
8 | .part {
9 | flex: 1;
10 | text-align: center;
11 | height: 100%;
12 | display: flex;
13 | flex-direction: column;
14 | justify-content: space-between;
15 | .item {
16 | font-size: 16px;
17 | color: #666;
18 | }
19 | .ratio {
20 | font-size: 30px;
21 | color: #15984B;
22 | }
23 | }
24 | }
25 | .monthlyConsumable {
26 | display: flex;
27 | height: 180px;
28 | .part {
29 | height: 100%;
30 | text-align: center;
31 | }
32 | .item {
33 | font-size: 16px;
34 | color: #666;
35 | text-align: center;
36 | margin-top: 7px;
37 | }
38 | }
39 | .horizontalProgress {
40 | margin-top: 36px;
41 | width: 100%;
42 | display: flex;
43 | align-items: center;
44 | .item {
45 | line-height: 30px;
46 | color: #333;
47 | margin-left: 20px;
48 | }
49 | }
50 | .monthlyQuality {
51 | margin-top: 20px;
52 | .item {
53 | color: #333;
54 | margin-top: 5px;
55 | }
56 | }
57 | .extra {
58 | display: inline-block;
59 | width: 10px;
60 | height: 10px;
61 | margin-right: 10px;
62 | }
63 | .vertical {
64 | position: absolute;
65 | top: 50%;
66 | transform: translate(-50%, -50%);
67 | left: 50%;
68 | width: 1px;
69 | height: 80px;
70 | background: #e8e8e8;
71 | }
72 | .surgeryCategory {
73 | height: 40px;
74 | border: 1px solid #E8E8E8;
75 | border-radius: 2px;
76 | display: flex;
77 | align-items: center;
78 | justify-content: space-between;
79 | margin-bottom: 20px;
80 | padding: 0 8px;
81 | .left {
82 | min-width: 120px;
83 | }
84 | .item {
85 | text-align: center;
86 | }
87 | }
88 | .badSpaceLegend {
89 | width: 148px;
90 | margin-bottom: 10px;
91 | .point {
92 | display: inline-block;
93 | width: 10px;
94 | height: 10px;
95 | margin-right: 10px;
96 | }
97 | .item {
98 | color: #333;
99 | }
100 | }
101 | .rowCardContent {
102 | position: relative;
103 | display: flex;
104 | align-items: center;
105 | width: 100%;
106 | margin-bottom: 45px;
107 | .part {
108 | flex: 1;
109 | text-align: center;
110 | .partOne {
111 | color: #333;
112 | padding: 10px 0;
113 | }
114 | .partTwo {
115 | font-size: 36px;
116 | padding-bottom: 10px;
117 | }
118 | .partThree {
119 | display: inline-block;
120 | }
121 | }
122 | .bedVertical {
123 | position: absolute;
124 | top: 50%;
125 | transform: translateY(-50%);
126 | width: 1px;
127 | height: 142px;
128 | background: #e8e8e8;
129 | }
130 | }
131 | .wrapCard {
132 | display: inline-block;
133 | width: 16.66%;
134 | padding: 0 10px;
135 | margin-bottom: 24px;
136 | .technologyCard {
137 | background-color: #FAFFF9;
138 | border-color: #B4D9C4;
139 | border-radius: 8px;
140 | .item {
141 | font-size: 14px;
142 | text-align: center;
143 | color: #666;
144 | height: 40px;
145 | display: flex;
146 | justify-content: center;
147 | align-items: center;
148 | }
149 | .count {
150 | text-align: center;
151 | color: #333;
152 | font-size: 18px;
153 | }
154 | .compare {
155 | border-top: 1px solid #B4D9C4;
156 | text-align: center;
157 | padding-top: 6px;
158 | margin-top: 8px;
159 | }
160 | }
161 | }
162 | .surgeryCard {
163 | height: 90px;
164 | border: 1px solid #E8E8E8;
165 | border-radius: 2px;
166 | margin-bottom: 20px;
167 | .item {
168 | height: 45px;
169 | line-height: 45px;
170 | width: 100%;
171 | border-bottom: 1px solid #E8E8E8;
172 | color: #4a4a4a;
173 | text-align: center;
174 | }
175 | .count {
176 | display: flex;
177 | height: 45px;
178 | line-height: 45px;
179 | width: 100%;
180 | .left {
181 | flex: 1;
182 | color: #15984b;
183 | font-size: 18px;
184 | text-align: center;
185 | }
186 | .right {
187 | flex: 1;
188 | color: #8e8181;
189 | text-align: center;
190 | border-left: 1px solid #e8e8e8;
191 | }
192 | }
193 | }
--------------------------------------------------------------------------------
/src/routes/Dashboard/Indicator/Consumable.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react'
2 | import { connect } from 'dva';
3 | import Compare from '../../../components/Compare';
4 | import { yuan, formatPercent } from '../../../utils/utils';
5 | import {
6 | Table
7 | } from 'antd';
8 | import styles from './Consumable.less';
9 |
10 | @connect(({ consumable, date, loading }) => ({
11 | consumable,
12 | date,
13 | loading: loading.effects['consumable/fetch'],
14 | }))
15 |
16 | export default class Consumable extends Component {
17 |
18 | state = {
19 | rangeDateType: this.props.date.indicator.rangeDateType,
20 | isOneDay: this.props.date.indicator.isOneDay
21 | };
22 |
23 | componentDidMount() {
24 | const { date } = this.props;
25 | this.props.dispatch({
26 | type: 'consumable/fetch',
27 | payload: {
28 | beginDate: date.indicator.beginDate,
29 | endDate: date.indicator.endDate
30 | }
31 | })
32 | }
33 |
34 | componentWillUnmount() {
35 | const { dispatch } = this.props;
36 | dispatch({
37 | type: 'consumable/clear',
38 | });
39 | }
40 |
41 | render() {
42 | const { rangeDateType, isOneDay } = this.state;
43 | const { consumable, loading, date } = this.props;
44 | const {
45 | diffDepartSupplyIncomeModule
46 | } = consumable;
47 | let columns = [
48 | {
49 | title: '科室名称',
50 | dataIndex: 'departName',
51 | key: 'departName',
52 | width: 200
53 | },
54 | {
55 | title: '门诊材料费',
56 | dataIndex: 'outpatientMaterialCostCount',
57 | key: 'outpatientMaterialCostCount',
58 | sorter: (a, b) => (a.outpatientMaterialCostCount || 0) - (b.outpatientMaterialCostCount || 0),
59 | render: text => {
60 | return yuan(text)
61 | },
62 | width: 200
63 | },
64 | {
65 | title: '住院材料费',
66 | dataIndex: 'inHospitalMaterialCostCount',
67 | key: 'inHospitalMaterialCostCount',
68 | sorter: (a, b) => (a.inHospitalMaterialCostCount || 0) - (b.inHospitalMaterialCostCount || 0),
69 | render: text => {
70 | return yuan(text)
71 | },
72 | width: 200
73 | },
74 | {
75 | title: '卫生总费用',
76 | dataIndex: 'totalHealthCostCount',
77 | key: 'totalHealthCostCount',
78 | sorter: (a, b) => (a.totalHealthCostCount || 0) - (b.totalHealthCostCount || 0),
79 | render: text => {
80 | return yuan(text)
81 | },
82 | width: 200
83 | },
84 | {
85 | title: '耗材收入占比',
86 | dataIndex: 'materialCostRate',
87 | key: 'materialCostRate',
88 | sorter: (a, b) => (a.materialCostRate || 0) - (b.materialCostRate || 0),
89 | render: text => {
90 | return formatPercent(text);
91 | },
92 | width: 200
93 | }
94 | ];
95 | if(rangeDateType === 'monthly') {
96 | const columnObject = {
97 | title: '耗材收入占比环比',
98 | dataIndex: 'materialCostRateMom',
99 | key: 'materialCostRateMom',
100 | sorter: (a, b) => (a.materialCostRateMom || 0) - (b.materialCostRateMom || 0),
101 | render: text => {
102 | return (
103 |
104 | )
105 | },
106 | width: 200
107 | }
108 | columns.push(columnObject);
109 | }
110 | return (
111 |
112 |
117 | {isOneDay ? date.indicator.beginDate : `${date.indicator.beginDate} --- ${date.indicator.endDate}`}
118 |
119 |
120 |
121 |
不同科室的耗材收入占比
122 |
123 |
130 | index % 2 === 0 ? 'stripe' : ''
131 | }
132 | />
133 |
134 |
135 |
136 |
137 | )
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/dist/iconfonts/iconfont.css:
--------------------------------------------------------------------------------
1 |
2 | @font-face {font-family: "iconfont";
3 | src: url('iconfont.eot?t=1522201475376'); /* IE9*/
4 | src: url('iconfont.eot?t=1522201475376#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAosAAsAAAAADzwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFZW70gAY21hcAAAAYAAAACIAAAB/vr40tRnbHlmAAACCAAABdYAAAg4ea6TXWhlYWQAAAfgAAAALwAAADYQ9G0GaGhlYQAACBAAAAAeAAAAJAfyA3pobXR4AAAIMAAAABYAAAAoJ+oAAGxvY2EAAAhIAAAAFgAAABYKGAfSbWF4cAAACGAAAAAdAAAAIAEdAIFuYW1lAAAIgAAAAUUAAAJtPlT+fXBvc3QAAAnIAAAAZAAAAH9eySQBeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2Bk/sc4gYGVgYOpk+kMAwNDP4RmfM1gxMjBwMDEwMrMgBUEpLmmMDgwVDzjYW7438AQw9zC0AgUZgTJAQAmZwyDeJzFkdERxCAIRBejRnOWkjJSUL4yKeEqpY3cAv5cBYF5ijs4OiyAAmAhO8mAXBBYnFTF9QWb6xkHz42ZuN+adDzPrIpXFuIdzavEu5lZULFS6xQrXgt57+n/GL5+54nTxT3hFzUFNmUtgTmkNTCXdA28boE5qT2A9W4BrPcTmC86AvQf2q8dkXicjVVNjNtEFJ7nsceJ1/l1bOfHTtbOJt5ussk2TpxNdptk2+22W9GWbkthUYVo+RMHEEhFXBAECgIhhEr3gKiEihBqEYdeEBKHVnBCAoGQ2gPqCqlC/PSA4MqFGp43pawWCbA879nzxt/M+943YyIQ8sd39BJNE4VMku1kkdxJCLAK2FHOBMtp1rgKqJag6qkodYqOJRbtGt0Bus1SWsNrlnUmshhEIQ+u1fCcGudAq9nj5qChmQCZXPZwsmQk6WmQ0k7+JX8f9y6ohaIR6037y9V+qjGuhJ6Wk8lMMvlaiAlCiOP4WBQe07WwEJaY/54Qy6qXCtu4AsgZJ3vHamQ8lzz+SvNxs6SHAYZDUHLj0Qv9RDaB9zNZTUlmxHgklM5GihMpePqHsbQim+XvCV6AuZ6hdTokKiGC1mgrXtkpJppooqAm0OjwbWxb/FdzZtfktP2qPT25ayZ/yDTh87mDADW7ClC1/StwcG4Da4hYjwdYULZFyjTdTaTQ9KCVQONwE1uxTIpgfhvB/CsjMKgFYHiFAzye4NoKpE5myQLZRw6TY+Qh8ijiu3nQE1EQrRo4iR603VYP+dVVa+QFDCVwiIWhBA5B1KbX0FKM/vXwv3os1aKDgbcMsOzdJCM/gAlvAnnecDcHOXyeyFEy8sE79k/kRg5K//b6+490+PuQkgB8OALnAh9g5AYbdhhgDkfYXOD9jXnx9j+59QD9/+7h+gPC3+ZzdkPTq8giMtNDY0dB07HWTPNabhkJzIO4PQ8Nb0RmIOsWaiIYGuU0ndEycTzS1ojOCNjIVi+oR4rpdnlEHFeLTBkTRnb36u5srjTTBEhpQO2F5QWbwlhyjOvc3QU5IdcmlXB6plNVYqqxsO5fFwSw1tfBEgT/uv9RZiqD92Sz+b1mmhUUnWBUI9lDdzUadx3K6ve05OiYXN7pODvL4bGxcLXbrW74paTqtQxqtVtK3L9/E+I6zgA30pXtlQw0DzSnzKphVM1Aa+JtbsaITiqoN5e0CcpQ2JQp5t9GwlBZeaBlr+01dOSpiDsGxY7sKa5a1LE5MagDeo5syWj94g1BuHFxw560uitHPjuy0rXa9tTS3g/2Lk3ZP7vuWdftHtmlDV13uGXhx//6Ei3Uj1/uz8/3Lx9fOT/rebPnV8B1v3I//E3du+gG+VDM5zleoS/iXqySnVhpKw8F0BiusxhsmabXhx72FFVcKDa35baKQWsXRaeNI+qAGWLCJQ/rv3GcwTScO/hcoTN+5zFlm35z/YGXaUjoZJ/9aHWwf/9+F1tndbC05xf/bLoac1OpcjgahquTPZETBqwOP8HR5bX73pvA8+yFE4qjrC0ffenCeHl8aQnNhbW1LyGdOlzcE+d5fj8/5Uhp6R1zS22C84oSgSTIgJwgpNQW+uBQRkQsDxapTBRm10HUUYCBv2XKTRxV9gKD4ixA+5bXmMA0dI1/RP72HHnK/7T7hWFe868zPA6uXQOLMf/6URYX91ROsUhMrFzVulqpE4pGWKekcsrVCovE2alKh8lx9o4AfinMiQ9K6XC4y+QE65hmEDeNIC52Om+sP3WxM29wBxD12qZZ/KOM7a28yOIyq3ytqqVu8BVOMKfjBPGIeKrSDWLnxOTzUkZ6WJJCc2IswroIj1HD6Aau0zlDNu17iWhkhpxEGr0y1reIZdU300cDReQhBkHlXaz7eMtrByLXBdSNqKaYWKzBDgjUUQM8G9paCgNqSitwgbzUlO56OwBP+j6HRz5ujBqKDL9XNcY9z/FMYYzSY53tW9h8X5pWkqosLe6LU4kp4PdAkcXwYBbunZXrBVP7WDPzVXleCvFvP6IoTzwJAt+XSroifSypGUea43kOaq1QmI+F+otciLOpRGUK/Dkebmzl9RuFhSV5MnWSSYIISrOpgCxI1HrdnGQRzQAwtAgrGadlNdSYZfjnYPUkR6nwFsa4Bv5gww3I6W/yvMBJ+QgXjUs0PpMXlD8BJ2d7FgAAeJxjYGRgYADiSRc/Toznt/nKwM3CAALXHoo0I+j/9SzCzC1ALgcDE0gUAFWYC1gAeJxjYGRgYG7438AQwwJk/X/HIswApFEAFwByAgR0AAB4nGNhYGBgfsnAwMKABTMi2AAjWgESAAAAAAAAAHYAnADEAWwB+gJiAsgDaAQcAAB4nGNgZGBg4GIoZeBkAAEmMI8LSP4H8xkAFZQBnwAAAHicZY9NTsMwEIVf+gekEqqoYIfkBWIBKP0Rq25YVGr3XXTfpk6bKokjx63UA3AejsAJOALcgDvwSCebNpbH37x5Y08A3OAHHo7fLfeRPVwyO3INF7gXrlN/EG6QX4SbaONVuEX9TdjHM6bCbXRheYPXuGL2hHdhDx18CNdwjU/hOvUv4Qb5W7iJO/wKt9Dx6sI+5l5XuI1HL/bHVi+cXqnlQcWhySKTOb+CmV7vkoWt0uqca1vEJlODoF9JU51pW91T7NdD5yIVWZOqCas6SYzKrdnq0AUb5/JRrxeJHoQm5Vhj/rbGAo5xBYUlDowxQhhkiMro6DtVZvSvsUPCXntWPc3ndFsU1P9zhQEC9M9cU7qy0nk6T4E9XxtSdXQrbsuelDSRXs1JErJCXta2VELqATZlV44RelzRiT8oZ0j/AAlabsgAAAB4nG3BWw6CMBAF0LkFedTEnbCophZ6ic6QkgZh9X746zni5MfLfx4ODVrc0KHHgBEed8Gnfdqhrm4jo+lU+OLjnXTllZOWpJF+ZdgzY9Cl27PVUoc58Qx21P7KpstJkS/Fuho7') format('woff'),
6 | url('iconfont.ttf?t=1522201475376') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
7 | url('iconfont.svg?t=1522201475376#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | i[class^='icon'] {
11 | font-family:"iconfont" !important;
12 | font-size:16px;
13 | font-style:normal;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | .icon-down:before { content: "\e602"; color: #F36969; font-size: 14px; margin-right: 5px;}
19 |
20 | .icon-up:before { content: "\e605"; color: #3AC9A8; font-size: 14px; margin-right: 5px;}
21 |
22 | .icon-icon-rili:before { content: "\e606"; }
23 |
24 | .icon-icon-rili1:before { content: "\e607"; }
25 |
26 | .icon-menjizhenrenci:before { content: "\e608"; }
27 |
28 | .icon-jiashicang:before { content: "\e609"; }
29 |
30 | .icon-shouru:before { content: "\e60a"; }
31 |
32 | .icon-feiyaowu:before { content: "\e60b"; }
33 |
34 | .icon-zhongyi:before { content: "\e60c"; }
35 |
36 |
--------------------------------------------------------------------------------