Browse Source

添加终端用户的标签,推荐配置

sophieChenyx 5 years ago
parent
commit
28d066d7e8

+ 1 - 2
.webpackrc

@@ -1,7 +1,6 @@
 {
   "entry": "src/index.js",
   "extraBabelPlugins": [
-    "transform-decorators-legacy",
     ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }]
   ],
   "define": {
@@ -21,5 +20,5 @@
   },
   "publicPath": "/",
   "disableDynamicImport": true,
-  "hash": true
+  "hash": false
 }

+ 1 - 0
README.md

@@ -51,6 +51,7 @@
   - 栏目类型
   - 侧边栏目
   - 推荐内容
+  - 用户配置
 - 交易管理
   - 购物车
   - 订单列表

+ 3 - 3
package.json

@@ -22,7 +22,7 @@
   "dependencies": {
     "@antv/data-set": "^0.8.0",
     "@babel/polyfill": "^7.0.0-beta.36",
-    "antd": "^3.1.0",
+    "antd": "^3.3.0",
     "babel-runtime": "^6.9.2",
     "bizcharts": "^3.1.3-beta.1",
     "bizcharts-plugin-slider": "^2.0.1",
@@ -54,7 +54,7 @@
   "devDependencies": {
     "babel-eslint": "^8.1.2",
     "babel-plugin-dva-hmr": "^0.4.1",
-    "babel-plugin-import": "^1.6.3",
+    "babel-plugin-import": "^1.6.7",
     "babel-plugin-transform-decorators-legacy": "^1.3.4",
     "cross-env": "^5.1.1",
     "cross-port-killer": "^1.0.1",
@@ -74,7 +74,7 @@
     "pro-download": "^1.0.1",
     "redbox-react": "^1.5.0",
     "regenerator-runtime": "^0.11.1",
-    "roadhog": "^2.1.0",
+    "roadhog": "^2.3.0",
     "roadhog-api-doc": "^0.3.4",
     "stylelint": "^8.4.0",
     "stylelint-config-standard": "^18.0.0"

+ 4 - 0
src/common/menu.js

@@ -1,6 +1,7 @@
 import React from 'react';
 import { isUrl } from '../utils/utils';
 import AXIcon from '../components/AXIcon';
+import UserConfig from '../routes/Frontend/ConfigUser'
 
 const menuData = () => {
   return [{
@@ -89,6 +90,9 @@ const menuData = () => {
     }, {
       name: '推荐内容',
       path: 'recommend',
+    }, {
+      name: '用户配置',
+      path: 'ConfigUser',
     }],
     authority: ['admin', 'platform'],
   }, {

+ 15 - 1
src/common/router.js

@@ -342,7 +342,21 @@ export const getRouterData = (app) => {
     '/frontend/recommend/poster-edit/:id': {
       component: dynamicWrapper(app, ['merchant', 'shelves'], () => import('../routes/Frontend/Recommend/RecommendPoster')),
     },
-    // 交易管理相关路由注册
+    '/frontend/ConfigUser': {
+      component: dynamicWrapper(app, ['configUser'], () => import('../routes/Frontend/ConfigUser')),
+    },
+    '/frontend/ConfigUser/list': {
+      component: dynamicWrapper(app, ['configUser', 'terminal'], () => import('../routes/Frontend/ConfigUser/ConfigUserLists')),
+    },
+    '/frontend/ConfigUser/tag/:id': {
+      component: dynamicWrapper(app, ['configUser', 'tag', 'tagType'], () => import('../routes/Frontend/ConfigUser/ConfigTag')),
+    },
+    '/frontend/ConfigUser/course-edit/:id': {
+      component: dynamicWrapper(app, ['configUser', 'tag', 'tagType'], () => import('../routes/Frontend/ConfigUser/ConfigRecommendCourse')),
+    },
+    '/frontend/ConfigUser/poster-edit/:id': {
+      component: dynamicWrapper(app, ['configUser', 'tag', 'tagType'], () => import('../routes/Frontend/ConfigUser/ConfigRecommendPoster')),
+    },
     '/trade/shopcart': {
       component: dynamicWrapper(app, [], () => import('../routes/Trade/ShopCart')),
     },

+ 19 - 0
src/components/AXTableSelector/columnsMap.js

@@ -266,6 +266,25 @@ const clMap = {
       dataIndex: 'name',
     }],
   },
+  allTag: {
+    columns: [{
+      title: '标签名称',
+      key: 1,
+      dataIndex: 'name',
+    }, {
+      title: '标签类型',
+      key: 2,
+      dataIndex: 'typeCode',
+    }, {
+      title: '所属标签组',
+      key: 3,
+      dataIndex: 'groupName',
+    }, {
+      title: '所属渠道',
+      key: 4,
+      dataIndex: 'merchantName',
+    }],
+  },
   Tag: {
     columns: [{
       title: '标签名称',

+ 197 - 0
src/models/configUser.js

@@ -0,0 +1,197 @@
+import { message } from 'antd';
+import { routerRedux } from 'dva/router';
+import { Hotax } from '../utils/config';
+import { query as queryUsers } from '../services/user';
+import { getLocalUser } from '../utils/authority';
+import {
+  queryUserTags,
+  createConfigUserTagItem,
+  queryTagDetail,
+  updateConfigUserTagItem,
+  deleteConfigUserTagItem,
+  queryConfigCourse,
+  updateConfigCourse,
+  copyTag,
+} from '../services/configUser';
+
+
+export default {
+  namespace: 'configUser',
+  state: {
+    list: [],
+    currentUser: {},
+    TagList: [],
+    userTagLists: [],
+    TagDetails: [],
+    courseList: [],
+  },
+  effects: {
+    *fetch(_, { call, put }) {
+      const response = yield call(queryUsers);
+      yield put({
+        type: 'save',
+        payload: response,
+      });
+    },
+    *fetchCurrent(_, { put }) {
+      // TODO 这里从本地读取用户信息,后期改成请求
+      const userInfo = getLocalUser();
+      yield put({
+        type: 'saveCurrentUser',
+        payload: userInfo,
+      });
+    },
+    // 用户终端下用户推荐课程相关;
+    *fetchConfigCourse({ payload }, { call, put }) {
+      const response = yield call(queryConfigCourse, payload);
+      if (response.success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            courseList: response.data || [],
+          },
+        });
+      }
+    },
+    *updateConfigCourse({ payload, states }, { call, put }) {
+      const response = yield call(updateConfigCourse, payload);
+      if (response.success) {
+        message.success('推荐课程列表更新成功');
+        yield put(routerRedux.push({
+          pathname: '/frontend/ConfigUser',
+          state: states,
+        }));
+      }
+    },
+    // 用户标签的相关
+    *fetchTagDetail({ payload }, { call, put }) {
+      const response = yield call(queryTagDetail, payload);
+      if (response.success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            TagDetails: response.data || [],
+          },
+        });
+      }
+    },
+    *fetchUserTags({ payload }, { call, put }) {
+      const response = yield call(queryUserTags, payload);
+      if (response.success) {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            userTagLists: response.data || [],
+          },
+        });
+      } else {
+        yield put({
+          type: 'querySuccess',
+          payload: {
+            userTagLists: [],
+          },
+        });
+      }
+    },
+    *createConfigUserTagItem({ payload, TagId }, { call, put, select }) {
+      const response = yield call(createConfigUserTagItem, payload);
+      if (response.success) {
+        message.success('创建终端标签成功');
+        const originalData = yield select(state => state.configUser.userTagLists);
+        const newData = originalData.map((data) => {
+          const { id, isNew, isEdit, ...rest } = data;
+          if (id === TagId) {
+            return { ...rest, id: response.data.id };
+          }
+          return { ...data };
+        });
+        yield put({
+          type: 'fixUserTagLists',
+          payload: newData,
+        });
+      }
+    },
+    *updateConfigUserTagItem({ payload, TagId }, { call, put, select }) {
+      const response = yield call(updateConfigUserTagItem, payload);
+      if (response.success) {
+        message.success('修改终端标签成功');
+        const originalData = yield select(state => state.configUser.userTagLists);
+        const newData = originalData.map((data) => {
+          const { isEdit, ...rest } = data;
+          if (data.id === TagId) {
+            return { ...rest };
+          }
+          return { ...data };
+        });
+        yield put({
+          type: 'fixUserTagLists',
+          payload: newData,
+        });
+      }
+    },
+    *deleteConfigUserTagItem({ payload }, { call, put, select }) {
+      const response = yield call(deleteConfigUserTagItem, payload);
+      if (response.success) {
+        message.success('删除终端标签成功');
+        const originalData = yield select(state => state.configUser.userTagLists);
+        const newData = originalData.map((data) => {
+          if (data.id === payload) {
+            return { ...data, status: Hotax.STATUS_DELETE };
+          }
+          return { ...data };
+        });
+        yield put({
+          type: 'fixUserTagLists',
+          payload: newData,
+        });
+      }
+    },
+    *copyTag({ payload }, { call }) {
+      const response = yield call(copyTag, payload);
+      if (response.success) {
+        message.success('成功复制标签关联产品到当前标签');
+      }
+    },
+  },
+  reducers: {
+    querySuccess(state, action) {
+      return {
+        ...state,
+        ...action.payload,
+      };
+    },
+    fixCourseList(state, action) {
+      return {
+        ...state,
+        courseList: action.payload,
+      };
+    },
+    save(state, action) {
+      return {
+        ...state,
+        list: action.payload,
+      };
+    },
+    fixUserTagLists(state, action) {
+      return {
+        ...state,
+        userTagLists: action.payload,
+      };
+    },
+    saveCurrentUser(state, action) {
+      return {
+        ...state,
+        currentUser: action.payload,
+      };
+    },
+    changeNotifyCount(state, action) {
+      return {
+        ...state,
+        currentUser: {
+          ...state.currentUser,
+          notifyCount: action.payload,
+        },
+      };
+    },
+  },
+};

