Browse Source

增加 统计概览> + 账号资源

sophieChenyx 6 years ago
parent
commit
5746e504d8

+ 1 - 2
.gitignore

@@ -13,11 +13,10 @@ _roadhog-api-doc
 .DS_Store
 npm-debug.log*
 yarn-error.log
-
+jsconfig.json
 /coverage
 .idea
 yarn.lock
 package-lock.json
 *bak
-jsconfig.json
 .prettierrc

+ 1 - 1
package.json

@@ -4,7 +4,7 @@
   "description": "Enterprise applications - Content Manage System",
   "private": true,
   "scripts": {
-    "precommit": "npm run lint-staged",
+    "_precommit": "npm run lint-staged",
     "start": "cross-env ESLINT=none roadhog dev",
     "start:no-proxy": "cross-env NO_PROXY=true ESLINT=none roadhog dev",
     "build": "cross-env NODE_ENV=production ESLINT=none roadhog build",

+ 4 - 0
src/common/menu.js

@@ -15,6 +15,10 @@ const menuData = () => {
       name: '销售详情',
       path: 'sold',
       icon: <AXIcon type="action" />,
+    }, {
+      name: '账号资源',
+      path: 'accounts',
+      icon: 'database',
     }],
     authority: ['admin', 'channel'],
   }, {

+ 21 - 0
src/common/router.js

@@ -196,6 +196,27 @@ export const getRouterData = (app) => {
     '/dashboard/sold': {
       component: dynamicWrapper(app, ['trade'], () => import('../routes/Dashboard/SnapshotList')),
     },
+
+    // 统计概览 贝尔安亲 需求增加
+    '/dashboard/accounts': {
+      component: dynamicWrapper(app, [], () => import('../routes/Dashboard/Accounts')),
+    },
+    // 总统计表
+    '/dashboard/accounts/totalList': {
+      component: dynamicWrapper(app, ['accounts'], () => import('../routes/Dashboard/Accounts/AccountsTotalList')),
+    },
+    // 校区列表
+    '/dashboard/accounts/campus': {
+      component: dynamicWrapper(app, ['accounts'], () => import('../routes/Dashboard/Accounts/AccountsCampus')),
+    },
+    // 终端用户
+    '/dashboard/accounts/terminals': {
+      component: dynamicWrapper(app, ['accounts'], () => import('../routes/Dashboard/Accounts/AccountsTerminals')),
+    },
+    // 即将逾期
+    '/dashboard/accounts/overdue': {
+      component: dynamicWrapper(app, ['accounts'], () => import('../routes/Dashboard/Accounts/AccountsOverdue')),
+    },
     // 异常相关路由注册
     '/exception/403': {
       component: dynamicWrapper(app, [], () => import('../routes/Exception/403')),

+ 21 - 1
src/components/AXList/StandardTableList.js

@@ -68,7 +68,7 @@ export default class StandardTableList extends PureComponent {
   getListHeader = () => {
     const {
       showStatusSelect,
-      header: { basicSearch, onAdvanceFilterClick, onCreateClick },
+      header: { basicSearch, onAdvanceFilterClick, onCreateClick, onDownload, campusAmount, terminalsAmount },
       footer: { pagination },
     } = this.props;
     const { keys } = basicSearch;
@@ -120,6 +120,17 @@ export default class StandardTableList extends PureComponent {
                 >高级筛选
                 </a>
               )}
+              {campusAmount !== undefined && (
+                <span style={{ marginLeft: 5, marginRight: 10}}>
+                当前共有<span style={{fontWeight:600, color:'#5E8732'}}>{campusAmount}</span>个校区
+                </span>
+              )}
+              {terminalsAmount !== undefined && (
+                <span>
+                <span style={{fontWeight:600, color:'#5E8732'}}>{terminalsAmount}</span>个终端用户
+                </span>
+              )}
+
               <Button icon="sync" onClick={this.handleRefreshBtnClick}>刷新</Button>
               {/* noCreate 参数控制是否显示新建按钮 */}
               {onCreateClick !== undefined && (
@@ -131,6 +142,15 @@ export default class StandardTableList extends PureComponent {
                 >新建
                 </Button>
               )}
+              {onDownload !== undefined && (
+                <Button
+                  icon="download"
+                  type="primary"
+                  style={{ marginLeft: 5 }}
+                  onClick={onDownload}
+                >下载
+                </Button>
+              )}
             </div>
           </div>
         </div>

+ 109 - 0
src/models/accounts.js

@@ -0,0 +1,109 @@
+
+import { stringify } from 'qs';
+import {
+  queryCampusList,
+  queryTerminalsList,
+  queryCampusAmount,
+  queryTerminalsAmount,
+  queryTotalList,
+} from '../services/accounts';
+
+export default {
+  namespace: 'accounts',
+  state: {
+    list: [],
+    pageNo: 1,
+    pageSize: 15,
+    totalSize: 0,
+    campusAmount: 0,
+    currentItem: {},
+  },
+  effects: {
+    *fetchCampusList({ payload }, { call, put }) {
+      const response = yield call(queryCampusList, payload);
+      if (response.success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: response.data.list || [],
+            pageSize: response.data.pageSize,
+            totalSize: response.data.totalSize,
+            pageNo: response.data.pageNo,
+          },
+        });
+      }
+    },
+    *fetchTerminalsList({ payload }, { call, put }) {
+      const response = yield call(queryTerminalsList, payload);
+      if (response.success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: response.data.list || [],
+            pageSize: response.data.pageSize,
+            totalSize: response.data.totalSize,
+            pageNo: response.data.pageNo,
+          },
+        });
+      }
+    },
+    *fetchCampusAmount({ payload }, { call, put }) {
+      const response = yield call(queryCampusAmount, payload);
+      if (response.success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            campusAmount: response.data,
+          },
+        });
+      }
+    },
+    *fetchTerminalsAmount({ payload }, { call, put }) {
+      const response = yield call(queryTerminalsAmount, payload);
+      if (response.success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            terminalsAmount: response.data,
+          },
+        });
+      }
+    },
+    *fetchTotalList({ payload }, { call, put }) {
+      const response = yield call(queryTotalList, payload);
+      if (response.success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: response.data.list || [],
+            pageSize: response.data.pageSize,
+            totalSize: response.data.totalSize,
+            pageNo: response.data.pageNo,
+          },
+        });
+      }
+    },
+    *fetchCampusExcel({ payload }, { call }) {
+      const url = '/api/stmt/campus/export';
+      window.location.href = url;
+    },
+    *fetchTerminalsExcel({ payload }, { call }) {
+      const url = '/api/stmt/terminal/user/export';
+      window.location.href = url;
+    },
+    *fetchOverdueExcel({ payload }, { call }) {
+      const url = '/api/stmt/terminal/user/export?fastExpired=1';
+      window.location.href = url;
+    },
+  },
+
+  reducers: {
+    querySuccess(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+  },
+
+};

