zhanghe 7 лет назад
Родитель
Сommit
849999f8d1
45 измененных файлов с 2668 добавлено и 318 удалено
  1. 58 10
      .roadhogrc.mock.js
  2. 36 0
      mock/group.js
  3. 3 3
      mock/signature.js
  4. 48 0
      mock/tag.js
  5. 2 0
      package.json
  6. 2 2
      src/common/menu.js
  7. 22 0
      src/common/router.js
  8. 70 5
      src/components/DataSearch/index.js
  9. 1 1
      src/components/DataSearch/index.less
  10. 41 0
      src/components/SelectModal/ForSelTable.js
  11. 63 0
      src/components/SelectModal/SelTable.js
  12. 161 0
      src/components/SelectModal/index.js
  13. 0 40
      src/components/SelectSearch/index.js
  14. 23 9
      src/components/Uploader/index.js
  15. 2 1
      src/index.js
  16. 101 0
      src/models/group/detail.js
  17. 113 0
      src/models/group/group.js
  18. 5 3
      src/models/login.js
  19. 0 20
      src/models/resource.js
  20. 102 0
      src/models/tag/detail.js
  21. 113 0
      src/models/tag/tag.js
  22. 2 8
      src/routes/Login/index.js
  23. 152 155
      src/routes/Resource/gallery/index.js
  24. 63 19
      src/routes/Resource/gallery/modal.js
  25. 1 1
      src/routes/Resource/gallery/table.js
  26. 6 10
      src/routes/Resource/video/table.js
  27. 223 0
      src/routes/Tag/detail/index.js
  28. 61 0
      src/routes/Tag/detail/modal.js
  29. 160 0
      src/routes/Tag/index.js
  30. 51 0
      src/routes/Tag/search.js
  31. 109 0
      src/routes/Tag/table.js
  32. 86 0
      src/routes/Tag/table.less
  33. 214 0
      src/routes/TagGroup/detail/index.js
  34. 61 0
      src/routes/TagGroup/detail/modal.js
  35. 168 0
      src/routes/TagGroup/index.js
  36. 51 0
      src/routes/TagGroup/search.js
  37. 105 0
      src/routes/TagGroup/table.js
  38. 82 0
      src/routes/TagGroup/table.less
  39. 32 0
      src/services/group.js
  40. 13 1
      src/services/resource.js
  41. 32 0
      src/services/tag.js
  42. 6 2
      src/utils/api.js
  43. 14 1
      src/utils/config.js
  44. 0 11
      src/utils/map.js
  45. 10 16
      src/utils/request.js

+ 58 - 10
.roadhogrc.mock.js

@@ -5,6 +5,8 @@ import { terminalList } from './mock/terminal';
 import { merchantList } from './mock/merchant';
 import { resourceList } from './mock/resource';
 import { signature } from './mock/signature';
+import { groupList } from './mock/group';
+import { tagList } from './mock/tag';
 import * as api from './src/utils/api';
 
 // mock数据持久化
@@ -12,6 +14,8 @@ global.campusList = campusList;
 global.terminalList = terminalList;
 global.merchantList = merchantList;
 global.resourceList = resourceList;
+global.groupList = groupList;
+global.tagList = tagList;
 global.signature = signature;
 
 // 操作成功响应内容
