Bladeren bron

feature(客户管理):新增客户管理模块

wanglj 3 jaren geleden
bovenliggende
commit
c1779a5c7f

+ 1 - 0
package.json

@@ -14,6 +14,7 @@
   },
   "dependencies": {
     "@riophae/vue-treeselect": "^0.4.0",
+    "await-to-js": "^3.0.0",
     "axios": "^0.24.0",
     "core-js": "^3.19.3",
     "dayjs": "^1.10.7",

+ 110 - 0
src/api/customer/index.js

@@ -0,0 +1,110 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-27 11:10:39
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-28 14:49:40
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\api\customer\index.js
+ */
+import micro_request from '@/utils/micro_request'
+const basePath = process.env.VUE_APP_CustomerPath
+export default {
+  // 客户详情
+  getDetail(query) {
+    return micro_request.postRequest(
+      basePath,
+      'Customer',
+      'GetEntityById',
+      query
+    )
+  },
+  // 客户编辑
+  updateCostomer(query) {
+    return micro_request.postRequest(basePath, 'Customer', 'UpdateById', query)
+  },
+  // 客户合并
+  mergeCustomer(query) {
+    return micro_request.postRequest(
+      basePath,
+      'Customer',
+      'Mergecustomer',
+      query
+    )
+  },
+  // 客户联系人详情
+  getContact(query) {
+    return micro_request.postRequest(
+      basePath,
+      'Contant',
+      'GetEntityById',
+      query
+    )
+  },
+  // 客户动态
+  dynamicsList(query) {
+    return micro_request.postRequest(
+      basePath,
+      'Customer',
+      'DynamicsList',
+      query
+    )
+  },
+  // 客户归属记录
+  getBelongs(query) {
+    return micro_request.postRequest(basePath, 'Belong', 'GetEntityById', query)
+  },
+  // 编辑联系人
+  updateContact(query) {
+    return micro_request.postRequest(basePath, 'Contant', 'UpdateById', query)
+  },
+  // 删除联系人
+  deleteContact(query) {
+    return micro_request.postRequest(basePath, 'Contant', 'DeleteById', query)
+  },
+  // 公海列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'Customer', 'GetList', query)
+  },
+  // 移入公海
+  moveToPubic(query) {
+    return micro_request.postRequest(basePath, 'Customer', 'MoveToPubic', query)
+  },
+  // 客户列表
+  getPublicList(query) {
+    return micro_request.postRequest(
+      basePath,
+      'Customer',
+      'PublicGetList',
+      query
+    )
+  },
+  // 客户删除
+  deleteCustomer(query) {
+    return micro_request.postRequest(basePath, 'Customer', 'DeleteById', query)
+  },
+  // 客户领取
+  receiveCustomer(query) {
+    // return micro_request.postRequest(basePath, 'Contant', 'Create', query)
+  },
+  // 省份
+  getProvinceInfo(query) {
+    return micro_request.postRequest(
+      basePath,
+      'District',
+      'GetProvinceInfo',
+      query
+    )
+  },
+  // 省份详情
+  getProvinceDetail(query) {
+    return micro_request.postRequest(basePath, 'District', 'GetList', query)
+  },
+  // 新建客户
+  createCustomer(query) {
+    return micro_request.postRequest(basePath, 'Customer', 'Create', query)
+  },
+  // 新建联系人
+  createContact(query) {
+    return micro_request.postRequest(basePath, 'Contant', 'Create', query)
+  },
+}

+ 10 - 3
src/store/modules/user.js