+ 123 - 0
src/routes/Dashboard/Accounts/AccountsCampus.js

@@ -0,0 +1,123 @@
+import React, { Component } from 'react';
+import moment from 'moment';
+import { connect } from 'dva';
+import { Card } from 'antd';
+import { StandardTableList } from '../../../components/AXList';
+import { addRowKey } from '../../../utils/utils';
+
+@connect(({ loading, accounts }) => ({
+  accounts,
+  loading: loading.models.accounts,
+}))
+
+export default class CampusAccountsPage extends Component {
+  constructor(props) {
+    super(props);
+    const { state } = props.location;
+    this.state = {
+      UIParams: (state || {}).UIParams, // 组件的状态参数
+      Queryers: (state || {}).Queryers, // 查询的条件参数
+    };
+  }
+  componentWillMount() {
+    this.props.dispatch({
+      type: 'accounts/fetchCampusList',
+      payload: { ...this.state.Queryers }
+    });
+  }
+
+  handleDownloadOperation = () => {
+    // window.open('/api/stmt/campus/export');
+    this.props.dispatch({
+      type: 'accounts/fetchCampusExcel',
+      payload: 'download',
+    })
+  };
+  handleFilterOperation = (params, states) => {
+    this.setState({
+      UIParams: states,
+      Queryers: params,
+    });
+    this.props.dispatch({
+      type: 'accounts/fetchCampusList',
+      payload: {
+        ...params,
+      },
+    });
+  };
+  render() {
+    const { loading,accounts } = this.props;
+    const { list, totalSize, pageSize, pageNo } = accounts;
+    const basicSearch = {
+      keys: [{
+        name: '校区编号',
+        field: 'code',
+      }, {
+        name: '校区名称',
+        field: 'name',
+      }],
+    };
+    const pagination = {
+      pageNo,
+      pageSize,
+      totalSize,
+    };
+    const columns = [{
+      title: '校区编号',
+      key: 1,
+      dataIndex: 'code',
+      width: '10%',
+    }, {
+      title: '校区名称',
+      key: 2,
+      dataIndex: 'name',
+      width: '25%',
+    },{
+      title: '所属省(直辖市)',
+      key: 3,
+      dataIndex: 'provinceName',
+      width: '12%',
+    }, {
+      title: '所属市(区)/县',
+      key: 4,
+      dataIndex: 'cityName',
+      width: '17%',
+    }, {
+      title: '联系人',
+      key: 5,
+      dataIndex: 'contactName',
+      width: '10%',
+    }, {
+      title: '联系电话',
+      key: 6,
+      dataIndex: 'mobile',
+      width: '10%',
+    }, {
+      title: '更新时间',
+      key: 7,
+      dataIndex: 'gmtModified',
+      render: text => moment(text).format('YYYY-MM-DD HH:mm:ss'),
+      width: '15%',
+    }];
+
+    return (
+      <Card>
+        <StandardTableList
+          columns={columns}
+          loading={loading}
+          dataSource={addRowKey(list)}
+          header={{
+            basicSearch,
+            onFilterClick: this.handleFilterOperation,
+            onDownload: this.handleDownloadOperation,
+          }}
+          footer={{
+            pagination,
+          }}
+          keepUIState={{ ...this.state.UIParams }}
+          showStatusSelect={false}
+        />
+      </Card>
+    );
+  }
+}

+ 145 - 0
src/routes/Dashboard/Accounts/AccountsOverdue.js