@@ -109,8 +113,9 @@ const remove = (dataset, id, res) => {
 
 // mock数据
 const proxy = {
+  // 资源
   [`POST ${api.resource.replace('/:id', '')}`]: (req, res) => {
-    res.send(create(global.resource, req.body));
+    res.send(create(global.resourceList, req.body));
   },
   [`DELETE ${api.resource}`]: (req, res) => {
     const { id } = req.params;
@@ -125,6 +130,7 @@ const proxy = {
   [`GET ${api.signature}`]: (req, res) => {
     res.send({ ...SUCCESS, data: global.signature });
   },
+  // 校区
   [`GET ${api.campuses}`]: (req, res) => {
     res.send(query(global.campusList, req.query));
   },
@@ -134,6 +140,7 @@ const proxy = {
   [`PUT ${api.campus.replace('/:id', '')}`]: (req, res) => {
     res.send(update(global.campusList, req.body));
   },
+  // 终端
   [`GET ${api.terminals}`]: (req, res) => {
     res.send(query(global.terminalList, req.query));
   },
@@ -147,22 +154,62 @@ const proxy = {
     const { id } = req.params;
     remove(global.terminalList, id, res);
   },
-  [`GET ${api.merchant}`]: (req, res) => {
+  // 厂商
+  [`POST ${api.merchant.replace('/:id', '')}`]: (req, res) => {
+    res.send(create(global.merchantList, req.body));
+  },
+  [`DELETE ${api.merchant}`]: (req, res) => {
     const { id } = req.params;
-    queryOne(global.merchantList, id, res);
+    remove(global.merchantList, id, res);
+  },
+  [`PUT ${api.merchant.replace('/:id', '')}`]: (req, res) => {
+    res.send(update(global.merchantList, req.body));
   },
   [`GET ${api.merchants}`]: (req, res) => {
     res.send(query(global.merchantList, req.query));
   },
-  [`POST ${api.merchant.replace('/:id', '')}`]: (req, res) => {
-    res.send(create(global.merchantList, req.body));
+  [`GET ${api.merchant}`]: (req, res) => {
+    const { id } = req.params;
+    queryOne(global.merchantList, id, res);
   },
-  [`PUT ${api.merchant.replace('/:id', '')}`]: (req, res) => {
-    res.send(update(global.merchantList, req.body));
+  // 标签组
+  [`POST ${api.group.replace('/:id', '')}`]: (req, res) => {
+    res.send(create(global.groupList, req.body));
   },
-  [`DELETE ${api.merchant}`]: (req, res) => {
+  [`DELETE ${api.group}`]: (req, res) => {
     const { id } = req.params;
-    remove(global.merchantList, id, res);
+    remove(global.groupList, id, res);
+  },
+  [`PUT ${api.group.replace('/:id', '')}`]: (req, res) => {
+    res.send(update(global.groupList, req.body));
+  },
+  [`GET ${api.groups}`]: (req, res) => {
+    console.log(`[${api.groups}]`, req.query);
+    res.send(query(global.groupList, req.query));
+  },
+  [`GET ${api.group}`]: (req, res) => {
+    const { id } = req.params;
+    queryOne(global.groupList, id, res);
+  },
+  // 标签
+  [`POST ${api.tag.replace('/:id', '')}`]: (req, res) => {
+    res.send(create(global.tagList, req.body));
+  },
+  [`DELETE ${api.tag}`]: (req, res) => {
+    const { id } = req.params;
+    remove(global.tagList, id, res);
+  },
+  [`PUT ${api.tag.replace('/:id', '')}`]: (req, res) => {
+    res.send(update(global.tagList, req.body));
+  },
+  [`GET ${api.tags}`]: (req, res) => {
+    console.log(`[${api.tags}]`, req.query);
+    res.send(query(global.tagList, req.query));
+  },
+  [`GET ${api.tag}`]: (req, res) => {
+    console.log(`[${api.tag}]`, req.params);
+    const { id } = req.params;
+    queryOne(global.tagList, id, res);
   },
 };
 
@@ -170,4 +217,5 @@ const proxy = {
 const noProxy = process.env.NO_PROXY === 'true';
 
 // 根据是否禁用代理来选择是mock数据还是真实接口
-export default noProxy ? {} : delay(proxy, 1000);
+export default noProxy ? {} : delay(proxy, 500);
+// export default noProxy ? {} : proxy;

+ 36 - 0
mock/group.js

@@ -0,0 +1,36 @@
+const groupList = [];
+for (let i = 1; i < 50; i++) {
+  groupList.push({
+    id: "d513401461284feea555a3e0abc5974a" + i,
+    code: "G-test-" + i,
+    name: "标签组" + i,
+    merchantId: "15",
+    sort: 2,
+    status: "NORMAL",
+    gmtCreated: 1512960192000,
+    gmtModified: 1512960192000,
+    tagList: [{
+      id: 'da60114402734d2eb3ac6589ef25564d',
+      name: `周边-云课堂`,
+      code: `T-test-1`,
+      type: 'SUPPORT',
+      sort: 1,
+      status: 'NORMAL',
+      merchantId: 0,
+      gmtCreated: 1512960192010,
+      gmtModified: 1512960192010,
+    },{
+      id: 'da60114402734d2eb3ac6589ef25589d',
+      name: `默认-EQ情商`,
+      code: `T-test-2`,
+      type: 'COURSE',
+      sort: 2,
+      status: 'NORMAL',
+      merchantId: 0,
+      gmtCreated: 1512960192099,
+      gmtModified: 1512960192099,
+    }],
+  })
+}
+
+module.exports = { groupList };

+ 3 - 3
mock/signature.js

@@ -1,8 +1,8 @@
 const signature = {
-  expire: 1515240873,
+  expire: 1515578421,
   accessid: "LTAIjmNjhgzDGQ5U",
-  signature: "xVAY7d5lnCvz8M79hdthV4YIt8I=",
-  policy: "eyJjb25kaXRpb25zIjogW1sic3RhcnRzLXdpdGgiLCAiJGtleSIsICJ1cGxvYWRfdGVzdC8iXV0sICJleHBpcmF0aW9uIjogIjIwMTgtMDEtMDZUMjA6MTQ6MzNaIn0=",
+  signature: "wwKF3NuSxlyjApS6AUSs+jSeeYc=",
+  policy: "eyJjb25kaXRpb25zIjogW1sic3RhcnRzLXdpdGgiLCAiJGtleSIsICJ1cGxvYWRfdGVzdC8iXV0sICJleHBpcmF0aW9uIjogIjIwMTgtMDEtMTFUMTE6MDc6MTRaIn0=",
   host: "http://oss-learn.oss-ap-southeast-1.aliyuncs.com",
   dir: "upload_test/",
 };

+ 48 - 0
mock/tag.js

@@ -0,0 +1,48 @@
+const tagList = [];
+for (let i = 1; i < 89; i++) {
+  tagList.push({
+    id: String(i),
+    name: '标签-' + i,
+    code: 'Tag-test-' + i,
+    groupId: 'd513401461284feea555a3e0abc5974a' + i,
+    groupName: "小学310" + i,
+    type: 'COURSE',
+    status: 'NORMAL',
+    gmtCreated: 1512956695000,
+    gmtModified: 1512971472000,
+    merchantId: "87",
+    itemList: [{
+      id: "061bf606584c4efdab1e0e6b7a71893b",
+      subId: "b3bfc6d5b15e474c95db98bd754cfeaf",
+      code: "K-test-311",
+      name: "小学310",
+      type: "COURSE",
+      merchantId: "87",
+      sort: 1,
+      status: "SALE",
+      gmtCreated: 1512981450000,
+      gmtModified: 1512981450000,
+      priceList: null,
+      tagList: null,
+      course: null,
+      support: null
+    },{
+      id: "061bf606584c4efdab1e0e6b7a71751b",
+      subId: "b3bfc6d5b15e474c95db98bd754cfeaf",
+      code: "K-test-311",
+      name: "小学330",
+      type: "COURSE",
+      merchantId: "87",
+      sort: 2,
+      status: "SALE",
+      gmtCreated: 1512981450000,
+      gmtModified: 1512981450000,
+      priceList: null,
+      tagList: null,
+      course: null,
+      support: null
+    }],
+  });
+}
+
+module.exports = { tagList };

+ 2 - 0
package.json

@@ -39,6 +39,8 @@
     "qs": "^6.5.0",
     "query-string": "^5.0.1",
     "rc-drawer-menu": "^0.5.0",
+    "rc-queue-anim": "^1.4.1",
+    "rc-tween-one": "^1.7.1",
     "react": "^16.0.0",
     "react-container-query": "^0.9.1",
     "react-document-title": "^2.0.3",

+ 2 - 2
src/common/menu.js

@@ -16,10 +16,10 @@ const menuData = [
     path: 'tag',
     children: [{
       name: '标签组',
-      path: 'tag-group',
+      path: 'group',
     },{
       name: '标签',
-      path: 'tag-item',
+      path: 'tag',
     }]
   },{
     name: '产品管理',

+ 22 - 0
src/common/router.js

@@ -59,6 +59,28 @@ export const getRouterData = (app) => {
       component: dynamicWrapper(app, ['merchant/detail'], () => import('../routes/Merchant/detail')),
       name: '修改厂商',
     },
+    '/tag/group': {
+      component: dynamicWrapper(app, ['group/group', 'merchant/merchant'], () => import('../routes/TagGroup')),
+    },
+    '/tag/group/add': {
+      component: dynamicWrapper(app, ['group/detail', 'merchant/merchant'], () => import('../routes/TagGroup/detail')),
+      name: '添加标签组',
+    },
+    '/tag/group/edit/:id': {
+      component: dynamicWrapper(app, ['group/detail', 'merchant/merchant'], () => import('../routes/TagGroup/detail')),
+      name: '修改标签组',
+    },
+    '/tag/tag': {
+      component: dynamicWrapper(app, ['tag/tag', 'merchant/merchant'], () => import('../routes/Tag')),
+    },
+    '/tag/tag/add': {
+      component: dynamicWrapper(app, ['tag/detail', 'group/group'], () => import('../routes/Tag/detail')),
+      name: '添加标签',
+    },
+    '/tag/tag/edit/:id': {
+      component: dynamicWrapper(app, ['tag/detail', 'group/group'], () => import('../routes/Tag/detail')),
+      name: '修改标签',
+    },
     '/user': {
       component: dynamicWrapper(app, [], () => import('../layouts/UserLayout')),
     },

+ 70 - 5
src/components/DataSearch/index.js

@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
 import { Input, Select, Button, Icon } from 'antd';
 import styles from './index.less';
 
+const Option = Select.Option;
+
 export default class DataSearch extends PureComponent {
   static propTypes = {
     size: PropTypes.string,
@@ -12,13 +14,15 @@ export default class DataSearch extends PureComponent {
     selectOptions: PropTypes.array,
     style: PropTypes.object,
     keyword: PropTypes.string,
+    filterSelectProps: PropTypes.object,
   }
   constructor(props) {
     super(props);
-    const { select, selectProps, keyword } = this.props;
+    const { select, selectOptions, selectProps, keyword, field } = this.props;
     this.state = {
       selectValue: select && selectProps ? selectProps.defaultValue : '',
       inputValue: keyword ? keyword : '',
+      mode: field && selectOptions.filter(item => item.value === field)[0].mode || 'input',
     }
   }
   handleSearch = () => {
@@ -29,9 +33,14 @@ export default class DataSearch extends PureComponent {
     this.props.onSearch && this.props.onSearch(query);
   }
   handleSelectChange = (value) => {
+    // 进行模式匹配
+    const { selectOptions } = this.props;
+    const match = selectOptions.filter(item => item.value === value)[0];
     this.setState({
       ...this.state,
       selectValue: value,
+      inputValue: '',
+      mode: match.mode,
     });
   }
   handleInputChange = (e) => {
@@ -40,21 +49,77 @@ export default class DataSearch extends PureComponent {
       inputValue: e.target.value,
     });
   }
+  handleInputSelectChange = (value) => {
+    this.setState({
+      ...this.state,
+      inputValue: value,
+    })
+  }
+  handleInputSelectClear = (value) => {
+    if (!value) {
+      this.setState({
+        inputValue: ''
+      }, () => this.handleSearch());
+    }
+  }
   handleClearInput = () => {
     this.setState({
       inputValue: '',
     }, () => this.handleSearch());
   }
+  renderKeyWordComponent = () => {
+    const { mode, inputValue } = this.state;
+    const { size, filterSelectProps = {} } = this.props;
+    const { data = [] } = filterSelectProps;
+    const suffix = inputValue ? <Icon type="close-circle" onClick={this.handleClearInput} /> : null;
+    const input = (
+      <Input
+        placeholder="请输入"
+        suffix={suffix}
+        onChange={this.handleInputChange}
+        onPressEnter={this.handleSearch}
+        size={size}
+        value={inputValue}
+      />
+    );
+    const select = (
+      <Select
+        allowClear
+        showSearch
+        size={size}
+        value={inputValue}
+        placeholder="请选择/请输入进行筛选"
+        optionFilterProp="children"
+        style={{ flexShrink: 1, flexGrow: 1 }}
+        onChange={this.handleInputSelectClear}
+        onSelect={this.handleInputSelectChange}
+        filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+      >
+        {data.map(item => <Option key={item.value} value={item.value}>{item.name}</Option>)}
+      </Select>
+    );
+
+    switch (mode) {
+      case 'input':
+        return input;
+        break;
+      case 'select':
+        return select;
+        break;
+      default:
+        return input;
+        break;
+    }
+  }
+
   render() {
     const { size, select, selectOptions, selectProps, style } = this.props;
-    const { inputValue } = this.state;
-    const suffix = inputValue ? <Icon type="close-circle" onClick={this.handleClearInput} /> : null;
     return (
       <Input.Group compact size={size} className={styles.search} style={style}>
         {select && <Select onChange={this.handleSelectChange} size={size} {...selectProps}>
-          {selectOptions && selectOptions.map((item, key) => <Select.Option value={item.value} key={key}>{item.name || item.value}</Select.Option>)}
+          {selectOptions && selectOptions.map((item, key) => <Option value={item.value} key={key}>{item.name || item.value}</Option>)}
         </Select>}
-        <Input placeholder="请输入" suffix={suffix} onChange={this.handleInputChange} onPressEnter={this.handleSearch} size={size} value={inputValue}/>
+        {this.renderKeyWordComponent()}
         <Button onClick={this.handleSearch} size={size} type="primary" icon="search">搜索</Button>
       </Input.Group>
     );

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

@@ -24,7 +24,7 @@
     }
 
     .ant-select {
-      width: 100px;
+      width: 120px;
       flex-shrink: 0;
       flex-grow: 0;
 

+ 41 - 0
src/components/SelectModal/ForSelTable.js

@@ -0,0 +1,41 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Table } from 'antd';
+
+export default class ForSelTable extends PureComponent {
+  static propTypes = {
+    list: PropTypes.array.isRequired,
+    listLoading: PropTypes.bool.isRequired,
+    pagination: PropTypes.object.isRequired,
+    columns: PropTypes.array.isRequired,
+    rowKeyName: PropTypes.string.isRequired,
+    styles: PropTypes.object,
+    onChange: PropTypes.func,
+    onAdd: PropTypes.func,
+  };
+
+  render() {
+    const { rowKeyName, list, listLoading, pagination, columns, onAdd, styles = {} } = this.props;
+    const addColumn = {
+      title: '选择',
+      dataIndex: 'select',
+      key: 'select',
+      render: (text, record) => (
+        <Button onClick={onAdd} size="small" type="primary">添加</Button>
+      ),
+    };
+    columns.push(addColumn);
+    return (
+      <Table
+        rowKey={rowKeyName}
+        bordered={true}
+        columns={columns}
+        dataSource={list}
+        loading={listLoading}
+        pagination={pagination}
+        className={styles.table}
+        onChange={onChange}
+      />
+    );
+  }
+}

+ 63 - 0
src/components/SelectModal/SelTable.js

@@ -0,0 +1,63 @@
+import React, { PureComponent } from 'react';
+import { Table, Button } from 'antd';
+import PropTypes from 'prop-types';
+import { cloneDeep } from 'lodash';
+
+const ButtonGroup = Button.Group;
+
+export default class SelTable extends PureComponent {
+  static propTypes = {
+    dataSource: PropTypes.array.isRequired,   // 表的数据源
+    columns: PropTypes.array.isRequired,      // 表的列的结构
+    rowKeyName: PropTypes.string.isRequired,  // table row key name
+    operSort: PropTypes.bool,                 // 开启排序功能
+    operDel: PropTypes.bool,                  // 开启删除功能
+    onUp: PropTypes.func,                     // 点击向上调整位置
+    onDown: PropTypes.func,                   // 点击向下调整位置
+  };
+
+  render() {
+    const { operSort, operDel, columns, dataSource, rowKeyName, onUp, onDown, ...rest } = this.props;
+    // 防止污染原始数据
+    const curSpaceColumns = cloneDeep(columns);
+
+    const sortColumn = {
+      title: '排序',
+      dataIndex: 'operation',
+      key: 'operation',
+      render: (text, record) => (
+        <ButtonGroup>
+          <Button onClick={() => onUp(record[rowKeyName])} size="small" icon="up-circle"></Button>
+          <Button onClick={() => onDown(record[rowKeyName])} size="small" icon="down-circle"></Button>
+        </ButtonGroup>
+      ),
+    };
+
+    const delColumn = {
+      title: '删除',
+      dataIndex: 'del',
+      key: 'del',
+      render: (text, record) => (
+        <Button size="small" icon="delete"></Button>
+      ),
+    };
+
+    if (operSort) {
+      curSpaceColumns.push(sortColumn);
+    }
+
+    if (operDel) {
+      curSpaceColumns.push(delColumn);
+    }
+
+    return (
+      <Table
+        bordered={true}
+        dataSource={dataSource}
+        columns={curSpaceColumns}
+        rowKey={rowKeyName}
+        {...rest}
+      />
+    );
+  }
+}

+ 161 - 0
src/components/SelectModal/index.js

@@ -0,0 +1,161 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { cloneDeep } from 'lodash';
+import { Modal, Button, Table } from 'antd';
+import DataSearch from '../DataSearch';
+import SelTable from './SelTable';
+import ForSelTable from './ForSelTable';
+
+const ButtonGroup = ButtonGroup;
+
+export default class SelectModal extends PureComponent {
+  static propTypes = {
+    mode: PropTypes.string,
+    rowKeyName: PropTypes.string,
+
+    fsTableDataSource: PropTypes.array,
+    fsTableColumns: PropTypes.array,
+    fstablePagination: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
+    fsTableLoading: PropTypes.bool,
+    onChange: PropTypes.func,
+
+    tableDataSource: PropTypes.array,
+    tableColumns: PropTypes.array,
+    tablePagination: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]),
+    operSort: PropTypes.bool,
+    operDel: PropTypes.bool,
+
+    searchProps: PropTypes.object,
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      tableTabLeft: true,
+      selTableData: this.props.tableDataSource,
+    };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    this.setState({ selTableData: nextProps.tableDataSource });
+  }
+
+  handleTableTabChange = () => {
+    const { tableTabLeft } = this.state;
+    this.setState({ tableTabLeft: !tableTabLeft });
+  }
+
+  // 向上调整一个位置
+  handleOnUp = (key) => {
+    const { selTableData } = this.state;
+    const { rowKeyName } = this.props;
+    const arr = cloneDeep(selTableData);
+
+    // 找到该行在列表中的索引
+    const index = arr.findIndex(item => item[rowKeyName] === key);
+    //第一个元素或者未找到元素不做操作
+    if (!index || -1 === index) return;
+    //与前一个元素进行位置互换
+    arr.splice(index, 1, ...arr.splice(index - 1, 1, arr[index]));
+    // 将sort字段重新赋值
+    arr.map((item, index) => item.sort !== undefined ? item.sort = index + 1 : null);
+
+    this.setState({ selTableData: arr });
+  }
+
+  // 向下调整一个位置
+  handleOnDown = (key) => {
+    const { selTableData } = this.state;
+    const { rowKeyName } = this.props;
+    const arr = cloneDeep(selTableData);
+
+    // 找到该行在列表中的索引
+    const index = arr.findIndex(item => item[rowKeyName] == key);
+    //最后一个元素或者未找到元素不做操作
+    if (index + 1 === arr.length || -1 === index) return;
+    //与后一个元素进行位置互换
+    arr.splice(index, 1, ...arr.splice(index + 1, 1, arr[index]));
+    // 将sort字段重新赋值
+    arr.map((item, index) => item.sort !== undefined ? item.sort = index + 1 : null);
+
+    this.setState({ selTableData: arr });
+  }
+
+  handleOnAdd = (id) => {
+
+  }
+
+  handleOnDel = (id) => {
+
+  }
+
+  handleOnCancel = () => {
+    const { onCancel } = this.props;
+    onCancel();
+  }
+
+  handleOnOk = () => {
+    const { selTableData } = this.state;
+    const { onOk } = this.props;
+    onOk(selTableData);
+  }
+
+  comboConverter = () => {
+    const { tableTabLeft, selTableData } = this.state;
+    const {
+      mode, rowKeyName, searchProps,
+      tableColumns, tablePagination, tableDataSource, operSort, operDel,
+      fsTableLoading, fsTableColumns, fsTableDataSource, fstablePagination,
+      onCancel, onOk, ...modalProps
+    } = this.props;
+
+    switch (mode) {
+      case 'sort':
+        return (
+          <Modal
+            onCancel={this.handleOnCancel}
+            onOk={this.handleOnOk}
+            {...modalProps}
+          >
+            <SelTable
+              dataSource={selTableData}
+              columns={tableColumns}
+              pagination={tablePagination}
+              rowKeyName={rowKeyName}
+              operSort={operSort}
+              operDel={operDel}
+              onUp={this.handleOnUp}
+              onDown={this.handleOnDown}
+            />
+          </Modal>
+        );
+        break;
+      // case 'single':
+      //   return (
+      //     <Modal {...modalProps}>
+      //       <DataSearch {...searchProps} />
+      //       <ForSelTable {...forSelTableProps}/>
+      //     </Modal>
+      //   );
+      //   break;
+      // case 'multiple':
+      //   return (
+      //     <Modal {...modalProps}>
+      //       <DataSearch />
+      //       <ButtonGroup>
+      //         <Button type="primary" onClick={this.handleTabChange} ghost={tableTabLeft ? false : true}>待选</Button>
+      //         <Button type="primary" onClick={this.handleTabChange} ghost={tableTabLeft ? true : false}>{`已选[]`}</Button>
+      //       </ButtonGroup>
+      //       {tableTabLeft ? <ForSelTable {...forSelTableProps} /> : <SelTable {...selTableProps}/>}
+      //     </Modal>
+      //   );
+      //   break;
+      default:
+        break;
+    }
+  }
+
+  render() {
+    return this.comboConverter();
+  }
+}

+ 0 - 40
src/components/SelectSearch/index.js

@@ -1,40 +0,0 @@
-import react, { PureComponent } from 'react';
-import { Select, Input, Icon } from 'antd';
-
-const Option = Select.Option;
-
-export default class SelectSearch extends PureComponent {
-  handleChange = () => {
-    console.log('changing...');
-  }
-
-  render() {
-    return (
-      <Select
-        showSearch
-        allowClear
-        style={{ width: 200 }}
-        placeholder="请选择人员"
-        optionFilterProp="children"
-        notFoundContent="无法找到"
-        onChange={() => this.handleChange()}
-      >
-          <Option value="jack">张贺</Option>
-          <Option value="lucy">露西</Option>
-          <Option value="tom">汤姆</Option>
-          <Option value="jack1">张贺</Option>
-          <Option value="lucy1">露西</Option>
-          <Option value="tom1">汤姆</Option>
-          <Option value="jack2">张贺</Option>
-          <Option value="jack3">张贺</Option>
-          <Option value="jack4">张贺</Option>
-          <Option value="lucy13">露西</Option>
-          <Option value="tom2">汤姆</Option>
-          <Option value="lucy2">露西</Option>
-          <Option value="tom3">汤姆</Option>
-          <Option value="lucy3">露西</Option>
-          <Option value="tom4">汤姆</Option>
-      </Select>
-    );
-  }
-}

+ 23 - 9
src/components/Uploader/index.js

@@ -2,12 +2,13 @@ import React, { PureComponent } from 'react';
 import PropTypes from 'prop-types';
 import { message, Upload, Icon, Modal } from 'antd';
 import styles from './index.less';
+import { getSignature } from '../../services/resource';
 
 const getFileList = (files) => {
   if (Array.isArray(files)) {
     return files.map((item, key) => {
       const urlArr = item.full_url.split('/');
-      return { url: item.full_url, id: item.id, uid: key, name: urlArr[urlArr.length - 1], status: 'done' };
+      return { url: item.full_url, id: '1', uid: key, name: urlArr[urlArr.length - 1], status: 'done' };
     });
   }
   if (files && !!files.length) {
@@ -46,19 +47,23 @@ export default class Uploader extends PureComponent {
       previewVisible: false,
       previewImage: '',
       files: getFileList(props.files),
+      signature: {},
+      fileName: '',
     };
   }
 
   componentWillReceiveProps(nextProps) {
+    // 当前属性中的文件对象是一个列表并且接受到的属性中files不为空
     if (Array.isArray(this.props.files) && !this.props.files.length && !!nextProps.files.length) {
       this.setState({ files: getFileList(nextProps.files) });
     }
   }
 
   render() {
-    const { previewVisible, previewImage, files } = this.state;
-    const { multiple = 1, onUpload, disabled, path, accept, signature } = this.props;
+    const { previewVisible, previewImage, files, signature, fileName } = this.state;
+    const { multiple = 1, onUpload, disabled, path, accept } = this.props;
 
+    // 根据单图/多图上传模式来选择返回一个还是多个文件对象
     const renderFiles = (fileList) => {
       const newFiles = fileList.map(file => {
         return file.response ? file.response.data.file : file;
@@ -79,7 +84,7 @@ export default class Uploader extends PureComponent {
         policy: signature.policy,
         signature: signature.signature,
         OSSAccessKeyId: signature.accessid,
-        key: `${signature.dir}${signature.path}` + '${filename}',
+        key: `${signature.dir}${fileName}`,
         success_action_status: '200',
       },
       disabled,
@@ -90,18 +95,27 @@ export default class Uploader extends PureComponent {
         this.setState({ previewImage: file.url || file.thumbUrl, previewVisible: true });
       },
       beforeUpload: (file, fileList) => {
-        return true;
+        // 根据图片名称,转换成路径地址
+        this.setState({
+          fileName: file.name.replace(/-/g, '/')
+        });
+        // 进行签名校验,失效则刷新签名
+        return getSignature().then(res => this.setState({signature: { ...res.data }}));
       },
       onChange: ({ file, fileList }) => {
+        // 检查图片大小,不能超过5M
         if (file && file.size > 5 * 1024 * 1024) {
           message.error('图片大小不能超过5M!');
-          return;
+          return false;
         }
+        // 满足要求,更新files对象
         this.setState({ files: fileList });
+        // 检查上传过程中图片的状态,上传成功回传fileName, signature, file
         if (file.percent === 100 && file.status === 'done') {
-          onUpload(renderFiles(fileList, 1));
+          onUpload(renderFiles(fileList, 1), fileName, signature);
         } else if (file.status === 'error') {
           message.error('图片上传失败!');
+          this.setState({ files: [] });
         }
       },
       onRemove: (file) => {
@@ -109,7 +123,7 @@ export default class Uploader extends PureComponent {
           return false;
         }
         const fileList = this.state.files.filter(item => item.uid !== file.uid);
-        onUpload(renderFiles(fileList, 0));
+        onUpload(renderFiles(fileList, 0), fileName, signature);
         return true;
       },
     };
@@ -128,7 +142,7 @@ export default class Uploader extends PureComponent {
     );
 
     return (
-      <div className="clearfix">
+      <div>
         <Upload {...uploadProps}>
           {multiple === true ? uploadButton : (files.length < multiple && uploadButton)}
         </Upload>

+ 2 - 1
src/index.js

@@ -11,9 +11,10 @@ import './index.less';
 const globalErrorHandler = (err) => {
   if (err.response && err.response.code === 10004) {
     message.error('登录失效,请重新登录!');
-    app._store.dispatch(routerRedux.push(router.login));
+    app._store.dispatch(routerRedux.push('/user/login'));
   }
   else {
+    console.error(err);
     notification.error({
       message: '程序错误',
       description: '应用内部发生未知错误,请查看日志或联系管理员!',

+ 101 - 0
src/models/group/detail.js

@@ -0,0 +1,101 @@
+import { queryOne, create, update } from '../../services/group';
+import { message } from 'antd';
+import pathToRegexp from 'path-to-regexp';
+
+export default {
+  namespace: 'groupDetail',
+
+  state: {
+    filters: {},
+    operType: 'create',
+    currentItem: {},
+    modalVisible: false,
+    itemLoading: false,
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen(({ pathname, state }) => {
+        const match = pathToRegexp('/tag/group/edit/:id').exec(pathname);
+        if (match) {
+          dispatch({ type: 'query', payload: { id: match[1] } });
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveOperType', payload: { operType: 'update' } });
+        }
+        if (pathname === '/tag/group/add') {
+          dispatch({ type: 'saveFilters', payload: state });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { itemLoading: true } });
+      const { data, success } = yield call(queryOne, payload);
+      if (success) {
+        yield put({ type: 'querySuccess', payload: { ...data } });
+      }
+      yield put({ type: 'changeLoading', payload: { itemLoading: false } });
+    },
+    * create ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(create, payload);
+      if (success) {
+        message.success('创建成功!');
+        yield put({ type: 'clearPage' });
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('创建失败!');
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('更新成功!');
+        yield put({ type: 'clearPage' });
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('更新失败!');
+      }
+    },
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    querySuccess(state, { payload }) {
+      return { ...state, currentItem: payload };
+    },
+
+    saveFilters(state, { payload: filters }) {
+      return { ...state, filters };
+    },
+
+    showModal(state) {
+      return { ...state, modalVisible: true };
+    },
+
+    hideModal(state) {
+      return { ...state, modalVisible: false };
+    },
+
+    saveOperType(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    saveSortResult(state, { payload: { tagList } }) {
+      const currentItem = { ...state.currentItem, tagList };
+      return { ...state, modalVisible: false, currentItem };
+    },
+
+    clearPage(state) {
+      return { ...state, currentItem: {}, itemLoading: false };
+    }
+  }
+}

+ 113 - 0
src/models/group/group.js

@@ -0,0 +1,113 @@
+import { query, create, update, remove } from '../../services/group';
+import modelExtend from 'dva-model-extend';
+import queryString from 'query-string';
+import { message } from 'antd';
+import { pageModel } from '../common';
+import config from '../../utils/config';
+import { checkSearchParams } from '../../utils/utils';
+
+export default modelExtend(pageModel, {
+  namespace: 'group',
+
+  state: {
+    currentItem: {},
+    itemLoading: false,
+    listLoading: false,
+    modalVisible: false,
+    modalType: 'create',
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen((location) => {
+        if (location.pathname === '/tag/group') {
+          const payload = checkSearchParams(queryString.parse(location.search));
+          dispatch({
+            type: 'query',
+            payload,
+          });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload = {} }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { listLoading: true }});
+      const { data, success } = yield call(query, payload);
+      if (success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: data.list,
+            pagination: {
+              current: Number(payload.pageNo) || 1,
+              pageSize: Number(payload.pageSize) || config.pageSize,
+              total: data.totalSize,
+            }
+          }
+        });
+      }
+      yield put({ type: 'changeLoading', payload: { listLoading: false }});
+    },
+    * create ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(create, payload);
+      if (success) {
+        message.success('创建成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('创建失败!');
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        yield put({ type: 'hideModal' });
+        message.success('更新成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('更新失败!');
+      }
+    },
+    * delete ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(remove, payload);
+      if (success) {
+        message.success('删除成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('删除失败!');
+      }
+    },
+    * recover ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('恢复成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('恢复失败!');
+      }
+    },
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    showModal(state, { payload }) {
+      return { ...state, ...payload, modalVisible: true };
+    },
+
+    hideModal(state) {
+      return { ...state, modalVisible: false };
+    },
+  }
+})

+ 5 - 3
src/models/login.js

@@ -1,6 +1,6 @@
 import { routerRedux } from 'dva/router';
+import { message } from 'antd';
 import { login, logout } from '../services/login';
-import { routers } from '../utils/map';
 import { addLocalUser } from '../utils/helper';
 
 export default {
@@ -14,14 +14,16 @@ export default {
       const { data, success } = yield call(login, payload);
       if (success) {
         addLocalUser(data);
-        yield put(routerRedux.push(routers.home));
+        message.success('登录成功')
+        yield put(routerRedux.push('/'));
       }
       yield put({ type: 'save', payload: { loading: false } });
     },
     *logout(_, { put, call }) {
       const { success } = yield call(logout);
       if (success) {
-        yield put(routerRedux.push(routers.login));
+        message.success('注销成功,请重新登录!')
+        yield put(routerRedux.push('/user/login'));
       }
     }
   },

+ 0 - 20
src/models/resource.js

@@ -5,13 +5,11 @@ import queryString from 'query-string';
 import { pageModel } from './common';
 import config from '../utils/config';
 import { checkSearchParams } from '../utils/utils';
-import { saveLocalSignature, getLocalSignature } from '../utils/helper';
 
 export default modelExtend(pageModel, {
   namespace: 'resource',
 
   state: {
-    signature: {},
     isPaused: false,
     currentItem: {},
     itemLoading: false,
@@ -108,24 +106,6 @@ export default modelExtend(pageModel, {
         message.error('恢复失败!');
       }
     },
-    * getSignAndShowModal ({ payload }, { call, put }) {
-      // 校验签名
-      let ossSignature;
-      const localSignature = getLocalSignature();
-      const expireTime = Math.floor((new Date()).getTime() / 1000 + 5).toString(); // 5s缓冲时间
-      // oss签名不存在或者已经过了有效期则请求新的签名
-      if (!localSignature || localSignature.expire <= expireTime) {
-        const { data, success } = yield call(getSignature, {});
-        if (success) {
-          saveLocalSignature(data);
-          ossSignature = data;
-        }
-      } else {
-        ossSignature = localSignature;
-      }
-      // 保存签名数据展示modal
-      yield put({ type: 'showModal', payload: { ...payload, signature: ossSignature } });
-    }
   },
 
   reducers: {

+ 102 - 0
src/models/tag/detail.js

@@ -0,0 +1,102 @@
+import { queryOne, create, update } from '../../services/tag';
+import { message } from 'antd';
+import pathToRegexp from 'path-to-regexp';
+import { Codes } from '../../utils/config';
+
+export default {
+  namespace: 'tagDetail',
+
+  state: {
+    filters: {},
+    operType: 'create',
+    currentItem: {},
+    modalVisible: false,
+    itemLoading: false,
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen(({ pathname, state, ...rest }) => {
+        const match = pathToRegexp('/tag/tag/edit/:id').exec(pathname);
+        if (match) {
+          dispatch({ type: 'query', payload: { id: match[1] } });
+          dispatch({ type: 'saveFilters', payload: state });
+          dispatch({ type: 'saveOperType', payload: { operType: 'update' } });
+        }
+        if (pathname === '/tag/tag/add') {
+          dispatch({ type: 'saveFilters', payload: state });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { itemLoading: true } });
+      const { data, success } = yield call(queryOne, payload);
+      if (success) {
+        yield put({ type: 'querySuccess', payload: { ...data } });
+      }
+      yield put({ type: 'changeLoading', payload: { itemLoading: false } });
+    },
+    * create ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(create, payload);
+      if (success) {
+        message.success('创建成功!');
+        yield put({ type: 'clearPage' });
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('创建失败!');
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('更新成功!');
+        yield put({ type: 'clearPage' });
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('更新失败!');
+      }
+    },
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    querySuccess(state, { payload }) {
+      return { ...state, currentItem: payload };
+    },
+
+    saveFilters(state, { payload: filters }) {
+      return { ...state, filters };
+    },
+
+    showModal(state) {
+      return { ...state, modalVisible: true };
+    },
+
+    hideModal(state) {
+      return { ...state, modalVisible: false };
+    },
+
+    saveOperType(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    saveSortResult(state, { payload: { itemList } }) {
+      const currentItem = { ...state.currentItem, itemList };
+      return { ...state, modalVisible: false, currentItem };
+    },
+
+    clearPage(state) {
+      return { ...state, currentItem: {}, itemLoading: false };
+    }
+  }
+}

+ 113 - 0
src/models/tag/tag.js

@@ -0,0 +1,113 @@
+import { query, create, update, remove } from '../../services/tag';
+import modelExtend from 'dva-model-extend';
+import queryString from 'query-string';
+import { message } from 'antd';
+import { pageModel } from '../common';
+import config from '../../utils/config';
+import { checkSearchParams } from '../../utils/utils';
+
+export default modelExtend(pageModel, {
+  namespace: 'tag',
+
+  state: {
+    currentItem: {},
+    itemLoading: false,
+    listLoading: false,
+    modalVisible: false,
+    modalType: 'create',
+  },
+
+  subscriptions: {
+    setup({ dispatch, history }) {
+      history.listen((location) => {
+        if (location.pathname === '/tag/tag') {
+          const payload = checkSearchParams(queryString.parse(location.search));
+          dispatch({
+            type: 'query',
+            payload,
+          });
+        }
+      });
+    }
+  },
+
+  effects: {
+    * query ({ payload = {} }, { call, put }) {
+      yield put({ type: 'changeLoading', payload: { listLoading: true }});
+      const { data, success } = yield call(query, payload);
+      if (success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            list: data.list,
+            pagination: {
+              current: Number(payload.pageNo) || 1,
+              pageSize: Number(payload.pageSize) || config.pageSize,
+              total: data.totalSize,
+            }
+          }
+        });
+      }
+      yield put({ type: 'changeLoading', payload: { listLoading: false }});
+    },
+    * create ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(create, payload);
+      if (success) {
+        message.success('创建成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('创建失败!');
+      }
+    },
+    * update ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        yield put({ type: 'hideModal' });
+        message.success('更新成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('更新失败!');
+      }
+    },
+    * delete ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(remove, payload);
+      if (success) {
+        message.success('删除成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('删除失败!');
+      }
+    },
+    * recover ({ payload, callback }, { call, put }) {
+      const { data, success } = yield call(update, payload);
+      if (success) {
+        message.success('恢复成功!');
+        if (callback) {
+          callback();
+        }
+      } else {
+        message.error('恢复失败!');
+      }
+    },
+  },
+
+  reducers: {
+    changeLoading(state, { payload }) {
+      return { ...state, ...payload };
+    },
+
+    showModal(state, { payload }) {
+      return { ...state, ...payload, modalVisible: true };
+    },
+
+    hideModal(state) {
+      return { ...state, modalVisible: false };
+    },
+  }
+})

+ 2 - 8
src/routes/Login/index.js

@@ -3,7 +3,6 @@ import { connect } from 'dva';
 import { routerRedux, Link } from 'dva/router';
 import { Form, Input, Tabs, Button, Icon, Checkbox, Row, Col, Alert } from 'antd';
 import styles from './index.less';
-import { actions, routers } from '../../utils/map';
 
 const FormItem = Form.Item;
 const { TabPane } = Tabs;
@@ -18,18 +17,13 @@ export default class Login extends Component {
     type: 'account',
   }
 
-  _actionDispatcher(type, payload = {}) {
-    const { dispatch } = this.props;
-    Object.keys(payload).map(key => {payload[key] ? null : delete payload[key]});
-    dispatch({ type, payload });
-  }
-
   handleSubmit = (e) => {
+    const { dispatch } = this.props;
     e.preventDefault();
     this.props.form.validateFields(err => {
       if (err) return;
       const values = this.props.form.getFieldsValue(['username', 'password']);
-      this._actionDispatcher(actions.userLogin, { ...values });
+      dispatch({ type: 'login/login', payload: { ...values } });
     });
   }
 

+ 152 - 155
src/routes/Resource/gallery/index.js

@@ -1,4 +1,4 @@
-import React, { PureComponent } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import queryString from 'query-string';
 import { connect } from 'dva';
@@ -9,166 +9,163 @@ import Search from './search';
 import ModalForm from './modal';
 import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
 
-@connect(state => ({ resource: state.resource }))
-export default class Gallery extends PureComponent {
-  static propTypes = {
-    resource: PropTypes.object,
-    location: PropTypes.object,
-    dispatch: PropTypes.func,
-  };
-
-  render() {
-    const { location, dispatch, resource } = this.props;
+function Gallery ({ location, dispatch, resource }) {
+  location.query = queryString.parse(location.search);
+  const { query, pathname } = location;
+  const { field, keyword, ...filters } = query;
+  const { list, listLoading, pagination, currentItem, itemLoading, modalVisible, modalType, signature } = resource;
 
-    location.query = queryString.parse(location.search);
-    const { query, pathname } = location;
-    const { field, keyword, ...filters } = query;
-    const { list, listLoading, pagination, currentItem, itemLoading, modalVisible, modalType, signature } = resource;
+  // 把携带的参数中空值项删除
+  Object.keys(filters).map(key => { filters[key] ? null : delete filters[key] });
+  // 如果搜索内容不为空则添加进filters中
+  if (field && keyword) {
+    filters[field] = keyword;
+  }
 
-    // 把携带的参数中空值项删除
-    Object.keys(filters).map(key => { filters[key] ? null : delete filters[key] });
-    // 如果搜索内容不为空则添加进filters中
-    if (field && keyword) {
-      filters[field] = keyword;
+  // 表头的搜索添加组件属性
+  const searchProps = {
+    field,
+    keyword,
+    onSearch: (payload) => {
+      if (!payload.keyword.length) {
+        delete payload.field;
+        delete payload.keyword;
+      }
+      dispatch(routerRedux.push({
+        pathname,
+        search: queryString.stringify({
+          ...payload
+        })
+      }));
+    },
+    onAdd: () => {
+      dispatch({
+        type: 'resource/showModal',
+        payload: {
+          modalType: 'create',
+        },
+      });
     }
+  };
+
+  // 列表组件的属性
+  const listProps = {
+    pagination,
+    location,
+    curStatus: filters.status,
+    dataSource: list,
+    loading: listLoading,
+    onChange: (pagination, filterArgs) => {
+      const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+      const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+        const newObj = { ...obj };
+        newObj[key] = getValue(filterArgs[key]);
+        return newObj;
+      }, {});
+      const data = { ...filters, ...tableFilters };
+      Object.keys(data).map(key => data[key] ? null : delete data[key]);
 
-    // 表头的搜索添加组件属性
-    const searchProps = {
-      field,
-      keyword,
-      onSearch: (payload) => {
-        if (!payload.keyword.length) {
-          delete payload.field;
-          delete payload.keyword;
+      dispatch(routerRedux.push({
+        pathname,
+        search: queryString.stringify({
+          ...data,
+          pageNo: pagination.current,
+          pageSize: pagination.pageSize
+        }),
+      }));
+    },
+    onEditItem: (item) => {
+      dispatch({
+        type: 'resource/showModal',
+        payload: {
+          modalType: 'update',
+          currentItem: item,
+        },
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname,
+              search: queryString.stringify(filters),
+            })
+          );
         }
-        dispatch(routerRedux.push({
-          pathname,
-          search: queryString.stringify({
-            ...payload
-          })
-        }));
-      },
-      onAdd: () => {
-        dispatch({
-          type: 'resource/getSignAndShowModal',
-          payload: {
-            modalType: 'create',
-          },
-        });
-      }
-    };
+      })
+    },
+    // 删除一条数据后,刷新页面保持筛选数据不变
+    onDeleteItem: (id) => {
+      dispatch({
+        type: 'resource/delete',
+        payload: id,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname,
+              search: queryString.stringify(filters),
+            })
+          );
+        }
+      });
+    },
+    // 恢复一条数据后,刷新页面并保持筛选数据不变
+    onRecoverItem: (payload) => {
+      dispatch({
+        type: 'resource/recover',
+        payload,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname,
+              search: queryString.stringify(filters),
+            })
+          );
+        }
+      });
+    }
+  };
 
-    // 列表组件的属性
-    const listProps = {
-      pagination,
-      location,
-      curStatus: filters.status,
-      dataSource: list,
-      loading: listLoading,
-      onChange: (pagination, filterArgs) => {
-        const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
-        const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
-          const newObj = { ...obj };
-          newObj[key] = getValue(filterArgs[key]);
-          return newObj;
-        }, {});
-        const data = { ...filters, ...tableFilters };
-        Object.keys(data).map(key => data[key] ? null : delete data[key]);
+  const modalProps = {
+    item: modalType === 'create' ? {} : currentItem,
+    visible: modalVisible,
+    maskClosable: false,
+    signature,
+    title: `${modalType === 'create' ? '添加图片' : '编辑图片'}`,
+    wrapClassName: 'vertical-center-modal',
+    onOk (data) {
+      dispatch({
+        type: `resource/${modalType}`,
+        payload: data,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname,
+              search: queryString.stringify(filters),
+            })
+          );
+        },
+      });
+    },
+    onCancel () {
+      dispatch({
+        type: 'resource/hideModal',
+      });
+    },
+  };
 