+ 156 - 0
src/routes/Frontend/ConfigUser/ConfigRecommendCourse.js

@@ -0,0 +1,156 @@
+import React, { Component } from 'react';
+import pathToRegexp from 'path-to-regexp';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Card, Modal, Button } from 'antd';
+import AXDragSortTable from '../../../components/AXDragSortTable';
+import Selector from '../../../components/AXTableSelector/Selector';
+import FooterToolbar from '../../../components/FooterToolbar/index';
+
+@connect(({ loading, merchant, shelves, configUser }) => ({
+  shelves,
+  merchant,
+  configUser,
+  sLoading: loading.models.product,
+  submitting: loading.models.merchant,
+}))
+
+export default class ConfigCourse extends Component {
+  state = {
+    productSelectorDestroy: true,
+  };
+  componentDidMount() {
+    this.props.dispatch({
+      type: 'configUser/fetchConfigCourse',
+      payload: { courseId: this.getMerchantId() },
+    });
+  }
+  getMerchantId = () => {
+    const match = pathToRegexp('/frontend/ConfigUser/course-edit/:id')
+      .exec(this.props.location.pathname);
+    return match[1];
+  }
+  handleSelectorModalShow = () => {
+    const { itemData } = this.props.location.state;
+    const { merchantId: id } = itemData;
+    this.setState({ productSelectorDestroy: false });
+    this.props.dispatch({
+      type: 'shelves/fetchCourseItemList',
+      payload: { merchantId: id },
+    });
+  }
+  handleSelectorChange = (params) => {
+    const { itemData } = this.props.location.state;
+    const { merchantId: id } = itemData;
+    this.props.dispatch({
+      type: 'shelves/fetchCourseItemList',
+      payload: {
+        merchantId: id,
+        ...params,
+      },
+    });
+  }
+  handleSelectorFinish = (rows) => {
+    this.setState({ productSelectorDestroy: true });
+    this.props.dispatch({
+      type: 'configUser/fixCourseList',
+      payload: rows,
+    });
+  }
+  handleSelectorCancel = () => {
+    this.setState({ productSelectorDestroy: true });
+  }
+  handleDragSortTableChange = (rows) => {
+    this.props.dispatch({
+      type: 'configUser/fixCourseList',
+      payload: rows,
+    });
+  }
+  handlePageBack = () => {
+    this.props.dispatch(routerRedux.push({
+      pathname: '/frontend/ConfigUser',
+      state: this.props.location.state,
+    }));
+  }
+  handlePageSubmit = () => {
+    const { configUser } = this.props;
+    const { courseList } = configUser;
+    const idList = courseList.map(item => item.pid);
+    this.props.dispatch({
+      type: 'configUser/updateConfigCourse',
+      payload: {
+        idList,
+        courseId: this.getMerchantId(),
+      },
+    });
+  }
+
+  render() {
+    const { productSelectorDestroy } = this.state;
+    const { shelves, configUser } = this.props;
+    const { courseList } = configUser;
+    // recommend事件
+    const productColumns = [{
+      title: '课程编号',
+      key: 1,
+      dataIndex: 'code',
+      width: '40%',
+    }, {
+      title: '课程名称',
+      key: 2,
+      dataIndex: 'name',
+    }];
+    const getProductModal = () => {
+      return (
+        <Modal
+          visible
+          width={1100}
+          footer={null}
+          title="课程资源"
+          maskClosable={false}
+          onCancel={this.handleSelectorCancel}
+        >
+          <Selector
+            multiple
+            selectorName="Course"
+            selectedRows={courseList}
+            list={shelves.list}
+            pageNo={shelves.pageNo}
+            pageSize={shelves.pageSize}
+            totalSize={shelves.totalSize}
+            onCancel={this.handleSelectorCancel}
+            onChange={this.handleSelectorChange}
+            onFinish={this.handleSelectorFinish}
+          />
+        </Modal>
+      );
+    };
+    return (
+      <div>
+        <Card
+          title={<a onClick={this.handleSelectorModalShow}>选择课程</a>}
+          style={{ marginBottom: 70 }}
+        >
+          <AXDragSortTable
+            columns={productColumns}
+            data={courseList}
+            onChange={this.handleDragSortTableChange}
+          />
+          {!productSelectorDestroy && getProductModal()}
+        </Card>
+        <FooterToolbar style={{ width: '100%' }}>
+          <Button
+            onClick={this.handlePageBack}
+            style={{ marginRight: 10 }}
+          >取消
+          </Button>
+          <Button
+            type="primary"
+            onClick={this.handlePageSubmit}
+          >提交
+          </Button>
+        </FooterToolbar>
+      </div>
+    );
+  }
+}