@@ -0,0 +1,145 @@
+import React, { Component } from 'react';
+import moment from 'moment';
+import { connect } from 'dva';
+import { Card, Badge } from 'antd';
+import { StandardTableList } from '../../../components/AXList';
+import { addRowKey } from '../../../utils/utils';
+import styles from './AccountsTerminals.less';
+
+@connect(({ loading, accounts }) => ({
+  accounts,
+  loading: loading.models.accounts,
+}))
+
+export default class OverdueAccountsPage extends Component {
+  constructor(props) {
+    super(props);
+    const { state } = props.location;
+    this.state = {
+      UIParams: (state || {}).UIParams, // 组件的状态参数
+      Queryers: (state || {}).Queryers, // 查询的条件参数
+    };
+  }
+  componentWillMount() {
+    this.props.dispatch({
+        type: 'accounts/fetchTerminalsList',
+        payload: {
+          fastExpired: 1,
+          ...this.state.Queryers,
+        }
+    })
+  }
+  // 下载
+  handleDownloadOperation = () => {
+    this.props.dispatch({
+      type: 'accounts/fetchOverdueExcel'
+    })
+  };
+
+  handleFilterOperation = (params, states) => {
+    this.setState({
+      UIParams: states,
+      Queryers: params,
+    });
+    this.props.dispatch({
+        type: 'accounts/fetchTerminalsList',
+        payload: {
+          fastExpired: 1,
+          ...params,
+        },
+    });
+  };
+  render() {
+    const { loading,accounts } = this.props;
+    const { list, totalSize, pageSize, pageNo } = accounts;
+    const basicSearch = {
+      keys: [{
+        name: '终端编号',
+        field: 'code',
+      }],
+    };
+    const pagination = {
+      pageNo,
+      pageSize,
+      totalSize,
+    };
+    const columns = [{
+      title: '终端编号',
+      key: 1,
+      dataIndex: 'ucode',
+      width: '15%',
+    }, {
+      title: '课程包编号',
+      key: 2,
+      dataIndex: 'pcode',
+      width: '10%',
+    }, {
+      title: '课程包名称',
+      key: 3,
+      dataIndex: 'pname',
+      width: '15%',
+    }, {
+      title: '权限有效期',
+      key: 4,
+      render: (_, record) => {
+        const { startTimeStr, endTimeStr } = record;
+        return (
+          <div className={styles.authDesc}>
+            <p><span>起始时间:&nbsp;&nbsp;</span>{`${startTimeStr}`}</p>
+            <p><span>到期时间:&nbsp;&nbsp;</span>{`${endTimeStr}`}</p>
+          </div>
+        );
+      },
+      dataIndex: 'cityName',
+      width: '15%',
+      align: 'center',
+    }, {
+      title: '权限有效时长',
+      key: 5,
+      dataIndex: 'endTime',
+      render: (text) => {
+        const day = moment(text).diff(moment(), 'days');
+        if (day < 0) {
+          return <Badge status="error" text="已到期" />;
+        }
+        return <span><span style={{ color: '#52c41a', fontWeight: 'bold' }}>{day}</span>天到期</span>;
+      },
+      width: '10%',
+    }, {
+      title: '校区名称',
+      key: 6,
+      dataIndex: 'campusName',
+      width: '16%',
+    }, {
+      title: '联系电话',
+      key: 7,
+      dataIndex: 'campusContactWay',
+      width: '12%',
+    }, {
+      title: '联系人',
+      key: 8,
+      dataIndex: 'campusContactName',
+      width: '6%',
+    }];
+    
+    return (
+      <Card>
+        <StandardTableList
+          columns={columns}
+          loading={loading}
+          dataSource={addRowKey(list)}
+          header={{
+            basicSearch,
+            onFilterClick: this.handleFilterOperation,
+            onDownload: this.handleDownloadOperation,
+          }}
+          footer={{
+            pagination,
+          }}
+          keepUIState={{ ...this.state.UIParams }}
+          showStatusSelect={false}
+        />
+      </Card>
+    );
+  }
+}

+ 144 - 0
src/routes/Dashboard/Accounts/AccountsTerminals.js

