Procházet zdrojové kódy

增加校区管理模块

zhanghe před 7 roky
rodič
revize
fd025909f7

+ 9 - 1
src/common/router.js

@@ -113,9 +113,17 @@ export const getRouterData = (app) => {
       component: dynamicWrapper(app, ['campus'], () => import('../routes/Campus/Campus')),
     },
     '/campus/list': {
-      component: dynamicWrapper(app, ['campus'], () => import('../routes/Campus/CampusList')),
+      component: dynamicWrapper(app, ['campus', 'merchant'], () => import('../routes/Campus/CampusList')),
       name: '校区列表',
     },
+    '/campus/create': {
+      component: dynamicWrapper(app, ['campus', 'merchant'], () => import('../routes/Campus/CampusCreate')),
+      name: '添加校区',
+    },
+    '/campus/edit/:id': {
+      component: dynamicWrapper(app, ['campus', 'merchant'], () => import('../routes/Campus/CampusCreate')),
+      name: '编辑校区',
+    },
     '/dashboard/analysis': {
       component: dynamicWrapper(app, ['chart'], () => import('../routes/Dashboard/Analysis')),
     },

+ 1 - 1
src/components/PageHeader/index.less

@@ -15,7 +15,7 @@
 
   .breadcrumb {
     font-weight: 500;
-    margin-bottom: 10px;
+    // margin-bottom: 10px;
   }
 
   .tabs {

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 4077 - 0
src/components/RBCityCascader/city.js


+ 33 - 0
src/components/RBCityCascader/index.js

@@ -0,0 +1,33 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Cascader } from 'antd';
+import { DICT_FIXED} from './city';
+import styles from './index.less';
+
+class RBCityCascader extends PureComponent {
+  static propTypes = {
+    placeholder: PropTypes.string,
+  };
+  static defaultProps = {
+    placeholder: '请选择省市'
+  };
+  handleOnChange = (value) => {
+    this.props.onChange(value);
+  }
+  render() {
+    const { value, placeholder } = this.props;
+    return (
+      <Cascader
+        allowClear
+        value={value}
+        popupClassName={styles.cascader}
+        expandTrigger={'hover'}
+        options={DICT_FIXED}
+        placeholder={placeholder}
+        onChange={this.handleOnChange}
+      />
+    );
+  }
+}
+
+export default RBCityCascader;

+ 9 - 0
src/components/RBCityCascader/index.less

@@ -0,0 +1,9 @@
+@import "~antd/lib/style/themes/default.less";
+
+.cascader {
+  :global {
+    .ant-cascader-menu {
+      min-width: 180px;
+    }
+  }
+}

+ 7 - 13
src/components/RBList/StandardTableList.js