+ 11 - 0
src/routes/Frontend/ConfigUser/ConfigRecommendPoster.js

@@ -0,0 +1,11 @@
+import React, { Component } from 'react';
+
+export default class ConfigPoster extends Component {
+  render() {
+    return (
+      <div>
+        <h2>用户终端海报接口暂未开放</h2>
+      </div>
+    );
+  }
+}

+ 559 - 0
src/routes/Frontend/ConfigUser/ConfigTag.js

@@ -0,0 +1,559 @@
+import React, { Component } from 'react';
+import pathToRegexp from 'path-to-regexp';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Card, Table, Modal, Popconfirm, Switch, Button, Input, Icon } from 'antd';
+import Selector from '../../../components/AXTableSelector/Selector';
+import FooterToolbar from '../../../components/FooterToolbar';
+import { boolToStatus, renderStatus, statusToBool } from '../../../utils/utils';
+import styles from './ConfigTag.less';
+
+@connect(({ loading, merchant, shelves, resource, configUser, tagType, tag }) => ({
+  shelves,
+  merchant,
+  resource,
+  configUser,
+  tagType,
+  tag,
+  loading: loading.models.tagType,
+  rLoading: loading.models.resource,
+  sLoading: loading.models.shelves,
+  mLoading: loading.models.merchant,
+}))
+export default class ConfigTag extends Component {
+  state = {
+    productSelectorDestroy: true,
+    resourceSelectorDestroy: true,
+    allTagSelectorDestroy: true,
+    productType: 'Course',
+    currentEditTagId: '',
+  };
+  componentDidMount() {
+    this.props.dispatch({
+      type: 'configUser/fetchUserTags',
+      payload: { configUserId: this.getConfigUserId() },
+    });
+    this.props.dispatch({
+      type: 'tagType/fetchTagTypeList',
+      payload: {},
+    });
+    this.props.dispatch({
+      type: 'tagType/fetchMerchantPoster',
+      payload: { merchantId: this.getConfigUserId() },
+    });
+  }
+  /**
+   * 1.从URL中提取configUserId
+   * @returns {String}
+   */
+  getConfigUserId = () => {
+    const match = pathToRegexp('/frontend/ConfigUser/tag/:id')
+      .exec(this.props.location.pathname);
+    return match[1];
+  };
+  /**
+   * 2.终端用户新建标签
+   */
+  handleTagItemCreate = () => {
+    const newData = [...this.props.configUser.userTagLists];
+    newData.push({
+      id: `new-poster-${newData.length + 1}`,
+      isNew: true,
+      isEdit: true,
+    });
+    this.props.dispatch({
+      type: 'configUser/fixUserTagLists',
+      payload: newData,
+    });
+  };
+  /**
+   * 3.删除一跳标签信息
+   * @param TagId
+   * @param isNew
+   */
+  handleTagItemDelete = (TagId, isNew) => {
+    if (isNew) {
+      const originalData = [...this.props.configUser.userTagLists];
+      const newData = originalData.filter(data => data.id !== TagId);
+      this.props.dispatch({
+        type: 'configUser/fixUserTagLists',
+        payload: newData,
+      });
+      return;
+    }
+    this.props.dispatch({
+      type: 'configUser/deleteConfigUserTagItem',
+      payload: TagId,
+    });
+  }
+  /**
+   * 4.编辑一条标签
+   * @param TagId
+   */
+  handleTagItemEdit = (TagId) => {
+    const newData = [...this.props.configUser.userTagLists];
+    for (const index in newData) {
+      if (newData[index].id === TagId) {
+        newData[index].isEdit = true;
+      }
+    }
+    this.props.dispatch({
+      type: 'configUser/fixUserTagLists',
+      payload: newData,
+    });
+  };
+  /**
+   * 5.根据flag,控制模态框的展现
+   * @param flag
+   * @param TagId
+   */
+  handleSelectorModalShow = (flag, TagId) => {
+    this.setState({
+      [`${flag}SelectorDestroy`]: false,
+      currentEditTagId: TagId,
+    });
+    if (flag === 'product') {
+      this.props.dispatch({
+        type: 'configUser/fetchTagDetail',
+        payload: TagId,
+      });
+      return;
+    }
+    if (flag === 'resource') {
+      this.props.dispatch({
+        type: 'tagType/fetchTagTypeList',
+        payload: {},
+      });
+    }
+    if (flag === 'allTag') {
+      this.props.dispatch({
+        type: 'tag/fetchTagList',
+      });
+    }
+  };
+  /**
+   * 6.控制模态框的销毁
+   */
+  handleSelectorCancel = (flag) => {
+    this.setState({ [`${flag}SelectorDestroy`]: true });
+  };
+  /**
+   * 7.模态框内的查询操作 完成
+   * @param {String} flag
+   * @param {Object} params
+   */
+  handleSelectorChange = (flag, params) => {
+    if (flag === 'product') {
+      const { productType } = this.state;
+      this.props.dispatch({
+        type: `shelves/fetch${productType}ItemList`,
+        payload: { ...params, merchantId: this.getConfigUserId() },
+      });
+      return;
+    }
+    if (flag === 'allTag') {
+      this.props.dispatch({
+        type: 'tag/fetchTagList',
+        payload: params,
+      });
+    }
+    if (flag === 'resource') {
+      this.props.dispatch({
+        type: 'tagType/fetchTagTypeList',
+        payload: params,
+      });
+    }
+  };
+  /**
+   * 8.响应选择完成操作 数据的回显 模态框的处理
+   * @param {String} flag
+   * @param {Array} rows
+   */
+  handleSelectorFinish = (flag, rows) => {
+    this.setState({ [`${flag}SelectorDestroy`]: true });
+    const { currentEditTagId } = this.state;
+    const originalData = [...this.props.configUser.userTagLists];
+    if (flag !== 'allTag') {
+      const newData = originalData.map((data) => {
+        if (flag === 'resource' && data.id === currentEditTagId) {
+          return { ...data, typeCode: rows[0].code };
+        }
+        return { ...data };
+      });
+      this.props.dispatch({
+        type: 'configUser/fixUserTagLists',
+        payload: newData,
+      });
+    } else {
+      this.props.dispatch({
+        type: 'configUser/copyTag',
+        payload: {
+          userTagId: currentEditTagId,
+          tagId: rows[0].id,
+        },
+      });
+    }
+  };
+  /**
+   * 9.修改排序值
+   * @param e
+   * @param TagId
+   */
+  handleSortInputChange = (e, TagId) => {
+    const originalData = [...this.props.configUser.userTagLists];
+    const newData = originalData.map((data) => {
+      if (data.id === TagId) {
+        return { ...data, sort: parseInt(e.target.value, 10) || 0 };
+      }
+      return { ...data };
+    });
+    this.props.dispatch({
+      type: 'configUser/fixUserTagLists',
+      payload: newData,
+    });
+  };
+  /**
+   * 10.修改标签名称
+   * @param e
+   * @param TagId
+   */
+  handleNameInputChange = (e, TagId) => {
+    const originalData = [...this.props.configUser.userTagLists];
+    const newData = originalData.map((data) => {
+      if (data.id === TagId) {
+        return { ...data, name: e.target.value };
+      }
+      return { ...data };
+    });
+    this.props.dispatch({
+      type: 'configUser/fixUserTagLists',
+      payload: newData,
+    });
+  };
+  /**
+   * 11.修改状态
+   * @param checked
+   * @param TagId
+   */
+  handleStatusSwitchChange = (checked, TagId) => {
+    const originalData = [...this.props.configUser.userTagLists];
+    const newData = originalData.map((data) => {
+      if (data.id === TagId) {
+        return { ...data, status: boolToStatus(checked) };
+      }
+      return { ...data };
+    });
+    this.props.dispatch({
+      type: 'configUser/fixUserTagLists',
+      payload: newData,
+    });
+  };
+  /**
+   * 12.提交标签的内容
+   * @param TagId
+   */
+  handleSaveOperation = (TagId) => {
+    const originalData = [...this.props.configUser.userTagLists];
+    const targetData = originalData.filter(data => data.id === TagId)[0];
+    const { id, sort, name, typeCode, status, isNew } = targetData;
+    if (isNew) {
+      this.props.dispatch({
+        type: 'configUser/createConfigUserTagItem',
+        payload: {
+          name, typeCode, sort, status: boolToStatus(status), uid: this.getConfigUserId(),
+        },
+        TagId: id,
+      });
+      return;
+    }
+    this.props.dispatch({
+      type: 'configUser/updateConfigUserTagItem',
+      payload: {
+        id, name, typeCode, sort, status: boolToStatus(status), uid: this.getConfigUserId(),
+      },
+      TagId: id,
+    });
+  };
+  /**
+   * 13.返回上一页 finished
+   */
+  handlePageBack = () => {
+    this.props.dispatch(routerRedux.push({
+      pathname: '/frontend/ConfigUser/list',
+      state: this.props.location.state,
+    }));
+  };
+  render() {
+    const { productSelectorDestroy, resourceSelectorDestroy, allTagSelectorDestroy } = this.state;
+    const { configUser, tagType, tag, loading } = this.props;
+    const { userTagLists } = configUser;
+    /* 海报列表格式设定 */
+    const userTagColumns = [{
+      title: '位置',
+      key: 1,
+      dataIndex: 'sort',
+      width: '10%',
+      render: (text, record) => {
+        const { id, isEdit } = record;
+        if (isEdit) {
+          return (
+            <Input
+              value={text}
+              onChange={e => this.handleSortInputChange(e, id)}
+              placeholder="必填项"
+              style={{ width: 100 }}
+            />
+          );
+        }
+        return text;
+      },
+      align: 'center',
+    }, {
+      title: '标签名称',
+      key: 2,
+      dataIndex: 'name',
+      width: '15%',
+      render: (text, record) => {
+        const { id, isEdit } = record;
+        if (isEdit) {
+          return (
+            <Input
+              value={text}
+              onChange={e => this.handleNameInputChange(e, id)}
+              placeholder="必填项"
+              style={{ width: 100 }}
+            />
+          );
+        }
+        return text;
+      },
+      align: 'center',
+    }, {
+      title: '标签类型',
+      key: 3,
+      dataIndex: 'typeCode',
+      width: '15%',
+      render: (text, record) => {
+        // 将标签类型更换,编辑状态下可更换
+        const { id, isNew, isEdit, typeCode = '标签类型选择(必选项)' } = record;
+        return (
+          <div className={styles.product}>
+            {isEdit && (
+              <div className={styles.mongolian}>
+                <a onClick={() => this.handleSelectorModalShow('resource', id)}>{isNew ? '选择' : '更换'}</a>
+              </div>
+            )}
+            {typeCode}
+          </div>
+        );
+      },
+      align: 'center',
+    }, {
+      title: '标签状态',
+      key: 4,
+      dataIndex: 'status',
+      width: '15%',
+      render: (text, record) => {
+        const { id, isEdit } = record;
+        if (isEdit) {
+          return (
+            <Switch
+              checked={statusToBool(text)}
+              checkedChildren="正常"
+              unCheckedChildren="删除"
+              onChange={checked => this.handleStatusSwitchChange(checked, id)}
+            />
+          );
+        }
+        return renderStatus(text);
+      },
+      align: 'center',
+    }, {
+      title: '标签关联产品',
+      key: 5,
+      render: (_, record) => {
+        const { id, isEdit } = record;
+        if (isEdit) {
+          return (
+            <div>
+              <p>此处暂时无法编辑,只做查看</p>
+            </div>
+          );
+        }
+        return (
+          <div>
+            <a onClick={() => this.handleSelectorModalShow('product', id)}>
+              <Icon type="double-right" />查看详情
+            </a>
+          </div>
+        );
+      },
+      width: '30%',
+      align: 'center',
+    }, {
+      title: '操作',
+      key: 6,
+      width: '15%',
+      render: (_, record) => {
+        const { id, isNew, isEdit } = record;
+        const getPopconfirmBtn = () => {
+          return (
+            <Popconfirm
+              placement="top"
+              title="确定要删除该标签?"
+              okText="确定"
+              cancelText="取消"
+              onConfirm={() => this.handleTagItemDelete(id, isNew)}
+            >
+              <Button
+                size="small"
+                className="delBtn"
+              >删除
+              </Button>
+            </Popconfirm>
+          );
+        };
+        if (isEdit) {
+          return (
+            <div>
+              <Button
+                size="small"
+                className="editBtn"
+                onClick={() => this.handleSaveOperation(id)}
+              >保存
+              </Button>
+              {getPopconfirmBtn()}
+            </div>
+          );
+        }
+        return (
+          <div>
+            <Button
+              size="small"
+              className="editBtn"
+              onClick={() => this.handleTagItemEdit(id)}
+            >编辑
+            </Button>
+            {getPopconfirmBtn()}
+            <Button
+              size="small"
+              className={styles.copyBtn}
+              onClick={() => this.handleSelectorModalShow('allTag', id)}
+            >复制
+            </Button>
+
+          </div>
+        );
+      },
+      align: 'right',
+    }];
+    const getProductModal = () => {
+      const columns = [
+        {
+          title: '产品编号',
+          dataIndex: 'code',
+        },
+        {
+          title: '产品名称',
+          dataIndex: 'name',
+        },
+      ];
+      const { productList = [] } = this.props.configUser.TagDetails;
+      return (
+        <Modal
+          visible
+          width={1100}
+          title="关联产品详情"
+          footer={null}
+          maskClosable={false}
+          onCancel={() => this.handleSelectorCancel('product')}
+        >
+          <Table
+            columns={columns}
+            dataSource={productList}
+            fixedName="Product"
+            onCancel={() => this.handleSelectorCancel('product')}
+          />
+        </Modal>
+      );
+    };
+    /* 标签模态框选择器 */
+    const getResourceModal = () => {
+      return (
+        <Modal
+          width={1100}
+          footer={null}
+          visible
+          title="标签类型"
+          maskClosable={false}
+          onCancel={() => this.handleSelectorCancel('resource')}
+        >
+          <Selector
+            multiple={false}
+            loading={loading}
+            selectorName="TagType"
+            list={tagType.list}
+            pageNo={tagType.pageNo}
+            pageSize={tagType.pageSize}
+            totalSize={tagType.totalSize}
+            onCancel={() => this.handleSelectorCancel('resource')}
+            onChange={data => this.handleSelectorChange('resource', data)}
+            onFinish={rows => this.handleSelectorFinish('resource', rows)}
+          />
+        </Modal>
+      );
+    };
+    /* 选择标签的对应的模态框 */
+    const getAllTagModal = () => {
+      return (
+        <Modal
+          width={1100}
+          footer={null}
+          visible
+          title="可选择的标签"
+          maskClosable={false}
+          onCancel={() => this.handleSelectorCancel('allTag')}
+        >
+          <Selector
+            multiple={false}
+            loading={loading}
+            selectorName="allTag"
+            list={tag.list}
+            pageNo={tag.pageNo}
+            pageSize={tag.pageSize}
+            totalSize={tag.totalSize}
+            onCancel={() => this.handleSelectorCancel('allTag')}
+            onChange={data => this.handleSelectorChange('allTag', data)}
+            onFinish={rows => this.handleSelectorFinish('allTag', rows)}
+          />
+        </Modal>
+      );
+    };
+    return (
+      <div>
+        <Card style={{ marginBottom: 70 }}>
+          <Table
+            pagination={false}
+            dataSource={userTagLists}
+            columns={userTagColumns}
+            rowKey={record => record.id}
+            className={styles.posterTable}
+          />
+          <Button
+            type="dashed"
+            icon="plus"
+            style={{ width: '100%', marginTop: 16, marginBottom: 8 }}
+            onClick={this.handleTagItemCreate}
+          >新建
+          </Button>
+          {!resourceSelectorDestroy && getResourceModal()}
+          {!productSelectorDestroy && getProductModal()}
+          {!allTagSelectorDestroy && getAllTagModal()}
+        </Card>
+        <FooterToolbar style={{ width: '100%' }}>
+          <Button type="primary" onClick={this.handlePageBack}>返回上一页</Button>
+        </FooterToolbar>
+      </div>
+    );
+  }
+}