-        dispatch(routerRedux.push({
-          pathname,
-          search: queryString.stringify({
-            ...data,
-            pageNo: pagination.current,
-            pageSize: pagination.pageSize
-          }),
-        }));
-      },
-      onEditItem: (item) => {
-        dispatch({
-          type: 'resource/showModal',
-          payload: {
-            modalType: 'update',
-            currentItem: item,
-          },
-          callback: () => {
-            dispatch(
-              routerRedux.push({
-                pathname,
-                search: queryString.stringify(filters),
-              })
-            );
-          }
-        })
-      },
-      // 删除一条数据后,刷新页面保持筛选数据不变
-      onDeleteItem: (id) => {
-        dispatch({
-          type: 'resource/delete',
-          payload: id,
-          callback: () => {
-            dispatch(
-              routerRedux.push({
-                pathname,
-                search: queryString.stringify(filters),
-              })
-            );
-          }
-        });
-      },
-      // 恢复一条数据后,刷新页面并保持筛选数据不变
-      onRecoverItem: (payload) => {
-        dispatch({
-          type: 'resource/recover',
-          payload,
-          callback: () => {
-            dispatch(
-              routerRedux.push({
-                pathname,
-                search: queryString.stringify(filters),
-              })
-            );
-          }
-        });
-      }
-    };
+  return (
+    <PageHeaderLayout>
+      <Card>
+        <Search { ...searchProps } />
+        <TableList { ...listProps } />
+        <ModalForm { ...modalProps } />
+      </Card>
+    </PageHeaderLayout>
+  );
+}
 