@@ -47,7 +47,6 @@ export default class StandardTableList extends PureComponent {
     } = this.props;
     this.state = {
       batchActionKey: keepUIState.batchActionKey,
-      isAdvancedLevel: keepUIState.isAdvancedLevel || false,
       searchSelectKey: keepUIState.searchSelectKey || getSearchField(basicSearch),
       searchInputValue: keepUIState.searchInputValue || '',
       selectedKeys: keepUIState.selectedKeys || [],
@@ -67,7 +66,7 @@ export default class StandardTableList extends PureComponent {
   getListHeader = () => {
     const {
       showStatusSelect,
-      header: { basicSearch, advancedSearch, onCreateClick },
+      header: { basicSearch, onAdvanceFilterClick, onCreateClick },
       footer: { pagination },
     } = this.props;
     const { keys } = basicSearch;
@@ -112,11 +111,12 @@ export default class StandardTableList extends PureComponent {
               />
             </div>
             <div className={styles.right}>
-              {advancedSearch &&
-                (this.state.isAdvancedLevel ?
-                  <a className={styles.searchLevel}>普通搜索</a> :
-                  <a className={styles.searchLevel}>高级搜索</a>
-                )
+              {onAdvanceFilterClick &&
+                <a
+                  className={styles.searchLevel}
+                  onClick={onAdvanceFilterClick}
+                >高级筛选
+                </a>
               }
               <Button icon="sync" onClick={this.handleRefreshBtnClick}>刷新</Button>
               {/* noCreate 参数控制是否显示新建按钮 */}
@@ -133,11 +133,6 @@ export default class StandardTableList extends PureComponent {
               }
             </div>
           </div>
-          {advancedSearch &&
-            this.state.isAdvancedLevel ?
-              <div className={styles.advancedSearch}>{advancedSearch}</div>
-            : null
-          }
         </div>
         <Alert
           message={(
@@ -239,7 +234,6 @@ export default class StandardTableList extends PureComponent {
     const { header: { basicSearch } } = this.props;
     this.setState({
       batchActionKey: undefined,
-      isAdvancedLevel: false,
       searchSelectKey: getSearchField(basicSearch),
       selectedKeys: [],
       searchInputValue: '',

+ 63 - 0
src/components/RBRemoteSelect/index.js

@@ -0,0 +1,63 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Select, Spin } from 'antd';
+import styles from './index.less';
+
+export default class RBRemoteSelect extends PureComponent {
+  static defaultProps = {
+    dataSource: [],
+    placeholder: '请输入检索内容进行选择',
+  };
+  static propTypes = {
+    dataSource: PropTypes.array.isRequired,
+    placeholder: PropTypes.string,
+    onFocus: PropTypes.func,
+  };
+  handleOnChange = (value) => {
+    if (this.props.onChange) {
+      this.props.onChange(value);
+    }
+  }
+  lastTimeStamp = 0;
+  handleOnSearch = (value) => {
+    const eventTimeStamp = new Date().getTime();
+    this.lastTimeStamp = eventTimeStamp;
+    //800ms后比较时间时间戳与上次事件的时间戳是否相等
+    //相等说明800ms未进行赋值,则触发搜索; 不相等说明还在继续输入,不触发搜索
+    setTimeout(() => {
+      if (this.lastTimeStamp === eventTimeStamp) {
+        this.props.onSearch(value);
+        this.lastTimeStamp = 0;
+      }
+    }, 800);
+  }
+  render() {
+    const { value, dataSource, placeholder, onSearch, fetching } = this.props;
+    return (
+      <div>
+        <Select
+          mode="multiple"
+          filterOption={false}
+          notFoundContent={fetching ? <Spin size="small" /> : null}
+          value={value}
+          onSearch={this.handleOnSearch}
+          onChange={this.handleOnChange}
+          placeholder={placeholder}
+        >
+          {
+            dataSource.map(
+              (option) => (
+                <Select.Option
+                  key={option.value}
+                  value={option.value}
+                >
+                  {option.text}
+                </Select.Option>
+              )
+            )
+          }
+        </Select>
+      </div>
+    );
+  }
+}

+ 0 - 0
src/components/RBRemoteSelect/index.less


+ 195 - 0
src/components/RBSelectTable/RBSingleSelectTable.js

@@ -0,0 +1,195 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Table, Input, Select, Pagination, Radio, Icon } from 'antd';
+import styles from './RBSingleSelectTable.less';
+
+function addRowKey(data) {
+  return data.map((item) => {
+    return {
+      ...item,
+      key: item.id,
+    };
+  });
+}
+const options = [{
+  text: '编号',
+  value: 'code',
+}, {
+  text: '名称',
+  value: 'name',
+}];
+
+export default class RBSingleSelectTable extends PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      selectRowKey: props.value,
+      searchSelectKey: 'code',
+      searchInputValue: '',
+      selectRowName: props.currentName,
+    };
+  }
+  static propTypes = {
+    dataSource: PropTypes.array,
+    loading: PropTypes.bool,
+    columns: PropTypes.array,
+    pagination: PropTypes.object,
+  };
+  static defaultProps = {
+    dataSource: [],
+    loading: false,
+    columns: [],
+    pagination: {},
+  };
+  componentWillReceiveProps(nextProps) {
+    let stateProps = {};
+    if (nextProps.value && (nextProps.value !== this.props.value)) {
+      stateProps.selectRowKey = nextProps.value;
+    }
+    if (nextProps.currentName && (nextProps.currentName !== this.props.currentName)) {
+      stateProps.selectRowName = nextProps.currentName;
+    }
+    this.setState({...stateProps});
+  }
+  handleSelectChange = (value) => {
+    this.setState({ searchSelectKey: value });
+  }
+  handleInputChange = (e) => {
+    this.setState({ searchInputValue: e.target.value });
+  }
+  handleFilterOperation = (kv) => {
+    const { searchSelectKey, searchInputValue } = this.state;
+    this.props.onFilterClick({
+      [searchSelectKey]: searchInputValue,
+      ...kv,
+    });
+  }
+  handleOnRowClick = (record) => {
+    this.setState({
+      selectRowKey: record.key,
+      selectRowName: record.name,
+    });
+    if (this.props.onChange) {
+      this.props.onChange(record.key);
+    }
+  }
+  handleSearchBtnClick = () => {
+    this.handleFilterOperation();
+  }
+  handleTableChange = (pagination) => {
+    const { current } = pagination;
+    this.handleFilterOperation({ pageNo: current });
+  }
+  handleListPageChange = (page, pageSize) => {
+    this.handleFilterOperation({
+      pageSize,
+      pageNo: page,
+    });
+  }
+  getListHeader = () => {
+    const { pagination } = this.props;
+    const paginationProps = {
+      current: pagination.pageNo,
+      pageSize: pagination.pageSize,
+      total: pagination.totalSize,
+      onChange: this.handleListPageChange,
+      showTotal: (total) => `共 ${total} 条`,
+    };
+    return (
+      <div className={styles.header}>
+        <div className={styles.headerSearch}>
+          <div className={styles.searchPage}>
+            <div className={styles.left}>
+              <Input.Search
+                enterButton
+                placeholder="请输入"
+                style={{ width: '75%' }}
+                addonBefore={
+                  <Select
+                    placeholder="请选择"
+                    value={this.state.searchSelectKey}
+                    onChange={this.handleSelectChange}
+                  >
+                    {options.map(item => (
+                      <Select.Option
+                        key={item.value}
+                        value={item.value}
+                      >
+                        {item.text}
+                      </Select.Option>))}
+                  </Select>
+                }
+                value={this.state.searchInputValue}
+                onChange={this.handleInputChange}
+                onSearch={this.handleSearchBtnClick}
+              />
+            </div>
+            <div className={styles.right}>
+              <Pagination {...paginationProps} />
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+  getListFooter = () => {
+    if (!this.state.selectRowKey) {
+      return (
+        <span>
+          <span className={styles.label}>当前已选:</span>
+          <span className={styles.notSelect}>
+            未选择<Icon type="frown" />
+          </span>
+        </span>
+      );
+    }
+    return (
+      <span>
+        <span className={styles.label}>当前已选:</span>
+        <span className={styles.selected}>
+          {this.state.selectRowName}
+        </span>
+      </span>
+    );
+  }
+
+  render() {
+    const { columns, dataSource, loading } = this.props;
+    const addColumnOnFirst = (columns) => {
+      const newColumns = [...columns];
+      newColumns.unshift({
+        key: '-1',
+        dataIndex: 'key',
+        width: 20,
+        render: (text) => {
+          return (
+            <Radio
+              key={text}
+              checked={this.state.selectRowKey === text}
+            />
+          );
+        }
+      });
+      return newColumns;
+    }
+    const onRowClick = (record) => {
+      return {
+        onClick: () => this.handleOnRowClick(record),
+      };
+    }
+    return (
+      <Table
+        bordered={false}
+        loading={loading}
+        columns={addColumnOnFirst(columns)}
+        title={this.getListHeader}
+        footer={this.getListFooter}
+        className={styles.table}
+        rowKey={record => record.key}
+        onRow={onRowClick}
+        dataSource={addRowKey(dataSource)}
+        pagination={false}
+      />
+    );
+  }
+}

+ 52 - 0
src/components/RBSelectTable/RBSingleSelectTable.less

@@ -0,0 +1,52 @@
+@import "~antd/lib/style/themes/default.less";
+
+.table {
+  :global {
+    .ant-table-title {
+      padding: 0;
+    }
+    .ant-table-footer {
+      padding: 10px;
+    }
+    .ant-table-tbody > tr > td {
+      padding: 5px;
+    }
+    .ant-table-thead > tr > th {
+      padding: 10px 5px;
+    }
+    .ant-table-row {
+      &:hover {
+        cursor: pointer;
+      }
+    }
+  }
+}
+
+.header {
+  .headerSearch {
+    .searchPage {
+      height: 35px;
+      .left {
+        width: 50%;
+        float: left;
+      }
+      .right {
+        float: right;
+      }
+    }
+  }
+}
+
+.notSelect {
+  padding: 5px;
+  font-weight: 400;
+}
+.selected {
+  padding: 5px;
+  color: #fff;
+  background: @primary-5;
+}
+.label {
+  font-weight: 500;
+  color: red;
+}

+ 5 - 0
src/components/RBSelectTable/index.js

@@ -0,0 +1,5 @@
+import RBSingleSelectTable from './RBSingleSelectTable';
+
+export {
+  RBSingleSelectTable,
+};

+ 1 - 1
src/layouts/BasicLayout.js

@@ -173,7 +173,7 @@ class BasicLayout extends React.PureComponent {
             isMobile={this.state.isMobile}
             onCollapse={this.handleMenuCollapse}
           />
-          <Content style={{ marginBottom: '50px' }}>
+          <Content style={{ marginBottom: 50 }}>
             <Switch>
               {
                 redirectData.map(item =>

+ 3 - 3
src/layouts/PageHeaderLayout.js

@@ -1,12 +1,12 @@
 import React from 'react';
 import { Link } from 'dva/router';
 import PageHeader from '../components/PageHeader';
+import styles from './PageHeaderLayout.less';
 
 export default ({ children, wrapperClassName, top, ...restProps }) => (
   <div className={wrapperClassName}>
     {top}
-    <PageHeader key="pageheader" {...restProps} linkElement={Link}>
-      {children ? <div>{children}</div> : null}
-    </PageHeader>
+    <PageHeader {...restProps} linkElement={Link} />
+    {children ? <div className={styles.content}>{children}</div> : null}
   </div>
 );

+ 11 - 0
src/layouts/PageHeaderLayout.less

@@ -0,0 +1,11 @@
+@import "~antd/lib/style/themes/default.less";
+
+.content {
+  margin: 15px 15px 0;
+}
+
+@media screen and (max-width: @screen-sm) {
+  .content {
+    margin: 15px 0 0;
+  }
+}

+ 317 - 0
src/routes/Campus/CampusCreate.js

@@ -0,0 +1,317 @@
+import React, { PureComponent } from 'react';
+import pathToRegexp from 'path-to-regexp';
+import { Card, Row, Col, Form, Input, Button, Switch, Popover, Icon } from 'antd';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import RBCityCascader from '../../components/RBCityCascader';
+import FooterToolbar from '../../components/FooterToolbar';
+import { RBSingleSelectTable } from '../../components/RBSelectTable';
+import {
+  renderStatus,
+  renderCategory,
+  provinceCodeToName,
+  provinceNameToCode,
+} from '../../utils/utils';
+import styles from './CampusCreate.less';
+
+const fieldLabels = {
+  merchant: '所属商户',
+  cityName: '所在城市',
+  zoneName: '校区名称',
+  contactName: '校区联系人',
+  mobile: '联系电话',
+  address: '收货地址',
+  depositBank: '开户行',
+  bankAccount: '银行账户',
+};
+const formItemLayout = {
+  labelCol: {
+    xs: { span: 24 },
+    sm: { span: 2 },
+  },
+  wrapperCol: {
+    xs: { span: 24 },
+    sm: { span: 14 },
+    md: { span: 10 },
+  },
+};
+const submitFormLayout = {
+  wrapperCol: {
+    xs: { span: 24, offset: 0 },
+    sm: { span: 12, offset: 6 },
+  },
+};
+
+@Form.create()
+@connect(({loading, merchant, campus}) => ({
+  campus,
+  merchant,
+  loading: loading.models.merchant,
+}))
+export default class CampusCreatePage extends PureComponent {
+  componentDidMount() {
+    //加载商户列表
+    this.props.dispatch({
+      type: 'merchant/fetchMerchantList',
+      payload: {},
+    });
+    //如果是编辑校区,加载校区详情
+    const matchId = this.isEdit();
+    if (matchId) {
+      this.props.dispatch({
+        type: 'campus/fetchCampusItem',
+        payload: {id: matchId},
+      });
+    }
+  }
+  componentWillUnmount() {
+    this.props.dispatch({
+      type: 'campus/cleanItemState',
+      payload: {},
+    });
+  }
+  isEdit = () => {
+    const { location } = this.props;
+    const match = pathToRegexp('/campus/edit/:id').exec(location.pathname);
+    if (match) {
+      return match[1];
+    }
+    return false;
+  }
+  handleMerchantTableChange = (params) => {
+    this.props.dispatch({
+      type: 'merchant/fetchMerchantList',
+      payload: params,
+    });
+  }
+  handlePageSubmit = () => {
+    this.props.form.validateFieldsAndScroll((error, values) => {
+      if (!error) {
+        const { cityName, ...restProps } = values;
+        restProps.provinceCode = provinceNameToCode(cityName[0]);
+        restProps.cityName = cityName[1];
+        const matchId = this.isEdit();
+        if (matchId) {
+          restProps.id = matchId;
+          this.props.dispatch({
+            type: 'campus/updateCampusItem',
+            payload: restProps,
+            states: this.props.location.state,
+          });
+        } else {
+          this.props.dispatch({
+            type: 'campus/createCampusItem',
+            payload: restProps,
+            states: this.props.location.state,
+          });
+        }
+      }
+    });
+  }
+  handlePageBack = () => {
+    this.props.dispatch(routerRedux.push({
+      pathname: '/campus/list',
+      state: this.props.location.state,
+    }));
+  }
+
+  render() {
+    const { form, merchant, loading, campus } = this.props;
+    const { getFieldDecorator, getFieldsError } = form;
+    const { list, totalSize, pageNo, pageSize } = merchant;
+    const { currentItem } = campus;
+
+    const pagination = { totalSize, pageSize, pageNo };
+    const cardProps = {
+      bordered: false,
+      className: styles.cardItem,
+    };
+
+    const merchantColumns = [{
+      title: '商户编号',
+      dataIndex: 'code',
+      key: 1,
+    }, {
+      title: '商户名称',
+      dataIndex: 'name',
+      key: 2,
+    }, {
+      title: '商户类型',
+      key: 3,
+      dataIndex: 'domain',
+      render: (text) => renderCategory(text),
+    }, {
+      title: '该条状态',
+      key: 4,
+      dataIndex: 'status',
+      render: (text) => renderStatus(text),
+    }];
+
+    const renderCityName = () => {
+      const { provinceCode, cityName } = currentItem;
+      if (!provinceCode && !cityName) {
+        return;
+      } else {
+        return [
+          provinceCodeToName(provinceCode),
+          cityName,
+        ];
+      }
+    }
+
+    const errors = getFieldsError();
+    const getErrorInfo = () => {
+      const errorCount = Object.keys(errors).filter(key => errors[key]).length;
+      if (!errors || errorCount === 0) {
+        return null;
+      }
+      const scrollToField = (fieldKey) => {
+        const labelNode = document.querySelector(`label[for="${fieldKey}"]`);
+        if (labelNode) {
+          labelNode.scrollIntoView(true);
+        }
+      };
+      const errorList = Object.keys(errors).map((key) => {
+        if (!errors[key]) {
+          return null;
+        }
+        return (
+          <li key={key} className={styles.errorListItem} onClick={() => scrollToField(key)}>
+            <Icon type="cross-circle-o" className={styles.errorIcon} />
+            <div className={styles.errorMessage}>{errors[key][0]}</div>
+            <div className={styles.errorField}>{fieldLabels[key]}</div>
+          </li>
+        );
+      });
+      return (
+        <span className={styles.errorIcon}>
+          <Popover
+            title="表单校验信息"
+            content={errorList}
+            overlayClassName={styles.errorPopover}
+            trigger="click"
+            getPopupContainer={trigger => trigger.parentNode}
+          >
+            <Icon type="exclamation-circle" />
+          </Popover>
+          {errorCount}
+        </span>
+      );
+    };
+
+    return (
+      <div>
+        <Card title={fieldLabels.merchant} {...cardProps} style={{marginBottom: 16}}>
+          <Form>
+            <Form.Item className={styles.firstFormItem}>
+              {getFieldDecorator('merchantId', {
+                rules: [{ required: true, message: '请选择商户!' }],
+                initialValue: currentItem.merchantId,
+              })(
+                <RBSingleSelectTable
+                  currentName={currentItem.merchantName}
+                  columns={merchantColumns}
+                  loading={loading}
+                  dataSource={list}
+                  pagination={pagination}
+                  onFilterClick={this.handleMerchantTableChange}
+                />
+              )}
+            </Form.Item>
+          </Form>
+        </Card>
+        <Card title="基本信息" {...cardProps} style={{marginBottom: 70}}>
+          <Form>
+            <Row gutter={16}>
+              <Col lg={8} md={12} sm={24}>
+                <Form.Item label={fieldLabels.cityName}>
+                  {getFieldDecorator('cityName', {
+                    rules: [{ required: true, message: '请选择校区地址!' }],
+                    initialValue: renderCityName(),
+                  })(
+                    <RBCityCascader />
+                  )}
+                </Form.Item>
+              </Col>
+              <Col lg={8} md={12} sm={24}>
+                <Form.Item label={fieldLabels.zoneName}>
+                  {getFieldDecorator('zoneName', {
+                    rules: [{ required: true, message: '校区名不能为空!' }],
+                    initialValue: currentItem.zoneName,
+                  })(
+                    <Input placeholder="请输入" />
+                  )}
+                </Form.Item>
+              </Col>
+              <Col lg={8} md={12} sm={24}>
+                <Form.Item label={fieldLabels.contactName}>
+                  {getFieldDecorator('contactName', {
+                    rules: [{ required: true, message: '联系人不能为空!' }],
+                    initialValue: currentItem.contactName,
+                  })(
+                    <Input placeholder="请输入" />
+                  )}
+                </Form.Item>
+              </Col>
+            </Row>
+            <Row gutter={16}>
+              <Col lg={8} md={12} sm={24}>
+                <Form.Item label={fieldLabels.mobile}>
+                  {getFieldDecorator('mobile', {
+                    rules: [
+                      {
+                        required: true, message: '联系电话不能为空!',
+                      }, {
+                        pattern: /^[1][3,4,5,7,8][0-9]{9}$/g, message: '请输入11位有效手机号!',
+                      }],
+                    initialValue: currentItem.mobile,
+                  })(
+                    <Input placeholder="请输入" />
+                  )}
+                </Form.Item>
+              </Col>
+              <Col lg={8} md={12} sm={24}>
+                <Form.Item label={fieldLabels.depositBank}>
+                  {getFieldDecorator('depositBank', {
+                    initialValue: currentItem.depositBank,
+                  })(
+                    <Input placeholder="请输入" />
+                  )}
+                </Form.Item>
+              </Col>
+              <Col lg={8} md={12} sm={24}>
+                <Form.Item label={fieldLabels.bankAccount}>
+                  {getFieldDecorator('bankAccount', {
+                    initialValue: currentItem.bankAccount,
+                  })(
+                    <Input placeholder="请输入" />
+                  )}
+                </Form.Item>
+              </Col>
+            </Row>
+            <Row gutter={16}>
+              <Col lg={8} md={12} sm={24}>
+                <Form.Item label={fieldLabels.address}>
+                  {getFieldDecorator('address', {
+                    initialValue: currentItem.address,
+                  })(
+                    <Input placeholder="请输入" />
+                  )}
+                </Form.Item>
+              </Col>
+            </Row>
+          </Form>
+        </Card>
+        <FooterToolbar style={{width: '100%'}}>
+          {getErrorInfo()}
+          <Button onClick={this.handlePageBack} style={{marginRight: 10}}>
+            取消
+          </Button>
+          <Button type="primary" onClick={this.handlePageSubmit}>
+            提交
+          </Button>
+        </FooterToolbar>
+      </div>
+    );
+  }
+}

+ 59 - 0
src/routes/Campus/CampusCreate.less

@@ -0,0 +1,59 @@
+@import "~antd/lib/style/themes/default.less";
+
+.cardItem {
+  :global {
+    .ant-card-head {
+      padding-left: 16px;
+    }
+  }
+}
+
+.form {
+  margin-bottom: 0;
+}
+
+.errorIcon {
+  cursor: pointer;
+  color: @error-color;
+  margin-right: 24px;
+  i {
+    margin-right: 4px;
+  }
+}
+
+.errorPopover {
+  :global {
+    .ant-popover-inner-content {
+      padding: 0;
+      max-height: 290px;
+      overflow: auto;
+      min-width: 256px;
+    }
+  }
+}
+
+.errorListItem {
+  list-style: none;
+  border-bottom: 1px solid @border-color-split;
+  padding: 8px 16px;
+  cursor: pointer;
+  transition: all .3s;
+  &:hover {
+    background: @primary-1;
+  }
+  &:last-child {
+    border: 0;
+  }
+  .errorIcon {
+    color: @error-color;
+    float: left;
+    margin-top: 4px;
+    margin-right: 12px;
+    padding-bottom: 22px;
+  }
+  .errorField {
+    font-size: 12px;
+    color: @text-color-secondary;
+    margin-top: 2px;
+  }
+}

+ 85 - 13
src/routes/Campus/CampusList.js

@@ -2,15 +2,39 @@ import React, { Component, Fragment } from 'react';
 import moment from 'moment';
 import { connect } from 'dva';
 import { routerRedux } from 'dva/router';
-import { Modal, Button, message } from 'antd';
+import { Card, Modal, Form, Button, message } from 'antd';
 import { StandardTableList } from '../../components/RBList';
+import RBRemoteSelect from '../../components/RBRemoteSelect';
 import { renderCategory, addRowKey } from '../../utils/utils';
 import styles from './CampusList.less';
 
 const Message = message;
 
-@connect(({loading, campus}) => ({
+const formItemLayout = {
+  labelCol: {
+    xs: { span: 24 },
+    sm: { span: 7 },
+  },
+  wrapperCol: {
+    xs: { span: 24 },
+    sm: { span: 15 },
+    md: { span: 13 },
+  },
+};
+function merchantDataFormatter(data) {
+  return data.map((item) => {
+    return {
+      text: item.name,
+      value: item.id,
+    };
+  });
+}
+
+@Form.create()
+@connect(({loading, campus, merchant}) => ({
   campus,
+  merchant,
+  fetching: loading.models.merchant,
   loading: loading.models.campus,
 }))
 export default class CampusListPage extends Component {
@@ -20,6 +44,7 @@ export default class CampusListPage extends Component {
     this.state = {
       UIParams: (state || {}).UIParams, // 组件的状态参数
       Queryers: (state || {}).Queryers, // 查询的条件参数
+      filterModalDestory: true,
     };
   }
   componentDidMount() {
@@ -50,13 +75,41 @@ export default class CampusListPage extends Component {
       Queryers: params,
     });
   }
+  handleModalFilterOperation = () => {
+    const { getFieldValue } = this.props.form;
+    const value = getFieldValue('merchantId');
+    this.props.dispatch({
+      type: 'campus/fetchCampusList',
+      payload: {
+        ...this.state.Queryers,
+        merchantId: value[0],
+      },
+    });
+    this.handleFilterModalDestory();
+  }
   handleBatchOperation = () => {
     Message.info('暂不支持批量操作!');
   }
+  handleFilterModalShow = () => {
+    this.setState({ filterModalDestory: false });
+  }
+  handleFilterModalDestory = () => {
+    this.setState({ filterModalDestory: true });
+  }
+  handleRemoteSelectSearch = (value) => {
+    this.props.dispatch({
+      type: 'merchant/fetchMerchantList',
+      payload: {
+        pageSize: 50,
+        name: value,
+      },
+    });
+  }
 
   render() {
-    const { loading, campus } = this.props;
+    const { loading, fetching, form, campus, merchant } = this.props;
     const { list, totalSize, pageSize, pageNo } = campus;
+    const { getFieldDecorator } = form;
 
     const renderOperation = (item) => {
       return (
@@ -70,7 +123,6 @@ export default class CampusListPage extends Component {
         </div>
       );
     }
-
     const batchActions = [{
       key: 'delete',
       name: '批量删除',
@@ -144,21 +196,15 @@ export default class CampusListPage extends Component {
       render: (_, record) => renderOperation(record),
       width: '6%',
     }];
-    const renderAdvancedSearch = () => {
-      return (
-        <div>测试
-        </div>
-      );
-    }
     return (
-      <Fragment>
+      <Card>
         <StandardTableList
           columns={columns}
           loading={loading}
           dataSource={addRowKey(list)}
           header={{
             basicSearch,
-            advancedSearch: renderAdvancedSearch(),
+            onAdvanceFilterClick: this.handleFilterModalShow,
             onFilterClick: this.handleFilterOperation,
             onCreateClick: this.handleCreateOperation,
           }}
@@ -170,7 +216,33 @@ export default class CampusListPage extends Component {
           keepUIState={{...this.state.UIParams}}
           showStatusSelect={false}
         />
-      </Fragment>
+        {!this.state.filterModalDestory &&
+          <Modal
+            width={600}
+            visible={true}
+            title="高级筛选"
+            okText="筛选"
+            cancelText="取消"
+            maskClosable={false}
+            onCancel={this.handleFilterModalDestory}
+            onOk={this.handleModalFilterOperation}
+          >
+            <Form>
+              <Form.Item label="所属商户" {...formItemLayout}>
+                {getFieldDecorator('merchantId', {
+                  initialValue: [],
+                })(
+                  <RBRemoteSelect
+                    fetching={fetching}
+                    dataSource={merchantDataFormatter(merchant.list)}
+                    onSearch={this.handleRemoteSelectSearch}
+                  />
+                )}
+              </Form.Item>
+            </Form>
+          </Modal>
+        }
+      </Card>
     );
   }
 }

+ 140 - 138
src/routes/Merchant/MerchantCreate.js

@@ -1,6 +1,6 @@
 import React, { PureComponent } from 'react';
 import pathToRegexp from 'path-to-regexp';
-import { Form, Input, Button, Radio, Switch } from 'antd';
+import { Card, Form, Input, Button, Radio, Switch } from 'antd';
 import { connect } from 'dva';
 import { routerRedux } from 'dva/router';
 import {
@@ -148,147 +148,149 @@ export default class MerchantCreatePage extends PureComponent {
     }
 
     return (
-      <Form onSubmit={this.handlePageSubmit}>
-        <Form.Item label={fieldLabels.type} {...formItemLayout}>
-          {getFieldDecorator('domain', {
-            rules: [{ required: true, message: '请选择商户类型!' }],
-            initialValue: currentItem.domain || DOMAIN_PJ,
-          })(
-            <Radio.Group className={styles.radio}>
-              {
-                domains.map(item =>
-                  (
-                    <Radio.Button
-                      key={item.value}
-                      value={item.value}
-                    >{item.title}
-                    </Radio.Button>
+      <Card>
+        <Form onSubmit={this.handlePageSubmit}>
+          <Form.Item label={fieldLabels.type} {...formItemLayout}>
+            {getFieldDecorator('domain', {
+              rules: [{ required: true, message: '请选择商户类型!' }],
+              initialValue: currentItem.domain || DOMAIN_PJ,
+            })(
+              <Radio.Group className={styles.radio}>
+                {
+                  domains.map(item =>
+                    (
+                      <Radio.Button
+                        key={item.value}
+                        value={item.value}
+                      >{item.title}
+                      </Radio.Button>
+                    )
                   )
-                )
-              }
-            </Radio.Group>
-          )}
-        </Form.Item>
-        <Form.Item hasFeedback label={fieldLabels.code} {...formItemLayout}>
-          {getFieldDecorator('code', {
-            rules: [
-              {
-                required: true, message: '请给商户编号!',
-              }, {
-                pattern: /^[0-9]{4,6}$/g, message: '请输入4-6位数字编号!',
-              },
-            ],
-            initialValue: currentItem.code,
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item hasFeedback label={fieldLabels.name} {...formItemLayout}>
-          {getFieldDecorator('name', {
-            rules: [{ required: true, message: '请给商户命名!' }],
-            initialValue: currentItem.name,
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item hasFeedback label={fieldLabels.contactName} {...formItemLayout}>
-          {getFieldDecorator('contactName', {
-            rules: [{ required: true, message: '请填写商户联系人!' }],
-            initialValue: currentItem.contactName,
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item hasFeedback label={fieldLabels.mobile} {...formItemLayout}>
-          {getFieldDecorator('mobile', {
-            rules: [
-              {
-                required: true, message: '请填写联系电话!',
+                }
+              </Radio.Group>
+            )}
+          </Form.Item>
+          <Form.Item hasFeedback label={fieldLabels.code} {...formItemLayout}>
+            {getFieldDecorator('code', {
+              rules: [
+                {
+                  required: true, message: '请给商户编号!',
+                }, {
+                  pattern: /^[0-9]{4,6}$/g, message: '请输入4-6位数字编号!',
+                },
+              ],
+              initialValue: currentItem.code,
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item hasFeedback label={fieldLabels.name} {...formItemLayout}>
+            {getFieldDecorator('name', {
+              rules: [{ required: true, message: '请给商户命名!' }],
+              initialValue: currentItem.name,
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item hasFeedback label={fieldLabels.contactName} {...formItemLayout}>
+            {getFieldDecorator('contactName', {
+              rules: [{ required: true, message: '请填写商户联系人!' }],
+              initialValue: currentItem.contactName,
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item hasFeedback label={fieldLabels.mobile} {...formItemLayout}>
+            {getFieldDecorator('mobile', {
+              rules: [
+                {
+                  required: true, message: '请填写联系电话!',
+                }, {
+                  pattern: /^[1][3,4,5,7,8][0-9]{9}$/g, message: '请输入11位有效手机号!',
+                }],
+              initialValue: currentItem.mobile,
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item hasFeedback label={fieldLabels.depositBank} {...formItemLayout}>
+            {getFieldDecorator('depositBank', {
+              rules: [{
+                required: true, message: '开户银行不能为空!',
+              }],
+              initialValue: currentItem.depositBank,
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item hasFeedback label={fieldLabels.bankAccount} {...formItemLayout}>
+            {getFieldDecorator('bankAccount', {
+              rules: [{
+                required: true, message: '银行账号不能为空!',
               }, {
-                pattern: /^[1][3,4,5,7,8][0-9]{9}$/g, message: '请输入11位有效手机号!',
+                pattern: /^[0-9]{1,22}$/g, message: '请输入有效的银行账户!',
               }],
-            initialValue: currentItem.mobile,
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item hasFeedback label={fieldLabels.depositBank} {...formItemLayout}>
-          {getFieldDecorator('depositBank', {
-            rules: [{
-              required: true, message: '开户银行不能为空!',
-            }],
-            initialValue: currentItem.depositBank,
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item hasFeedback label={fieldLabels.bankAccount} {...formItemLayout}>
-          {getFieldDecorator('bankAccount', {
-            rules: [{
-              required: true, message: '银行账号不能为空!',
-            }, {
-              pattern: /^[0-9]{1,22}$/g, message: '请输入有效的银行账户!',
-            }],
-            initialValue: currentItem.bankAccount,
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item label={fieldLabels.licenseId} {...formItemLayout}>
-          {getFieldDecorator('licenseId', {
-            initialValue: currentItem.licenseId,
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item label={fieldLabels.taxNumber} {...formItemLayout}>
-          {getFieldDecorator('taxNumber', {
-            initialValue: currentItem.taxNumber,
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item label={fieldLabels.receiptType} {...formItemLayout}>
-          {getFieldDecorator('receiptType', {
-            initialValue: currentItem.receiptType || 'COMMON',
-          })(
-            <Radio.Group className={styles.radio}>
-              {
-                receipts.map(item =>
-                  (
-                    <Radio.Button
-                      key={item.value}
-                      value={item.value}
-                    >{item.title}
-                    </Radio.Button>
+              initialValue: currentItem.bankAccount,
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item label={fieldLabels.licenseId} {...formItemLayout}>
+            {getFieldDecorator('licenseId', {
+              initialValue: currentItem.licenseId,
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item label={fieldLabels.taxNumber} {...formItemLayout}>
+            {getFieldDecorator('taxNumber', {
+              initialValue: currentItem.taxNumber,
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item label={fieldLabels.receiptType} {...formItemLayout}>
+            {getFieldDecorator('receiptType', {
+              initialValue: currentItem.receiptType || 'COMMON',
+            })(
+              <Radio.Group className={styles.radio}>
+                {
+                  receipts.map(item =>
+                    (
+                      <Radio.Button
+                        key={item.value}
+                        value={item.value}
+                      >{item.title}
+                      </Radio.Button>
+                    )
                   )
-                )
-              }
-            </Radio.Group>
-          )}
-        </Form.Item>
-        <Form.Item label="状态" {...formItemLayout}>
-          {getFieldDecorator('status', {
-            valuePropName: 'checked',
-            initialValue: isChecked(currentItem.status),
-          })(
-            <Switch
-              checkedChildren="启用"
-              unCheckedChildren="不启用"
-            />
-          )}
-        </Form.Item>
-        <Form.Item {...submitFormLayout} style={{ marginTop: 32 }}>
-          <Button onClick={this.handlePageBack}>取消</Button>
-          <Button
-            type="primary"
-            htmlType="submit"
-            loading={submitting}
-            style={{ marginLeft: 8 }}
-          >提交
-          </Button>
-        </Form.Item>
-      </Form>
+                }
+              </Radio.Group>
+            )}
+          </Form.Item>
+          <Form.Item label="状态" {...formItemLayout}>
+            {getFieldDecorator('status', {
+              valuePropName: 'checked',
+              initialValue: isChecked(currentItem.status),
+            })(
+              <Switch
+                checkedChildren="启用"
+                unCheckedChildren="不启用"
+              />
+            )}
+          </Form.Item>
+          <Form.Item {...submitFormLayout} style={{ marginTop: 32 }}>
+            <Button onClick={this.handlePageBack}>取消</Button>
+            <Button
+              type="primary"
+              htmlType="submit"
+              loading={submitting}
+              style={{ marginLeft: 8 }}
+            >提交
+            </Button>
+          </Form.Item>
+        </Form>
+      </Card>
     );
   }
 }

+ 43 - 41
src/routes/Merchant/MerchantDespoit.js

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import { connect } from 'dva';
 import { routerRedux } from 'dva/router';
-import { Form, Input, Button } from 'antd';
+import { Card, Form, Input, Button } from 'antd';
 import { toDecimal2 } from '../../utils/utils';
 import styles from './MerchantDespoit.less';
 
@@ -65,46 +65,48 @@ export default class MerchantDespoitPage extends PureComponent {
     const { currentItem } = location.state;
     const { getFieldDecorator } = form;
     return (
-      <Form onSubmit={this.handlePageSubmit}>
-        <Form.Item label={fieldLabels.name} {...formItemLayout}>
-          <span>{`${currentItem.name}【${currentItem.code}】`}</span>
-        </Form.Item>
-        <Form.Item label={fieldLabels.balance} {...formItemLayout}>
-          <span className={styles.balance}>{toDecimal2(currentItem.balance)}元</span>
-        </Form.Item>
-        <Form.Item label={fieldLabels.quantity} {...formItemLayout}>
-          {getFieldDecorator('quantity', {
-            rules: [
-              {
-                required: true, type: 'string', message: '充值金额不能为空!',
-              }, {
-                pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '充值金额格式错误!',
-              }
-            ],
-          })(
-            <Input placeholder="请输入充值金额" addonAfter="元" />
-          )}
-        </Form.Item>
-        <Form.Item label={fieldLabels.note} {...formItemLayout}>
-          {getFieldDecorator('note', {
-          })(
-            <Input placeholder="请输入" />
-          )}
-        </Form.Item>
-        <Form.Item {...submitFormLayout} style={{ marginTop: 32 }}>
-          <Button
-            onClick={this.handlePageBack}
-          >取消
-          </Button>
-          <Button
-            type="primary"
-            htmlType="submit"
-            loading={submitting}
-            style={{ marginLeft: 8 }}
-          >提交
-          </Button>
-        </Form.Item>
-      </Form>
+      <Card>
+        <Form onSubmit={this.handlePageSubmit}>
+          <Form.Item label={fieldLabels.name} {...formItemLayout}>
+            <span>{`${currentItem.name}【${currentItem.code}】`}</span>
+          </Form.Item>
+          <Form.Item label={fieldLabels.balance} {...formItemLayout}>
+            <span className={styles.balance}>{toDecimal2(currentItem.balance)}元</span>
+          </Form.Item>
+          <Form.Item label={fieldLabels.quantity} {...formItemLayout}>
+            {getFieldDecorator('quantity', {
+              rules: [
+                {
+                  required: true, type: 'string', message: '充值金额不能为空!',
+                }, {
+                  pattern: /(^[1-9]([0-9]+)?(\.[0-9]{1,2})?$)|(^(0){1}$)|(^[0-9]\.[0-9]([0-9])?$)/, message: '充值金额格式错误!',
+                }
+              ],
+            })(
+              <Input placeholder="请输入充值金额" addonAfter="元" />
+            )}
+          </Form.Item>
+          <Form.Item label={fieldLabels.note} {...formItemLayout}>
+            {getFieldDecorator('note', {
+            })(
+              <Input placeholder="请输入" />
+            )}
+          </Form.Item>
+          <Form.Item {...submitFormLayout} style={{ marginTop: 32 }}>
+            <Button
+              onClick={this.handlePageBack}
+            >取消
+            </Button>
+            <Button
+              type="primary"
+              htmlType="submit"
+              loading={submitting}
+              style={{ marginLeft: 8 }}
+            >提交
+            </Button>
+          </Form.Item>
+        </Form>
+      </Card>
     );
   }
 }

+ 4 - 10
src/routes/Merchant/MerchantList.js

@@ -1,8 +1,8 @@
-import React, { Component, Fragment } from 'react';
+import React, { Component } from 'react';
 import moment from 'moment';
 import { connect } from 'dva';
 import { routerRedux } from 'dva/router';
-import { Modal, Button, message } from 'antd';
+import { Card, Modal, Button, message } from 'antd';
 import { StandardTableList } from '../../components/RBList';
 import Authorized from '../../utils/Authorized';
 import { renderStatus, renderCategory, addRowKey } from '../../utils/utils';
@@ -182,7 +182,7 @@ export default class MerchantListPage extends Component {
       width: '18%',
     }];
     return (
-      <Fragment>
+      <Card>
         <StandardTableList
           columns={columns}
           loading={loading}
@@ -199,13 +199,7 @@ export default class MerchantListPage extends Component {
           }}
           keepUIState={{...this.state.UIParams}}
         />
-        <Modal
-          title="账户充值"
-          cancelText="取消"
-          okText="充值"
-        >
-        </Modal>
-      </Fragment>
+      </Card>
     );
   }
 }

+ 33 - 0
src/routes/tableColumns.js

@@ -0,0 +1,33 @@
+import React from 'react';
+import { Radio } from 'antd';
+import { renderStatus, renderCategory } from '../utils/utils';
+
+const MerchantColumns = [{
+  title: '',
+  key: 0,
+  dataIndex: 'key',
+  render: (text) => <Radio key={text} />,
+  width: 20,
+}, {
+  title: '商户编号',
+  dataIndex: 'code',
+  key: 1,
+}, {
+  title: '商户名称',
+  dataIndex: 'name',
+  key: 2,
+}, {
+  title: '商户类型',
+  key: 3,
+  dataIndex: 'domain',
+  render: (text) => renderCategory(text),
+}, {
+  title: '该条状态',
+  key: 4,
+  dataIndex: 'status',
+  render: (text) => renderStatus(text),
+}];
+
+export {
+  MerchantColumns,
+};

+ 51 - 0
src/utils/utils.js

@@ -8,6 +8,44 @@ import {
   DOMAIN_PJ,
 } from './config';
 
+const ProvinceCodes = {
+  11: '北京',
+  12: '天津',
+  13: '河北省',
+  14: '山西省',
+  15: '内蒙古自治区',
+  21: '辽宁省',
+  22: '吉林省',
+  23: '黑龙江省',
+  31: '上海',
+  32: '江苏省',
+  33: '浙江省',
+  34: '安徽省',
+  35: '福建省',
+  36: '江西省',
+  37: '山东省',
+  41: '河南省',
+  42: '湖北省',
+  43: '湖南省',
+  44: '广东省',
+  45: '广西壮族自治区',
+  46: '海南省',
+  50: '重庆',
+  51: '四川省',
+  52: '贵州省',
+  53: '云南省',
+  54: '西藏自治区',
+  61: '陕西省',
+  62: '甘肃省',
+  63: '青海省',
+  64: '宁夏回族自治区',
+  65: '新疆维吾尔自治区',
+  71: '台湾',
+  81: '香港特别行政区',
+  82: '澳门特别行政区',
+  99: '海外',
+};
+
 export function fixedZero(val) {
   return val * 1 < 10 ? `0${val}` : val;
 }
@@ -213,3 +251,16 @@ export function toDecimal2(x) {
   }
   return s;
 }
+
+export function provinceCodeToName(pcode) {
+  return ProvinceCodes[pcode];
+}
+
+export function provinceNameToCode(pname) {
+  const match = Object.keys(ProvinceCodes).filter((code) => {
+    if (ProvinceCodes[code] === pname) {
+      return true;
+    }
+  });
+  return match[0];
+}