+ 68 - 0
src/routes/Frontend/ConfigUser/ConfigTag.less

@@ -0,0 +1,68 @@
+@import "../../../../node_modules/antd/lib/style/themes/default.less";
+
+.posterTable {
+  :global {
+    .ant-table-title {
+      padding: 0 0 16px 0;
+    }
+    .ant-table-footer {
+      padding: 10px;
+    }
+    .ant-table-tbody > tr > td {
+      padding: 5px 10px;
+    }
+    .ant-table-thead > tr > th {
+      padding: 10px 5px;
+    }
+  }
+}
+
+.mongolian {
+  z-index: 10;
+  display: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  a {
+    position: relative;
+    top: 45%;
+    transform: translateY(-45%);
+    padding: 5px 15px;
+    background: #fff;
+    color: #73777a;
+  }
+}
+
+.cover {
+  position: relative;
+  margin: 0;
+  width: 100%;
+  height: 100%;
+  img {
+    width: 100%;
+    height: 100%;
+  }
+  &:hover {
+    .mongolian {
+      display: block;
+      background: rgba(0, 193, 222, .8);
+    }
+  }
+}
+
+.product {
+  position: relative;
+  &:hover {
+    .mongolian {
+      display: block;
+      background: rgba(0, 193, 222, .8);
+    }
+  }
+}
+
+.copyBtn {
+  margin-left: 10px;
+  font-weight: 500;
+}