@@ -0,0 +1,144 @@
+import React, { Component } from 'react';
+import moment from 'moment';
+import { connect } from 'dva';
+import { Card, Badge } from 'antd';
+import { StandardTableList } from '../../../components/AXList';
+import { addRowKey } from '../../../utils/utils';
+import styles from './AccountsTerminals.less';
+
+
+@connect(({ loading, accounts }) => ({
+  accounts,
+  loading: loading.models.accounts,
+}))
+
+export default class TerminalsAccountsPage extends Component {
+  constructor(props) {
+    super(props);
+    const { state } = props.location;
+    this.state = {
+      UIParams: (state || {}).UIParams, // 组件的状态参数
+      Queryers: (state || {}).Queryers, // 查询的条件参数
+    };
+  }
+  componentWillMount() {
+    this.props.dispatch({
+      type: 'accounts/fetchTerminalsList',
+      payload: { ...this.state.Queryers }
+    })
+  }
+  // 下载
+  handleDownloadOperation = () => {
+    this.props.dispatch({
+      type: 'accounts/fetchTerminalsExcel',
+    })
+  };
+
+  handleFilterOperation = (params, states) => {
+    this.setState({
+      UIParams: states,
+      Queryers: params,
+    });
+    this.props.dispatch({
+      type: 'accounts/fetchTerminalsList',
+      payload: {
+        ...params,
+      },
+    });
+  };
+  render() {
+    const { loading,accounts } = this.props;
+    const { list, totalSize, pageSize, pageNo } = accounts;
+
+    const basicSearch = {
+      keys: [{
+        name: '终端编号',
+        field: 'code',
+      }],
+    };
+    const pagination = {
+      pageNo,
+      pageSize,
+      totalSize,
+    };
+
+    const columns = [{
+      title: '终端编号',
+      key: 1,
+      dataIndex: 'ucode',
+      width: '15%',
+    }, {
+      title: '课程包编号',
+      key: 2,
+      dataIndex: 'pcode',
+      width: '10%',
+    }, {
+      title: '课程包名称',
+      key: 3,
+      dataIndex: 'pname',
+      width: '15%',
+    }, {
+      title: '权限有效期',
+      key: 4,
+      render: (_, record) => {
+        const { startTimeStr, endTimeStr } = record;
+        return (
+          <div className={styles.authDesc}>
+            <p><span>起始时间:&nbsp;&nbsp;</span>{`${startTimeStr}`}</p>
+            <p><span>到期时间:&nbsp;&nbsp;</span>{`${endTimeStr}`}</p>
+          </div>
+        );
+      },
+      dataIndex: 'cityName',
+      width: '15%',
+      align: 'center',
+    }, {
+      title: '权限有效时长',
+      key: 5,
+      dataIndex: 'endTime',
+      render: (text) => {
+        const day = moment(text).diff(moment(), 'days');
+        if (day < 0) {
+          return <Badge status="error" text="已到期" />;
+        }
+        return <span><span style={{ color: '#52c41a', fontWeight: 'bold' }}>{day}</span>天到期</span>;
+      },
+      width: '10%',
+    }, {
+      title: '校区名称',
+      key: 6,
+      dataIndex: 'campusName',
+      width: '16%',
+    }, {
+      title: '联系电话',
+      key: 7,
+      dataIndex: 'campusContactWay',
+      width: '12%',
+    }, {
+      title: '联系人',
+      key: 8,
+      dataIndex: 'campusContactName',
+      width: '6%',
+    }];
+
+    return (
+      <Card>
+        <StandardTableList
+          columns={columns}
+          loading={loading}
+          dataSource={addRowKey(list)}
+          header={{
+            basicSearch,
+            onFilterClick: this.handleFilterOperation,
+            onDownload: this.handleDownloadOperation,
+          }}
+          footer={{
+            pagination,
+          }}
+          keepUIState={{ ...this.state.UIParams }}
+          showStatusSelect={false}
+        />
+      </Card>
+    );
+  }
+}

+ 9 - 0
src/routes/Dashboard/Accounts/AccountsTerminals.less

@@ -0,0 +1,9 @@
+@import "../../../../node_modules/antd/lib/style/themes/default.less";
+.authDesc {
+  p {
+    margin-bottom: unset !important;
+  }
+  & > p > span {
+    font-weight: bold;
+  }
+}

+ 116 - 0
src/routes/Dashboard/Accounts/AccountsTotalList.js

@@ -0,0 +1,116 @@
+
+import React, { Component } from 'react';
+import { connect } from 'dva';
+import { Card } from 'antd';
+import { StandardTableList } from '../../../components/AXList';
+import { addRowKey, renderStatus } from '../../../utils/utils';
+
+@connect(({ loading, accounts }) => ({
+  accounts,
+  loading: loading.models.accounts,
+}))
+
+export default class TotalListAccountsPage extends Component {
+  constructor(props) {
+    super(props);
+    const { state } = props.location;
+    this.state = {
+      UIParams: (state || {}).UIParams, // 组件的状态参数
+      Queryers: (state || {}).Queryers, // 查询的条件参数
+    };
+  }
+  componentWillMount() {
+    this.props.dispatch({
+      type: 'accounts/fetchCampusAmount',
+      payload: {}
+    });
+    this.props.dispatch({
+      type: 'accounts/fetchTerminalsAmount',
+      payload: {}
+    });
+    this.props.dispatch({
+      type: 'accounts/fetchTotalList',
+      payload: { ...this.state.Queryers }
+    });
+  }
+  handleFilterOperation = (params, states) => {
+    this.setState({
+      UIParams: states,
+      Queryers: params,
+    });
+    this.props.dispatch({
+      type: 'accounts/fetchTotalList',
+      payload: {
+        ...params,
+      },
+    });
+  };
+  
+  render() {
+    const { loading,accounts } = this.props;
+    const { list, totalSize, pageSize, pageNo, campusAmount, terminalsAmount} = accounts;
+    const basicSearch = {
+      keys: [{
+        name: '终端编号',
+        field: 'code',
+      }],
+    };
+    const pagination = {
+      pageNo,
+      pageSize,
+      totalSize,
+    };
+    const columns = [{
+      title: '校区名称',
+      key: 1,
+      dataIndex: 'campusName',
+      width: '30%',
+    }, {
+      title: '终端编号',
+      key: 2,
+      dataIndex: 'code',
+      width: '17%',
+    }, {
+      title: '终端名称',
+      key: 3,
+      dataIndex: 'name',
+      width: '15%',
+    }, {
+      title: '联系电话',
+      key: 4,
+      dataIndex: 'mobile',
+      width: '18%',
+    }, {
+      title: '联系人',
+      key: 5,
+      dataIndex: 'merchantContactName',
+      width: '10%',
+    }, {
+      title: '课程状态',
+      key: 6,
+      dataIndex: 'status',
+      render: text => renderStatus(text),
+      width: '10%',
+    }];
+    return (
+      <Card>
+        <StandardTableList
+          columns={columns}
+          loading={loading}
+          dataSource={addRowKey(list)}
+          header={{
+            basicSearch,
+            campusAmount,
+            terminalsAmount,
+            onFilterClick: this.handleFilterOperation,
+          }}
+          footer={{
+            pagination,
+          }}
+          keepUIState={{ ...this.state.UIParams }}
+          showStatusSelect={false}
+        />
+      </Card>
+    );
+  }
+}