-    const modalProps = {
-      item: modalType === 'create' ? {} : currentItem,
-      visible: modalVisible,
-      maskClosable: false,
-      signature,
-      title: `${modalType === 'create' ? '添加图片' : '编辑图片'}`,
-      wrapClassName: 'vertical-center-modal',
-      onOk (data) {
-        dispatch({
-          type: `resource/${modalType}`,
-          payload: data,
-          callback: () => {
-            dispatch(
-              routerRedux.push({
-                pathname,
-                search: queryString.stringify(filters),
-              })
-            );
-          },
-        });
-      },
-      onCancel () {
-        dispatch({
-          type: 'resource/hideModal',
-        });
-      },
-    };
+Gallery.propTypes = {
+  resource: PropTypes.object,
+  location: PropTypes.object,
+  dispatch: PropTypes.func,
+};
 
-    return (
-      <PageHeaderLayout>
-        <Card>
-          <Search { ...searchProps } />
-          <TableList { ...listProps } />
-          <ModalForm { ...modalProps } />
-        </Card>
-      </PageHeaderLayout>
-    );
-  }
-}
+export default connect(({ resource }) => ({ resource }))(Gallery);

+ 63 - 19
src/routes/Resource/gallery/modal.js

@@ -4,7 +4,7 @@ import { message, Form, Switch, Input, Modal } from 'antd';
 import Uploader from '../../../components/Uploader';
 import { Codes, statuses } from '../../../utils/config';
 