+ 188 - 0
src/routes/Frontend/ConfigUser/ConfigUserLists.js

@@ -0,0 +1,188 @@
+import React, { Component } from 'react';
+import { connect } from 'dva';
+import { routerRedux } from 'dva/router';
+import { Card, Form, Menu, Dropdown, Icon } from 'antd';
+import { StandardTableList } from '../../../components/AXList/index';
+import Ellipsis from '../../../components/Ellipsis/index';
+import { addRowKey, renderStatus } from '../../../utils/utils';
+
+@Form.create()
+@connect(({ loading, campus, merchant, terminal }) => ({
+  campus,
+  merchant,
+  terminal,
+  fetching1: loading.models.merchant,
+  fetching2: loading.models.campus,
+  loading: loading.models.terminal,
+}))
+
+export default class ConfigUserLists extends Component {
+  constructor(props) {
+    super(props);
+    const { state } = props.location;
+    this.state = {
+      UIParams: (state || {}).UIParams, // 组件的状态参数
+      Queryers: (state || {}).Queryers, // 查询的条件参数
+      filterModalDestroy: true,
+      itemData: {},
+    };
+  }
+  componentDidMount() {
+    this.props.dispatch({
+      type: 'terminal/fetchTerminalList',
+      payload: { ...this.state.Queryers },
+    });
+  }
+  // 跳转到配置标签的page
+  handleEditOperation = (item) => {
+    this.props.dispatch(routerRedux.push({
+      pathname: `/frontend/ConfigUser/tag/${item.id}`,
+      state: {
+        currentItem: item,
+        ...this.state,
+      },
+    }));
+  }
+  // 跳转到配置课程页面
+  handleDeviceCourseOperation = (item) => {
+    const { id } = item;
+    this.props.dispatch(routerRedux.push({
+      pathname: `/frontend/ConfigUser/course-edit/${id}`,
+      state: this.state,
+    }));
+  }
+  // 跳到配置海报的页面
+  handleDevicePosterOperation = (item) => {
+    const { id } = item;
+    this.props.dispatch(routerRedux.push({
+      pathname: `/frontend/ConfigUser/poster-edit/${id}`,
+      state: this.state,
+    }));
+  }
+  handleFilterOperation = (params, states) => {
+    this.props.dispatch({
+      type: 'terminal/fetchTerminalList',
+      payload: params,
+    });
+    this.setState({
+      UIParams: states,
+      Queryers: params,
+    });
+  }
+  // 将item中的数据传送给子栏目
+  transferData = (item) => {
+    this.setState(() => {
+      return {
+        itemData: item,
+      };
+    });
+  }
+  render() {
+    const { loading, terminal } = this.props;
+    const { list, totalSize, pageSize, pageNo } = terminal;
+    const { itemData } = this.state;
+
+    const renderCampusName = (name) => {
+      return (
+        <Ellipsis tooltip lines={1}>{name}</Ellipsis>
+      );
+    };
+    const menu = (
+      <Menu>
+        <Menu.Item>
+          <a onClick={() => this.handleDevicePosterOperation(itemData)}>
+            推荐海报
+          </a>
+        </Menu.Item>
+        <Menu.Item>
+          <a onClick={() => this.handleDeviceCourseOperation(itemData)}>
+            推荐课程
+          </a>
+        </Menu.Item>
+      </Menu>
+    );
+    const renderOperation = (item) => {
+      return (
+        <div>
+          <a
+            onClick={() => this.handleEditOperation(item)}
+          >标签配置
+          </a>
+          &nbsp;&nbsp;&nbsp;
+          <Dropdown overlay={menu} trigger={['click']}>
+            <a
+              onClick={() => this.transferData(item)}
+            >推荐配置 <Icon type="down" />
+            </a>
+          </Dropdown>
+        </div>
+      );
+    };
+    const basicSearch = {
+      keys: [{
+        name: '终端编号',
+        field: 'code',
+      }, {
+        name: '终端名称',
+        field: 'name',
+      }],
+    };
+    const pagination = {
+      pageNo,
+      pageSize,
+      totalSize,
+    };
+    const columns = [{
+      title: '终端编号',
+      key: 1,
+      dataIndex: 'code',
+      width: '20%',
+    }, {
+      title: '终端名称',
+      key: 2,
+      dataIndex: 'name',
+      width: '20%',
+    }, {
+      title: '所属校区',
+      key: 3,
+      dataIndex: 'campusName',
+      render: text => renderCampusName(text),
+      width: '22%',
+    }, {
+      title: '所属渠道',
+      key: 4,
+      dataIndex: 'merchantName',
+      width: '12%',
+    }, {
+      title: '账号状态',
+      key: 5,
+      dataIndex: 'status',
+      render: text => renderStatus(text, '已禁用'),
+      width: '8%',
+    }, {
+      title: '操作',
+      key: 6,
+      dataIndex: 'operation',
+      render: (_, record) => renderOperation(record),
+      width: '20%',
+      align: 'right',
+    }];
+    return (
+      <Card>
+        <StandardTableList
+          columns={columns}
+          loading={loading}
+          dataSource={addRowKey(list)}
+          header={{
+            basicSearch,
+            onFilterClick: this.handleFilterOperation,
+          }}
+          footer={{
+            pagination,
+          }}
+          keepUIState={{ ...this.state.UIParams }}
+        />
+      </Card>
+    );
+  }
+}

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

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