+ 69 - 0
src/routes/Dashboard/Accounts/index.js

@@ -0,0 +1,69 @@
+import React, { Component } from 'react';
+import { Redirect, Route, Switch, routerRedux } from 'dva/router';
+import { connect } from 'dva';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import { getRoutes } from '../../../utils/utils';
+@connect()
+export default class AccountsPage extends Component {
+  handleTabChange = (key) => {
+    const { dispatch, match } = this.props;
+    switch (key) {
+      // 终端课程包
+      case 'terminals':
+        dispatch(routerRedux.push(`${match.url}/terminals`));
+        break;
+      // 校区列表
+      case 'campus':
+        dispatch(routerRedux.push(`${match.url}/campus`));
+        break;
+      // 即将逾期
+      case 'overdue':
+        dispatch(routerRedux.push(`${match.url}/overdue`));
+        break;
+      // 总统计表
+      case 'totalList':
+        dispatch(routerRedux.push(`${match.url}/totalList`));
+        break;
+      default:
+        break;
+    }
+  };
+  render() {
+    const tabList = [
+      {
+        key: 'terminals',
+        tab: '已开通账号',
+      },
+      {
+        key: 'campus',
+        tab: '已开通校区',
+      },
+      {
+        key: 'overdue',
+        tab: '即将到期(30天)',
+      },
+      {
+        key: 'totalList',
+        tab: '总统计表',
+      },
+    ];
+
+    const { match, routerData, location } = this.props;
+    const routes = getRoutes(match.path, routerData);
+
+    return (
+      <PageHeaderLayout
+        tabList={tabList}
+        tabActiveKey={location.pathname.replace(`${match.path}/`, '')}
+        onTabChange={this.handleTabChange}
+      >
+        <Switch>
+          {routes.map(item => (
+            <Route key={item.key} path={item.path} component={item.component} exact={item.exact} />
+          ))}
+          <Redirect exact from="/dashboard/accounts" to="/dashboard/accounts/terminals" />
+        </Switch>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 1 - 0
src/routes/Dashboard/SnapshotList.js

@@ -12,6 +12,7 @@ const Message = message;
   trade,
   loading: loading.models.trade,
 }))