-const FormItem = Form.Item
+const FormItem = Form.Item;
 
 @Form.create()
 export default class ModalForm extends PureComponent {
@@ -12,23 +12,43 @@ export default class ModalForm extends PureComponent {
     item: PropTypes.object,
     form: PropTypes.object,
     onOk: PropTypes.func,
+    onCancel: PropTypes.func,
   };
-  
-  state = { file: '' };
+
+  constructor(props) {
+    super(props);
+    this.state = { item: props.item };
+  }
 
   componentWillReceiveProps(nextProps) {
-    this.setState({ file: nextProps.item.url });
+    if (nextProps.item) {
+      this.setState({ item: { ...nextProps.item } });
+    }
   }
 
-  handleSingleUpload = (file) => {
+  // 图片上传成功
+  handleSingleUpload = (file, fileName, signature) => {
     if (file) {
+      const separatorIndex = file.name.lastIndexOf('.');
+      const name = file.name.substring(0, separatorIndex);
+      const suffix = file.name.substring(separatorIndex + 1, file.name.length);
+      const data = {
+        name,
+        format: suffix,
+        size: file.size,
+        code: name,
+        url: `${signature.host}/${signature.dir}${fileName}`,
+      };
       message.success('图片上传成功!');
+      this.setState({ item: { ...this.state.item, ...data } });
+    } else {
+      // 图片被移除,重置state为空
+      this.setState({ item: {} });
     }
-    this.setState({ file });
   }
 
   handleOk = () => {
-    const { form: { validateFields, resetFields }, onOk } = this.props;
+    const { form: { validateFields, getFieldsValue }, item, onOk } = this.props;
     validateFields((errors) => {
       if (errors) { return; }
       const data = {
@@ -36,14 +56,20 @@ export default class ModalForm extends PureComponent {
       };
       item.id ? data.id = item.id : null;
       onOk(data);
-      resetFields();
     });
   }
 
+  handleCancel = () => {
+    const { onCancel, form: { resetFields } } = this.props;
+    //退出模态框,把state中的item重置为空
+    this.setState({ item: {} });
+    resetFields();
+    onCancel();
+  }
+
   render() {
-    const { item = {}, form: { getFieldDecorator }, signature, modalType, ...modalProps } = this.props;
-    const { file } = this.state;
-    const modalOpts = { ...modalProps, onOk: this.handleOk };
+    const { signature, form: { getFieldDecorator }, ...modalProps } = this.props;
+    const { item } = this.state;
 
     const formItemLayout = {
       labelCol: {
@@ -54,9 +80,23 @@ export default class ModalForm extends PureComponent {
       },
     };
 
+    const modalOpts = {
+      ...modalProps,
+      onOk: this.handleOk,
+      onCancel: this.handleCancel,
+    };
+
     return (
-      <Modal key={item.id} {...modalOpts}>
+      <Modal key={this.props.item.id} {...modalOpts}>
         <Form layout="horizontal">
+          <FormItem label="单图上传" {...formItemLayout}>
+            <Uploader
+              accept="image"
+              files={item.url}
+              signature={signature}
+              onUpload={::this.handleSingleUpload}
+            />
+          </FormItem>
           <FormItem label="图片编号:" hasFeedback {...formItemLayout}>
             {getFieldDecorator('code', {
               rules: [{ required: true, type: 'string', message: "此项为必填项!" }],
@@ -69,19 +109,23 @@ export default class ModalForm extends PureComponent {
               initialValue: item.name,
             })(<Input />)}
           </FormItem>
-          <FormItem label="单图上传" {...formItemLayout}>
-            <Uploader
-              accept="image"
-              files={file}
-              signature={signature}
-              onUpload={::this.handleSingleUpload}
-            />
+          <FormItem label="图片格式:" {...formItemLayout}>
+            {getFieldDecorator('format', {
+              initialValue: item.format,
+            })(<Input placeholder="自动生成" />)}
+          </FormItem>
+          <FormItem label="图片大小:" {...formItemLayout}>
+            {getFieldDecorator('size', {
+              initialValue: item.size,
+            })(<Input placeholder="自动生成" suffix={"字节"} />)}
           </FormItem>
+          {/*
           <FormItem label="使用状态:" {...formItemLayout}>
             {getFieldDecorator('status', {
               valuePropsName: 'checked',
           })(<Switch defaultChecked={item.status || Codes.CODE_NORMAL === Codes.CODE_NORMAL ? true : false} checkedChildren="使用中" unCheckedChildren="已删除" />)}
           </FormItem>
+          */}
         </Form>
       </Modal>
     );

+ 1 - 1
src/routes/Resource/gallery/table.js

@@ -56,7 +56,7 @@ export default class TableList extends PureComponent {
       dataIndex: 'name',
       key: 'name',
     },{
-      title: '图片大小',
+      title: '图片大小(B)',
       dataIndex: 'size',
       key: 'size',
     },{

+ 6 - 10
src/routes/Resource/video/table.js

@@ -18,16 +18,12 @@ export default class TableList extends PureComponent {
     onPlayVideo: PropTypes.func.isRequired,
   };
 
-  handleOperateItem = (record) => {
-    const { onDeleteItem, onRecoverItem } = this.props;
+  handleDeleteItem = (record) => {
+    const { onDeleteItem } = this.props;
     confirm({
-      title: `您确定要${record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}该条记录?`,
+      title: `您确定要${record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}该视频?`,
       onOk () {
-        if (record.status === Codes.CODE_NORMAL) {
-          onDeleteItem({id: record.id});
-        } else if (record.status === Codes.CODE_DELETE) {
-          onRecoverItem({ id: record.id, status: Codes.CODE_NORMAL });
-        }
+        onDeleteItem(record.id);
       },
     })
   }
@@ -44,7 +40,7 @@ export default class TableList extends PureComponent {
       dataIndex: 'name',
       key: 'name',
     },{
-      title: '视频大小(M)',
+      title: '视频大小(B)',
       dataIndex: 'size',
       key: 'size',
     },{
@@ -73,7 +69,7 @@ export default class TableList extends PureComponent {
         <div>
           <a onClick={() => onPlayVideo(record)}>播放</a>
           <span className={styles.splitLine} />
-          <a onClick={() => this.handleOperateItem(record)}>{record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}</a>
+          <a onClick={() => this.handleDeleteItem(record)}>{record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}</a>
         </div>
       )
     }];

+ 223 - 0
src/routes/Tag/detail/index.js

@@ -0,0 +1,223 @@
+import React, { PureComponent } from 'react';
+import { routerRedux } from 'dva/router';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { Spin, Badge, Table, Radio, Card, Form, Input, Icon, Button, Select } from 'antd';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import ItemSortModal from './modal';
+import { Codes, itemStatuses, tagType } from '../../../utils/config';
+
+const FormItem = Form.Item;
+const Option = Select.Option;
+const RadioGroup = Radio.Group;
+
+@Form.create()
+@connect(state => ({
+  tagDetail: state.tagDetail,
+  group: state.group,
+}))
+export default class TagDetail extends PureComponent {
+  static propTypes = {
+    tagDetail: PropTypes.object,
+  };
+
+  componentDidMount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'group/query',
+      payload: {
+        pageNo: 1,
+        pageSize: 1000,
+        status: Codes.CODE_NORMAL,
+      }
+    });
+  }
+
+  handleModalShow = () => {
+    const { dispatch } = this.props;
+    dispatch({ type: 'tagDetail/showModal' });
+  }
+
+  handleModalCancel = () => {
+    const { dispatch } = this.props;
+    dispatch({ type: 'tagDetail/hideModal' });
+  }
+
+  handleModalOk = (data) => {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'tagDetail/saveSortResult',
+      payload: { itemList: data }
+    });
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault()
+    const {
+      dispatch,
+      form: {
+        validateFields,
+        getFieldsValue,
+        resetFields
+      },
+      tagDetail: {
+        operType,
+        currentItem,
+        filters,
+      }
+    } = this.props;
+    validateFields((errors) => {
+      if (errors) { return; }
+      const data = {
+        ...currentItem,
+        ...getFieldsValue(),
+      };
+      dispatch({
+        type: `tagDetail/${operType}`,
+        payload: data,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname: '/tag/tag',
+              search: queryString.stringify(filters),
+            })
+          );
+        }
+      })
+      resetFields();
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch, tagDetail: { filters } } = this.props;
+    dispatch({ type: 'tagDetail/clearPage' });
+    dispatch(
+      routerRedux.push({
+        pathname: '/tag/tag',
+        search: queryString.stringify(filters),
+      })
+    );
+  }
+
+  render() {
+    const { dispatch, form: { getFieldDecorator }, tagDetail, group } = this.props;
+    const { itemLoading, currentItem, filters, modalVisible } = tagDetail;
+    const { list } = group;
+    const { itemList, name, code, type, groupId } = currentItem;
+
+    const itemTableColumns = [{
+      title: '位置',
+      dataIndex: 'sort',
+      key: 'sort',
+    },{
+      title: '商品名称',
+      dataIndex: 'name',
+      key: 'name',
+    },{
+      title: '商品状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (text, record) => {
+        const statusMap = {[Codes.CODE_SALE]: 'success', [Codes.CODE_DELETE]: 'error'};
+        return (<Badge status={statusMap[record.status]} text={itemStatuses[record.status]} />);
+      },
+    },{
+      title: '商品类型',
+      dataIndex: 'type',
+      key: 'type',
+      render: (text, record) => (<span>{tagType[record.type]}</span>),
+    }];
+
+    const formItemLayout = {
+      labelCol: {
+        span: 7,
+      },
+      wrapperCol: {
+        span: 12,
+      },
+    };
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Spin spinning={itemLoading}>
+          <Card>
+            <Form layout="horizontal" onSubmit={this.handlePageSubmit}>
+              <FormItem label="标签编号:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('code', {
+                  rules: [{ required: true, type: 'string', message: "编号为必填项!" }],
+                  initialValue: code,
+                })(<Input />)}
+              </FormItem>
+              <FormItem label="标签名称:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('name', {
+                  rules: [{ required: true, type: 'string', message: "名称为必填项!" }],
+                  initialValue: name,
+                })(<Input />)}
+              </FormItem>
+              <FormItem label="标签类型:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('type', {
+                  rules: [{ required: true, type: 'string', message: "编号为必填项!" }],
+                  initialValue: type || Codes.CODE_COURSE,
+                })(
+                  <RadioGroup>
+                    {Object.keys(tagType).map(key => <Radio value={key} key={key}>{tagType[key]}</Radio>)}
+                  </RadioGroup>
+                )}
+              </FormItem>
+              <FormItem label="所属标签组" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('groupId', {
+                  rules: [{ required: true, type: 'string', message: "标签组为必选项!" }],
+                  initialValue: groupId,
+                })(
+                  <Select
+                    showSearch
+                    allowClear
+                    placeholder="请输入标签组编号或者名称进行筛选"
+                    optionFilterProp="children"
+                    filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+                  >
+                    {list.map(item => <Option value={item.id} key={item.id}>{`${item.code}/${item.name}`}</Option>)}
+                  </Select>
+                )}
+              </FormItem>
+              <FormItem label="商品排序" {...formItemLayout}>
+                <Button onClick={this.handleModalShow} disabled={!(itemList || []).length} type="primary" size="small" icon="edit">排序</Button>
+              </FormItem>
+              <FormItem wrapperCol={{ offset: 7, span: 12 }}>
+                <Table
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>
+                      该标签下不包含任何商品,无法排序,请先去关联商品吧!</span>
+                  }}
+                  dataSource={itemList}
+                  columns={itemTableColumns}
+                  rowKey={record => record.id}
+                  bordered
+                  pagination={false}
+                />
+              </FormItem>
+              <FormItem {...submitFormLayout} style={{ marginTop: 32 }}>
+                <Button onClick={this.handlePageCancel}>取消</Button>
+                <Button type="primary" style={{ marginLeft: 35 }} htmlType="submit">提交</Button>
+              </FormItem>
+            </Form>
+            <ItemSortModal
+              itemList={itemList || []}
+              rowKeyName="id"
+              modalVisible={modalVisible}
+              onCancel={this.handleModalCancel}
+              onOk={this.handleModalOk}
+            />
+          </Card>
+        </Spin>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 61 - 0
src/routes/Tag/detail/modal.js