@@ -90,9 +90,16 @@ const actions = {
    * @returns
    */
   async getUserInfo({ commit, dispatch }) {
-    const {
-      data: { username, avatar, roles, permissions },
-    } = await userApi.getUserInfo()
+    // const {
+    //   data: { username, avatar, roles, permissions },
+    // } = await userApi.getUserInfo()
+    // console.log(username, avatar, roles, permissions)
+
+    const res = await userApi.getUserInfo()
+    const username = res.data.entity.userName
+    const avatar = res.data.entity.avatar
+    let roles
+    let permissions
     /**
      * 检验返回数据是否正常,无对应参数,将使用默认用户名,头像,Roles和Permissions
      * username {String}

+ 3 - 3
src/utils/micro_request.js

@@ -73,16 +73,16 @@ service.postRequest = function postRequest(basePath, srvName, funcName, data) {
     data = nullParam
   }
 
-  // console.log( basePath ,'   basePath   ')
+  // console.log(basePath, '   basePath   ')
   var base_Path = ''
   if (basePath == process.env.VUE_APP_FOSHAN_PATH) {
     base_Path = process.env.VUE_APP_MicroSrvProxy_foshan_API + basePath
   } else if (basePath == process.env.VUE_APP_AdminPath) {
     base_Path = process.env.VUE_APP_MicroSrvProxy_API + basePath
+  } else {
+    base_Path = process.env.VUE_APP_MicroSrvProxy_API + basePath
   }
-
   // console.log(base_Path, '   base_Path   ')
-
   return service.request({
     url: base_Path,
     method: 'post',

+ 292 - 297
src/vab/components/VabColumnBar/index.vue

@@ -1,29 +1,26 @@
 <template>
-  <el-scrollbar
-    class="vab-column-bar-container"
-    :class="{
+  <el-scrollbar class="vab-column-bar-container"
+                :class="{
       'is-collapse': collapse,
       ['vab-column-bar-container-' + theme.columnStyle]: true,
     }">
     <vab-logo />
-    <el-tabs
-      v-model="extra.first"
-      tab-position="left"
-      @tab-click="handleTabClick">
+    <el-tabs v-model="extra.first"
+             tab-position="left"
+             @tab-click="handleTabClick">
       <template v-for="(route, index) in handleRoutes">
-        <el-tab-pane :key="index + route.name" :name="route.name">
+        <el-tab-pane :key="index + route.name"
+                     :name="route.name">
           <template slot="label">
-            <div
-              class="vab-column-grid"
-              :class="{
+            <div class="vab-column-grid"
+                 :class="{
                 ['vab-column-grid-' + theme.columnStyle]: true,
               }"
-              :title="translateTitle(route.meta.title)">
+                 :title="translateTitle(route.meta.title)">
               <div>
-                <vab-icon
-                  v-if="route.meta.icon"
-                  :icon="route.meta.icon"
-                  :is-custom-svg="route.meta.isCustomSvg" />
+                <vab-icon v-if="route.meta.icon"
+                          :icon="route.meta.icon"
+                          :is-custom-svg="route.meta.isCustomSvg" />
                 <span>
                   {{ translateTitle(route.meta.title) }}
                 </span>
@@ -34,375 +31,373 @@
       </template>
     </el-tabs>
 
-    <el-menu
-      :background-color="variables['column-second-menu-background']"
-      :default-active="activeMenu"
-      :default-openeds="defaultOpeneds"
-      mode="vertical"
-      :unique-opened="uniqueOpened">
+    <el-menu :background-color="variables['column-second-menu-background']"
+             :default-active="activeMenu"
+             :default-openeds="defaultOpeneds"
+             mode="vertical"
+             :unique-opened="uniqueOpened">
       <el-divider>
         {{ translateTitle(handleGroupTitle) }}
       </el-divider>
       <template v-for="route in handlePartialRoutes">
-        <vab-menu
-          v-if="route.meta && !route.meta.hidden"
-          :key="route.path"
-          :item="route" />
+        <vab-menu v-if="route.meta && !route.meta.hidden"
+                  :key="route.path"
+                  :item="route" />
       </template>
     </el-menu>
   </el-scrollbar>
 </template>
 
 <script>
-  import { translateTitle } from '@/utils/i18n'
-  import variables from '@/vab/styles/variables/variables.scss'
-  import { mapActions, mapGetters } from 'vuex'
-  import { defaultOpeneds, openFirstMenu, uniqueOpened } from '@/config'
-  import { handleActivePath, handleMatched } from '@/utils/routes'
-
-  export default {
-    name: 'VabColumnBar',
-    data() {
-      return {
-        activeMenu: '',
-        groupTitle: '',
-        defaultOpeneds,
-        uniqueOpened,
-        variables,
-      }
+import { translateTitle } from '@/utils/i18n'
+import variables from '@/vab/styles/variables/variables.scss'
+import { mapActions, mapGetters } from 'vuex'
+import { defaultOpeneds, openFirstMenu, uniqueOpened } from '@/config'
+import { handleActivePath, handleMatched } from '@/utils/routes'
+
+export default {
+  name: 'VabColumnBar',
+  data () {
+    return {
+      activeMenu: '',
+      groupTitle: '',
+      defaultOpeneds,
+      uniqueOpened,
+      variables,
+    }
+  },
+  computed: {
+    ...mapGetters({
+      collapse: 'settings/collapse',
+      routes: 'routes/routes',
+      activeName: 'routes/activeName',
+      theme: 'settings/theme',
+      extra: 'settings/extra',
+    }),
+    handleRoutes () {
+      return this.routes.filter(
+        (route) => route.meta && route.meta.hidden !== true
+      )
     },
-    computed: {
-      ...mapGetters({
-        collapse: 'settings/collapse',
-        routes: 'routes/routes',
-        activeName: 'routes/activeName',
-        theme: 'settings/theme',
-        extra: 'settings/extra',
-      }),
-      handleRoutes() {
-        return this.routes.filter(
-          (route) => route.meta && route.meta.hidden !== true
-        )
-      },
-      handleActiveMenu() {
-        return this.routes.find((route) => route.name === this.extra.first)
-      },
-      handlePartialRoutes() {
-        const activeMenu = this.handleActiveMenu
-        return activeMenu ? activeMenu.children : []
-      },
-      handleGroupTitle() {
-        const activeMenu = this.handleActiveMenu
-        return activeMenu ? activeMenu.meta.title : ''
-      },
+    handleActiveMenu () {
+      return this.routes.find((route) => route.name === this.extra.first)
     },
-    watch: {
-      $route: {
-        handler(route) {
-          this.handleNoColumn()
-          this.activeMenu = handleActivePath(route)
-          const firstMenu = route.matched[0].name
-          if (this.extra.first !== firstMenu) {
-            this.extra.first = firstMenu
-            this.handleTabClick(true)
-          }
-        },
-        immediate: true,
-      },
-      activeName: {
-        handler(val) {
-          const matched = handleMatched(this.routes, val)
-          this.extra.first = matched[0].name
-          this.activeMenu = matched[matched.length - 1].path
-        },
-      },
+    handlePartialRoutes () {
+      const activeMenu = this.handleActiveMenu
+      return activeMenu ? activeMenu.children : []
     },
-    methods: {
-      ...mapActions({
-        openSideBar: 'settings/openSideBar',
-        foldSideBar: 'settings/foldSideBar',
-      }),
-      translateTitle,
-      handleTabClick(handler) {
+    handleGroupTitle () {
+      const activeMenu = this.handleActiveMenu
+      return activeMenu ? activeMenu.meta.title : ''
+    },
+  },
+  watch: {
+    $route: {
+      handler (route) {
         this.handleNoColumn()
-        if (handler !== true && openFirstMenu)
-          this.$router.push(this.handleActiveMenu)
+        this.activeMenu = handleActivePath(route)
+        const firstMenu = route.matched[0].name
+        if (this.extra.first !== firstMenu) {
+          this.extra.first = firstMenu
+          this.handleTabClick(true)
+        }
       },
-      handleNoColumn() {
-        this.$nextTick(() => {
-          if (this.theme.layout === 'column' && this.$route.meta.noColumn) {
-            this.foldSideBar()
-            if (document.querySelector('.fold-unfold'))
-              document.querySelector('.fold-unfold').style = 'display:none'
-          } else {
-            this.openSideBar()
-            if (document.querySelector('.fold-unfold'))
-              document.querySelector('.fold-unfold').style = ''
-          }
-        })
+      immediate: true,
+    },
+    activeName: {
+      handler (val) {
+        const matched = handleMatched(this.routes, val)
+        this.extra.first = matched[0].name
+        this.activeMenu = matched[matched.length - 1].path
       },
     },
-  }
+  },
+  methods: {
+    ...mapActions({
+      openSideBar: 'settings/openSideBar',
+      foldSideBar: 'settings/foldSideBar',
+    }),
+    translateTitle,
+    handleTabClick (handler) {
+      this.handleNoColumn()
+      if (handler !== true && openFirstMenu)
+        this.$router.push(this.handleActiveMenu)
+    },
+    handleNoColumn () {
+      this.$nextTick(() => {
+        if (this.theme.layout === 'column' && this.$route.meta.noColumn) {
+          this.foldSideBar()
+          if (document.querySelector('.fold-unfold'))
+            document.querySelector('.fold-unfold').style = 'display:none'
+        } else {
+          this.openSideBar()
+          if (document.querySelector('.fold-unfold'))
+            document.querySelector('.fold-unfold').style = ''
+        }
+      })
+    },
+  },
+}
 </script>
 
 <style lang="scss" scoped>
-  @mixin active {
-    &:hover {
-      color: $base-color-blue;
-      background-color: $base-column-second-menu-background-active !important;
-
-      i,
-      svg {
-        color: $base-color-blue;
-      }
-    }
+@mixin active {
+  &:hover {
+    color: $base-color-blue;
+    background-color: $base-column-second-menu-background-active !important;
 
-    &.is-active {
+    i,
+    svg {
       color: $base-color-blue;
-      background-color: $base-column-second-menu-background-active !important;
     }
   }
 
-  .vab-column-bar-container {
-    position: fixed;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    width: $base-left-menu-width;
-    height: 100vh;
-    overflow: hidden;
-    background: $base-column-second-menu-background;
-    box-shadow: $base-box-shadow;
+  &.is-active {
+    color: $base-color-blue;
+    background-color: $base-column-second-menu-background-active !important;
+  }
+}
+
+.vab-column-bar-container {
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  width: $base-left-menu-width;
+  height: 100vh;
+  overflow: hidden;
+  background: $base-column-second-menu-background;
+  box-shadow: $base-box-shadow;
+
+  ::v-deep {
+    * {
+      transition: $base-transition;
+    }
+  }
 
+  &-vertical,
+  &-card,
+  &-arrow {
     ::v-deep {
-      * {
-        transition: $base-transition;
+      .el-tabs + .el-menu {
+        left: $base-left-menu-width-min;
+        width: $base-left-menu-width - $base-left-menu-width-min;
+        border: 0;
       }
     }
+  }
 
-    &-vertical,
-    &-card,
-    &-arrow {
-      ::v-deep {
-        .el-tabs + .el-menu {
-          left: $base-left-menu-width-min;
-          width: $base-left-menu-width - $base-left-menu-width-min;
-          border: 0;
+  &-horizontal {
+    ::v-deep {
+      .logo-container-column {
+        .logo {
+          width: $base-left-menu-width-min * 1.3 !important;
         }
-      }
-    }
 
-    &-horizontal {
-      ::v-deep {
-        .logo-container-column {
-          .logo {
-            width: $base-left-menu-width-min * 1.3 !important;
-          }
-
-          .title {
-            margin-left: $base-left-menu-width-min * 1.3 !important;
-          }
+        .title {
+          margin-left: $base-left-menu-width-min * 1.3 !important;
         }
+      }
 
-        .el-tabs + .el-menu {
-          left: $base-left-menu-width-min * 1.3;
-          width: $base-left-menu-width - $base-left-menu-width-min * 1.3;
-          border: 0;
-        }
+      .el-tabs + .el-menu {
+        left: $base-left-menu-width-min * 1.3;
+        width: $base-left-menu-width - $base-left-menu-width-min * 1.3;
+        border: 0;
       }
     }
+  }
 
-    &-card {
-      ::v-deep {
-        .el-tabs {
-          .el-tabs__item {
-            padding: 5px !important;
+  &-card {
+    ::v-deep {
+      .el-tabs {
+        .el-tabs__item {
+          padding: 5px !important;
 
-            .vab-column-grid {
-              width: $base-left-menu-width-min - 10 !important;
-              height: $base-left-menu-width-min - 10 !important;
-              border-radius: 5px;
-            }
+          .vab-column-grid {
+            width: $base-left-menu-width-min - 10 !important;
+            height: $base-left-menu-width-min - 10 !important;
+            border-radius: 5px;
+          }
 
-            &.is-active {
-              background: transparent !important;
+          &.is-active {
+            background: transparent !important;
 
-              .vab-column-grid {
-                background: $base-color-blue;
-              }
+            .vab-column-grid {
+              background: $base-color-blue;
             }
           }
         }
+      }
 
-        .el-tabs + .el-menu {
-          left: $base-left-menu-width-min + 10;
-          width: $base-left-menu-width - $base-left-menu-width-min - 20;
-        }
+      .el-tabs + .el-menu {
+        left: $base-left-menu-width-min + 10;
+        width: $base-left-menu-width - $base-left-menu-width-min - 20;
+      }
 
-        .el-submenu .el-submenu__title,
-        .el-menu-item {
-          min-width: 180px;
-          border-radius: 5px;
-        }
+      .el-submenu .el-submenu__title,
+      .el-menu-item {
+        min-width: 180px;
+        border-radius: 5px;
       }
     }
+  }
 
-    &-arrow {
-      ::v-deep {
-        .el-tabs {
-          .el-tabs__item {
-            &.is-active {
+  &-arrow {
+    ::v-deep {
+      .el-tabs {
+        .el-tabs__item {
+          &.is-active {
+            background: transparent !important;
+
+            .vab-column-grid {
               background: transparent !important;
 
-              .vab-column-grid {
-                background: transparent !important;
-
-                &:after {
-                  position: absolute;
-                  right: 0;
-                  width: 0;
-                  height: 0;
-                  overflow: hidden;
-                  content: '';
-                  border-color: transparent #{$base-color-white} transparent transparent;
-                  border-style: solid dashed dashed;
-                  border-width: 8px;
-                }
+              &:after {
+                position: absolute;
+                right: 0;
+                width: 0;
+                height: 0;
+                overflow: hidden;
+                content: '';
+                border-color: transparent #{$base-color-white} transparent transparent;
+                border-style: solid dashed dashed;
+                border-width: 8px;
               }
             }
           }
         }
+      }
 
-        .el-tabs + .el-menu {
-          left: $base-left-menu-width-min + 10;
-          width: $base-left-menu-width - $base-left-menu-width-min - 20;
-        }
+      .el-tabs + .el-menu {
+        left: $base-left-menu-width-min + 10;
+        width: $base-left-menu-width - $base-left-menu-width-min - 20;
+      }
 
-        .el-submenu .el-submenu__title,
-        .el-menu-item {
-          min-width: 180px;
-          border-radius: 5px;
-        }
+      .el-submenu .el-submenu__title,
+      .el-menu-item {
+        min-width: 180px;
+        border-radius: 5px;
       }
     }
+  }
 
-    .vab-column-grid {
-      display: flex;
-      align-items: center;
-      width: $base-left-menu-width-min;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      word-break: break-all;
-      white-space: nowrap;
-
-      &-vertical,
-      &-card,
-      &-arrow {
-        justify-content: center;
-        height: $base-left-menu-width-min;
-
-        > div {
-          svg {
-            position: relative;
-            top: 8px;
-            display: block;
-            width: $base-font-size-default + 4;
-            height: $base-font-size-default + 4;
-          }
+  .vab-column-grid {
+    display: flex;
+    align-items: center;
+    width: $base-left-menu-width-min;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    word-break: break-all;
+    white-space: nowrap;
 
-          [class*='ri-'] {
-            display: block;
-            height: 20px;
-          }
+    &-vertical,
+    &-card,
+    &-arrow {
+      justify-content: center;
+      height: $base-left-menu-width-min;
+
+      > div {
+        svg {
+          position: relative;
+          top: 8px;
+          display: block;
+          width: $base-font-size-default + 4;
+          height: $base-font-size-default + 4;
         }
-      }
 
-      &-horizontal {
-        justify-content: left;
-        width: $base-left-menu-width-min * 1.3;
-        height: $base-left-menu-width-min / 1.3;
-        padding-left: $base-padding / 2;
+        [class*='ri-'] {
+          display: block;
+          height: 20px;
+        }
       }
     }
 
-    ::v-deep {
-      .el-scrollbar__wrap {
-        overflow-x: hidden;
-      }
+    &-horizontal {
+      justify-content: left;
+      width: $base-left-menu-width-min * 1.3;
+      height: $base-left-menu-width-min / 1.3;
+      padding-left: $base-padding / 2;
+    }
+  }
 
-      .el-tabs {
-        position: fixed;
+  ::v-deep {
+    .el-scrollbar__wrap {
+      overflow-x: hidden;
+    }
 
-        .el-tabs__header.is-left {
-          margin-right: 0 !important;
+    .el-tabs {
+      position: fixed;
 
-          .el-tabs__nav-wrap.is-left {
-            margin-right: 0 !important;
-            background: $base-column-first-menu-background;
+      .el-tabs__header.is-left {
+        margin-right: 0 !important;
 
-            .el-tabs__nav-scroll {
-              height: 100%;
-              overflow-y: auto;
+        .el-tabs__nav-wrap.is-left {
+          margin-right: 0 !important;
+          background: $base-column-first-menu-background;
 
-              &::-webkit-scrollbar {
-                width: 0px;
-                height: 0px;
-              }
+          .el-tabs__nav-scroll {
+            height: 100%;
+            overflow-y: auto;
+
+            &::-webkit-scrollbar {
+              width: 0px;
+              height: 0px;
             }
           }
         }
+      }
 
-        .el-tabs__nav {
-          height: calc(100vh - #{$base-logo-height});
-          background: $base-column-first-menu-background;
-        }
+      .el-tabs__nav {
+        height: calc(100vh - #{$base-logo-height});
+        background: $base-column-first-menu-background;
+      }
 
-        .el-tabs__item {
-          height: auto;
-          padding: 0;
-          color: $base-color-white;
+      .el-tabs__item {
+        height: auto;
+        padding: 0;
+        color: $base-color-white;
 
-          &.is-active {
-            background: $base-color-blue;
-          }
+        &.is-active {
+          background: $base-color-blue;
         }
       }
+    }
 
-      .el-tabs__active-bar.is-left,
-      .el-tabs--left .el-tabs__nav-wrap.is-left::after {
-        display: none;
-      }
+    .el-tabs__active-bar.is-left,
+    .el-tabs--left .el-tabs__nav-wrap.is-left::after {
+      display: none;
+    }
 
-      .el-menu {
-        border: 0;
+    .el-menu {
+      border: 0;
 
-        .el-divider {
-          margin: 0 0 $base-margin 0;
-          background-color: #f6f6f6;
+      .el-divider {
+        margin: 0 0 $base-margin 0;
+        background-color: #f6f6f6;
 
-          &__text {
-            color: $base-color-black;
-          }
+        &__text {
+          color: $base-color-black;
         }
+      }
 
-        .el-menu-item,
-        .el-submenu__title {
-          height: $base-menu-item-height;
-          overflow: hidden;
-          line-height: $base-menu-item-height;
-          text-overflow: ellipsis;
-          white-space: nowrap;
-          vertical-align: middle;
+      .el-menu-item,
+      .el-submenu__title {
+        height: $base-menu-item-height;
+        overflow: hidden;
+        line-height: $base-menu-item-height;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        vertical-align: middle;
 
-          @include active;
-        }
+        @include active;
       }
     }
+  }
 
-    &.is-collapse {
-      ::v-deep {
-        width: 0;
-      }
+  &.is-collapse {
+    ::v-deep {
+      width: 0;
     }
   }
+}
 </style>

+ 37 - 23
src/vab/components/VabFooter/index.vue

@@ -1,38 +1,52 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-13 10:28:32
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-15 15:03:05
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\vab\components\VabFooter\index.vue
+-->
 <template>
   <footer class="vab-footer">
     Copyright
     <vab-icon icon="copyright-line" />
-    {{ fullYear }} {{ title }}
+    {{ fullYear }}
+    <!-- {{ title }} -->
+    由大数华创提供技术支持
   </footer>
 </template>
 
 <script>
-  import { title } from '@/config'
+import { title } from '@/config'
 
-  export default {
-    name: 'VabFooter',
-    data() {
-      return {
-        fullYear: new Date().getFullYear(),
-        title,
-      }
-    },
-  }
+export default {
+  name: 'VabFooter',
+  data () {
+    return {
+      fullYear: new Date().getFullYear(),
+      title,
+    }
+  },
+}
 </script>
 
 <style lang="scss" scoped>
-  .vab-footer {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    min-height: 55px;
-    padding: 0 $base-padding 0 $base-padding;
-    color: rgba(0, 0, 0, 0.45);
-    background: $base-color-white;
-    border-top: 1px dashed $base-border-color;
+.vab-footer {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 40px;
+  margin-top: 10px;
+  margin-left: -$base-padding;
+  margin-right: -$base-padding;
+  border-left: 1px solid rgba(40, 44, 52, 0.1);
+  padding: 0 $base-padding 0 $base-padding;
+  color: rgba(0, 0, 0, 0.45);
+  background: $base-color-white;
+  // border-top: 1px dashed $base-border-color;
 
-    i {
-      margin: 0 5px;
-    }
+  i {
+    margin: 0 5px;
   }
+}
 </style>

+ 129 - 121
src/vab/components/VabNav/index.vue

@@ -1,31 +1,39 @@
 <template>
   <div class="vab-nav">
     <el-row :gutter="15">
-      <el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="4">
+      <el-col :lg="12"
+              :md="12"
+              :sm="12"
+              :xl="12"
+              :xs="4">
         <div class="left-panel">
           <vab-fold v-if="layout !== 'float'" />
-          <el-tabs
-            v-if="layout === 'comprehensive'"
-            v-model="extra.first"
-            tab-position="top"
-            @tab-click="handleTabClick">
+          <el-tabs v-if="layout === 'comprehensive'"
+                   v-model="extra.first"
+                   tab-position="top"
+                   @tab-click="handleTabClick">
             <template v-for="(route, index) in handleRoutes">
-              <el-tab-pane :key="index + route.name" :name="route.name">
+              <el-tab-pane :key="index + route.name"
+                           :name="route.name">
                 <span slot="label">
-                  <vab-icon
-                    v-if="route.meta.icon"
-                    :icon="route.meta.icon"
-                    :is-custom-svg="route.meta.isCustomSvg"
-                    style="min-width: 16px" />
+                  <vab-icon v-if="route.meta.icon"
+                            :icon="route.meta.icon"
+                            :is-custom-svg="route.meta.isCustomSvg"
+                            style="min-width: 16px" />
                   {{ translateTitle(route.meta.title) }}
                 </span>
               </el-tab-pane>
             </template>
           </el-tabs>
-          <vab-breadcrumb v-else class="hidden-xs-only" />
+          <vab-breadcrumb v-else
+                          class="hidden-xs-only" />
         </div>
       </el-col>
-      <el-col :lg="12" :md="12" :sm="12" :xl="12" :xs="20">
+      <el-col :lg="12"
+              :md="12"
+              :sm="12"
+              :xl="12"
+              :xs="20">
         <div class="right-panel">
           <vab-error-log />
           <vab-full-screen />
@@ -39,145 +47,145 @@
 </template>
 
 <script>
-  import { translateTitle } from '@/utils/i18n'
-  import { mapGetters } from 'vuex'
-  import { openFirstMenu } from '@/config'
+import { translateTitle } from '@/utils/i18n'
+import { mapGetters } from 'vuex'
+import { openFirstMenu } from '@/config'
 
-  export default {
-    name: 'VabNav',
-    props: {
-      layout: {
-        type: String,
-        default: '',
-      },
+export default {
+  name: 'VabNav',
+  props: {
+    layout: {
+      type: String,
+      default: '',
     },
-    data() {
-      return {
-        firstMenu: '',
-      }
+  },
+  data () {
+    return {
+      firstMenu: '',
+    }
+  },
+  computed: {
+    ...mapGetters({
+      extra: 'settings/extra',
+      routes: 'routes/routes',
+    }),
+    handleRoutes () {
+      return this.routes.filter(
+        (route) => route.meta && route.meta.hidden !== true
+      )
     },
-    computed: {
-      ...mapGetters({
-        extra: 'settings/extra',
-        routes: 'routes/routes',
-      }),
-      handleRoutes() {
-        return this.routes.filter(
-          (route) => route.meta && route.meta.hidden !== true
-        )
-      },
-      handleActiveMenu() {
-        return this.routes.find((route) => route.name === this.extra.first)
-      },
-      handlePartialRoutes() {
-        const activeMenu = this.handleActiveMenu
-        return activeMenu ? activeMenu.children : []
-      },
+    handleActiveMenu () {
+      return this.routes.find((route) => route.name === this.extra.first)
     },
-    watch: {
-      $route: {
-        handler(route) {
-          const firstMenu = route.matched[0].name
-          if (this.extra.first !== firstMenu) {
-            this.extra.first = firstMenu
-            this.handleTabClick(true)
-          }
-        },
-        immediate: true,
-      },
+    handlePartialRoutes () {
+      const activeMenu = this.handleActiveMenu
+      return activeMenu ? activeMenu.children : []
     },
-    methods: {
-      translateTitle,
-      handleTabClick(handler) {
-        if (handler !== true && openFirstMenu)
-          this.$router.push(this.handleActiveMenu)
+  },
+  watch: {
+    $route: {
+      handler (route) {
+        const firstMenu = route.matched[0].name
+        if (this.extra.first !== firstMenu) {
+          this.extra.first = firstMenu
+          this.handleTabClick(true)
+        }
       },
+      immediate: true,
     },
-  }
+  },
+  methods: {
+    translateTitle,
+    handleTabClick (handler) {
+      if (handler !== true && openFirstMenu)
+        this.$router.push(this.handleActiveMenu)
+    },
+  },
+}
 </script>
 
 <style lang="scss" scoped>
-  .vab-nav {
-    position: relative;
-    height: $base-nav-height;
-    padding-right: $base-padding;
-    padding-left: $base-padding;
-    overflow: hidden;
-    user-select: none;
-    background: $base-color-white;
-    box-shadow: $base-box-shadow;
+.vab-nav {
+  position: relative;
+  height: $base-nav-height;
+  padding-right: $base-padding;
+  padding-left: $base-padding;
+  overflow: hidden;
+  user-select: none;
+  background: $base-color-white;
+  box-shadow: $base-box-shadow;
 
-    .left-panel {
-      display: flex;
-      align-items: center;
-      justify-items: center;
-      height: $base-nav-height;
+  .left-panel {
+    display: flex;
+    align-items: center;
+    justify-items: center;
+    height: $base-nav-height;
 
-      ::v-deep {
-        .fold-unfold {
-          margin-right: $base-margin;
-        }
+    ::v-deep {
+      .fold-unfold {
+        margin-right: $base-margin;
+      }
 
-        .el-tabs {
-          width: 100%;
-          margin-left: $base-margin;
+      .el-tabs {
+        width: 100%;
+        margin-left: $base-margin;
 
-          .el-tabs__header {
-            margin: 0;
+        .el-tabs__header {
+          margin: 0;
 
-            > .el-tabs__nav-wrap {
-              display: flex;
-              align-items: center;
+          > .el-tabs__nav-wrap {
+            display: flex;
+            align-items: center;
 
-              .el-icon-arrow-left,
-              .el-icon-arrow-right {
-                font-weight: 600;
-                color: $base-color-grey;
-              }
+            .el-icon-arrow-left,
+            .el-icon-arrow-right {
+              font-weight: 600;
+              color: $base-color-grey;
             }
           }
+        }
 
-          .el-tabs__item {
-            text-align: center;
-            > div {
-              display: flex;
-              align-items: center;
+        .el-tabs__item {
+          text-align: center;
+          > div {
+            display: flex;
+            align-items: center;
 
-              i {
-                margin-right: 3px;
-              }
+            i {
+              margin-right: 3px;
             }
           }
         }
+      }
 
-        .el-tabs__nav-wrap::after {
-          display: none;
-        }
+      .el-tabs__nav-wrap::after {
+        display: none;
       }
     }
+  }
 
-    .right-panel {
-      display: flex;
-      align-content: center;
-      align-items: center;
-      justify-content: flex-end;
-      height: $base-nav-height;
+  .right-panel {
+    display: flex;
+    align-content: center;
+    align-items: center;
+    justify-content: flex-end;
+    height: $base-nav-height;
+
+    ::v-deep {
+      [class*='ri-'] {
+        margin-left: $base-margin;
+        color: $base-color-grey;
+        cursor: pointer;
+      }
 
-      ::v-deep {
+      button {
         [class*='ri-'] {
-          margin-left: $base-margin;
-          color: $base-color-grey;
+          margin-left: 0;
+          color: $base-color-white;
           cursor: pointer;
         }
-
-        button {
-          [class*='ri-'] {
-            margin-left: 0;
-            color: $base-color-white;
-            cursor: pointer;
-          }
-        }
       }
     }
   }
+}
 </style>

+ 9 - 0
src/vab/plugins/element.js

@@ -1,8 +1,17 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-13 10:28:33
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-16 16:33:15
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\vab\plugins\element.js
+ */
 import Vue from 'vue'
 import ElementUI from 'element-ui'
 import '@/vab/styles/variables/element-variables.scss'
 import 'element-ui/lib/theme-chalk/display.css'
 import i18n from '@/i18n'
+ElementUI.Dialog.props.closeOnClickModal.default = false
 
 Vue.use(ElementUI, {
   size: 'small',

+ 1 - 2
src/vab/plugins/vab.js

@@ -162,9 +162,8 @@ Vue.prototype.$baseNotify = (
  */
 Vue.prototype.$baseTableHeight = (formType) => {
   let height = window.innerHeight
-  const paddingHeight = 270
+  const paddingHeight = 245
   const formHeight = 50
-
   if ('number' === typeof formType) {
     height = height - paddingHeight - formHeight * formType
   } else {

+ 15 - 1
src/vab/styles/vab.scss

@@ -44,7 +44,16 @@ html {
     background: $base-color-background;
     -webkit-font-smoothing: antialiased;
     -moz-osx-font-smoothing: grayscale;
-
+    p,
+    h3,
+    h4 {
+      margin: 0;
+    }
+    ul {
+      margin: 0;
+      padding: 0;
+      list-style: none;
+    }
     #app {
       height: 100vh;
       overflow: auto;
@@ -56,6 +65,7 @@ html {
         .vab-app-main {
           width: 100%;
           padding: $base-padding;
+          padding-bottom: 0;
           overflow: hidden;
           transition: $base-transition;
 
@@ -72,6 +82,9 @@ html {
           }
         }
       }
+      .el-pagination {
+        text-align: right;
+      }
     }
 
     * {
@@ -421,6 +434,7 @@ html {
       }
       .vab-app-main {
         padding: calc(#{$base-padding} - 5px) !important;
+        padding-bottom: 0;
         .el-card {
           margin-bottom: calc(#{$base-margin} - 5px) !important;
         }

+ 1 - 1
src/vab/styles/variables/variables.scss

@@ -75,7 +75,7 @@ $base-tag-item-height: 34px;
 $base-menu-item-height: 50px;
 //app-main的高度
 $base-keep-alive-height: calc(
-  100vh - #{$base-nav-height} - #{$base-tabs-height} - #{$base-padding} * 2 - 55px
+  100vh - #{$base-nav-height} - #{$base-tabs-height} - #{$base-padding} * 2 - 40px
 );
 //纵向左侧导航未折叠的宽度
 $base-left-menu-width: 266px;

+ 78 - 0
src/views/customer/components/Allocate.vue

@@ -0,0 +1,78 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-26 14:34:34
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-26 17:24:04
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\components\allocate.vue
+-->
+<template>
+  <el-dialog :visible.sync="visible"
+             width="30%"
+             @close="handleClose"
+             title="分配客户">
+    <Transfer ref="transfer"></Transfer>
+    <el-form :model="form"
+             label-width="80px">
+      <el-form-item label="销售代表">
+        <el-input v-model="form.allocate"
+                  readonly>
+          <el-button slot="append"
+                     icon="el-icon-search"
+                     @click="choose"></el-button>
+        </el-input>
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button type="primary"
+                 size="mini">保存</el-button>
+      <el-button size="mini"
+                 @click="visible = false">取消</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import Transfer from './Transfer.vue'
+export default {
+  data () {
+    const generateData = _ => {
+      const data = [];
+      const cities = ['上海', '北京', '广州', '深圳', '南京', '西安', '成都'];
+      const pinyin = ['shanghai', 'beijing', 'guangzhou', 'shenzhen', 'nanjing', 'xian', 'chengdu'];
+      cities.forEach((city, index) => {
+        data.push({
+          label: city,
+          key: index,
+          pinyin: pinyin[index]
+        });
+      });
+      return data;
+    };
+    return {
+      visible: false,
+      innerVisible: false,
+      form: {
+        allocate: ''
+      },
+      allocate: [],
+      data: generateData(),
+      options: []
+    }
+  },
+  components: {
+    Transfer
+  },
+  methods: {
+    handleClose () {
+
+    },
+    choose () {
+      this.$refs.transfer.innerVisible = true
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 168 - 0
src/views/customer/components/Contact.vue

@@ -0,0 +1,168 @@
+<template>
+  <!-- 新建联系人弹窗 -->
+  <el-dialog :visible.sync="contactVisible"
+             @close="contactClose"
+             :title="title">
+    <el-form ref="contactForm"
+             :model="contactForm"
+             :rules="contactRules">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="姓名"
+                        prop="cuctName">
+            <el-input placeholder="请输入姓名"
+                      v-model="contactForm.cuctName"></el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="客户名称"
+                        prop="custName">
+            <el-input placeholder="请输入客户名称"
+                      disabled
+                      v-model="contactForm.custName"></el-input>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="手机号码"
+                        prop="telephone">
+            <el-input placeholder="请输入手机号码"
+                      v-model="contactForm.telephone">
+            </el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="微信"
+                        prop="wechat">
+            <el-input placeholder="请输入微信"
+                      v-model="contactForm.wechat"></el-input>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="邮箱"
+                        prop="email">
+            <el-input placeholder="请输入邮箱"
+                      v-model="contactForm.email"></el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="职务"
+                        prop="postion">
+            <el-input placeholder="请输入职务"
+                      v-model="contactForm.postion"></el-input>
+            </el-date-picker>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="是否关键决策人"
+                        prop="policy">
+            <el-radio-group v-model="contactForm.policy">
+              <el-radio :label="0">否</el-radio>
+              <el-radio :label="1">是</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="性别"
+                        prop="cuctGender">
+            <el-radio-group v-model="contactForm.cuctGender">
+              <el-radio label="10">男</el-radio>
+              <el-radio label="20">女</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="备注"
+                    prop="remark">
+        <el-input type="textarea"
+                  placeholder="请输入备注"
+                  v-model="contactForm.remark"
+                  resize="none"
+                  :rows="5"
+                  maxlength="500"
+                  show-word-limit></el-input>
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button v-show="contactForm.id"
+                 type="primary"
+                 @click="contactEdit">保存</el-button>
+      <el-button v-show="!contactForm.id"
+                 type="primary"
+                 @click="contactSave">保存</el-button>
+      <el-button @click="contactVisible = false">取消</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import to from 'await-to-js'
+import api from '@/api/customer'
+export default {
+  data () {
+    var validateTel = (rule, value, callback) => {
+      const reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
+      if (value === '') {
+        callback(new Error('请输入手机号码'));
+      } else if (!reg.test(value)) {
+        callback(new Error('请输入正确手机号码'));
+      } else {
+        callback();
+      }
+    };
+    return {
+      title: '新增联系人',
+      contactVisible: false,
+      contactForm: {
+        custId: '',//客户ID  (必传) 
+        custName: '',//客户名称      
+        cuctName: '',//联系人名字
+        cuctGender: '10',//性别(10男20女)
+        postion: '',//职位
+        telephone: '',//电话
+        wechat: '',//微信
+        email: '',//邮箱
+        remark: '',//备注
+        policy: 0//是否决策人 0 不是  1 是
+      },
+      contactRules: {
+        cuctName: [{ required: true, trigger: 'blur', message: '请输入联系人姓名' }],
+        custName: [{ required: true, trigger: 'blur', message: '请输入客户名称' }],
+        telephone: [{ required: true, trigger: 'blur', validator: validateTel }]
+      }
+    }
+  },
+  methods: {
+    // 联系人新建
+    async contactSave () {
+      let params = { ...this.contactForm }
+      const [err, res] = await to(api.createContact(params))
+      if (err) return
+      this.$message.success(res.msg)
+      this.contactVisible = false
+      this.$emit('contactSave')
+    },
+    // 联系人编辑
+    async contactEdit () {
+      let params = { ...this.contactForm }
+      const [err, res] = await to(api.updateContact(params))
+      if (err) return
+      this.$message.success(res.msg)
+      this.contactVisible = false
+      this.$emit('contactSave')
+    },
+    contactClose () {
+      this.$refs.contactForm.resetFields()
+      this.contactForm.custId = ''
+    },
+  }
+}
+</script>
+
+<style>
+</style>

+ 315 - 0
src/views/customer/components/Edit.vue

@@ -0,0 +1,315 @@
+<template>
+  <el-dialog :visible.sync="editVisible"
+             @close="handleClose"
+             :title="title">
+    <el-form ref="editForm"
+             :model="editForm"
+             :rules="editRules">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="客户名称"
+                        prop="custName">
+            <el-input placeholder="请输入客户名称"
+                      v-model="editForm.custName"></el-input>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="助记名"
+                        prop="abbrName">
+            <el-input placeholder="请输入助记名"
+                      v-model="editForm.abbrName"></el-input>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="客户行业"
+                        prop="custIndustry">
+            <el-select style="width:100%"
+                       placeholder="请选择客户行业"
+                       v-model="editForm.custIndustry">
+              <el-option v-for="item in industryOptions"
+                         :key="item.value"
+                         :label="item.value"
+                         :value="item.value"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="客户级别"
+                        prop="custLevel">
+            <el-select style="width:100%"
+                       placeholder="请选择客户级别"
+                       v-model="editForm.custLevel">
+              <el-option v-for="item in levelOptions"
+                         :key="item.value"
+                         :label="item.value"
+                         :value="item.value"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="客户来源"
+                        prop="source">
+            <el-select style="width:100%"
+                       placeholder="请选择客户来源"
+                       v-model="editForm.source">
+              <el-option v-for="item in sourceOptions"
+                         :key="item.value"
+                         :label="item.value"
+                         :value="item.value"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="下次联系时间"
+                        prop="followUpDate">
+            <el-date-picker style="width:100%"
+                            type="date"
+                            value-format="yyyy-MM-dd"
+                            placeholder="选择下次联系时间"
+                            v-model="editForm.followUpDate">
+            </el-date-picker>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="所在地区"
+                        required>
+            <el-row style="width:100%;padding-top:32px"
+                    :gutter="4">
+              <el-col :span="8">
+                <el-select placeholder="省"
+                           value-key="id"
+                           @change="provinceChange"
+                           v-model="editForm.province">
+                  <el-option v-for="item in provinceOptions"
+                             :key="item.id"
+                             :label="item.distName"
+                             :value="item"></el-option>
+                </el-select>
+              </el-col>
+              <el-col :span="8">
+                <el-select placeholder="市"
+                           value-key="id"
+                           @change="cityChange"
+                           v-model="editForm.city">
+                  <el-option v-for="item in (editForm.province ? editForm.province.children : [])"
+                             :key="item.id"
+                             :label="item.distName"
+                             :value="item"></el-option>
+                </el-select>
+              </el-col>
+              <el-col :span="8">
+                <el-select placeholder="区"
+                           value-key="id"
+                           @change="$forceUpdate()"
+                           v-model="editForm.region">
+                  <el-option v-for="item in (editForm.city ? editForm.city.children : [])"
+                             :key="item.id"
+                             :label="item.distName"
+                             :value="item"></el-option>
+                </el-select>
+              </el-col>
+            </el-row>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="详细地址"
+                        prop="custAddress">
+            <el-input placeholder="请输入详细地址"
+                      v-model="editForm.custAddress"></el-input>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="备注"
+                    prop="remark">
+        <el-input type="textarea"
+                  placeholder="请输入备注"
+                  v-model="editForm.remark"
+                  resize="none"
+                  :rows="5"
+                  maxlength="500"
+                  show-word-limit></el-input>
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button type="primary"
+                 @click="customerSave"
+                 v-show="!editForm.id">保存</el-button>
+      <el-button type="primary"
+                 @click="customerEdit"
+                 v-show="editForm.id">保存</el-button>
+      <el-button @click="createContact"
+                 v-if="!editForm.id">保存并新建联系人</el-button>
+      <el-button @click="editVisible = false">取消</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import to from 'await-to-js'
+import api from '@/api/customer'
+export default {
+  data () {
+    return {
+      title: '新增客户信息',
+      // 新增编辑客户弹窗
+      editVisible: false,
+      editForm: {
+        custName: '',    // 客户名称
+        abbrName: '',     // 助记名
+        custLocation: '', // 所在地区
+        custAddress: '',  // 详细地址
+        custStatus: '',   // 客户状态(10正常20)
+        followUpDate: '', // 最后跟进时间
+        custIndustry: '',  // 客户行业  (没数据)
+        custLevel: '',    // 客户级别  (没数据)
+        source: '',//客户来源
+        province: '',//省
+        city: '',//市
+        region: '', //区
+      },
+      editRules: {
+        custName: [{ required: true, trigger: 'blur', message: '请输入客户名称' }],
+        custIndustry: [{ required: true, trigger: 'change', message: '请选择客户行业' }],
+        custLevel: [{ required: true, trigger: 'change', message: '请选择客户级别' }],
+        source: [{ required: true, trigger: 'change', message: '请选择客户级别' }],
+      },
+      provinceOptions: [],
+      provinceDetail: [],
+      industryOptions: [], //客户行业
+      levelOptions: [],//客户级别
+      sourceOptions: [],//客户来源
+
+    }
+  },
+  mounted () {
+    this.getOptions()
+  },
+  methods: {
+    async init (ids) {
+      if (!ids) {
+        this.title = '新增客户信息'
+        this.editVisible = true
+        return
+      }
+      this.title = '编辑客户'
+      const [err, res] = await to(api.getDetail({ ids }))
+      if (err) return
+      if (res.data.list[0]) this.editForm = res.data.list[0]
+      else return
+      this.editVisible = true
+      this.showLocation()
+    },
+    getOptions () {
+      Promise.all([
+        api.getProvinceDetail(),
+        this.getDicts('CustomerLevel'),
+        this.getDicts('CustomerIndustry'),
+        this.getDicts('CustomerSource'),
+      ]).then(([province, level, industry, source]) => {
+        this.provinceOptions = province.data.list || []
+        this.levelOptions = level.data.values || []
+        this.industryOptions = industry.data.values || []
+        this.sourceOptions = source.data.values || []
+      }).catch(err => console.log(err))
+    },
+    // 保存客户
+    async customerSave () {
+      let params = { ...this.editForm }
+      const [valid] = await to(this.$refs.editForm.validate())
+      if (valid == false) return
+      if (!params.province.id) return this.$message.warning('请选择所在地区')
+      let arr = []
+      arr.push(params.province.distName)
+      if (params.city.id) arr.push(params.city.distName)
+      if (params.region.id) arr.push(params.region.distName)
+      params.custLocation = arr.join('/')
+      if (!params.followUpDate) params.followUpDate = null
+      const [err, res] = await to(api.createCustomer(params))
+      if (err) return
+      if (res.code == 200) this.$message.success(res.msg)
+      else return
+      this.editVisible = false
+      this.$emit('customerSave')
+      return {
+        id: res.data.lastId,
+        name: params.custName
+      }
+    },
+    // 编辑客户
+    async customerEdit () {
+      let params = { ...this.editForm }
+      const [valid] = await to(this.$refs.editForm.validate())
+      if (valid == false) return
+      if (!params.province.id) return this.$message.warning('请选择所在地区')
+      let arr = []
+      arr.push(params.province.distName)
+      if (params.city.id) arr.push(params.city.distName)
+      if (params.region.id) arr.push(params.region.distName)
+      params.custLocation = arr.join('/')
+      if (!params.followUpDate) params.followUpDate = null
+      const [err, res] = await to(api.updateCostomer(params))
+      if (err) return
+      if (res.code == 200) this.$message.success(res.msg)
+      else return
+      this.editVisible = false
+      this.$emit('customerSave')
+    },
+    // 联系人弹窗
+    async createContact () {
+      const res = await this.customerSave()
+      if (!res) return
+      this.$emit('createContact', res)
+    },
+    // 省份改变
+    async handleProvinceChange (val) {
+      const [err, res] = await to(api.getProvinceDetail({ Id: val.id }))
+      if (err) return
+      this.provinceDetail = res.data.list || []
+    },
+    handleClose () {
+      this.editForm = {
+        custName: '',    // 客户名称
+        abbrName: '',     // 助记名
+        custLocation: '', // 所在地区
+        custAddress: '',  // 详细地址
+        custStatus: '',   // 客户状态(10正常20)
+        followUpDate: '', // 最后跟进时间
+        custIndustry: '',  // 客户行业  (没数据)
+        custLevel: '',    // 客户级别  (没数据)
+        source: '',//客户来源
+        province: '',//省
+        city: '',//市
+        region: '', //区
+      }
+      this.$refs.editForm.resetFields()
+    },
+    showLocation () {
+      const arr = this.editForm.custLocation.split('/')
+      console.log(arr, 'arr');
+      if (!arr.length) return
+      this.editForm.province = this.provinceOptions.find(item => item.distName == arr[0])
+      if (arr[1]) this.editForm.city = this.editForm.province.children.find(item => item.distName == arr[1])
+      if (arr[2]) this.editForm.region = this.editForm.city.children.find(item => item.distName == arr[2])
+    },
+    provinceChange (val) {
+      this.editForm.city = {}
+      this.editForm.region = {}
+      this.editForm.custDistCode = val.id
+      this.$forceUpdate()
+    },
+    cityChange () {
+      this.editForm.region = {}
+      this.$forceUpdate()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 189 - 0
src/views/customer/components/Merge.vue

@@ -0,0 +1,189 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-27 09:33:48
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-28 14:53:47
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\components\Merge.vue
+-->
+<template>
+  <el-dialog :visible.sync="visible"
+             @close="handleClose"
+             title="合并客户">
+    <el-alert type="warning"
+              :closable="false"
+              show-icon>
+      <div slot="title">
+        <h3>特别提示</h3>
+        <p>1、合并后系统只保留目标客户,同时将另一个客户的联系人、商机、订单、附件、销售动态等迁移到目标客户。</p>
+        <p>2、红色字段表示两个客户该字段值不同。</p>
+      </div>
+    </el-alert>
+    <div class="merge">
+      <ul class="title">
+        <li :class="{differ:flag.custName}">目标客户</li>
+        <li :class="{differ:flag.abbrName}">助记名</li>
+        <li :class="{differ:flag.custIndustry}">客户行业</li>
+        <li :class="{differ:flag.custLevel}">客户级别</li>
+        <li :class="{differ:flag.source}">客户来源</li>
+        <li :class="{differ:flag.followUpDate}">下次联系时间</li>
+        <li :class="{differ:flag.custLocation}">所在地区</li>
+        <li :class="{differ:flag.custAddress}">详细地址</li>
+        <li :class="{differ:flag.remark}">备注</li>
+      </ul>
+      <ul class="each"
+          v-for="(item, index) in list"
+          :key="index">
+        <li>
+          <el-radio v-model="form.custName"
+                    :label="item.custName"
+                    @change="targetChange(item)"></el-radio>
+        </li>
+        <li>
+          <el-radio v-model="form.abbrName"
+                    :label="item.abbrName"></el-radio>
+        </li>
+        <li>
+          <el-radio v-model="form.custIndustry"
+                    :label="item.custIndustry"></el-radio>
+        </li>
+        <li>
+          <el-radio v-model="form.custLevel"
+                    :label="item.custLevel"></el-radio>
+        </li>
+        <li>
+          <el-radio v-model="form.source"
+                    :label="item.source"></el-radio>
+        </li>
+        <li>
+          <el-radio v-model="form.followUpDate"
+                    :label="item.followUpDate"></el-radio>
+        </li>
+        <li>
+          <el-radio v-model="form.custLocation"
+                    :label="item.custLocation"></el-radio>
+        </li>
+        <li>
+          <el-radio v-model="form.custAddress"
+                    :label="item.custAddress"></el-radio>
+        </li>
+        <li>
+          <el-radio v-model="form.remark"
+                    :label="item.remark"></el-radio>
+        </li>
+      </ul>
+    </div>
+    <span slot="footer">
+      <el-button @click="visible = false">取消</el-button>
+      <el-button type="primary"
+                 @click="handleConfirm">合并</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+import api from '@/api/customer'
+import to from 'await-to-js'
+export default {
+  data () {
+    return {
+      visible: false,
+      ids: [],
+      form: {
+        id: '',
+        custName: '',    // 客户名称
+        abbrName: '',     // 助记名
+        custLocation: '', // 所在地区
+        custAddress: '',  // 详细地址
+        custStatus: '',   // 客户状态(10正常20)
+        followUpDate: '', // 最后跟进时间
+        custIndustry: '',  // 客户行业  (没数据)
+        custLevel: '',    // 客户级别  (没数据)
+        source: '',//客户来源
+      },
+      flag: {
+        custName: false,    // 客户名称
+        abbrName: false,     // 助记名
+        custLocation: false, // 所在地区
+        custAddress: false,  // 详细地址
+        custStatus: false,   // 客户状态(10正常20)
+        followUpDate: false, // 最后跟进时间
+        custIndustry: false,  // 客户行业  (没数据)
+        custLevel: false,    // 客户级别  (没数据)
+        source: false,//客户来源
+      },
+      list: []
+    }
+  },
+  mounted () {
+  },
+  methods: {
+    init (res, ids) {
+      this.list = res.data.list
+      this.ids = ids
+      this.form = { ...res.data.list[0] }
+      this.form.ChooseId = this.ids.filter(item => item != this.form.id)
+      for (const key in this.form) {
+        const arr = this.list.filter(it => it[key] == this.form[key])
+        if (arr.length !== this.list.length) this.flag[key] = true
+      }
+      this.visible = true
+    },
+    targetChange (row) {
+      this.form.id = row.id
+      this.form.ChooseId = this.ids.filter(item => item != row.id)
+    },
+    handleClose () {
+
+    },
+    async handleConfirm () {
+      let params = { ...this.form }
+      const [err, res] = await to(api.mergeCustomer(params))
+      if (err) return
+      this.$message.success(res.msg)
+      this.visible = false
+      this.$emit('refresh')
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.differ {
+  color: #f56c6c;
+}
+::v-deep .el-alert {
+  p {
+    margin: 0;
+    font-weight: normal;
+  }
+  .el-alert__icon {
+    font-size: 28px;
+    width: 28px;
+  }
+}
+.merge {
+  height: 361px;
+  overflow-y: auto;
+  display: flex;
+  ul {
+    border: 1px solid #dcdfe6;
+    & + ul {
+      border-left: none;
+    }
+  }
+  li {
+    height: 40px;
+    line-height: 40px;
+    border-bottom: 1px solid #dcdfe6;
+  }
+  .title {
+    width: 100px;
+    text-align: center;
+  }
+  .each {
+    flex: 1;
+    text-align: center;
+  }
+}
+</style>

+ 84 - 0
src/views/customer/components/Shift.vue

@@ -0,0 +1,84 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-26 17:21:07
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-26 17:45:03
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\components\Shift.vue
+-->
+<template>
+  <el-dialog :visible.sync="visible"
+             width="30%"
+             @close="handleClose"
+             title="转移客户">
+    <Transfer ref="transfer"></Transfer>
+    <el-form :model="form"
+             :rules="rules"
+             ref="form"
+             label-width="80px">
+      <el-form-item label="接收对象"
+                    prop="allocate">
+        <el-input readonly
+                  v-model="form.allocate">
+          <el-button slot="append"
+                     icon="el-icon-search"
+                     @click="choose"></el-button>
+        </el-input>
+      </el-form-item>
+      <el-form-item label="转移相关"
+                    prop="about">
+        <el-checkbox-group v-model="form.about">
+          <el-checkbox label="客户"></el-checkbox>
+        </el-checkbox-group>
+      </el-form-item>
+      <el-form-item label="备注信息"
+                    prop="remark">
+        <el-input v-model="form.remark"
+                  type="textarea"
+                  :rows="5"
+                  maxlength="500"
+                  show-word-limit
+                  resize="none"></el-input>
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button size="mini"
+                 @click="visible = false">取消</el-button>
+      <el-button size="mini"
+                 type="primary">确定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+import Transfer from './Transfer.vue'
+export default {
+  data () {
+    return {
+      visible: false,
+      form: {
+        allocate: '',
+        about: ['客户'],
+        remark: ''
+      },
+      rules: {
+        allocate: [{ required: true, message: '请选择接收对象', trigger: 'change' }],
+        about: [{ required: true, message: '请选择转移相关', trigger: 'change' }]
+      }
+    }
+  },
+  components: {
+    Transfer
+  },
+  methods: {
+    handleClose () {
+      this.$refs.form.resetFields()
+    },
+    choose () {
+      this.$refs.transfer.innerVisible = true
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 66 - 0
src/views/customer/components/ToOpen.vue

@@ -0,0 +1,66 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-26 17:21:07
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-28 14:10:30
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\components\ToOpen.vue
+-->
+<template>
+  <el-dialog :visible.sync="visible"
+             width="30%"
+             @close="handleClose"
+             title="转移客户">
+    <el-form :model="form"
+             ref="form"
+             label-width="100px">
+      <el-form-item label="移入公海原因"
+                    prop="remark">
+        <el-input v-model="form.remark"
+                  type="textarea"
+                  :rows="5"
+                  maxlength="300"
+                  show-word-limit
+                  resize="none"></el-input>
+        <span>* 转移到公海后此客户数据将属于公共资源,原归属人员不能再维护跟进和更新此客户数据。</span>
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button size="mini"
+                 @click="visible = false">取消</el-button>
+      <el-button size="mini"
+                 type="primary"
+                 @click="handleConfirm">确定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+import to from 'await-to-js'
+import api from '@/api/customer'
+export default {
+  data () {
+    return {
+      visible: false,
+      form: {
+        ids: [],
+        remark: ''
+      }
+    }
+  },
+  methods: {
+    handleClose () {
+      this.$refs.form.resetFields()
+    },
+    async handleConfirm () {
+      const [err, res] = await to(api.moveToPubic({ ...this.form }))
+      if (err) return
+      this.$message.success(res.msg)
+      this.visible = false
+      this.$emit('refresh')
+    }
+  }
+}
+</script>
+
+<style>
+</style>

+ 150 - 0
src/views/customer/components/Transfer.vue

@@ -0,0 +1,150 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-26 15:00:19
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-26 17:23:50
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\components\Transfer.vue
+-->
+<template>
+  <el-dialog width="40%"
+             title="选择销售工程师"
+             :visible.sync="innerVisible"
+             append-to-body>
+    <el-row class="transfer">
+      <el-col :span="12">
+        <header>
+          <el-input v-model="keyword"
+                    placeholder="请输入关键字"
+                    suffix-icon="el-icon-search"
+                    clearable></el-input>
+        </header>
+        <el-dropdown>
+          <span class="el-dropdown-link">
+            按字母顺序查看<i class="el-icon-arrow-down el-icon--right"></i>
+          </span>
+          <el-dropdown-menu slot="dropdown">
+            <el-dropdown-item>按创建顺序查看</el-dropdown-item>
+          </el-dropdown-menu>
+        </el-dropdown>
+        <ul class="options">
+          <li v-for="(item,index) in options"
+              :key="index">
+            <span>{{item.name}}</span>
+            <i class="el-icon-arrow-right"
+               @click="transfer(index)"></i>
+          </li>
+        </ul>
+      </el-col>
+      <el-col :span="12">
+        <header>
+          <span>已选: {{selected.length}}个员工</span>
+          <el-button type="text"
+                     :disabled="selected.length == 0"
+                     @click="clear">清空</el-button>
+        </header>
+        <ul class="options">
+          <li v-for="(item,index) in selected"
+              :key="index">
+            <span>{{item.name}}</span>
+          </li>
+        </ul>
+      </el-col>
+    </el-row>
+    <span slot="footer">
+      <el-button type="primary"
+                 size="mini">保存</el-button>
+      <el-button size="mini"
+                 @click="innerVisible = false">取消</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  data () {
+    return {
+      innerVisible: false,
+      keyword: '',
+      options: [],
+      selected: []
+    }
+  },
+  mounted () {
+    this.options = [
+      {
+        name: 'wlj'
+      }
+    ]
+  },
+  methods: {
+    transfer (index) {
+      const arr = this.options.splice(index, 1)
+      if (arr[0]) this.selected.push(arr[0])
+    },
+    clear () {
+      this.selected = []
+      this.options = [
+        {
+          name: 'wlj'
+        }
+      ]
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.transfer {
+  height: 500px;
+  border: 1px solid #ebeef5;
+  .el-col {
+    height: 100%;
+    &:first-child {
+      border-right: 1px solid #ebeef5;
+    }
+    .el-dropdown {
+      height: 50px;
+      line-height: 50px;
+      margin: 0 8px;
+      width: calc(100% - 16px);
+      border-bottom: 1px solid #ebeef5;
+      span {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+      }
+    }
+    .options {
+      margin: 0;
+      padding: 0 10px;
+      list-style: none;
+      height: 400px;
+      overflow-y: auto;
+      li {
+        height: 50px;
+        line-height: 50px;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        & i {
+          transition: all 0.3s;
+          cursor: pointer;
+          &:hover {
+            color: #1d66dc;
+            font-weight: bold;
+          }
+        }
+      }
+    }
+  }
+  header {
+    height: 50px;
+    padding: 9px 8px;
+    border-bottom: 1px solid #ebeef5;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+}
+</style>

+ 517 - 0
src/views/customer/detail.vue

@@ -0,0 +1,517 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-26 09:30:47
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-28 16:28:54
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\detail.vue
+-->
+<template>
+  <div class="detail">
+    <el-row :gutter="10">
+      <el-col :span="16">
+        <div class="title">
+          <p>客户</p>
+          <h3>
+            {{detail.custName}}
+            <span>
+              <template v-if="private == 1">
+                <el-button size="mini"
+                           @click="handleShift">转移客户</el-button>
+                <el-button size="mini"
+                           @click="handleToOpen">移入公海</el-button>
+                <el-button size="mini">创建项目</el-button>
+              </template>
+              <template v-else>
+                <el-button size="mini"
+                           @click="handleReceive">领取客户</el-button>
+                <el-button size="mini"
+                           @click="$refs.allocate.visible = true">分配客户</el-button>
+              </template>
+            </span>
+          </h3>
+        </div>
+        <header>
+          <el-descriptions :column="6"
+                           direction="vertical"
+                           :colon="false">
+            <el-descriptions-item label="客户编码"
+                                  label-class-name="my-label"
+                                  content-class-name="my-content">{{detail.custCode}}</el-descriptions-item>
+            <el-descriptions-item label="助记名"
+                                  label-class-name="my-label"
+                                  content-class-name="my-content">{{detail.abbrName}}</el-descriptions-item>
+            <el-descriptions-item label="客户级别"
+                                  label-class-name="my-label"
+                                  content-class-name="my-content">{{detail.custLevel}}</el-descriptions-item>
+            <el-descriptions-item label="客户行业"
+                                  label-class-name="my-label"
+                                  content-class-name="my-content">{{detail.custIndustry}}</el-descriptions-item>
+            <el-descriptions-item label="客户状态"
+                                  label-class-name="my-label"
+                                  content-class-name="my-content">{{detail.custStatus == 10 ? '正常' : '异常'}}</el-descriptions-item>
+            <el-descriptions-item label="最后跟进时间"
+                                  label-class-name="my-label"
+                                  content-class-name="my-content">{{detail.followUpDate}}</el-descriptions-item>
+          </el-descriptions>
+        </header>
+        <el-tabs v-model="activeName"
+                 @tab-click="handleClick">
+          <el-tab-pane label="详细信息"
+                       name="first">详细信息</el-tab-pane>
+          <el-tab-pane label="联系人"
+                       name="second">
+            <vab-query-form>
+              <vab-query-form-left-panel :span="12">
+                <el-input placeholder="请输入单据名称/编号"
+                          prefix-icon="el-icon-search"
+                          style="width:50%"></el-input>
+              </vab-query-form-left-panel>
+              <vab-query-form-right-panel :span="12">
+                <el-button icon="el-icon-plus"
+                           @click="addContact">
+                  新建联系人
+                </el-button>
+              </vab-query-form-right-panel>
+            </vab-query-form>
+            <el-table v-loading="listLoading"
+                      :data="contactList"
+                      border
+                      height="calc(100% - 42px)"
+                      @selection-change="setSelectRows">
+              <el-table-column type="selection"
+                               align="center" />
+              <el-table-column align="center"
+                               label="姓名"
+                               prop="cuctName"></el-table-column>
+              <el-table-column align="center"
+                               label="岗位"
+                               prop="postion"></el-table-column>
+              <el-table-column align="center"
+                               label="电话"
+                               prop="telephone"></el-table-column>
+              <el-table-column align="center"
+                               label="微信"
+                               prop="wechat"></el-table-column>
+              <el-table-column align="center"
+                               label="邮箱"
+                               prop="email"></el-table-column>
+              <el-table-column align="center"
+                               label="是否决策人">
+                <template slot-scope="scope">
+                  <el-switch v-model="scope.row.policy"
+                             disabled
+                             :active-value="1"
+                             :inactive-value="0"></el-switch>
+                </template>
+              </el-table-column>
+              <el-table-column align="center"
+                               label="操作">
+                <template slot-scope="scope">
+                  <el-button type="text"
+                             @click="contactEdit(scope.row)">编辑</el-button>
+                  <el-button type="text"
+                             @click="contactDel(scope.row)">删除</el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-tab-pane>
+          <el-tab-pane label="项目记录"
+                       name="third">项目记录</el-tab-pane>
+          <el-tab-pane label="合同记录"
+                       name="fourth">合同记录</el-tab-pane>
+          <el-tab-pane label="工单记录"
+                       name="fifth">工单记录</el-tab-pane>
+          <el-tab-pane label="归属记录"
+                       name="sixth">
+            <el-table v-loading="listLoading"
+                      :data="belongs"
+                      border
+                      height="calc(100% - 42px)">
+              <el-table-column align="center"
+                               label="归属销售"
+                               prop="saleName"></el-table-column>
+              <el-table-column align="center"
+                               label="原来归属"
+                               prop="origSaleName"></el-table-column>
+              <el-table-column align="center"
+                               label="操作方式"
+                               prop="opnType">
+                <template slot-scope="scope">
+                  {{scope.row.opnType == 10 ? '分配' : '转移'}}
+                </template>
+              </el-table-column>
+              <el-table-column align="center"
+                               label="操作人"
+                               prop="opnPeople"></el-table-column>
+              <el-table-column align="center"
+                               label="操作时间"
+                               prop="opnDatetime"></el-table-column>
+              <el-table-column align="center"
+                               label="备注"
+                               prop="remark">
+              </el-table-column>
+            </el-table>
+          </el-tab-pane>
+        </el-tabs>
+      </el-col>
+      <el-col :span="8">
+        <div class="buttons">
+          <el-button type="primary"
+                     @click="handleEdit">编辑</el-button>
+          <el-button @click="handleDelete">删除</el-button>
+        </div>
+        <ul class="records">
+          <li v-for="(value, key) in records"
+              :key="key">
+            <div class="date">
+              <h2>{{key.split('-')[2]}}</h2>
+              <h3>{{key.split('-').splice(0, 2).join('.')}}</h3>
+            </div>
+            <ul class="content">
+              <li v-for="item in records[key]">
+                <!-- <el-avatar class="user-avatar"
+                           :src="avatar" /> -->
+                <vab-icon class="user-avatar"
+                          icon="account-circle-fill" />
+                <div class="text">
+                  <p class="action">{{item.opnPeople}} {{item.opnType}}</p>
+                  <p>{{item.opnDate}}</p>
+                  <p v-if="item.opnContent.custName">客户名称:<span>{{item.opnContent.custName}}</span></p>
+                  <template v-else-if="item.opnContent.cuctName">
+                    <p>联系人名称:<span>{{item.opnContent.cuctName}}</span></p>
+                    <p>职务:{{item.opnContent.postion}}</p>
+                    <p>手机:{{item.opnContent.telephone}}</p>
+                  </template>
+                </div>
+              </li>
+            </ul>
+          </li>
+        </ul>
+      </el-col>
+    </el-row>
+    <Contact ref="contact"
+             @contactSave="contactSave"></Contact>
+    <Edit ref="edit"
+          @customerSave="customerSave"></Edit>
+    <!-- 分配客户 -->
+    <Allocate ref="allocate"></Allocate>
+    <!-- 转移客户 -->
+    <Shift ref="shift"></Shift>
+    <!-- 移入公海 -->
+    <ToOpen ref="toOpen"
+            @refresh="back"></ToOpen>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import api from '@/api/customer'
+import to from 'await-to-js'
+import Contact from './components/Contact'
+import Edit from './components/Edit'
+import Allocate from './components/Allocate'
+import Shift from './components/Shift'
+import ToOpen from './components/ToOpen'
+export default {
+  data () {
+    return {
+      id: '',
+      private: '',
+      detail: {
+        custCode: '',//客户编码
+        abbrName: '',//助记名
+        level: '', //客户级别
+        indusTry: '',//客户行业
+        custStatus: '',//客户状态
+        followUpDate: ''//最后跟进时间
+      },
+      activeName: 'first',
+      listLoading: false,
+      contactList: [],
+      selectRows: [],
+      records: [],//操作记录
+      belongs: []
+    }
+  },
+  components: {
+    Edit,
+    Contact,
+    Allocate,
+    Shift,
+    ToOpen
+  },
+  computed: {
+    ...mapGetters({
+      avatar: 'user/avatar',
+      username: 'user/username',
+    }),
+  },
+  mounted () {
+    this.id = this.$route.query.id
+    this.private = this.$route.query.private
+    this.init()
+    this.getDynamics()
+  },
+  methods: {
+    async init () {
+      const [err, res] = await to(api.getDetail({ ids: [parseInt(this.id)] }))
+      if (err) return
+      if (res.data.list[0]) this.detail = res.data.list[0]
+    },
+    async getDynamics () {
+      const [err, res] = await to(api.dynamicsList({ custId: parseInt(this.id) }))
+      if (err) return
+      if (res.data.list[0]) {
+        let obj = res.data.list[0]
+        for (const key in obj) {
+          for (const item of obj[key]) {
+            item.opnContent = JSON.parse(item.opnContent)
+          }
+        }
+        this.records = obj
+      }
+    },
+    setSelectRows (val) {
+      this.selectRows = val
+    },
+    async handleClick (tab) {
+      let err, res
+      if (tab.name == 'second') {
+        [err, res] = await to(api.getContact({ custId: parseInt(this.id) }))
+        if (err) return
+        this.contactList = res.data.list || []
+      } else if (tab.name == 'sixth' && this.belongs.length == 0) {
+        [err, res] = await to(api.getBelongs({ Id: parseInt(this.id) }))
+        if (err) return
+        this.belongs = res.data.list || []
+      }
+    },
+    // 添加联系人
+    addContact () {
+      this.$refs.contact.contactForm.custId = this.detail.id
+      this.$refs.contact.contactForm.custName = this.detail.custName
+      this.$refs.contact.contactVisible = true
+    },
+    // 保存联系人
+    contactSave () {
+      this.handleClick({ name: 'second' })
+      this.getDynamics()
+    },
+    // 编辑客户
+    handleEdit () {
+      this.$refs.edit.title = '编辑客户'
+      this.$refs.edit.editForm = { ...this.detail }
+      this.$refs.edit.editVisible = true
+      this.$refs.edit.showLocation()
+    },
+    // 编辑联系人
+    contactEdit (row) {
+      this.$refs.contact.contactForm = { ...row }
+      this.$refs.contact.contactVisible = true
+    },
+    // 删除联系人
+    contactDel (row) {
+      this.$confirm('确认删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const [err, res] = await to(api.deleteContact({ Id: row.id, custId: parseInt(this.id) }))
+        if (err) return
+        if (res.code == 200) {
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          });
+          this.contactSave()
+        }
+      }).catch(() => {
+      })
+    },
+    // 转移客户
+    handleShift () {
+      this.$refs.shift.visible = true
+    },
+    // 移入公海
+    handleToOpen () {
+      this.$refs.toOpen.form.ids = [parseInt(this.id)]
+      this.$refs.toOpen.visible = true
+    },
+    // 客户删除
+    handleDelete () {
+      this.$confirm('确认删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const [err, res] = await to(api.deleteCustomer({ Id: parseInt(this.id) }))
+        if (err) return
+        if (res.code == 200) {
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          });
+          this.$router.go(-1)
+        }
+      }).catch(() => {
+      })
+    },
+    back () {
+      this.$router.push('/customer/openSea')
+    },
+    // 领取
+    handleReceive () {
+      this.$confirm('确认领取客户?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const [err, res] = await to(api.receiveCustomer({ ids: this.id }))
+        if (err) return
+        if (res.code == 200) {
+          this.$message({
+            type: 'success',
+            message: '领取成功!'
+          });
+        }
+      }).catch(() => {
+      })
+    },
+    customerSave () {
+      this.init()
+      this.getDynamics()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$base: '.detail';
+#{$base} {
+  height: calc(100vh - 60px - 50px - 12px * 2 - 40px);
+  display: flex;
+  padding: 20px 40px;
+  > .el-row {
+    flex: 1;
+    > .el-col {
+      height: 100%;
+    }
+  }
+  .title {
+    p,
+    h3 {
+      margin: 0;
+    }
+    p {
+      font-size: 14px;
+      font-weight: 400;
+      line-height: 22px;
+    }
+    h3 {
+      font-size: 24px;
+      font-weight: 500;
+      line-height: 36px;
+      color: #333;
+      display: flex;
+      justify-content: space-between;
+    }
+  }
+  header {
+    height: 74px;
+    background: rgba(196, 196, 196, 0.5);
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    padding: 0 20px;
+    margin-top: 16px;
+    ::v-deep .el-descriptions__body {
+      background: transparent;
+    }
+    ::v-deep .my-label {
+      font-size: 14px;
+      font-weight: 600;
+      color: #fff;
+    }
+    ::v-deep .my-content {
+      font-size: 14px;
+      font-weight: 600;
+      color: #333;
+    }
+  }
+  .el-tabs {
+    height: calc(100% - 148px);
+    display: flex;
+    flex-direction: column;
+    ::v-deep .el-tabs__content {
+      flex: 1;
+      .el-tab-pane {
+        height: 100%;
+      }
+    }
+  }
+  .buttons {
+    padding-top: 26px;
+    text-align: right;
+    height: 58px;
+  }
+  .records {
+    margin: 0;
+    padding: 10px 20px;
+    list-style: none;
+    height: calc(100% - 58px);
+    overflow-y: auto;
+    > li {
+      display: flex;
+      & + li {
+        margin-top: 10px;
+      }
+    }
+    .date {
+      width: 100px;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      h2,
+      h3 {
+        margin: 0;
+      }
+      h2 {
+        font-size: 26px;
+        line-height: 32px;
+      }
+    }
+    .content {
+      flex: 1;
+      list-style: none;
+      li {
+        display: flex;
+        & + li {
+          margin-top: 20px;
+        }
+      }
+      .user-avatar {
+        font-size: 40px;
+      }
+      .text {
+        padding-left: 20px;
+        p {
+          font-weight: 500;
+          margin: 0;
+          line-height: 20px;
+          span {
+            color: #1d66dc;
+          }
+        }
+        p:nth-child(2) {
+          margin-bottom: 10px;
+        }
+        .action {
+          font-weight: bold;
+          color: #333;
+        }
+      }
+    }
+  }
+}
+</style>

+ 152 - 0
src/views/customer/follow.vue

@@ -0,0 +1,152 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-15 15:38:21
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-28 18:01:16
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\follow.vue
+-->
+<template>
+  <div class="follow-container">
+    <vab-query-form>
+      <vab-query-form-left-panel :span="12">
+
+      </vab-query-form-left-panel>
+      <vab-query-form-right-panel :span="12">
+
+      </vab-query-form-right-panel>
+    </vab-query-form>
+    <div class="follow"></div>
+    <!-- 新增编辑客户弹窗 -->
+    <Edit ref="edit"
+          @createContact="createContact"
+          @customerSave="customerSave"></Edit>
+    <!-- 新建联系人弹窗 -->
+    <Contact ref="contact"></Contact>
+    <!-- 分配客户 -->
+    <Allocate ref="allocate"></Allocate>
+  </div>
+</template>
+
+<script>
+import to from 'await-to-js'
+import api from '@/api/customer'
+import Contact from './components/Contact'
+import Edit from './components/Edit'
+import Allocate from './components/Allocate'
+export default {
+  name: 'OpenSea',
+  data () {
+    return {
+      listLoading: false,
+      layout: 'total, sizes, prev, pager, next, jumper',
+      list: [],
+      total: 0,
+      queryForm: {
+        pageNum: 1,
+        pageSize: 10,
+        custCode: '',   // 客户编码
+        custName: '',    //客户名称
+        custIndustry: '',    // 客户行业  ()
+        custLevel: '',     //客户级别
+      },
+      selectRows: []
+    }
+  },
+  components: {
+    Contact,
+    Edit,
+    Allocate
+  },
+  mounted () {
+    this.fetchData()
+    this.getOptions()
+  },
+  methods: {
+    getOptions () {
+
+    },
+    async fetchData () {
+
+    },
+    reset () {
+      this.queryForm = {
+        pageNum: 1,
+        pageSize: 10
+      }
+      this.fetchData()
+    },
+    // 客户编辑
+    async handleEdit (row) {
+      this.$refs.edit.init([row.id])
+    },
+    // 客户详情
+    handleDetail (row) {
+      this.$router.push({
+        path: '/customer/detail',
+        query: {
+          id: row.id
+        }
+      })
+    },
+    // 客户删除
+    handleDelete (row) {
+      this.$confirm('确认删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const [err, res] = await to(api.deleteCustomer({ Id: row.id }))
+        if (err) return
+        if (res.code == 200) {
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          });
+          this.fetchData()
+        }
+      }).catch(() => {
+      })
+    },
+    // 联系人弹窗
+    createContact (res) {
+      this.$refs.contact.contactForm.custId = res.id
+      this.$refs.contact.contactForm.custName = res.name
+      this.$refs.contact.contactVisible = true
+    },
+    customerSave () {
+      this.fetchData()
+    },
+    handleClose (form) {
+      this.$refs[form].resetFields()
+    },
+    // 领取
+    handleReceive () {
+      if (!this.selectRows.length) return this.$message.warning('请选择客户')
+      const arr = this.selectRows.map(item => item.id)
+      this.$confirm('确认领取客户?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const [err, res] = await to(api.receiveCustomer({ ids: arr.join() }))
+        if (err) return
+        if (res.code == 200) {
+          this.$message({
+            type: 'success',
+            message: '领取成功!'
+          });
+        }
+      }).catch(() => {
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$base: '.follow';
+.follow {
+  height: calc(100vh - 228px);
+}
+</style>

+ 276 - 0
src/views/customer/list.vue

@@ -0,0 +1,276 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-26 16:34:37
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-28 16:01:12
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\list.vue
+-->
+<template>
+  <div class="list-container">
+    <el-tabs v-model="activeName"
+             @tab-click="handleClick">
+      <el-tab-pane label="全部客户"
+                   name="first"></el-tab-pane>
+      <el-tab-pane label="我的客户"
+                   name="second"></el-tab-pane>
+      <el-tab-pane label="下属的客户"
+                   name="third"></el-tab-pane>
+    </el-tabs>
+    <el-row :gutter="10"
+            style="margin-bottom:10px">
+      <el-col :span="4">
+        <el-input placeholder="客户编码"
+                  v-model="queryForm.custCode"></el-input>
+      </el-col>
+      <el-col :span="4">
+        <el-input placeholder="客户名称"
+                  v-model="queryForm.custName"></el-input>
+      </el-col>
+      <el-col :span="4">
+        <el-input placeholder="客户行业"
+                  v-model="queryForm.indusTry"></el-input>
+      </el-col>
+      <el-col :span="12">
+        <el-button icon="el-icon-plus"
+                   type="primary"
+                   @click="fetchData">查询</el-button>
+        <el-button icon="el-icon-refresh-right"
+                   @click="reset">重置</el-button>
+      </el-col>
+    </el-row>
+    <vab-query-form>
+      <vab-query-form-left-panel :span="12">
+        <el-button size="mini"
+                   type="primary"
+                   icon="el-icon-plus"
+                   @click="$refs.edit.init()">新建</el-button>
+        <el-button size="mini"
+                   type="primary"
+                   icon="el-icon-plus"
+                   @click="handleShift">转移客户</el-button>
+        <el-button size="mini"
+                   type="primary"
+                   icon="el-icon-plus"
+                   @click="handleToOpen">移入公海</el-button>
+        <el-button size="mini"
+                   type="primary"
+                   icon="el-icon-plus"
+                   @click="handleMerge">合并客户</el-button>
+      </vab-query-form-left-panel>
+      <vab-query-form-right-panel :span="12">
+        <el-button icon="el-icon-download"></el-button>
+        <el-button icon="el-icon-setting"></el-button>
+      </vab-query-form-right-panel>
+    </vab-query-form>
+    <el-table v-loading="listLoading"
+              :data="list"
+              border
+              height="calc(100vh - 394px)"
+              @selection-change="setSelectRows">
+      <el-table-column show-overflow-tooltip
+                       type="selection"
+                       align="center" />
+      <el-table-column align="center"
+                       label="客户编码"
+                       prop="custCode"></el-table-column>
+      <el-table-column align="center"
+                       label="客户名称"
+                       prop="custName"></el-table-column>
+      <el-table-column align="center"
+                       label="助记名"
+                       prop="abbrName"></el-table-column>
+      <el-table-column align="center"
+                       label="所在地区"
+                       prop="custLocation"></el-table-column>
+      <el-table-column align="center"
+                       label="客户行业"
+                       prop="custIndustry"></el-table-column>
+      <el-table-column align="center"
+                       label="客户级别"
+                       prop="custLevel"></el-table-column>
+      <el-table-column align="center"
+                       label="客户状态"
+                       prop="custStatus">
+        <template slot-scope="scope">
+          {{scope.row.custStatus == 10 ? '正常' : '异常'}}
+        </template>
+      </el-table-column>
+      <el-table-column align="center"
+                       label="最后跟进时间"
+                       prop="followUpDate"></el-table-column>
+      <el-table-column align="center"
+                       label="创建人"
+                       prop="createdName"></el-table-column>
+      <el-table-column align="center"
+                       label="创建时间"
+                       prop="createdTime"></el-table-column>
+      <el-table-column align="center"
+                       label="操作">
+        <template slot-scope="scope">
+          <el-button type="text"
+                     @click="handleEdit(scope.row)">编辑</el-button>
+          <el-button type="text"
+                     @click="handleDetail(scope.row)">详情</el-button>
+          <el-button type="text"
+                     @click="handleDelete(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination background
+                   :current-page="queryForm.pageNum"
+                   :layout="layout"
+                   :page-size="queryForm.pageSize"
+                   :total="total"
+                   @current-change="handleCurrentChange"
+                   @size-change="handleSizeChange" />
+    <!-- 新增编辑客户弹窗 -->
+    <Edit ref="edit"
+          @createContact="createContact"
+          @customerSave="fetchData"></Edit>
+    <!-- 新建联系人弹窗 -->
+    <Contact ref="contact"></Contact>
+    <!-- 转移客户 -->
+    <Shift ref="shift"></Shift>
+    <!-- 移入公海 -->
+    <ToOpen ref="toOpen"
+            @refresh="fetchData"></ToOpen>
+    <!-- 合并客户 -->
+    <Merge ref="merge"
+           @refresh="fetchData"></Merge>
+  </div>
+</template>
+
+<script>
+import to from 'await-to-js'
+import api from '@/api/customer'
+import Edit from './components/Edit'
+import Contact from './components/Contact'
+import Shift from './components/Shift'
+import ToOpen from './components/ToOpen'
+import Merge from './components/Merge'
+export default {
+  data () {
+    return {
+      activeName: 'first',
+      layout: 'total, sizes, prev, pager, next, jumper',
+      queryForm: {
+        custCode: '',
+        custName: '',
+        indusTry: '',
+        pageNum: 1,
+        pageSize: 10
+      },
+      total: 0,
+      listLoading: false,
+      list: [],
+      selectRows: []
+    }
+  },
+  components: {
+    Edit,
+    Contact,
+    Shift,
+    ToOpen,
+    Merge
+  },
+  mounted () {
+    this.fetchData()
+  },
+  methods: {
+    handleClick (tab) {
+      console.log(tab, 'tab');
+      this.fetchData()
+    },
+    async fetchData () {
+      this.listLoading = true
+      const params = { ...this.queryForm }
+      const [err, res] = await to(api.getList(params))
+      if (err) return this.listLoading = false
+      this.list = res.data.list || []
+      this.total = res.data.total
+      this.listLoading = false
+    },
+    reset () {
+      this.queryForm = {
+        pageNum: 1,
+        pageSize: 10,
+        custCode: '',   // 客户编码
+        custName: '',    //客户名称
+        indusTry: ''    // 客户行业  ()
+      }
+      this.fetchData()
+    },
+    // 客户编辑
+    handleEdit (row) {
+      this.$refs.edit.init([row.id])
+    },
+    // 联系人弹窗
+    async createContact () {
+      this.$refs.contact.contactForm.custId = res.id
+      this.$refs.contact.contactForm.custName = res.name
+      this.$refs.contact.contactVisible = true
+    },
+    // 客户详情
+    handleDetail (row) {
+      this.$router.push({
+        path: '/customer/detail',
+        query: {
+          id: row.id,
+          private: 1
+        }
+      })
+    },
+    // 客户删除
+    handleDelete (row) {
+      this.$confirm('确认删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const [err, res] = await to(api.deleteCustomer({ Id: row.id }))
+        if (err) return
+        if (res.code == 200) {
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          });
+        }
+      }).catch(() => {
+      })
+    },
+    handleSizeChange (val) {
+      this.queryForm.pageSize = val
+      this.fetchData()
+    },
+    handleCurrentChange (val) {
+      this.queryForm.pageNum = val
+      this.fetchData()
+    },
+    setSelectRows (val) {
+      this.selectRows = val
+    },
+    // 转移客户
+    handleShift () {
+      this.$refs.shift.visible = true
+    },
+    // 移入公海
+    handleToOpen () {
+      if (!this.selectRows.length) return this.$message.warning('请选择要移入公海的客户')
+      this.$refs.toOpen.form.ids = this.selectRows.map(item => item.id)
+      this.$refs.toOpen.visible = true
+    },
+    async handleMerge () {
+      if (this.selectRows.length < 2) return this.$message.warning('请选择两个以上客户进行合并')
+      const ids = this.selectRows.map(item => item.id)
+      const [err, res] = await to(api.getDetail({ ids }))
+      if (err) return
+      this.$refs.merge.init(res, ids)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$base: '.list';
+</style>

+ 273 - 0
src/views/customer/openSea.vue

@@ -0,0 +1,273 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-15 15:38:21
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-28 16:10:14
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\openSea.vue
+-->
+<template>
+  <div class="open-sea-container">
+    <el-row :gutter="10"
+            style="margin-bottom:10px">
+      <el-col :span="4">
+        <el-input placeholder="客户编码"
+                  v-model="queryForm.custCode"></el-input>
+      </el-col>
+      <el-col :span="4">
+        <el-input placeholder="客户名称"
+                  v-model="queryForm.custName"></el-input>
+      </el-col>
+      <el-col :span="4">
+        <el-input placeholder="客户行业"
+                  v-model="queryForm.custIndustry"></el-input>
+      </el-col>
+      <el-col :span="4">
+        <el-input placeholder="客户级别"
+                  v-model="queryForm.custLevel"></el-input>
+      </el-col>
+      <el-col :span="8">
+        <el-button icon="el-icon-plus"
+                   type="primary"
+                   @click="fetchData">查询</el-button>
+        <el-button icon="el-icon-refresh-right"
+                   @click="reset">重置</el-button>
+      </el-col>
+    </el-row>
+    <vab-query-form>
+      <vab-query-form-left-panel :span="12">
+        <el-button icon="el-icon-plus"
+                   type="primary"
+                   @click="$refs.edit.init()">
+          新建
+        </el-button>
+        <el-button icon="el-icon-plus"
+                   type="primary"
+                   @click="$refs.allocate.visible = true">
+          分配
+        </el-button>
+        <el-button icon="el-icon-plus"
+                   type="primary"
+                   @click="handleReceive">
+          领取
+        </el-button>
+      </vab-query-form-left-panel>
+      <vab-query-form-right-panel :span="12">
+        <el-button icon="el-icon-download"></el-button>
+        <el-button icon="el-icon-setting"></el-button>
+      </vab-query-form-right-panel>
+    </vab-query-form>
+    <el-table v-loading="listLoading"
+              :data="list"
+              border
+              height="calc(100vh - 340px)"
+              @selection-change="setSelectRows">
+      <el-table-column show-overflow-tooltip
+                       type="selection"
+                       align="center" />
+      <el-table-column align="center"
+                       label="客户编码"
+                       prop="custCode"></el-table-column>
+      <el-table-column align="center"
+                       label="客户名称"
+                       prop="custName"></el-table-column>
+      <el-table-column align="center"
+                       label="助记名"
+                       prop="abbrName"></el-table-column>
+      <el-table-column align="center"
+                       label="所在地区"
+                       prop="custLocation"></el-table-column>
+      <el-table-column align="center"
+                       label="客户行业"
+                       prop="custIndustry"></el-table-column>
+      <el-table-column align="center"
+                       label="客户级别"
+                       prop="custLevel"></el-table-column>
+      <el-table-column align="center"
+                       label="客户状态"
+                       prop="custStatus">
+        <template slot-scope="scope">
+          {{scope.row.custStatus == 10 ? '正常' : '异常'}}
+        </template>
+      </el-table-column>
+      <el-table-column align="center"
+                       label="最后跟进时间"
+                       prop="followUpDate"></el-table-column>
+      <el-table-column align="center"
+                       label="创建人"
+                       prop="createdName"></el-table-column>
+      <el-table-column align="center"
+                       label="创建时间"
+                       prop="createdTime"></el-table-column>
+      <el-table-column align="center"
+                       label="操作">
+        <template slot-scope="scope">
+          <el-button type="text"
+                     @click="handleEdit(scope.row)">编辑</el-button>
+          <el-button type="text"
+                     @click="handleDetail(scope.row)">详情</el-button>
+          <el-button type="text"
+                     @click="handleDelete(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination background
+                   :current-page="queryForm.pageNum"
+                   :layout="layout"
+                   :page-size="queryForm.pageSize"
+                   :total="total"
+                   @current-change="handleCurrentChange"
+                   @size-change="handleSizeChange" />
+    <!-- 新增编辑客户弹窗 -->
+    <Edit ref="edit"
+          @createContact="createContact"
+          @customerSave="customerSave"></Edit>
+    <!-- 新建联系人弹窗 -->
+    <Contact ref="contact"></Contact>
+    <!-- 分配客户 -->
+    <Allocate ref="allocate"></Allocate>
+  </div>
+</template>
+
+<script>
+import to from 'await-to-js'
+import api from '@/api/customer'
+import Contact from './components/Contact'
+import Edit from './components/Edit'
+import Allocate from './components/Allocate'
+export default {
+  name: 'OpenSea',
+  data () {
+    return {
+      listLoading: false,
+      layout: 'total, sizes, prev, pager, next, jumper',
+      list: [],
+      total: 0,
+      queryForm: {
+        pageNum: 1,
+        pageSize: 10,
+        custCode: '',   // 客户编码
+        custName: '',    //客户名称
+        custIndustry: '',    // 客户行业  ()
+        custLevel: '',     //客户级别
+      },
+      selectRows: []
+    }
+  },
+  components: {
+    Contact,
+    Edit,
+    Allocate
+  },
+  mounted () {
+    this.fetchData()
+    this.getOptions()
+  },
+  methods: {
+    getOptions () {
+      Promise.all([api.getProvinceInfo()]).then(([province]) => {
+        this.provinceOptions = province.data.list || []
+      }).catch(err => console.log(err))
+    },
+    async fetchData () {
+      this.listLoading = true
+      const params = { ...this.queryForm }
+      const [err, res] = await to(api.getPublicList(params))
+      if (err) return this.listLoading = false
+      this.list = res.data.list || []
+      this.total = res.data.total
+      this.listLoading = false
+    },
+    reset () {
+      this.queryForm = {
+        pageNum: 1,
+        pageSize: 10,
+        custCode: '',   // 客户编码
+        custName: '',    //客户名称
+        custIndustry: '',    // 客户行业  ()
+        custLevel: '',     //客户级别
+      }
+      this.fetchData()
+    },
+    handleSizeChange (val) {
+      this.queryForm.pageSize = val
+      this.fetchData()
+    },
+    handleCurrentChange (val) {
+      this.queryForm.pageNum = val
+      this.fetchData()
+    },
+    setSelectRows (val) {
+      this.selectRows = val
+    },
+    // 客户编辑
+    async handleEdit (row) {
+      this.$refs.edit.init([row.id])
+    },
+    // 客户详情
+    handleDetail (row) {
+      this.$router.push({
+        path: '/customer/detail',
+        query: {
+          id: row.id
+        }
+      })
+    },
+    // 客户删除
+    handleDelete (row) {
+      this.$confirm('确认删除?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const [err, res] = await to(api.deleteCustomer({ Id: row.id }))
+        if (err) return
+        if (res.code == 200) {
+          this.$message({
+            type: 'success',
+            message: '删除成功!'
+          });
+          this.fetchData()
+        }
+      }).catch(() => {
+      })
+    },
+    // 联系人弹窗
+    createContact (res) {
+      this.$refs.contact.contactForm.custId = res.id
+      this.$refs.contact.contactForm.custName = res.name
+      this.$refs.contact.contactVisible = true
+    },
+    customerSave () {
+      this.fetchData()
+    },
+    handleClose (form) {
+      this.$refs[form].resetFields()
+    },
+    // 领取
+    handleReceive () {
+      if (!this.selectRows.length) return this.$message.warning('请选择客户')
+      const arr = this.selectRows.map(item => item.id)
+      this.$confirm('确认领取客户?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        const [err, res] = await to(api.receiveCustomer({ ids: arr.join() }))
+        if (err) return
+        if (res.code == 200) {
+          this.$message({
+            type: 'success',
+            message: '领取成功!'
+          });
+        }
+      }).catch(() => {
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$base: '.open-sea';
+</style>

+ 138 - 0
src/views/opportunity/all.vue

@@ -0,0 +1,138 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-15 11:16:25
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-15 16:04:09
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\opportunity\all.vue
+-->
+<template>
+  <div class="opportunity-container">
+    <vab-query-form>
+      <vab-query-form-left-panel :span="12">
+        <el-button icon="el-icon-plus"
+                   type="primary">
+          添加
+        </el-button>
+        <el-button icon="el-icon-delete">
+          转移商机
+        </el-button>
+      </vab-query-form-left-panel>
+      <vab-query-form-right-panel :span="12">
+        <el-form :inline="true"
+                 :model="queryForm"
+                 @submit.native.prevent>
+          <el-form-item>
+            <el-input v-model.trim="queryForm.userName"
+                      clearable
+                      placeholder="请输入用户名" />
+          </el-form-item>
+          <el-form-item>
+            <el-button icon="el-icon-search"
+                       type="primary"
+                       @click="fetchData">
+              查询
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </vab-query-form-right-panel>
+    </vab-query-form>
+    <el-table v-loading="listLoading"
+              :data="list"
+              height="calc(100vh - 295px)"
+              @selection-change="setSelectRows">
+      <el-table-column show-overflow-tooltip
+                       type="selection" />
+      <el-table-column align="center"
+                       label="商机标题"
+                       prop=""></el-table-column>
+      <el-table-column align="center"
+                       label="关联客户"
+                       prop=""></el-table-column>
+      <el-table-column align="center"
+                       label="审批状态"
+                       prop=""></el-table-column>
+      <el-table-column align="center"
+                       label="商机状态"
+                       prop=""></el-table-column>
+      <el-table-column align="center"
+                       label="商机类别"
+                       prop=""></el-table-column>
+      <el-table-column align="center"
+                       label="商机金额"
+                       prop=""></el-table-column>
+      <el-table-column align="center"
+                       label="最后跟进"
+                       prop=""></el-table-column>
+      <el-table-column align="center"
+                       label="下次联系时间"
+                       prop=""></el-table-column>
+      <el-table-column align="center"
+                       label="操作">
+        <template slot-scope="scope">
+          <el-button type="text"></el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination background
+                   :current-page="queryForm.pageNum"
+                   :layout="layout"
+                   :page-size="queryForm.pageSize"
+                   :total="total"
+                   @current-change="handleCurrentChange"
+                   @size-change="handleSizeChange" />
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'OpportunityAll',
+  data () {
+    return {
+      listLoading: false,
+      layout: 'total, sizes, prev, pager, next, jumper',
+      list: [],
+      queryForm: {
+        pageSize: 10,
+        pageNum: 1
+      },
+      total: 0,
+      selectRows: []
+    }
+  },
+  computed: {
+    height () {
+      return this.$baseTableHeight(1)
+    },
+  },
+  mounted () {
+    this.fetchData()
+  },
+  methods: {
+    async fetchData () {
+      this.listLoading = true
+      this.list = [
+        { id: 1 }
+      ]
+      this.listLoading = false
+    },
+    handleSizeChange (val) {
+      this.queryForm.pageSize = val
+      this.fetchData()
+    },
+    handleCurrentChange (val) {
+      this.queryForm.pageNum = val
+      this.fetchData()
+    },
+    setSelectRows (val) {
+      this.selectRows = val
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+$base: '.opportunity';
+#{$base}-container {
+}
+</style>