+
 export default class SnapshotListPage extends Component {
   componentDidMount() {
     this.props.dispatch({

+ 32 - 0
src/routes/Frontend/Tag/index.js

@@ -0,0 +1,32 @@
+import React, { Component } from 'react';
+import { Redirect, Route, Switch } from 'dva/router';
+import { connect } from 'dva';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import { getRoutes } from '../../../utils/utils';
+
+@connect()
+export default class Tag extends Component {
+  render() {
+    const { match, routerData } = this.props;
+    const routes = getRoutes(match.path, routerData);
+    return (
+      <PageHeaderLayout>
+        <Switch>
+          {
+            routes.map(item =>
+              (
+                <Route
+                  key={item.key}
+                  path={item.path}
+                  component={item.component}
+                  exact={item.exact}
+                />
+              )
+            )
+          }
+          <Redirect exact from="/frontend/tag" to="/frontend/tag/list" />
+        </Switch>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 489 - 0
src/routes/Product/Support/SupportCreate.js

@@ -0,0 +1,489 @@
+import React, { Component } from 'react';
+import pathToRegexp from 'path-to-regexp';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Form, Modal, Card, Button, Input, Switch, Row, Col, Carousel, Select } from 'antd';
+import { renderStatus, statusToBool, boolToStatus, genAbsolutePicUrl } from '../../../utils/utils';
+import AXDragSortTable from '../../../components/AXDragSortTable';
+import Selector from '../../../components/AXTableSelector/Selector';
+import FooterToolbar from '../../../components/FooterToolbar';
+import styles from './SupportCreate.less';
+
+const fieldLabels = {
+  code: '配套编号',
+  title: '配套标题',
+  subTitle: '配套副标题',
+  name: '配套全称',
+  merchant: '内容提供商',
+  digest: '配套概要',
+  detail: '配套详情',
+  coverUrl: '配套封面图',
+  imgList: '配套滚动图册',
+  status: '配套状态',
+};
+const formItemLayout = {
+  labelCol: {
+    xs: { span: 24 },
+    sm: { span: 3 },
+  },
+  wrapperCol: {
+    xs: { span: 24 },
+    sm: { span: 14 },
+    md: { span: 12 },
+  },
+};
+
+@connect(({ loading, product, resource, merchant }) => ({
+  product,
+  resource,
+  merchant,
+  pLoading: loading.models.product,
+  rLoading: loading.models.resource,
+  submitting: loading.models.product,
+}))
+@Form.create()
+export default class SupportCreatePage extends Component {
+  state = {
+    coverSelectorDestroy: true,
+    carouselSelectorDestroy: true,
+    supportSelectorDestroy: true,
+  };
+  componentWillMount() {
+    const match = pathToRegexp('/product/support/create').exec(this.props.location.pathname);
+    if (match) {
+      this.cleanPageState();
+    }
+  }
+  componentDidMount() {
+    const matchId = this.isEdit();
+    if (matchId) {
+      this.props.dispatch({
+        type: 'product/fetchProductItem',
+        payload: { pid: matchId },
+      });
+    }
+    this.props.dispatch({
+      type: 'merchant/fetchMerchantList',
+      payload: { pageSize: 1000 }, // TODO 以后商户多了需要改写交互样式
+    });
+  }
+  isEdit = () => {
+    const { location } = this.props;
+    const match = pathToRegexp('/product/support/edit/:id').exec(location.pathname);
+    if (match) {
+      return match[1];
+    }
+    return false;
+  };
+  cleanPageState = () => {
+    this.props.dispatch({
+      type: 'product/cleanItemState',
+      payload: {},
+    });
+  };
+  selectorDataFetcher = (name, params) => {
+    switch (name) {
+      case 'cover':
+        this.props.dispatch({
+          type: 'resource/fetchImageList',
+          payload: params,
+        });
+        break;
+      case 'carousel':
+        this.props.dispatch({
+          type: 'resource/fetchImageList',
+          payload: params,
+        });
+        break;
+      case 'support':
+        this.props.dispatch({
+          type: 'product/fetchSupportList',
+          payload: params,
+        });
+        break;
+      default:
+        break;
+    }
+  };
+  currentItemFormatter = (name, rows) => {
+    let payload;
+    switch (name) {
+      case 'cover':
+        payload = { coverUrl: rows[0].path };
+        break;
+      case 'carousel':
+        payload = { imgList: rows.map(row => row.path) };
+        break;
+      case 'support':
+        payload = { supportList: rows };
+        break;
+      default:
+        break;
+    }
+    return payload;
+  };
+  handleSelectorModalShow = (name) => {
+    this.setState({
+      [`${name}SelectorDestroy`]: false,
+    });
+    this.selectorDataFetcher(name);
+  };
+  handleSelectorChange = (name, params) => {
+    this.selectorDataFetcher(name, params);
+  };
+  handleSelectorFinish = (name, rows) => {
+    this.setState({
+      [`${name}SelectorDestroy`]: true,
+    });
+    const payload = this.currentItemFormatter(name, rows);
+    this.props.dispatch({
+      payload,
+      type: 'product/fixCurrentItem',
+    });
+  };
+  handleDragSortTableChange = (name, rows) => {
+    const payload = this.currentItemFormatter(name, rows);
+    this.props.dispatch({
+      payload,
+      type: 'product/fixCurrentItem',
+    });
+  };
+  handleSelectorCancel = (name) => {
+    this.setState({
+      [`${name}SelectorDestroy`]: true,
+    });
+  };
+  handlePageBack = () => {
+    this.props.dispatch(routerRedux.push({
+      pathname: '/product/support/list',
+      state: this.props.location.state,
+    }));
+  };
+  handlePageSubmit = (e) => {
+    e.preventDefault();
+    this.props.form.validateFieldsAndScroll((err, values) => {
+      if (!err) {
+        // 从表单提取基础信息字段
+        const { status, title, subTitle, name, ...rest } = values;
+        const newName = `${title}_${subTitle}`;
+        const newVals = { title, subTitle, name: newName, status: boolToStatus(status), ...rest };
+
+        // 从props中提取coverUrl、imgList、supportList字段
+        const { product } = this.props;
+        const { currentItem } = product;
+        const { imgList, supportList, coverUrl } = currentItem;
+
+        // 防止supportList为空
+        let supportIdList;
+        if (supportList && supportList.length) {
+          supportIdList = supportList.map(item => item.id);
+        }
+
+        // 更新或者创建操作
+        const matchId = this.isEdit();
+        if (matchId) {
+          const params = {
+            imgList,
+            coverUrl,
+            supportList: supportIdList,
+            id: matchId,
+            ...newVals,
+          };
+          this.props.dispatch({
+            type: 'product/updateSupportItem',
+            payload: params,
+            states: this.props.location.state,
+          });
+        } else {
+          const params = {
+            imgList,
+            coverUrl,
+            supportList: supportIdList,
+            ...newVals,
+          };
+          this.props.dispatch({
+            type: 'product/createSupportItem',
+            payload: params,
+            states: this.props.location.state,
+          });
+        }
+      }
+    });
+  };
+
+  render() {
+    const {
+      form, submitting, rLoading, pLoading, product, resource, merchant,
+    } = this.props;
+    const {
+      supportSelectorDestroy, coverSelectorDestroy, carouselSelectorDestroy,
+    } = this.state;
+    const {
+      currentItem,
+    } = product;
+    const {
+      code, title, subTitle, name, digest, detail, status, coverUrl, cpId,
+      imgList = [], supportList = [],
+    } = currentItem;
+    const {
+      getFieldDecorator,
+    } = form;
+
+    const supportColumns = [{
+      title: '配套编号',
+      dataIndex: 'code',
+      key: 1,
+      width: '20%',
+      render: (text, record) => (
+        <a
+          className="a-link"
+          target="_blank"
+          rel="noopener noreferrer"
+          href={`/product/support/edit/${record.id}`}
+        >
+          {text}
+        </a>
+      ),
+    }, {
+      title: '配套名称',
+      dataIndex: 'name',
+      key: 2,
+      width: '30%',
+      render: (text, record) => (
+        <a
+          className="a-link"
+          target="_blank"
+          rel="noopener noreferrer"
+          href={`/product/support/edit/${record.id}`}
+        >
+          {text}
+        </a>
+      ),
+    }, {
+      title: '配套状态',
+      dataIndex: 'status',
+      key: 3,
+      render: text => renderStatus(text),
+    }];
+
+    const getMerchants = () => {
+      const { list } = merchant;
+      return list.map(item => ({
+        text: item.name,
+        key: item.id,
+      }));
+    };
+
+    const getResourceModal = (isCover) => {
+      return (
+        <Modal
+          width={isCover ? 900 : 1100}
+          footer={null}
+          visible
+          title="图片资源"
+          maskClosable={false}
+          onCancel={() => this.handleSelectorCancel(isCover ? 'cover' : 'carousel')}
+        >
+          <Selector
+            loading={rLoading}
+            list={resource.list}
+            pageNo={resource.pageNo}
+            pageSize={resource.pageSize}
+            totalSize={resource.totalSize}
+            multiple={!isCover}
+            selectorName={isCover ? 'PictureSingle' : 'Picture'}
+            onCancel={() => this.handleSelectorCancel(isCover ? 'cover' : 'carousel')}
+            onChange={data => this.handleSelectorChange(isCover ? 'cover' : 'carousel', data)}
+            onFinish={rows => this.handleSelectorFinish(isCover ? 'cover' : 'carousel', rows)}
+          />
+        </Modal>
+      );
+    };
+    const getSupportModal = () => {
+      return (
+        <Modal
+          width={1100}
+          footer={null}
+          visible
+          title="配套资源"
+          maskClosable={false}
+          onCancel={() => this.handleSelectorCancel('support')}
+        >
+          <Selector
+            multiple
+            loading={pLoading}
+            selectorName="Support"
+            list={product.list}
+            pageNo={product.pageNo}
+            pageSize={product.pageSize}
+            totalSize={product.totalSize}
+            onCancel={() => this.handleSelectorCancel('support')}
+            onChange={data => this.handleSelectorChange('support', data)}
+            onFinish={rows => this.handleSelectorFinish('support', rows)}
+          />
+        </Modal>
+      );
+    };
+
+    const renderSupportCardName = () => {
+      return (
+        <Button
+          type="primary"
+          onClick={() => this.handleSelectorModalShow('support')}
+        >相关配套
+        </Button>
+      );
+    };
+    return (
+      <div>
+        {/* 基础信息Card */}
+        <Card title="基础信息" style={{ marginBottom: 16 }}>
+          <Form>
+            <Form.Item hasFeedback label={fieldLabels.code} {...formItemLayout}>
+              {getFieldDecorator('code', {
+                rules: [
+                  {
+                    required: true, message: '请填写配套编号',
+                  }, {
+                    pattern: /^[a-zA-Z0-9|-]+$/g, message: '编号包含非法字符',
+                  },
+                ],
+                initialValue: code,
+              })(
+                <Input placeholder="请输入" />
+              )}
+            </Form.Item>
+            {this.isEdit() && (
+              <Form.Item label={fieldLabels.name} {...formItemLayout}>
+                {getFieldDecorator('name', {
+                  initialValue: name,
+                })(
+                  <Input disabled />
+                )}
+              </Form.Item>
+            )}
+            <Form.Item hasFeedback label={fieldLabels.title} {...formItemLayout}>
+              {getFieldDecorator('title', {
+                rules: [{ required: true, message: '请填写配套标题' }],
+                initialValue: title,
+              })(
+                <Input placeholder="请输入" />
+              )}
+            </Form.Item>
+            <Form.Item hasFeedback label={fieldLabels.subTitle} {...formItemLayout}>
+              {getFieldDecorator('subTitle', {
+                rules: [{ required: true, message: '请填写配套副标题' }],
+                initialValue: subTitle,
+              })(
+                <Input placeholder="请输入" />
+              )}
+            </Form.Item>
+            <Form.Item hasFeedback label={fieldLabels.merchant} {...formItemLayout}>
+              {getFieldDecorator('cpId', {
+                rules: [{ required: true, message: '请选择供应商' }],
+                initialValue: cpId,
+              })(
+                <Select placeholder="请选择">
+                  {
+                    getMerchants().map(item => (
+                      <Select.Option key={item.key} value={item.key}>
+                        {item.text}
+                      </Select.Option>
+                    ))
+                  }
+                </Select>
+              )}
+            </Form.Item>
+            <Form.Item label={fieldLabels.digest} {...formItemLayout}>
+              {getFieldDecorator('digest', {
+                initialValue: digest,
+              })(
+                <Input.TextArea rows={4} placeholder="请输入" />
+              )}
+            </Form.Item>
+            <Form.Item label={fieldLabels.detail} {...formItemLayout}>
+              {getFieldDecorator('detail', {
+                initialValue: detail,
+              })(
+                <Input.TextArea rows={6} placeholder="请输入" />
+              )}
+            </Form.Item>
+            <Form.Item label={fieldLabels.status} {...formItemLayout}>
+              {getFieldDecorator('status', {
+                valuePropName: 'checked',
+                initialValue: statusToBool(status),
+              })(
+                <Switch
+                  checkedChildren="正常"
+                  unCheckedChildren="删除"
+                />
+              )}
+            </Form.Item>
+          </Form>
+        </Card>
+        <Row gutter={16} style={{ marginBottom: 16 }}>
+          <Col span={12}>
+            <Card
+              title={
+                <Button
+                  type="primary"
+                  onClick={() => this.handleSelectorModalShow('cover')}
+                >配套封面
+                </Button>
+              }
+              className={styles.picCard}
+            >
+              {coverUrl && <img src={coverUrl && genAbsolutePicUrl(coverUrl)} alt="" />}
+              {!coverSelectorDestroy && getResourceModal(true)}
+            </Card>
+          </Col>
+          <Col span={12}>
+            <Card
+              title={
+                <Button
+                  type="primary"
+                  onClick={() => this.handleSelectorModalShow('carousel')}
+                >详情图册
+                </Button>
+              }
+              className={styles.carouselCard}
+            >
+              <Carousel autoplay>
+                {
+                  imgList.map(
+                    path => (
+                      <img key={path} src={genAbsolutePicUrl(path)} alt="" />
+                    )
+                  )
+                }
+              </Carousel>
+              {!carouselSelectorDestroy && getResourceModal(false)}
+            </Card>
+          </Col>
+        </Row>
+        {/* 相关周边配套选择Card */}
+        <Card title={renderSupportCardName()} style={{ marginBottom: 70 }}>
+          <AXDragSortTable
+            columns={supportColumns}
+            data={supportList}
+            onChange={rows => this.handleDragSortTableChange('support', rows)}
+          />
+          {!supportSelectorDestroy && getSupportModal()}
+        </Card>
+        <FooterToolbar style={{ width: '100%' }}>
+          <Button
+            onClick={this.handlePageBack}
+            style={{ marginRight: 10 }}
+          >取消
+          </Button>
+          <Button
+            type="primary"
+            loading={submitting}
+            onClick={this.handlePageSubmit}
+          >提交
+          </Button>
+        </FooterToolbar>
+      </div>
+    );
+  }
+}

+ 32 - 0
src/services/accounts.js

@@ -0,0 +1,32 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+import { api, Hotax } from '../utils/config';
+
+export async function queryCampusList(params) {
+  const newParams = {
+    pageSize: Hotax.PAGE_SIZE,
+    ...params,
+  };
+  return request(`${api.accountsCampusList}?${stringify(newParams)}`);
+}
+export async function queryTerminalsList(params) {
+  const newParams = {
+    pageSize: Hotax.PAGE_SIZE,
+    ...params,
+  };
+  return request(`${api.accountsTerminalsList}?${stringify(newParams)}`);
+}
+export async function queryTotalList(params) {
+  const newParams = {
+    pageSize: Hotax.PAGE_SIZE,
+    ...params,
+  };
+  return request(`${api.acconutsTotalList}?${stringify(newParams)}`);
+}
+
+export async function queryCampusAmount() {
+  return request(`${api.acconutsCampusAmount}`);
+}
+export async function queryTerminalsAmount() {
+  return request(`${api.acconutsTerminalsAmount}`);
+}

+ 7 - 0
src/utils/config.js

@@ -144,6 +144,13 @@ const apiObj = {
   userTagCopy: '/userTag/copy',
   userRecommend: '/user/userRecommend/uid',
   userDevice: '/userDevice/list',
+  accountsCampusList: '/stmt/campus/list',
+  accountsCampusDownload: '/stmt/campus/export',
+  accountsTerminalsList: '/stmt/terminal/user/page',
+  accountsTerminalsDownload: '/stmt/terminal/user/export',
+  acconutsTotalList: '/stmt/stats/page',
+  acconutsCampusAmount: '/stmt/stats/campus/totalsize',
+  acconutsTerminalsAmount: '/stmt/stats/terminal/user/totalsize',
 };
 
 /**

+ 1 - 2
src/utils/request.js

@@ -52,6 +52,7 @@ function checkContentStatus(data) {
  * @param  {object} [options] The options we want to pass to "fetch"
  * @return {object}           An object containing either "data" or "err"
  */
+
 export default function request(url, options) {
   const defaultOptions = {
     credentials: 'include',
@@ -69,7 +70,6 @@ export default function request(url, options) {
         newOptions.body = JSON.stringify(newOptions.body);
       }
     } else {
-      // newOptions.body is FormData
       newOptions.headers = {
         Accept: 'application/json',
         'Content-Type': 'multipart/form-data',
@@ -77,7 +77,6 @@ export default function request(url, options) {
       };
     }
   }
-
   return fetch(url, newOptions)
     .then(checkHttpStatus)
     .then((response) => {