@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Badge } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import { Codes, tagType, itemStatuses } from '../../../utils/config';
+
+export default class ItemSortModal extends PureComponent {
+  static propTypes = {
+    itemList: PropTypes.array.isRequired,
+    modalVisible: PropTypes.bool.isRequired,
+    rowKeyName: PropTypes.string.isRequired,
+  };
+
+  render() {
+    const { itemList, modalVisible, rowKeyName, onCancel, onOk } = this.props;
+    const modalProps = {
+      title: '商品排序',
+      maskClosable: false,
+      visible: modalVisible,
+      destroyOnClose: true,
+      onCancel,
+      onOk,
+    };
+    const selTableProps = {
+      operSort: true,
+      tablePagination: false,
+      tableDataSource: itemList,
+      rowKeyName: rowKeyName,
+      tableColumns: [{
+        title: '位置',
+        dataIndex: 'sort',
+        key: 'sort',
+      },{
+        title: '商品名称',
+        dataIndex: 'name',
+        key: 'name',
+      },{
+        title: '商品状态',
+        dataIndex: 'status',
+        key: 'status',
+        render: (text, record) => {
+          const statusMap = {[Codes.CODE_SALE]: 'success', [Codes.CODE_DELETE]: 'error'};
+          return (<Badge status={statusMap[record.status]} text={itemStatuses[record.status]} />);
+        },
+      },{
+        title: '商品类型',
+        dataIndex: 'type',
+        key: 'type',
+        render: (text, record) => (<span>{tagType[record.type]}</span>),
+      }],
+    };
+
+    return (
+      <SelectModal
+        mode="sort"
+        { ...modalProps }
+        { ...selTableProps }
+      />
+    );
+  }
+}

+ 160 - 0
src/routes/Tag/index.js

@@ -0,0 +1,160 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Card } from 'antd';
+import TableList from './table';
+import Search from './search';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import { Codes } from '../../utils/config';
+
+@connect(state => ({
+  tag: state.tag,
+  merchant: state.merchant,
+}))
+export default class Tag extends PureComponent {
+  static propTypes = {
+    group: PropTypes.object,
+    location: PropTypes.object,
+    dispatch: PropTypes.func,
+  };
+
+  componentDidMount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'merchant/query',
+      payload: {
+        pageNo: 1,
+        status: Codes.CODE_NORMAL,
+        pageSize: 20,
+      }
+    });
+  }
+
+  render() {
+    const { location, dispatch, tag, merchant } = this.props;
+
+    location.query = queryString.parse(location.search);
+    const { query, pathname } = location;
+    const { field, keyword, ...filters } = query;
+    const { list, listLoading, pagination, currentItem, itemLoading, modalVisible, modalType } = tag;
+
+    // 把携带的参数中空值项删除
+    Object.keys(filters).map(key => { filters[key] ? null : delete filters[key] });
+    // 如果搜索内容不为空则添加进filters中
+    if (field && keyword) {
+      filters.field = field;
+      filters.keyword = keyword;
+    }
+
+    const searchProps = {
+      field,
+      keyword,
+      filterSelectProps: {
+        data: merchant.list.map(item => { return { value: item.id, name: item.name } }),
+        onSelectSearch: (value) => {
+          dispatch({
+            type: 'merchant/query',
+            payload: {
+              name: value,
+              status: Codes.CODE_NORMAL
+            }
+          });
+        },
+      },
+      onSearch: (payload) => {
+        if (!payload.keyword.length) {
+          delete payload.field;
+          delete payload.keyword;
+        }
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...payload
+          })
+        }));
+      },
+      onAdd: () => {
+        dispatch(
+          routerRedux.push({
+            pathname: '/tag/tag/add',
+            state: filters,
+          })
+        );
+      }
+    };
+
+    const listProps = {
+      pagination,
+      location,
+      dataSource: list,
+      loading: listLoading,
+      curStatus: filters.status,
+      onChange: (pagination, filterArgs) => {
+        const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+        const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+          const newObj = { ...obj };
+          newObj[key] = getValue(filterArgs[key]);
+          return newObj;
+        }, {});
+
+        const data = { ...filters, ...tableFilters };
+        Object.keys(data).map(key => data[key] ? null : delete data[key]);
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...data,
+            pageNo: pagination.current,
+            pageSize: pagination.pageSize,
+          }),
+        }));
+      },
+      onEditItem: (item) => {
+        dispatch(
+          routerRedux.push({
+            pathname: `/tag/tag/edit/${item.id}`,
+            state: filters,
+          })
+        );
+      },
+      onDeleteItem: (id) => {
+        dispatch({
+          type: 'tag/delete',
+          payload: id,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      },
+      onRecoverItem: (payload) => {
+        dispatch({
+          type: 'tag/recover',
+          payload,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      }
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Card>
+          <Search { ...searchProps } />
+          <TableList { ...listProps } />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 51 - 0
src/routes/Tag/search.js

@@ -0,0 +1,51 @@
+import react, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Button, Form, Row, Col, Icon } from 'antd';
+import DataSearch from '../../components/DataSearch';
+
+@Form.create()
+export default class Search extends PureComponent {
+  static propTypes = {
+    form: PropTypes.object.isRequired,
+    onSearch: PropTypes.func,
+    onAdd: PropTypes.func,
+    field: PropTypes.string,
+    keyword: PropTypes.string,
+  };
+
+  render() {
+    const { field, keyword, onSearch, onAdd, filterSelectProps } = this.props;
+
+    const searchGroupProps = {
+      field,
+      keyword,
+      filterSelectProps,
+      size: 'default',
+      select: true,
+      selectOptions: [{
+        value: 'name', name: '标签名称', mode: 'input',
+      },{
+        value: 'code', name: '标签编号', mode: 'input',
+      },{
+        value: 'merchantId', name: '渠道名称', mode: 'select',
+      }],
+      selectProps: {
+        defaultValue: field || 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    return (
+      <Row gutter={24}>
+        <Col lg={10} md={12} sm={16} xs={24} style={{ marginBottom: 16 }}>
+          <DataSearch { ...searchGroupProps } />
+        </Col>
+        <Col lg={{ offset: 7, span: 7 }} md={12} sm={8} xs={24} style={{ marginBottom: 16, textAlign: 'right' }}>
+          <Button type="ghost" onClick={onAdd}><Icon type="plus-circle" />添加</Button>
+        </Col>
+      </Row>
+    );
+  }
+}

+ 109 - 0
src/routes/Tag/table.js

@@ -0,0 +1,109 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import classnames from 'classnames';
+import queryString from 'query-string';
+import { Modal, Table, Menu, Icon, Badge } from 'antd';
+import AnimTableBody from '../../components/Animation/AnimTableBody';
+import styles from './table.less';
+import { statuses, Codes } from '../../utils/config';
+
+const confirm = Modal.confirm;
+
+export default class TableList extends PureComponent {
+  static propTypes = {
+    location: PropTypes.object,
+    onChange: PropTypes.func.isRequired,
+    onDeleteItem: PropTypes.func.isRequired,
+    onEditItem: PropTypes.func.isRequired,
+  };
+
+  handleOperateItem = (record) => {
+    const { onDeleteItem, onRecoverItem } = this.props;
+    confirm({
+      title: `您确定要${record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}该条记录?`,
+      onOk () {
+        if (record.status === Codes.CODE_NORMAL) {
+          onDeleteItem({id: record.id});
+        } else if (record.status === Codes.CODE_DELETE) {
+          onRecoverItem({ id: record.id, status: Codes.CODE_NORMAL });
+        }
+      },
+    })
+  }
+
+  render() {
+    const { curStatus, onDeleteItem, onRecoverItem, onEditItem, location, pagination, ...tableProps } = this.props;
+
+    const columns = [{
+      title: '标签编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '标签名称',
+      dataIndex: 'name',
+      key: 'name',
+    },{
+      title: '所属标签组',
+      dataIndex: 'groupName',
+      key: 'groupName',
+    },{
+      title: '渠道平台',
+      dataIndex: 'merchantId',
+      key: 'merchantId',
+    },{
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (text, record) => {
+        const statusMap = {[Codes.CODE_NORMAL]: 'success', [Codes.CODE_DELETE]: 'error'};
+        return (<Badge status={statusMap[record.status]} text={statuses[record.status]} />);
+      },
+      filters: Object.keys(statuses).map(key => ({ text: statuses[key], value: key })),
+      filterMultiple: false,
+      filteredValue: [curStatus],
+    },{
+      title: '添加时间',
+      dataIndex: 'gmtCreated',
+      key: 'gmtCreated',
+      render: (text, record) => (
+        <div>{moment(text).format('YYYY-MM-DD')}</div>
+      )
+    },{
+      title: '操作',
+      dataIndex: 'operation',
+      key: 'operation',
+      render: (text, record) => (
+        <div>
+          <a onClick={() => onEditItem(record)}>编辑</a>
+          <span className={styles.splitLine} />
+            <a onClick={() => this.handleOperateItem(record)}>{record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}</a>
+        </div>
+      )
+    }];
+
+    // 数据table列表表头的筛选按钮点击重置后status与domain值为空,此时删除该参数
+    columns.map(item => {
+      item.dataIndex === 'status' && !curStatus ? delete item.filteredValue : null;
+    });
+
+    tableProps.pagination = !!pagination && { ...pagination, showSizeChanger: true, showQuickJumper: true, showTotal: total => `共 ${total} 条`};
+    const getBodyWrapperProps = {
+      page: location.query.page,
+      current: tableProps.pagination.current,
+    };
+    const getBodyWrapper = (body) => (<AnimTableBody {...getBodyWrapperProps} body={body} />);
+
+    return (
+      <Table
+        simple
+        bordered
+        { ...tableProps }
+        columns={columns}
+        className={classnames({ [styles.table]: true, [styles.motion]: true })}
+        rowKey={record => record.id}
+        getBodyWrapper={getBodyWrapper}
+      />
+    );
+  }
+}

+ 86 - 0
src/routes/Tag/table.less

@@ -0,0 +1,86 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.table {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 62px;
+    }
+  }
+
+  &.motion {
+    :global {
+      .ant-table-tbody > tr > td,
+      .ant-table-thead > tr > th {
+        &:nth-child(1) {
+          width: 14%;
+        }
+
+        &:nth-child(2) {
+          width: 14%;
+        }
+
+        &:nth-child(3) {
+          width: 14%;
+        }
+
+        &:nth-child(4) {
+          width: 14%;
+        }
+
+        &:nth-child(5) {
+          width: 14%;
+        }
+
+        &:nth-child(6) {
+          width: 14%;
+        }
+
+        &:nth-child(7) {
+          width: 16%;
+        }
+      }
+
+      .ant-table-thead {
+        & > tr {
+          transition: none;
+          display: block;
+
+          & > th {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+        }
+      }
+
+      .ant-table-tbody {
+        & > tr {
+          transition: none;
+          display: block;
+          border-bottom: 1px solid #f5f5f5;
+
+          & > td {
+            border-bottom: none;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+
+          &.ant-table-expanded-row-level-1 > td {
+            height: auto;
+          }
+        }
+      }
+    }
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 214 - 0
src/routes/TagGroup/detail/index.js

@@ -0,0 +1,214 @@
+import React, { PureComponent } from 'react';
+import { routerRedux } from 'dva/router';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { Spin, Badge, Table, Card, Form, Input, Icon, Button, Select } from 'antd';
+import PageHeaderLayout from '../../../layouts/PageHeaderLayout';
+import TagSortModal from './modal';
+import { Codes, statuses, tagType } from '../../../utils/config';
+
+const FormItem = Form.Item;
+const Option = Select.Option;
+
+@Form.create()
+@connect(state => ({
+  groupDetail: state.groupDetail,
+  merchant: state.merchant,
+}))
+export default class GroupDetail extends PureComponent {
+  static propTypes = {
+    groupDetail: PropTypes.object,
+    merchant: PropTypes.object,
+  };
+
+  // 组件一挂载完成,立即请求1000条厂商数据
+  // TODO 有弊端,待后期优化
+  componentDidMount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'merchant/query',
+      payload: {
+        pageNo: 1,
+        pageSize: 1000,
+        status: Codes.CODE_NORMAL
+      }
+    });
+  }
+
+  handleModalShow = () => {
+    const { dispatch } = this.props;
+    dispatch({ type: 'groupDetail/showModal' });
+  }
+
+  handleModalCancel = () => {
+    const { dispatch } = this.props;
+    dispatch({ type: 'groupDetail/hideModal' });
+  }
+
+  handleModalOk = (data) => {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'groupDetail/saveSortResult',
+      payload: { tagList: data }
+    });
+  }
+
+  handlePageSubmit = (e) => {
+    e.preventDefault()
+    const {
+      dispatch,
+      form: {
+        validateFields,
+        getFieldsValue,
+        resetFields
+      },
+      groupDetail: {
+        operType,
+        currentItem,
+        filters,
+      }
+    } = this.props;
+    validateFields((errors) => {
+      if (errors) { return; }
+      const data = {
+        ...currentItem,
+        ...getFieldsValue(),
+      };
+      dispatch({
+        type: `groupDetail/${operType}`,
+        payload: data,
+        callback: () => {
+          dispatch(
+            routerRedux.push({
+              pathname: '/tag/group',
+              search: queryString.stringify(filters),
+            })
+          );
+        }
+      })
+      resetFields();
+    });
+  }
+
+  handlePageCancel = () => {
+    const { dispatch, groupDetail: { filters } } = this.props;
+    dispatch({ type: 'groupDetail/clearPage' });
+    dispatch(
+      routerRedux.push({
+        pathname: '/tag/group',
+        search: queryString.stringify(filters),
+      })
+    );
+  }
+
+  render() {
+    const { dispatch, form: { getFieldDecorator }, groupDetail, merchant } = this.props;
+    const { itemLoading, currentItem, filters, modalVisible } = groupDetail;
+    const { tagList = [], name, code, merchantId } = currentItem;
+    const { list } = merchant;
+
+    const tagTableColumns = [{
+      title: '位置',
+      dataIndex: 'sort',
+      key: 'sort',
+    },{
+      title: '标签名称',
+      dataIndex: 'name',
+      key: 'name',
+    },{
+      title: '标签状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (text, record) => {
+        const statusMap = {[Codes.CODE_NORMAL]: 'success', [Codes.CODE_DELETE]: 'error'};
+        return (<Badge status={statusMap[record.status]} text={statuses[record.status]} />);
+      },
+    },{
+      title: '标签类型',
+      dataIndex: 'type',
+      key: 'type',
+      render: (text, record) => (<span>{tagType[record.type]}</span>),
+    }];
+
+    const formItemLayout = {
+      labelCol: {
+        span: 7,
+      },
+      wrapperCol: {
+        span: 12,
+      },
+    };
+    const submitFormLayout = {
+      wrapperCol: {
+        xs: { span: 24, offset: 0 },
+        sm: { span: 10, offset: 7 },
+      },
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Spin spinning={itemLoading}>
+          <Card>
+            <Form layout="horizontal" onSubmit={this.handlePageSubmit}>
+              <FormItem label="标签组编号:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('code', {
+                  rules: [{ required: true, type: 'string', message: "编号为必填项!" }],
+                  initialValue: code,
+                })(<Input />)}
+              </FormItem>
+              <FormItem label="标签组名称:" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('name', {
+                  rules: [{ required: true, type: 'string', message: "名称为必填项!" }],
+                  initialValue: name,
+                })(<Input />)}
+              </FormItem>
+              <FormItem label="渠道平台" hasFeedback {...formItemLayout}>
+                {getFieldDecorator('merchantId', {
+                  rules: [{ required: true, type: 'string', message: "渠道为必选项!" }],
+                  initialValue: merchantId,
+                })(
+                  <Select
+                    showSearch
+                    allowClear
+                    placeholder="请输入渠道编号或者名称进行筛选"
+                    optionFilterProp="children"
+                    filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0}
+                  >
+                    {list.map(item => <Option value={item.id} key={item.id}>{`${item.code}/${item.name}`}</Option>)}
+                  </Select>
+                )}
+              </FormItem>
+              <FormItem label="标签排序" {...formItemLayout}>
+                <Button onClick={this.handleModalShow} disabled={!tagList.length} type="primary" size="small" icon="edit">排序</Button>
+              </FormItem>
+              <FormItem wrapperCol={{ offset: 7, span: 12 }}>
+                <Table
+                  locale={{
+                    emptyText: <span style={{ color: "#C6D0D6" }}>&nbsp;&nbsp;<Icon type="frown-o"/>该标签组下不包含任何标签,无法排序,请先去关联标签吧!</span>
+                  }}
+                  dataSource={tagList}
+                  columns={tagTableColumns}
+                  rowKey={record => record.id}
+                  bordered
+                  pagination={false}
+                />
+              </FormItem>
+              <FormItem {...submitFormLayout} style={{ marginTop: 32 }}>
+                <Button onClick={this.handlePageCancel}>取消</Button>
+                <Button type="primary" style={{ marginLeft: 35 }} htmlType="submit">提交</Button>
+              </FormItem>
+            </Form>
+            <TagSortModal
+              tagList={tagList}
+              rowKeyName="id"
+              modalVisible={modalVisible}
+              onCancel={this.handleModalCancel}
+              onOk={this.handleModalOk}
+            />
+          </Card>
+        </Spin>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 61 - 0
src/routes/TagGroup/detail/modal.js