+ 2 - 1
src/routes/Frontend/Recommend/RecommendPoster.js

@@ -29,6 +29,7 @@ function dataSort(array) {
   sLoading: loading.models.shelves,
   mLoading: loading.models.merchant,
 }))
+
 export default class RecommendPosterEditPage extends Component {
   state = {
     productSelectorDestroy: true,
@@ -79,7 +80,7 @@ export default class RecommendPosterEditPage extends Component {
   };
   /**
    * 4.删除一条海报
-   * @param postId
+   * @param posterId
    * @param isNew
    */
   handlePosterItemDelete = (posterId, isNew) => {

+ 1 - 0
src/routes/Frontend/Tag/TagList.less

@@ -14,3 +14,4 @@
   color: #fff;
   font-weight: 500;
 }
+

+ 0 - 1
src/routes/Frontend/TagType/index.js

@@ -9,7 +9,6 @@ export default class TagType extends Component {
   render() {
     const { match, routerData } = this.props;
     const routes = getRoutes(match.path, routerData);
-
     return (
       <PageHeaderLayout>
         <Switch>

+ 104 - 0
src/services/configUser.js

@@ -0,0 +1,104 @@
+import { stringify } from 'qs';
+import request from '../utils/request';
+import { api } from '../utils/config';
+
+export async function queryUserTags(params) {
+  const id = params.configUserId;
+  return request(`${api.configUser}/${id}`);
+}
+
+export async function createConfigUserTagItem(params) {
+  const options = {
+    method: 'POST',
+    body: params,
+  };
+  return request(`${api.configUserTag}`, options);
+}
+
+
+export async function queryTagDetail(id) {
+  return request(`${api.configUserTag}/${id}`);
+}
+
+export async function updateConfigUserTagItem(params) {
+  const options = {
+    method: 'PUT',
+    body: params,
+  };
+  return request(`${api.configUserTag}`, options);
+}
+
+export async function deleteConfigUserTagItem(id) {
+  const options = {
+    method: 'DELETE',
+  };
+  return request(`${api.configUserTag}/${id}`, options);
+}
+
+export async function depositMerchantItem(params) {
+  const options = {
+    method: 'POST',
+    body: params,
+  };
+  return request(`${api.deposit}`, options);
+}
+
+export async function queryMerchantRecommend({ merchantId }) {
+  return request(`${api.recommend}/${merchantId}`);
+}
+
+export async function updateMerchantRecommend({ merchantId, idList }) {
+  const options = {
+    method: 'PUT',
+    body: idList,
+  };
+  return request(`${api.recommend}/${merchantId}`, options);
+}
+
+export async function queryMerchantPoster({ merchantId }) {
+  return request(`${api.poster}/${merchantId}`);
+}
+
+export async function queryMerchantPosterItem({ posterId }) {
+  return request(`${api.posterItem}/${posterId}`);
+}
+
+export async function createMerchantPosterItem(params) {
+  const options = {
+    method: 'POST',
+    body: params,
+  };
+  return request(`${api.posterItem}`, options);
+}
+
+export async function updateMerchantPosterItem(params) {
+  const options = {
+    method: 'PUT',
+    body: params,
+  };
+  return request(`${api.posterItem}`, options);
+}
+
+export async function deleteMerchantPosterItem({ posterId }) {
+  const options = {
+    method: 'DELETE',
+  };
+  return request(`${api.posterItem}/${posterId}`, options);
+}
+
+export async function queryConfigCourse(params) {
+  const id = params.courseId;
+  return request(`${api.configCourse}/${id}`);
+}
+
+export async function copyTag(params) {
+  return request(`${api.copyTag}?${stringify(params)}`);
+}
+
+export async function updateConfigCourse({ courseId, idList }) {
+  const options = {
+    method: 'PUT',
+    body: idList,
+  };
+  return request(`${api.configCourse}/${courseId}`, options);
+}

+ 4 - 0
src/utils/config.js

@@ -128,6 +128,10 @@ const apiObj = {
   orderSend: '/order/send',
   orderReceive: '/order/receive',
   snapshot: '/order/snapshot',
+  configUser: '/user/userTag/uid',
+  configUserTag: '/userTag',
+  configCourse: '/user/userRecommend/uid',
+  copyTag: '/userTag/copy',
 };
 
 /**

+ 0 - 1
src/utils/utils.js

@@ -61,7 +61,6 @@ export function getTimeDistance(type) {
     now.setHours(0);
     now.setMinutes(0);
     now.setSeconds(0);
-
     if (day === 0) {
       day = 6;
     } else {