@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Badge } from 'antd';
+import SelectModal from '../../../components/SelectModal';
+import { Codes, tagType, statuses } from '../../../utils/config';
+
+export default class TagSortModal extends PureComponent {
+  static propTypes = {
+    tagList: PropTypes.array.isRequired,
+    modalVisible: PropTypes.bool.isRequired,
+    rowKeyName: PropTypes.string.isRequired,
+  };
+
+  render() {
+    const { tagList, modalVisible, rowKeyName, onCancel, onOk } = this.props;
+    const modalProps = {
+      title: '标签排序',
+      maskClosable: false,
+      visible: modalVisible,
+      destroyOnClose: true,
+      onCancel,
+      onOk,
+    };
+    const selTableProps = {
+      operSort: true,
+      tablePagination: false,
+      tableDataSource: tagList,
+      rowKeyName: rowKeyName,
+      tableColumns: [{
+        title: '位置',
+        dataIndex: 'sort',
+        key: 'sort',
+      },{
+        title: '标签名称',
+        dataIndex: 'name',
+        key: 'name',
+      },{
+        title: '标签状态',
+        dataIndex: 'status',
+        key: 'status',
+        render: (text, record) => {
+          const statusMap = {[Codes.CODE_NORMAL]: 'success', [Codes.CODE_DELETE]: 'error'};
+          return (<Badge status={statusMap[record.status]} text={statuses[record.status]} />);
+        },
+      },{
+        title: '标签类型',
+        dataIndex: 'type',
+        key: 'type',
+        render: (text, record) => (<span>{tagType[record.type]}</span>),
+      }],
+    };
+
+    return (
+      <SelectModal
+        mode="sort"
+        { ...modalProps }
+        { ...selTableProps }
+      />
+    );
+  }
+}

+ 168 - 0
src/routes/TagGroup/index.js

@@ -0,0 +1,168 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Card } from 'antd';
+import TableList from './table';
+import Search from './search';
+import PageHeaderLayout from '../../layouts/PageHeaderLayout';
+import { Codes } from '../../utils/config';
+
+@connect(state => ({
+  group: state.group,
+  merchant: state.merchant,
+}))
+export default class TagGroup extends PureComponent {
+  static propTypes = {
+    group: PropTypes.object,
+    location: PropTypes.object,
+    dispatch: PropTypes.func,
+  };
+
+  componentDidMount() {
+    const { dispatch } = this.props;
+    dispatch({
+      type: 'merchant/query',
+      payload: {
+        pageNo: 1,
+        status: Codes.CODE_NORMAL,
+        pageSize: 20,
+      }
+    });
+  }
+
+  render() {
+    const { location, dispatch, group, merchant } = this.props;
+
+    location.query = queryString.parse(location.search);
+    const { query, pathname } = location;
+    const { field, keyword, ...filters } = query;
+    const { list, listLoading, pagination, currentItem, itemLoading, modalVisible, modalType } = group;
+
+    // 把携带的参数中空值项删除
+    Object.keys(filters).map(key => { filters[key] ? null : delete filters[key] });
+    // 如果搜索内容不为空则添加进filters中
+    if (field && keyword) {
+      filters.field = field;
+      filters.keyword = keyword;
+    }
+
+    const searchProps = {
+      field,
+      keyword,
+      filterSelectProps: {
+        data: merchant.list.map(item => { return { value: item.id, name: item.name } }),
+        onSelectSearch: (value) => {
+          dispatch({
+            type: 'merchant/query',
+            payload: {
+              name: value,
+              status: Codes.CODE_NORMAL
+            }
+          });
+        },
+      },
+      onSearch: (payload) => {
+        if (!payload.keyword.length) {
+          delete payload.field;
+          delete payload.keyword;
+        }
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...payload
+          })
+        }));
+      },
+      onAdd: () => {
+        dispatch(
+          routerRedux.push({
+            pathname: '/tag/group/add',
+            state: filters,
+          })
+        );
+      }
+    };
+
+    const listProps = {
+      pagination,
+      location,
+      dataSource: list,
+      loading: listLoading,
+      curStatus: filters.status,
+      onChange: (pagination, filterArgs) => {
+        const getValue = obj => Object.keys(obj).map(key => obj[key]).join(',');
+        const tableFilters = Object.keys(filterArgs).reduce((obj, key) => {
+          const newObj = { ...obj };
+          newObj[key] = getValue(filterArgs[key]);
+          return newObj;
+        }, {});
+
+        // // 如果filters中包含field,keyword整理成k:v形式
+        // const newFilters = { ...filters };
+        // if (newFilters.field && newFilters.keyword) {
+        //   newFilters[newFilters.field] = newFilters.keyword;
+        //   delete newFilters.field;
+        //   delete newFilters.keyword;
+        // }
+        //
+        const data = { ...filters, ...tableFilters };
+        Object.keys(data).map(key => data[key] ? null : delete data[key]);
+        dispatch(routerRedux.push({
+          pathname,
+          search: queryString.stringify({
+            ...data,
+            pageNo: pagination.current,
+            pageSize: pagination.pageSize,
+          }),
+        }));
+      },
+      onEditItem: (item) => {
+        dispatch(
+          routerRedux.push({
+            pathname: `/tag/group/edit/${item.id}`,
+            state: filters,
+          })
+        );
+      },
+      onDeleteItem: (id) => {
+        dispatch({
+          type: 'group/delete',
+          payload: id,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      },
+      onRecoverItem: (payload) => {
+        dispatch({
+          type: 'group/recover',
+          payload,
+          callback: () => {
+            dispatch(
+              routerRedux.push({
+                pathname,
+                search: queryString.stringify(filters),
+              })
+            );
+          }
+        });
+      }
+    };
+
+    return (
+      <PageHeaderLayout>
+        <Card>
+          <Search { ...searchProps } />
+          <TableList { ...listProps } />
+        </Card>
+      </PageHeaderLayout>
+    );
+  }
+}

+ 51 - 0
src/routes/TagGroup/search.js

@@ -0,0 +1,51 @@
+import react, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import { Button, Form, Row, Col, Icon } from 'antd';
+import DataSearch from '../../components/DataSearch';
+
+@Form.create()
+export default class Search extends PureComponent {
+  static propTypes = {
+    form: PropTypes.object.isRequired,
+    onSearch: PropTypes.func,
+    onAdd: PropTypes.func,
+    field: PropTypes.string,
+    keyword: PropTypes.string,
+  };
+
+  render() {
+    const { field, keyword, onSearch, onAdd, filterSelectProps } = this.props;
+
+    const searchGroupProps = {
+      field,
+      keyword,
+      filterSelectProps,
+      size: 'default',
+      select: true,
+      selectOptions: [{
+        value: 'name', name: '标签组名称', mode: 'input',
+      },{
+        value: 'code', name: '标签组编号', mode: 'input',
+      },{
+        value: 'merchantId', name: '渠道名称', mode: 'select',
+      }],
+      selectProps: {
+        defaultValue: field || 'name',
+      },
+      onSearch: (value) => {
+        onSearch(value);
+      },
+    };
+
+    return (
+      <Row gutter={24}>
+        <Col lg={10} md={12} sm={16} xs={24} style={{ marginBottom: 16 }}>
+          <DataSearch { ...searchGroupProps } />
+        </Col>
+        <Col lg={{ offset: 7, span: 7 }} md={12} sm={8} xs={24} style={{ marginBottom: 16, textAlign: 'right' }}>
+          <Button type="ghost" onClick={onAdd}><Icon type="plus-circle" />添加</Button>
+        </Col>
+      </Row>
+    );
+  }
+}

+ 105 - 0
src/routes/TagGroup/table.js

@@ -0,0 +1,105 @@
+import React, { PureComponent } from 'react';
+import PropTypes from 'prop-types';
+import moment from 'moment';
+import classnames from 'classnames';
+import queryString from 'query-string';
+import { Modal, Table, Menu, Icon, Badge } from 'antd';
+import AnimTableBody from '../../components/Animation/AnimTableBody';
+import styles from './table.less';
+import { statuses, Codes } from '../../utils/config';
+
+const confirm = Modal.confirm;
+
+export default class TableList extends PureComponent {
+  static propTypes = {
+    location: PropTypes.object,
+    onChange: PropTypes.func.isRequired,
+    onDeleteItem: PropTypes.func.isRequired,
+    onEditItem: PropTypes.func.isRequired,
+  };
+
+  handleOperateItem = (record) => {
+    const { onDeleteItem, onRecoverItem } = this.props;
+    confirm({
+      title: `您确定要${record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}该条记录?`,
+      onOk () {
+        if (record.status === Codes.CODE_NORMAL) {
+          onDeleteItem({id: record.id});
+        } else if (record.status === Codes.CODE_DELETE) {
+          onRecoverItem({ id: record.id, status: Codes.CODE_NORMAL });
+        }
+      },
+    })
+  }
+
+  render() {
+    const { curStatus, onDeleteItem, onRecoverItem, onEditItem, location, pagination, ...tableProps } = this.props;
+
+    const columns = [{
+      title: '标签组编号',
+      dataIndex: 'code',
+      key: 'code',
+    },{
+      title: '标签组名称',
+      dataIndex: 'name',
+      key: 'name',
+    },{
+      title: '渠道平台',
+      dataIndex: 'merchantId',
+      key: 'merchantId',
+    },{
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (text, record) => {
+        const statusMap = {[Codes.CODE_NORMAL]: 'success', [Codes.CODE_DELETE]: 'error'};
+        return (<Badge status={statusMap[record.status]} text={statuses[record.status]} />);
+      },
+      filters: Object.keys(statuses).map(key => ({ text: statuses[key], value: key })),
+      filterMultiple: false,
+      filteredValue: [curStatus],
+    },{
+      title: '添加时间',
+      dataIndex: 'gmtCreated',
+      key: 'gmtCreated',
+      render: (text, record) => (
+        <div>{moment(text).format('YYYY-MM-DD')}</div>
+      )
+    },{
+      title: '操作',
+      dataIndex: 'operation',
+      key: 'operation',
+      render: (text, record) => (
+        <div>
+          <a onClick={() => onEditItem(record)}>编辑</a>
+          <span className={styles.splitLine} />
+            <a onClick={() => this.handleOperateItem(record)}>{record.status === Codes.CODE_NORMAL ? '删除' : '恢复'}</a>
+        </div>
+      )
+    }];
+
+    // 数据table列表表头的筛选按钮点击重置后status与domain值为空,此时删除该参数
+    columns.map(item => {
+      item.dataIndex === 'status' && !curStatus ? delete item.filteredValue : null;
+    });
+
+    tableProps.pagination = !!pagination && { ...pagination, showSizeChanger: true, showQuickJumper: true, showTotal: total => `共 ${total} 条`};
+    const getBodyWrapperProps = {
+      page: location.query.page,
+      current: tableProps.pagination.current,
+    };
+    const getBodyWrapper = (body) => (<AnimTableBody {...getBodyWrapperProps} body={body} />);
+
+    return (
+      <Table
+        simple
+        bordered
+        { ...tableProps }
+        columns={columns}
+        className={classnames({ [styles.table]: true, [styles.motion]: true })}
+        rowKey={record => record.id}
+        getBodyWrapper={getBodyWrapper}
+      />
+    );
+  }
+}

+ 82 - 0
src/routes/TagGroup/table.less

@@ -0,0 +1,82 @@
+@import "~antd/lib/style/themes/default.less";
+@import "../../utils/utils.less";
+
+.table {
+  :global {
+    .ant-table-tbody > tr > td,
+    .ant-table-thead > tr > th {
+      height: 62px;
+    }
+  }
+
+  &.motion {
+    :global {
+      .ant-table-tbody > tr > td,
+      .ant-table-thead > tr > th {
+        &:nth-child(1) {
+          width: 17%;
+        }
+
+        &:nth-child(2) {
+          width: 17%;
+        }
+
+        &:nth-child(3) {
+          width: 17%;
+        }
+
+        &:nth-child(4) {
+          width: 17%;
+        }
+
+        &:nth-child(5) {
+          width: 17%;
+        }
+
+        &:nth-child(6) {
+          width: 15%;
+        }
+      }
+
+      .ant-table-thead {
+        & > tr {
+          transition: none;
+          display: block;
+
+          & > th {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+        }
+      }
+
+      .ant-table-tbody {
+        & > tr {
+          transition: none;
+          display: block;
+          border-bottom: 1px solid #f5f5f5;
+
+          & > td {
+            border-bottom: none;
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+          }
+
+          &.ant-table-expanded-row-level-1 > td {
+            height: auto;
+          }
+        }
+      }
+    }
+  }
+}
+
+.splitLine {
+  background: @border-color-split;
+  display: inline-block;
+  margin: 0 8px;
+  width: 1px;
+  height: 12px;
+}

+ 32 - 0
src/services/group.js

@@ -0,0 +1,32 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+import { groups, group } from '../utils/api';
+
+export async function query(params) {
+  return request(`${groups}?${stringify(params)}`);
+}
+
+export async function queryOne({ id }) {
+  return request(`${group.replace('/:id', `/${id}`)}`);
+}
+
+export async function create(params) {
+  const options = {
+    method: 'POST',
+    body: JSON.stringify(params),
+  };
+  return request(`${group.replace('/:id', '')}`, options);
+}
+
+export async function update(params) {
+  const options = {
+    method: 'PUT',
+    body: JSON.stringify(params),
+  };
+  return request(`${group.replace('/:id', '')}`, options);
+}
+
+export async function remove({ id }) {
+  const options = { method: 'DELETE' }
+  return request(`${group.replace('/:id', `/${id}`)}`, options);
+}

+ 13 - 1
src/services/resource.js

@@ -1,9 +1,21 @@
 import { stringify } from 'qs';
 import request from '../utils/request';
 import { resources, resource, signature } from '../utils/api';
+import { saveLocalSignature, getLocalSignature } from '../utils/helper';
 
 export async function getSignature (params) {
-  return request(`${signature}?${stringify(params)}`);
+  const localSignature = getLocalSignature();
+  const expireTime = Math.floor((new Date()).getTime() / 1000 + 5).toString(); // 5s缓冲时间
+  // oss签名不存在或者已经过了有效期则请求新的签名
+  let newSignature;
+  if (!localSignature || localSignature.expire <= expireTime) {
+    return request(`${signature}?${stringify(params)}`);
+    // request(`${signature}?${stringify(params)}`).then(res => { newSignature = res.data });
+    // saveLocalSignature(newSignature);
+    // return { data: newSignature };
+  } else {
+    return { data: localSignature };
+  }
 }
 
 export async function query (params) {

+ 32 - 0
src/services/tag.js

@@ -0,0 +1,32 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+import { tags, tag } from '../utils/api';
+
+export async function query(params) {
+  return request(`${tags}?${stringify(params)}`);
+}
+
+export async function queryOne({ id }) {
+  return request(`${tag.replace('/:id', `/${id}`)}`);
+}
+
+export async function create(params) {
+  const options = {
+    method: 'POST',
+    body: JSON.stringify(params),
+  };
+  return request(`${tag.replace('/:id', '')}`, options);
+}
+
+export async function update(params) {
+  const options = {
+    method: 'PUT',
+    body: JSON.stringify(params),
+  };
+  return request(`${tag.replace('/:id', '')}`, options);
+}
+
+export async function remove({ id }) {
+  const options = { method: 'DELETE' }
+  return request(`${tag.replace('/:id', `/${id}`)}`, options);
+}

+ 6 - 2
src/utils/api.js

@@ -3,7 +3,7 @@ import config from './config';
 module.exports = {
   resources: `${config.apiHost}/resource/list`,
   resource: `${config.apiHost}/resource/:id`,
-  signature: `${config.apiHost}/signature`,
+  signature: `${config.apiHost}/oss/signature`,
   cmsUsers: `${config.apiHost}/cms/user/list`,
   currentCmsUser: `${config.apiHost}/cms/user`,
   userLogin: `${config.apiHost}/login`,
@@ -12,6 +12,10 @@ module.exports = {
   campus: `${config.apiHost}/campus/:id`,
   terminals: `${config.apiHost}/user/list`,
   terminal: `${config.apiHost}/user/:id`,
-  merchants: `${config.apiHost}/merchants/list`,
+  merchants: `${config.apiHost}/merchant/list`,
   merchant: `${config.apiHost}/merchant/:id`,
+  groups: `${config.apiHost}/group/list`,
+  group: `${config.apiHost}/group/:id`,
+  tags: `${config.apiHost}/tag/list`,
+  tag: `${config.apiHost}/tag/:id`,
 };

+ 14 - 1
src/utils/config.js

@@ -7,9 +7,12 @@ Codes.CODE_LIVE  = 2;
 Codes.CODE_IMAGE = 3;
 Codes.CODE_NORMAL = 'NORMAL';
 Codes.CODE_DELETE = 'DEL';
+Codes.CODE_COURSE = 'COURSE';
+Codes.CODE_SUPPORT = 'SUPPORT';
+Codes.CODE_SALE = 'SALE';
 
 module.exports = {
-  // apiHost: 'http://lj.dev.cms.api.com:9100',
+  // apiHost: 'http://lj.dev.cms.api.com:8500',
   apiHost: '/api',
   // 每页返回数据量
   pageSize: 10,
@@ -20,6 +23,11 @@ module.exports = {
     [Codes.CODE_NORMAL]: '使用中',
     [Codes.CODE_DELETE]: '已删除',
   },
+  // 商品出售状态
+  itemStatuses: {
+    [Codes.CODE_SALE]: '出售中',
+    [Codes.CODE_DELETE]: '已下架',
+  },
   domains: {
     2010: '供应商',
     3010: '渠道商',
@@ -31,4 +39,9 @@ module.exports = {
     [Codes.CODE_LIVE] : '直播',
     [Codes.CODE_IMAGE]: '图片',
   },
+  // 标签类型
+  tagType: {
+    [Codes.CODE_COURSE] : '课程',
+    [Codes.CODE_SUPPORT]: '周边',
+  }
 };

+ 0 - 11
src/utils/map.js

@@ -1,11 +0,0 @@
-const actions = {
-  userLogin: 'login/login',
-  userLogout: 'login/logout',
-};
-
-const routers = {
-  home: '/',
-  login: '/user/login',
-};
-
-module.exports = { actions, routers };

+ 10 - 16
src/utils/request.js

@@ -1,6 +1,6 @@
 import fetch from 'dva/fetch';
 import { stringify } from 'qs';
-import { notification } from 'antd';
+import { message, notification } from 'antd';
 
 // HTTP响应状态码
 const httpCodeMessage = {
@@ -24,6 +24,7 @@ const httpCodeMessage = {
 // 自定义响应状态码
 const customCodeMessage = {
   10004: 'Token认证失败',
+  800  : '数据不存在',
 };
 
 /**
@@ -35,7 +36,7 @@ function httpErrorHandler(response) {
   }
   const errortext = httpCodeMessage[response.status] || response.statusText;
   notification.error({
-    message: `请求错误 ${response.status}: ${response.url}`,
+    message: `HTTP错误 ${response.status}: ${response.url}`,
     description: errortext,
   });
   const error = new Error(errortext);
@@ -51,30 +52,26 @@ function httpErrorHandler(response) {
 function apiErrorHandler(data) {
   if (!data.success) {
     const errortext = customCodeMessage[data.code] || data.message;
-    notification.error({
-      message: `操作错误 ${data.code}`,
-      description: errortext,
-    });
     // Token认证失败,错误继续外抛,让全局app对象捕获,跳转到登录界面
     if (data.code === 10004) {
       const error = new Error(errortext);
       error.response = data;
       throw error;
+    } else {
+      message.error(`请求错误 错误代码:${data.code} 错误信息:${errortext}`);
     }
   }
   return data;
 }
 
 /**
- * @desc 处理未预知到的错误
+ * @desc 处理未预知到的网络错误
  * @return {[object]} {error}
  */
 function unpredictableErrorHandler(error) {
   if (!error.response) {
-    notification.error({
-      message: '网络请求错误',
-      description: '出现未预知的网络问题,请检查日志或联系管理员!',
-    });
+    console.error(error);
+    message.error('出现未知的网络问题,请检查日志或联系管理员');
   }
   if (error.response && error.response.code === 10004) {
     throw error;
@@ -89,12 +86,9 @@ function unpredictableErrorHandler(error) {
 function timeoutErrorHandler(error) {
   // 这里只会捕获两种错误,一是超时错误,一是认证失效错误,认证失效错误继续外抛
   if (!error.response) {
-    notification.error({
-      message: `请求超时`,
-      description: '请求失败,请确认网络状态是否可用。',
-    });
+    message.error('请求超时,请检查网络状态是否可用!');
   }
-  if (error && error.response.code === 10004) {
+  else if (error && error.response.code === 10004) {
     throw error;
   }
   return { error };