Browse Source

Merge branch 'develop' of http://code.dashoo.cn/chengjian/opms_frontend into feature/product_management

wangxingcheng 3 years ago
parent
commit
2e8b4d4fd9

+ 2 - 0
.env.development

@@ -7,3 +7,5 @@ VUE_APP_MicroSrvProxy_API=http://127.0.0.1:9981/
 # 登录验证微服务名称
 VUE_APP_AdminPath=dashoo.opms.admin-0.0.1
 
+# 业务接口微服务名称
+VUE_APP_ParentPath=dashoo.opms.parent-0.0.1

+ 3 - 0
.env.production

@@ -3,3 +3,6 @@ VUE_APP_TENANT=default
 # 登录验证微服务名称
 VUE_APP_AdminPath=dashoo.opms.admin-0.0.1
 VUE_APP_MicroSrvProxy_API=http://127.0.0.1:9981/
+
+# 业务接口微服务名称
+VUE_APP_ParentPath=dashoo.opms.parent-0.0.1

+ 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",

+ 47 - 0
src/api/customer/follow.js

@@ -0,0 +1,47 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-29 09:40:35
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-29 16:17:12
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\api\customer\follow.js
+ */
+import micro_request from '@/utils/micro_request'
+const basePath = process.env.VUE_APP_ParentPath
+export default {
+  // 跟进记录列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'FollowUp', 'GetList', query)
+  },
+  // 跟进记录详情
+  getListByDay(query) {
+    return micro_request.postRequest(
+      basePath,
+      'FollowUp',
+      'GetListByDay',
+      query
+    )
+  },
+  // 添加跟进记录
+  addFollowUp(query) {
+    return micro_request.postRequest(basePath, 'FollowUp', 'Create', query)
+  },
+  // 评论列表
+  getComment(query) {
+    return micro_request.postRequest(
+      basePath,
+      'FollowUpComment',
+      'GetList',
+      query
+    )
+  },
+  // 添加评论
+  addComment(query) {
+    return micro_request.postRequest(
+      basePath,
+      'FollowUpComment',
+      'Create',
+      query
+    )
+  },
+}

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

@@ -0,0 +1,119 @@
+/*
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-27 11:10:39
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-30 11:24:07
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\api\customer\index.js
+ */
+import micro_request from '@/utils/micro_request'
+const basePath = process.env.VUE_APP_ParentPath
+export default {
+  // 客户详情
+  getDetail(query) {
+    return micro_request.postRequest(
+      basePath,
+      'Customer',
+      'GetEntityById',
+      query
+    )
+  },
+  // 客户详情内容摘要
+  getAbstract(query) {
+    return micro_request.postRequest(
+      basePath,
+      'Customer',
+      'CustAbstract',
+      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)
+  },
+}

+ 50 - 0
src/api/plat/task.js

@@ -0,0 +1,50 @@
+import micro_request from '@/utils/micro_request'
+
+const basePath = process.env.VUE_APP_ParentPath
+export default {
+  // 督办列表
+  getTaskList(query) {
+    return micro_request.postRequest(basePath, 'Task', 'GetList', query)
+  },
+  // 创建督办
+  createTask(query) {
+    return micro_request.postRequest(basePath, 'Task', 'Create', query)
+  },
+  // 更新督办状态
+  updateTaskStatus(query) {
+    return micro_request.postRequest(basePath, 'Task', 'ChangeStatus', query)
+  },
+  // 统计各类型督办数量
+  statisticsTaskNumber(query) {
+    return micro_request.postRequest(
+      basePath,
+      'Task',
+      'StatisticsTaskNumber',
+      query
+    )
+  },
+  // 数据导出
+  exportTasks(query) {
+    return micro_request.postRequest(basePath, 'Task', 'Export', query)
+  },
+  // 评论列表
+  getTaskCommentList(query) {
+    return micro_request.postRequest(basePath, 'TaskComment', 'GetList', query)
+  },
+  // 添加评论
+  createTaskComment(query) {
+    return micro_request.postRequest(basePath, 'TaskComment', 'Create', query)
+  },
+  // 日志列表
+  getTaskLogList(query) {
+    return micro_request.postRequest(basePath, 'TaskLog', 'GetList', query)
+  },
+  // 督办进展列表
+  getTaskProgressList(query) {
+    return micro_request.postRequest(basePath, 'TaskProgress', 'GetList', query)
+  },
+  // 添加督办进展
+  createTaskProgress(query) {
+    return micro_request.postRequest(basePath, 'TaskProgress', '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}

+ 28 - 0
src/utils/base64ToFile.js

@@ -0,0 +1,28 @@
+const util = {
+  // 创建一个a标签,并做下载点击事件
+  downloadFile: function (blob, fileName) {
+    const link = document.createElement('a')
+    link.href = window.URL.createObjectURL(blob)
+    link.download = fileName
+    // 此写法兼容可火狐浏览器
+    document.body.appendChild(link)
+    const evt = document.createEvent('MouseEvents')
+    evt.initEvent('click', false, false)
+    link.dispatchEvent(evt)
+    document.body.removeChild(link)
+  },
+  // 将Base64文件转为 Blob
+  buildBlobByByte: function (data) {
+    const raw = window.atob(data)
+    const rawLength = raw.length
+    const uInt8Array = new Uint8Array(rawLength)
+    for (let i = 0; i < rawLength; ++i) {
+      uInt8Array[i] = raw.charCodeAt(i)
+    }
+    return new Blob([uInt8Array])
+  },
+}
+// 二进制数组 生成文件
+export default function downloadFileByByte(data, fileName) {
+  return util.downloadFile(util.buildBlobByByte(data), fileName)
+}

+ 3 - 3
src/utils/micro_request.js

@@ -73,7 +73,7 @@ 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
@@ -82,10 +82,10 @@ service.postRequest = function postRequest(basePath, srvName, funcName, data) {
     process.env.VUE_APP_ParentPath
   ) {
     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',

+ 17 - 3
src/vab/components/VabFooter/index.vue

@@ -1,8 +1,18 @@
+<!--
+ * @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>
 
@@ -25,11 +35,15 @@
     display: flex;
     align-items: center;
     justify-content: center;
-    min-height: 55px;
+    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;
+    // border-top: 1px dashed $base-border-color;
 
     i {
       margin: 0 5px;

+ 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-29 11:26:07
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\components\allocate.vue
+-->
+<template>
+  <el-dialog
+    title="分配客户"
+    :visible.sync="visible"
+    width="30%"
+    @close="handleClose">
+    <Transfer ref="transfer" />
+    <el-form label-width="80px" :model="form">
+      <el-form-item label="销售代表">
+        <el-input v-model="form.allocate" readonly>
+          <el-button slot="append" icon="el-icon-search" @click="choose" />
+        </el-input>
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button size="mini" type="primary">保存</el-button>
+      <el-button size="mini" @click="visible = false">取消</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+  import Transfer from './Transfer.vue'
+  export default {
+    components: {
+      Transfer,
+    },
+    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: [],
+      }
+    },
+    methods: {
+      handleClose() {},
+      choose() {
+        this.$refs.transfer.innerVisible = true
+      },
+    },
+  }
+</script>
+
+<style></style>

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

@@ -0,0 +1,161 @@
+<template>
+  <!-- 新建联系人弹窗 -->
+  <el-dialog
+    :title="title"
+    :visible.sync="contactVisible"
+    @close="contactClose">
+    <el-form ref="contactForm" :model="contactForm" :rules="contactRules">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="姓名" prop="cuctName">
+            <el-input v-model="contactForm.cuctName" placeholder="请输入姓名" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="客户名称" prop="custName">
+            <el-input
+              v-model="contactForm.custName"
+              disabled
+              placeholder="请输入客户名称" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="手机号码" prop="telephone">
+            <el-input
+              v-model="contactForm.telephone"
+              placeholder="请输入手机号码" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="微信" prop="wechat">
+            <el-input v-model="contactForm.wechat" placeholder="请输入微信" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="邮箱" prop="email">
+            <el-input v-model="contactForm.email" placeholder="请输入邮箱" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="职务" prop="postion">
+            <el-input v-model="contactForm.postion" placeholder="请输入职务" />
+          </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
+          v-model="contactForm.remark"
+          maxlength="500"
+          placeholder="请输入备注"
+          resize="none"
+          :rows="5"
+          show-word-limit
+          type="textarea" />
+      </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>

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

@@ -0,0 +1,340 @@
+<template>
+  <el-dialog :title="title" :visible.sync="editVisible" @close="handleClose">
+    <el-form ref="editForm" :model="editForm" :rules="editRules">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="客户名称" prop="custName">
+            <el-input
+              v-model="editForm.custName"
+              placeholder="请输入客户名称" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="助记名" prop="abbrName">
+            <el-input v-model="editForm.abbrName" placeholder="请输入助记名" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="客户行业" prop="custIndustry">
+            <el-select
+              v-model="editForm.custIndustry"
+              placeholder="请选择客户行业"
+              style="width: 100%">
+              <el-option
+                v-for="item in industryOptions"
+                :key="item.value"
+                :label="item.value"
+                :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="客户级别" prop="custLevel">
+            <el-select
+              v-model="editForm.custLevel"
+              placeholder="请选择客户级别"
+              style="width: 100%">
+              <el-option
+                v-for="item in levelOptions"
+                :key="item.value"
+                :label="item.value"
+                :value="item.value" />
+            </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
+              v-model="editForm.source"
+              placeholder="请选择客户来源"
+              style="width: 100%">
+              <el-option
+                v-for="item in sourceOptions"
+                :key="item.value"
+                :label="item.value"
+                :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="下次联系时间" prop="followUpDate">
+            <el-date-picker
+              v-model="editForm.followUpDate"
+              placeholder="选择下次联系时间"
+              style="width: 100%"
+              type="date"
+              value-format="yyyy-MM-dd" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="所在地区" required>
+            <el-row :gutter="4" style="width: 100%; padding-top: 32px">
+              <el-col :span="8">
+                <el-select
+                  v-model="editForm.province"
+                  placeholder="省"
+                  value-key="id"
+                  @change="provinceChange">
+                  <el-option
+                    v-for="item in provinceOptions"
+                    :key="item.id"
+                    :label="item.distName"
+                    :value="item" />
+                </el-select>
+              </el-col>
+              <el-col :span="8">
+                <el-select
+                  v-model="editForm.city"
+                  placeholder="市"
+                  value-key="id"
+                  @change="cityChange">
+                  <el-option
+                    v-for="item in editForm.province
+                      ? editForm.province.children
+                      : []"
+                    :key="item.id"
+                    :label="item.distName"
+                    :value="item" />
+                </el-select>
+              </el-col>
+              <el-col :span="8">
+                <el-select
+                  v-model="editForm.region"
+                  placeholder="区"
+                  value-key="id"
+                  @change="$forceUpdate()">
+                  <el-option
+                    v-for="item in editForm.city ? editForm.city.children : []"
+                    :key="item.id"
+                    :label="item.distName"
+                    :value="item" />
+                </el-select>
+              </el-col>
+            </el-row>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="详细地址" prop="custAddress">
+            <el-input
+              v-model="editForm.custAddress"
+              placeholder="请输入详细地址" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="备注" prop="remark">
+        <el-input
+          v-model="editForm.remark"
+          maxlength="500"
+          placeholder="请输入备注"
+          resize="none"
+          :rows="5"
+          show-word-limit
+          type="textarea" />
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button v-show="!editForm.id" type="primary" @click="customerSave">
+        保存
+      </el-button>
+      <el-button v-show="editForm.id" type="primary" @click="customerEdit">
+        保存
+      </el-button>
+      <el-button v-if="!editForm.id" @click="createContact">
+        保存并新建联系人
+      </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('/')
+        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]
+          )
+        } else {
+          this.editForm.city = {}
+        }
+
+        if (arr[2]) {
+          this.editForm.region = this.editForm.city.children.find(
+            (item) => item.distName == arr[2]
+          )
+        } else {
+          this.editForm.region = {}
+        }
+      },
+      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>

+ 91 - 0
src/views/customer/components/FollowDetail.vue

@@ -0,0 +1,91 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-29 18:00:08
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-30 09:03:39
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\components\FollowDetail.vue
+-->
+<template>
+  <el-dialog title="跟进详情" :visible.sync="visible">
+    <el-descriptions border class="margin-top" :column="1" size="medium">
+      <el-descriptions-item label="跟进类型">
+        {{ formatType(form.followType) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="跟进时间">
+        {{ form.followDate }}
+      </el-descriptions-item>
+      <el-descriptions-item label="跟进描述">
+        {{ form.followDate }}
+      </el-descriptions-item>
+      <el-descriptions-item label="客户名称">
+        {{ form.custName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="联系人">
+        {{ form.contactsName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="相关附件">
+        {{ form.files }}
+      </el-descriptions-item>
+      <el-descriptions-item label="评论数量">
+        {{ form.commentNumber }}
+      </el-descriptions-item>
+      <el-descriptions-item label="创建时间">
+        {{ form.createdTime }}
+      </el-descriptions-item>
+    </el-descriptions>
+    <span slot="footer">
+      <!-- <el-button>编辑跟进</el-button>
+        <el-button>删除跟进</el-button> -->
+      <el-button @click="visible = false">关闭</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        visible: false,
+        form: {
+          id: '',
+          followType: '',
+          followDate: '',
+          followContent: '',
+          targetId: '',
+          targetType: '',
+          targetName: '',
+          custId: '',
+          custName: '',
+          contactsId: 0,
+          contactsName: '',
+          reminders: '',
+          nextTime: '',
+          remark: '',
+          createdBy: '',
+          createdName: '',
+          createdTime: '',
+          updatedBy: '',
+          updatedName: '',
+          updatedTime: '',
+          deletedTime: '',
+        },
+      }
+    },
+    methods: {
+      init(form) {
+        this.form = form
+        this.visible = true
+      },
+      formatType(val) {
+        let str = ''
+        if (val == 10) str = '电话'
+        else if (val == 20) str = '邮件'
+        else if (val == 30) str = '拜访'
+        return str
+      },
+    },
+  }
+</script>
+
+<style></style>

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

@@ -0,0 +1,174 @@
+<!--
+ * @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 title="合并客户" :visible.sync="visible" @close="handleClose">
+    <el-alert :closable="false" show-icon type="warning">
+      <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 v-for="(item, index) in list" :key="index" class="each">
+        <li>
+          <el-radio
+            v-model="form.custName"
+            :label="item.custName"
+            @change="targetChange(item)" />
+        </li>
+        <li>
+          <el-radio v-model="form.abbrName" :label="item.abbrName" />
+        </li>
+        <li>
+          <el-radio v-model="form.custIndustry" :label="item.custIndustry" />
+        </li>
+        <li>
+          <el-radio v-model="form.custLevel" :label="item.custLevel" />
+        </li>
+        <li>
+          <el-radio v-model="form.source" :label="item.source" />
+        </li>
+        <li>
+          <el-radio v-model="form.followUpDate" :label="item.followUpDate" />
+        </li>
+        <li>
+          <el-radio v-model="form.custLocation" :label="item.custLocation" />
+        </li>
+        <li>
+          <el-radio v-model="form.custAddress" :label="item.custAddress" />
+        </li>
+        <li>
+          <el-radio v-model="form.remark" :label="item.remark" />
+        </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>

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

@@ -0,0 +1,78 @@
+<!--
+ * @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
+    title="转移客户"
+    :visible.sync="visible"
+    width="30%"
+    @close="handleClose">
+    <Transfer ref="transfer" />
+    <el-form ref="form" label-width="80px" :model="form" :rules="rules">
+      <el-form-item label="接收对象" prop="allocate">
+        <el-input v-model="form.allocate" readonly>
+          <el-button slot="append" icon="el-icon-search" @click="choose" />
+        </el-input>
+      </el-form-item>
+      <el-form-item label="转移相关" prop="about">
+        <el-checkbox-group v-model="form.about">
+          <el-checkbox label="客户" />
+        </el-checkbox-group>
+      </el-form-item>
+      <el-form-item label="备注信息" prop="remark">
+        <el-input
+          v-model="form.remark"
+          maxlength="500"
+          resize="none"
+          :rows="5"
+          show-word-limit
+          type="textarea" />
+      </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 {
+    components: {
+      Transfer,
+    },
+    data() {
+      return {
+        visible: false,
+        form: {
+          allocate: '',
+          about: ['客户'],
+          remark: '',
+        },
+        rules: {
+          allocate: [
+            { required: true, message: '请选择接收对象', trigger: 'change' },
+          ],
+          about: [
+            { required: true, message: '请选择转移相关', trigger: 'change' },
+          ],
+        },
+      }
+    },
+    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
+    title="转移客户"
+    :visible.sync="visible"
+    width="30%"
+    @close="handleClose">
+    <el-form ref="form" label-width="100px" :model="form">
+      <el-form-item label="移入公海原因" prop="remark">
+        <el-input
+          v-model="form.remark"
+          maxlength="300"
+          resize="none"
+          :rows="5"
+          show-word-limit
+          type="textarea" />
+        <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>

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

@@ -0,0 +1,151 @@
+<!--
+ * @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
+    append-to-body
+    title="选择销售工程师"
+    :visible.sync="innerVisible"
+    width="40%">
+    <el-row class="transfer">
+      <el-col :span="12">
+        <header>
+          <el-input
+            v-model="keyword"
+            clearable
+            placeholder="请输入关键字"
+            suffix-icon="el-icon-search" />
+        </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
+            :disabled="selected.length == 0"
+            type="text"
+            @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 size="mini" type="primary">保存</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>

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

@@ -0,0 +1,796 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-26 09:30:47
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-30 14:38:40
+ * @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="privateCus == 1">
+                <el-button @click="handleShift">转移客户</el-button>
+                <el-button @click="handleToOpen">移入公海</el-button>
+                <el-button>创建项目</el-button>
+              </template>
+              <template v-else>
+                <el-button @click="handleReceive">领取客户</el-button>
+                <el-button @click="$refs.allocate.visible = true">
+                  分配客户
+                </el-button>
+              </template>
+            </span>
+          </h3>
+        </div>
+        <header>
+          <el-descriptions :colon="false" :column="6" direction="vertical">
+            <el-descriptions-item
+              content-class-name="my-content"
+              label="客户编码"
+              label-class-name="my-label">
+              {{ detail.custCode }}
+            </el-descriptions-item>
+            <el-descriptions-item
+              content-class-name="my-content"
+              label="助记名"
+              label-class-name="my-label">
+              {{ detail.abbrName }}
+            </el-descriptions-item>
+            <el-descriptions-item
+              content-class-name="my-content"
+              label="客户级别"
+              label-class-name="my-label">
+              {{ detail.custLevel }}
+            </el-descriptions-item>
+            <el-descriptions-item
+              content-class-name="my-content"
+              label="客户行业"
+              label-class-name="my-label">
+              {{ detail.custIndustry }}
+            </el-descriptions-item>
+            <el-descriptions-item
+              content-class-name="my-content"
+              label="客户状态"
+              label-class-name="my-label">
+              {{ detail.custStatus == 10 ? '正常' : '异常' }}
+            </el-descriptions-item>
+            <el-descriptions-item
+              content-class-name="my-content"
+              label="最后跟进时间"
+              label-class-name="my-label">
+              {{ detail.followUpDate }}
+            </el-descriptions-item>
+          </el-descriptions>
+        </header>
+        <el-tabs v-model="activeName" @tab-click="handleClick">
+          <el-tab-pane label="详细信息" name="first">
+            <el-descriptions border :column="2" size="medium">
+              <el-descriptions-item label="客户级别">
+                {{ detail.custLevel }}
+              </el-descriptions-item>
+              <el-descriptions-item label="下次联系时间">
+                {{ detail.followUpDate }}
+              </el-descriptions-item>
+              <el-descriptions-item label="所在地区">
+                {{ detail.custLocation }}
+              </el-descriptions-item>
+              <el-descriptions-item label="详细地址">
+                {{ detail.custAddress }}
+              </el-descriptions-item>
+              <el-descriptions-item label="创建人">
+                {{ detail.createdName }}
+              </el-descriptions-item>
+              <el-descriptions-item label="创建时间">
+                {{ detail.createdTime }}
+              </el-descriptions-item>
+              <el-descriptions-item label="跟进次数">
+                {{ abstract.followContent }}
+              </el-descriptions-item>
+              <el-descriptions-item label="未跟进时长">
+                {{ abstract.notFollowDay }}
+              </el-descriptions-item>
+              <el-descriptions-item label="商机数量">
+                {{ abstract.business }}
+              </el-descriptions-item>
+              <el-descriptions-item label="商机总额">
+                {{ abstract.businessTotal }}
+              </el-descriptions-item>
+              <el-descriptions-item label="成交次数">
+                {{ abstract.dealCotal }}
+              </el-descriptions-item>
+              <el-descriptions-item label="成交总额">
+                {{ abstract.dealTotal }}
+              </el-descriptions-item>
+              <el-descriptions-item label="回款总额">
+                {{ abstract.paymentTotal }}
+              </el-descriptions-item>
+              <el-descriptions-item label="未回款总额">
+                {{ abstract.notPaymentTotal }}
+              </el-descriptions-item>
+              <el-descriptions-item label="开票总额" :span="24">
+                {{ abstract.drawTotal }}
+              </el-descriptions-item>
+              <el-descriptions-item label="备注" :span="24">
+                {{ detail.remark }}
+              </el-descriptions-item>
+            </el-descriptions>
+          </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%" />
+              </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"
+              border
+              :data="contactList"
+              height="calc(100% - 42px)"
+              @selection-change="setSelectRows">
+              <el-table-column align="center" type="selection" />
+              <el-table-column align="center" label="姓名" prop="cuctName" />
+              <el-table-column align="center" label="岗位" prop="postion" />
+              <el-table-column align="center" label="电话" prop="telephone" />
+              <el-table-column align="center" label="微信" prop="wechat" />
+              <el-table-column align="center" label="邮箱" prop="email" />
+              <el-table-column align="center" label="是否决策人">
+                <template slot-scope="scope">
+                  <el-switch
+                    v-model="scope.row.policy"
+                    :active-value="1"
+                    disabled
+                    :inactive-value="0" />
+                </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">
+            <ul class="follow">
+              <li v-for="(date, index) in followList" :key="index">
+                <div class="date">
+                  <h2>{{ date.followDay.split('-')[2] }}</h2>
+                  <h3>
+                    {{ date.followDay.split('-').splice(0, 2).join('.') }}
+                  </h3>
+                </div>
+                <ul class="content">
+                  <li v-for="(item, idx) in date.followupList" :key="idx">
+                    <!-- <el-avatar class="user-avatar"
+              :src="avatar" />-->
+                    <div class="text-container">
+                      <vab-icon
+                        class="user-avatar"
+                        icon="account-circle-fill" />
+                      <div class="text">
+                        <p class="action">
+                          <span>
+                            {{ item.contactsName }} 跟进({{
+                              formatType(item.followType)
+                            }})
+                          </span>
+                          <span>
+                            <vab-icon icon="time-line" />
+                            {{ item.followDate }}
+                          </span>
+                        </p>
+                        <p>{{ item.followContent }}</p>
+                        <div class="footer">
+                          <p>
+                            来自客户:
+                            <span>{{ item.custName }}</span>
+                          </p>
+                          <div>
+                            <el-button size="mini" @click="showDetail(item)">
+                              <vab-icon icon="arrow-right-circle-fill" />
+                              详情
+                            </el-button>
+                            <el-button size="mini" @click="showComment(item)">
+                              评论({{ item.commentNumber }})
+                            </el-button>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                    <transition name="height">
+                      <ul v-if="item.showComment" class="comments">
+                        <li v-for="comment in item.comments" :key="comment.id">
+                          <vab-icon
+                            class="user-avatar"
+                            icon="account-circle-fill" />
+                          <div class="text">
+                            <p>{{ comment.createdName }}</p>
+                            <p>{{ comment.content }}</p>
+                            <p>{{ comment.createdTime }}</p>
+                          </div>
+                        </li>
+                      </ul>
+                    </transition>
+                  </li>
+                </ul>
+              </li>
+            </ul>
+          </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-tab-pane>
+          <el-tab-pane label="归属记录" name="seventh">
+            <el-table
+              v-loading="listLoading"
+              border
+              :data="belongs"
+              height="calc(100% - 42px)">
+              <el-table-column
+                align="center"
+                label="归属销售"
+                prop="saleName" />
+              <el-table-column
+                align="center"
+                label="原来归属"
+                prop="origSaleName" />
+              <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
+                align="center"
+                label="操作时间"
+                prop="opnDatetime" />
+              <el-table-column align="center" label="备注" prop="remark" />
+            </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, index) in records[key]" :key="index">
+                <!-- <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" />
+    <Edit ref="edit" @customerSave="customerSave" />
+    <!-- 分配客户 -->
+    <Allocate ref="allocate" />
+    <!-- 转移客户 -->
+    <Shift ref="shift" />
+    <!-- 移入公海 -->
+    <ToOpen ref="toOpen" @refresh="back" />
+    <!-- 跟进详情 -->
+    <FollowDetail ref="followDetail" />
+  </div>
+</template>
+
+<script>
+  import { mapGetters } from 'vuex'
+  import api from '@/api/customer'
+  import follow from '@/api/customer/follow'
+  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'
+  import FollowDetail from './components/FollowDetail.vue'
+  export default {
+    name: 'CustomerDetail',
+    components: {
+      Edit,
+      Contact,
+      Allocate,
+      Shift,
+      ToOpen,
+      FollowDetail,
+    },
+    data() {
+      return {
+        id: '',
+        privateCus: '',
+        detail: {
+          custCode: '', //客户编码
+          abbrName: '', //助记名
+          level: '', //客户级别
+          indusTry: '', //客户行业
+          custStatus: '', //客户状态
+          followUpDate: '', //最后跟进时间
+        },
+        abstract: {
+          followContent: '', //跟进次数
+          notFollowDay: '', //未跟进天数
+          business: '', //商机数量
+          businessTotal: '', //商机总额
+          dealCotal: '', //成交次数
+          dealTotal: '', //成交总额
+          paymentTotal: '', //回款总额
+          notPaymentTotal: '', //未回款总额
+          drawTotal: '', //开票总额
+        },
+        activeName: 'first',
+        listLoading: false,
+        contactList: [],
+        selectRows: [],
+        records: [], //操作记录
+        followList: [], //跟进记录
+        belongs: [],
+      }
+    },
+    computed: {
+      ...mapGetters({
+        avatar: 'user/avatar',
+        username: 'user/username',
+      }),
+    },
+    mounted() {
+      this.id = this.$route.query.id
+      this.privateCus = this.$route.query.privateCus
+      this.init()
+      this.getDynamics()
+    },
+    methods: {
+      async init() {
+        Promise.all([
+          api.getDetail({ ids: [parseInt(this.id)] }),
+          api.getAbstract({ id: parseInt(this.id) }),
+        ]).then(([detail, abstract]) => {
+          if (detail.data.list[0]) this.detail = detail.data.list[0]
+          if (abstract.data.list) this.abstract = abstract.data.list
+        })
+      },
+      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 == 'third') {
+          let params = {
+            custId: this.id,
+            DaysBeforeToday: 9999,
+          }
+          ;[err, res] = await to(follow.getListByDay(params))
+          if (err) return
+          this.followList = res.data.list || []
+        } else if (tab.name == 'seventh' && 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()
+      },
+      formatType(val) {
+        let str = ''
+        if (val == 10) str = '电话'
+        else if (val == 20) str = '邮件'
+        else if (val == 30) str = '拜访'
+        return str
+      },
+      // 跟进记录详情
+      showDetail(row) {
+        this.$refs.followDetail.init({ ...row })
+      },
+      // 展开评论
+      showComment(row) {
+        if (!row.comments.length) return this.$message.warning('暂无评论')
+        row.showComment = !row.showComment
+        this.$forceUpdate()
+      },
+    },
+  }
+</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: 28px;
+      text-align: right;
+    }
+    .records {
+      margin: 0;
+      padding: 10px 20px;
+      list-style: none;
+      height: calc(100% - 60px);
+      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: 10px;
+          }
+        }
+        .user-avatar {
+          font-size: 40px;
+        }
+        .text {
+          flex: 1;
+          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;
+          }
+        }
+      }
+    }
+    .follow {
+      height: 100%;
+      padding: 10px 20px;
+      overflow: 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 {
+          border: 1px solid rgb(215, 232, 244);
+          background: rgb(247, 251, 254);
+          border-radius: 4px;
+          padding: 8px;
+          overflow: hidden;
+
+          .text-container {
+            display: flex;
+          }
+          .comments {
+            padding-left: 60px;
+            margin-top: 10px;
+            max-height: 200px;
+            overflow: auto;
+            li {
+              display: flex;
+              border-top: 1px solid #e3e5e7;
+              .text {
+                flex: 1;
+                padding: 0 10px;
+                p {
+                  font-weight: 500;
+                  margin: 0;
+                  line-height: 32px;
+                }
+                p:first-child {
+                  line-height: 30px;
+                  font-weight: bold;
+                }
+                p:last-child {
+                  font-size: 12px;
+                  color: #9499a0;
+                  text-align: right;
+                }
+              }
+            }
+          }
+          + li {
+            margin-top: 10px;
+          }
+        }
+        .user-avatar {
+          font-size: 40px;
+        }
+        .text {
+          flex: 1;
+          padding-left: 20px;
+          padding-right: 10px;
+          p {
+            font-weight: 500;
+            margin: 0;
+            line-height: 32px;
+            span {
+              color: #1d66dc;
+            }
+          }
+          .action {
+            display: flex;
+            justify-content: space-between;
+            span:first-child {
+              font-weight: bold;
+              color: #333;
+            }
+          }
+          .footer {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+          }
+        }
+      }
+    }
+  }
+  .height-enter-active,
+  .height-leave-active {
+    transition: all 0.5s;
+  }
+  .height-enter-to,
+  .height-leave {
+    height: 200px;
+  }
+  .height-enter, .height-leave-to /* .fade-leave-active below version 2.1.8 */ {
+    height: 0;
+  }
+</style>

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

@@ -0,0 +1,389 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-15 15:38:21
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-30 14:36:03
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\customer\follow.vue
+-->
+<template>
+  <div class="follow-container">
+    <el-row :gutter="10" style="margin-bottom: 10px">
+      <el-col :span="4">
+        <el-input v-model="queryForm.custId" placeholder="客户" />
+      </el-col>
+      <!-- <el-col :span="4">
+        <el-date-picker
+          v-model="queryForm.date"
+          end-placeholder="结束日期"
+          placeholder="时间范围"
+          start-placeholder="开始日期"
+          style="width: 100%"
+          type="daterange" />
+      </el-col> -->
+      <el-col :span="4">
+        <el-input v-model="queryForm.daysBeforeToday" placeholder="查询天数">
+          <template slot="append">天</template>
+        </el-input>
+      </el-col>
+      <el-col :span="4">
+        <el-input v-model="queryForm.targetType" placeholder="跟进类型" />
+      </el-col>
+      <el-col :span="4">
+        <el-input v-model="queryForm.indusTry" placeholder="跟进人员" />
+      </el-col>
+      <el-col :span="4">
+        <el-input v-model="queryForm.managerId" placeholder="创建者" />
+      </el-col>
+      <el-col :span="4">
+        <el-button icon="el-icon-plus" type="primary" @click="search">
+          查询
+        </el-button>
+        <el-button @click="addFollowUp">添加</el-button>
+        <el-button icon="el-icon-refresh-right" @click="reset">重置</el-button>
+      </el-col>
+    </el-row>
+    <div class="follow">
+      <ul
+        v-infinite-scroll="load"
+        class="records infinite-list"
+        :infinite-scroll-disabled="loadFlag"
+        :infinite-scroll-immediate-check="false">
+        <li v-for="(date, index) in records" :key="index">
+          <div class="date">
+            <h2>{{ date.followDay.split('-')[2] }}</h2>
+            <h3>{{ date.followDay.split('-').splice(0, 2).join('.') }}</h3>
+          </div>
+          <ul class="content">
+            <li v-for="(item, idx) in date.followupList" :key="idx">
+              <!-- <el-avatar class="user-avatar"
+              :src="avatar" />-->
+              <vab-icon class="user-avatar" icon="account-circle-fill" />
+              <div class="text">
+                <p class="action">
+                  <span>
+                    {{ item.contactsName }} 跟进({{
+                      formatType(item.followType)
+                    }})
+                  </span>
+                  <span>
+                    <vab-icon icon="time-line" />
+                    {{ item.followDate }}
+                  </span>
+                </p>
+                <p>{{ item.followContent }}</p>
+                <div class="footer">
+                  <p>
+                    来自客户:
+                    <span>{{ item.custName }}</span>
+                  </p>
+                  <div>
+                    <el-button size="mini" @click="showDetail(item)">
+                      <vab-icon icon="arrow-right-circle-fill" />
+                      详情
+                    </el-button>
+                    <el-button size="mini" @click="showComment(item.id)">
+                      <!-- <vab-icon icon="chat-3-fill" /> -->
+                      评论({{ item.commentNumber }})
+                    </el-button>
+                  </div>
+                </div>
+              </div>
+            </li>
+          </ul>
+        </li>
+      </ul>
+      <div class="comment">
+        <ul>
+          <li v-for="item in comments" :key="item.id">
+            <vab-icon class="user-avatar" icon="account-circle-fill" />
+            <div class="text">
+              <p>{{ item.createdName }}</p>
+              <p>{{ item.content }}</p>
+              <p>{{ item.createdTime }}</p>
+            </div>
+          </li>
+        </ul>
+        <div class="form">
+          <el-input
+            v-model="comment"
+            maxlength="300"
+            resize="none"
+            :rows="5"
+            show-word-limit
+            type="textarea" />
+          <el-button size="mini" type="primary" @click="handleComment">
+            评论
+          </el-button>
+        </div>
+      </div>
+    </div>
+    <!-- 跟进详情 -->
+    <FollowDetail ref="followDetail" />
+  </div>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import api from '@/api/customer/follow'
+  import FollowDetail from './components/FollowDetail.vue'
+  export default {
+    name: 'Follow',
+    components: {
+      FollowDetail,
+    },
+    data() {
+      return {
+        listLoading: false,
+        queryForm: {
+          custId: '',
+          targetType: '',
+          targetId: '',
+          managerId: '',
+          daysBeforeToday: 20,
+          date: [],
+        },
+        loadFlag: true,
+        records: [],
+        followId: '',
+        comment: '',
+        comments: [],
+        visible: false,
+        form: {
+          id: '',
+          followType: '',
+          followDate: '',
+          followContent: '',
+          targetId: '',
+          targetType: '',
+          targetName: '',
+          custId: '',
+          custName: '',
+          contactsId: 0,
+          contactsName: '',
+          reminders: '',
+          nextTime: '',
+          remark: '',
+          createdBy: '',
+          createdName: '',
+          createdTime: '',
+          updatedBy: '',
+          updatedName: '',
+          updatedTime: '',
+          deletedTime: '',
+        },
+      }
+    },
+    mounted() {
+      this.fetchData()
+      this.getOptions()
+    },
+    methods: {
+      getOptions() {},
+      search() {
+        this.queryForm.daysBeforeToday = 20
+        this.fetchData()
+      },
+      async fetchData() {
+        let params = { ...this.queryForm }
+        const [err, res] = await to(api.getListByDay(params))
+        if (err) return console.log(err, 'err')
+        this.records = res.data.list || []
+        await this.$nextTick()
+        this.loadFlag = false
+      },
+      load() {
+        this.queryForm.daysBeforeToday += 20
+        console.log('到底了', this.queryForm.daysBeforeToday)
+        this.fetchData()
+      },
+      reset() {
+        this.queryForm = {
+          custId: '',
+          targetType: '',
+          targetId: '',
+          managerId: '',
+          daysBeforeToday: '',
+          date: [],
+        }
+        this.fetchData()
+      },
+      // 详情
+      showDetail(row) {
+        this.$refs.followDetail.init({ ...row })
+      },
+      async showComment(id) {
+        this.followId = id
+        const [err, res] = await to(api.getComment({ followId: id + '' }))
+        if (err) return
+        this.comments = res.data.list || []
+      },
+      formatType(val) {
+        let str = ''
+        if (val == 10) str = '电话'
+        else if (val == 20) str = '邮件'
+        else if (val == 30) str = '拜访'
+        return str
+      },
+      // 评论
+      async handleComment() {
+        let str = ''
+        if (!this.followId) str = '请选择跟进记录'
+        else if (!this.comment) str = '请输入评论'
+        if (str) return this.$message.warning(str)
+        let params = {
+          followId: this.followId + '',
+          content: this.comment,
+          remark: '',
+        }
+        const [err, res] = await to(api.addComment(params))
+        if (err) return
+        this.$message.success(res.msg)
+        this.showComment(this.followId)
+        this.comment = ''
+        this.fetchData()
+      },
+      addFollowUp() {
+        let params = {
+          followType: '20',
+          followDate: '2022-10-29',
+          followContent: '跟进内容',
+          targetId: 1,
+          targetType: '20',
+          targetName: 'dashoo',
+          custId: 26,
+          custName: '测试客户1348',
+          contactsId: 55,
+          contactsName: 'wanglj',
+          reminders: '',
+          nextTime: this.parseTime(new Date()),
+          remark: '',
+        }
+        api.addFollowUp(params)
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  $base: '.follow';
+  .follow {
+    height: calc(100vh - 240px);
+    display: flex;
+    .comment {
+      width: 300px;
+      display: flex;
+      flex-direction: column;
+      border-radius: 4px;
+      border: 1px solid rgb(215, 232, 244);
+      ul {
+        flex: 1;
+        overflow-y: auto;
+        padding: 10px;
+        li {
+          display: flex;
+          border-bottom: 1px solid #e3e5e7;
+          .text {
+            flex: 1;
+            padding: 0 10px;
+            p {
+              font-weight: 500;
+              margin: 0;
+              line-height: 32px;
+            }
+            p:first-child {
+              line-height: 30px;
+              font-weight: bold;
+            }
+            p:last-child {
+              font-size: 12px;
+              color: #9499a0;
+              text-align: right;
+            }
+          }
+        }
+        .user-avatar {
+          font-size: 30px;
+        }
+      }
+      .form {
+        padding: 4px;
+        text-align: right;
+        .el-textarea {
+          margin-bottom: 4px;
+        }
+      }
+    }
+    .records {
+      flex: 1;
+      margin: 0;
+      height: 100%;
+      padding: 10px 20px;
+      list-style: none;
+      overflow: 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;
+          border: 1px solid rgb(215, 232, 244);
+          background: rgb(247, 251, 254);
+          border-radius: 4px;
+          padding: 8px;
+          + li {
+            margin-top: 10px;
+          }
+        }
+        .user-avatar {
+          font-size: 40px;
+        }
+        .text {
+          flex: 1;
+          padding-left: 20px;
+          padding-right: 10px;
+          p {
+            font-weight: 500;
+            margin: 0;
+            line-height: 32px;
+            span {
+              color: #1d66dc;
+            }
+          }
+          .action {
+            display: flex;
+            justify-content: space-between;
+            span:first-child {
+              font-weight: bold;
+              color: #333;
+            }
+          }
+          .footer {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+          }
+        }
+      }
+    }
+  }
+</style>

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

@@ -0,0 +1,261 @@
+<!--
+ * @Author: wanglj 471442253@qq.com
+ * @Date: 2022-12-26 16:34:37
+ * @LastEditors: wanglj
+ * @LastEditTime: 2022-12-30 11:21:20
+ * @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 label="我的客户" name="second" />
+      <el-tab-pane label="下属的客户" name="third" />
+    </el-tabs>
+    <el-row :gutter="10" style="margin-bottom: 10px">
+      <el-col :span="4">
+        <el-input v-model="queryForm.custCode" placeholder="客户编码" />
+      </el-col>
+      <el-col :span="4">
+        <el-input v-model="queryForm.custName" placeholder="客户名称" />
+      </el-col>
+      <el-col :span="4">
+        <el-input v-model="queryForm.indusTry" placeholder="客户行业" />
+      </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
+          icon="el-icon-plus"
+          size="mini"
+          type="primary"
+          @click="$refs.edit.init()">
+          新建
+        </el-button>
+        <el-button
+          icon="el-icon-plus"
+          size="mini"
+          type="primary"
+          @click="handleShift">
+          转移客户
+        </el-button>
+        <el-button
+          icon="el-icon-plus"
+          size="mini"
+          type="primary"
+          @click="handleToOpen">
+          移入公海
+        </el-button>
+        <el-button
+          icon="el-icon-plus"
+          size="mini"
+          type="primary"
+          @click="handleMerge">
+          合并客户
+        </el-button>
+      </vab-query-form-left-panel>
+      <vab-query-form-right-panel :span="12">
+        <el-button icon="el-icon-download" />
+        <el-button icon="el-icon-setting" />
+      </vab-query-form-right-panel>
+    </vab-query-form>
+    <el-table
+      v-loading="listLoading"
+      border
+      :data="list"
+      height="calc(100vh - 394px)"
+      @selection-change="setSelectRows">
+      <el-table-column align="center" show-overflow-tooltip type="selection" />
+      <el-table-column align="center" label="客户编码" prop="custCode" />
+      <el-table-column align="center" label="客户名称" prop="custName" />
+      <el-table-column align="center" label="助记名" prop="abbrName" />
+      <el-table-column align="center" label="所在地区" prop="custLocation" />
+      <el-table-column align="center" label="客户行业" prop="custIndustry" />
+      <el-table-column align="center" label="客户级别" prop="custLevel" />
+      <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 align="center" label="创建人" prop="createdName" />
+      <el-table-column align="center" label="创建时间" prop="createdTime" />
+      <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" />
+    <!-- 新建联系人弹窗 -->
+    <Contact ref="contact" />
+    <!-- 转移客户 -->
+    <Shift ref="shift" />
+    <!-- 移入公海 -->
+    <ToOpen ref="toOpen" @refresh="fetchData" />
+    <!-- 合并客户 -->
+    <Merge ref="merge" @refresh="fetchData" />
+  </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 {
+    components: {
+      Edit,
+      Contact,
+      Shift,
+      ToOpen,
+      Merge,
+    },
+    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: [],
+      }
+    },
+    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(res) {
+        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,
+            privateCus: 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>

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

@@ -0,0 +1,257 @@
+<!--
+ * @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 v-model="queryForm.custCode" placeholder="客户编码" />
+      </el-col>
+      <el-col :span="4">
+        <el-input v-model="queryForm.custName" placeholder="客户名称" />
+      </el-col>
+      <el-col :span="4">
+        <el-input v-model="queryForm.custIndustry" placeholder="客户行业" />
+      </el-col>
+      <el-col :span="4">
+        <el-input v-model="queryForm.custLevel" placeholder="客户级别" />
+      </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 icon="el-icon-setting" />
+      </vab-query-form-right-panel>
+    </vab-query-form>
+    <el-table
+      v-loading="listLoading"
+      border
+      :data="list"
+      height="calc(100vh - 340px)"
+      @selection-change="setSelectRows">
+      <el-table-column align="center" show-overflow-tooltip type="selection" />
+      <el-table-column align="center" label="客户编码" prop="custCode" />
+      <el-table-column align="center" label="客户名称" prop="custName" />
+      <el-table-column align="center" label="助记名" prop="abbrName" />
+      <el-table-column align="center" label="所在地区" prop="custLocation" />
+      <el-table-column align="center" label="客户行业" prop="custIndustry" />
+      <el-table-column align="center" label="客户级别" prop="custLevel" />
+      <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 align="center" label="创建人" prop="createdName" />
+      <el-table-column align="center" label="创建时间" prop="createdTime" />
+      <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" />
+    <!-- 新建联系人弹窗 -->
+    <Contact ref="contact" />
+    <!-- 分配客户 -->
+    <Allocate ref="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',
+    components: {
+      Contact,
+      Edit,
+      Allocate,
+    },
+    data() {
+      return {
+        listLoading: false,
+        layout: 'total, sizes, prev, pager, next, jumper',
+        list: [],
+        total: 0,
+        queryForm: {
+          pageNum: 1,
+          pageSize: 10,
+          custCode: '', // 客户编码
+          custName: '', //客户名称
+          custIndustry: '', // 客户行业  ()
+          custLevel: '', //客户级别
+        },
+        selectRows: [],
+      }
+    },
+    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>

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

@@ -0,0 +1,112 @@
+<!--
+ * @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 align="center" label="关联客户" prop="" />
+      <el-table-column align="center" label="审批状态" prop="" />
+      <el-table-column align="center" label="商机状态" prop="" />
+      <el-table-column align="center" label="商机类别" prop="" />
+      <el-table-column align="center" label="商机金额" prop="" />
+      <el-table-column align="center" label="最后跟进" prop="" />
+      <el-table-column align="center" label="下次联系时间" prop="" />
+      <el-table-column align="center" label="操作">
+        <!-- <template slot-scope="scope">
+          <el-button type="text" />
+        </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>

+ 106 - 0
src/views/plat/task/components/CommentAdd.vue

@@ -0,0 +1,106 @@
+<!-- eslint-disable vue/no-mutating-props -->
+<template>
+  <el-dialog
+    title="评论"
+    :visible.sync="selfVisible"
+    width="500px"
+    @close="close"
+    @open="open">
+    <el-form ref="form" label-width="80px" :model="form" :rules="rules">
+      <el-row>
+        <el-col :span="24">
+          <el-form-item label="评论" prop="content">
+            <el-input
+              v-model="form.content"
+              placeholder="请输入评论"
+              type="textarea" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <!-- eslint-disable-next-line vue/no-mutating-props -->
+      <el-button @click="selfVisible = false">取 消</el-button>
+      <el-button type="primary" @click="save">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+  import taskApi from '@/api/plat/task'
+
+  export default {
+    name: 'CommentAdd',
+    props: {
+      selfVisible: {
+        type: Boolean,
+        default: false,
+      },
+      theTask: {
+        type: Object,
+        // eslint-disable-next-line vue/require-valid-default-prop
+        default: {},
+      },
+      doRefresh: {
+        type: Function,
+        default: undefined,
+      },
+    },
+    data() {
+      return {
+        // 新增数据表单
+        form: {
+          taskId: '',
+          content: '',
+          remark: '',
+        },
+        // 校验规则
+        rules: {
+          content: [
+            { required: true, message: '评论不能为空', trigger: 'blur' },
+          ],
+        },
+      }
+    },
+    watch: {
+      selfVisible(val) {
+        this.$emit('update:selfVisible', val)
+      },
+    },
+    methods: {
+      // 打开弹窗
+      open() {
+        this.getData()
+        if (this.$refs['form']) {
+          this.$refs['form'].resetFields()
+        }
+        this.form.taskId = ''
+        this.form.content = ''
+        this.form.remark = ''
+      },
+      // 关闭弹窗
+      close() {
+        if (this.$refs['form']) {
+          this.$refs['form'].resetFields()
+        }
+        // eslint-disable-next-line vue/no-mutating-props
+        this.selfVisible = false
+      },
+      // 保存数据
+      save() {
+        this.$refs['form'].validate(async (valid) => {
+          if (valid) {
+            // eslint-disable-next-line vue/no-mutating-props
+            this.selfVisible = false
+            this.form.taskId = this.theTask.id
+            const { msg } = await taskApi.createTaskComment(this.form)
+            this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+            if (this.doRefresh) {
+              this.doRefresh()
+            }
+          }
+        })
+      },
+    },
+  }
+</script>

+ 147 - 0
src/views/plat/task/components/ProgressAdd.vue

@@ -0,0 +1,147 @@
+<!-- eslint-disable vue/no-mutating-props -->
+<template>
+  <el-dialog
+    title="评论"
+    :visible.sync="selfVisible"
+    width="500px"
+    @close="close"
+    @open="open">
+    <el-form ref="form" label-width="80px" :model="form" :rules="rules">
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="进展时间" prop="progDate">
+            <el-date-picker
+              v-model="form.progDate"
+              placeholder="请选择进展时间"
+              style="width: 100%"
+              type="datetime"
+              value-format="yyyy-MM-dd HH:mm:ss" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="附件" prop="progFile">
+            <!-- 此处附件上传组件不可用,等以后再调整 -->
+            <!-- <el-button size="mini" type="primary">上传</el-button> -->
+            <el-link
+              v-show="form.progFile != ''"
+              @click="showFile(form.progFile)">
+              查看附件
+            </el-link>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="进展说明" prop="progDesc">
+            <el-input
+              v-model="form.progDesc"
+              placeholder="请输入进展说明"
+              type="textarea" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input
+              v-model="form.remark"
+              placeholder="请输入备注"
+              type="textarea" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <!-- eslint-disable-next-line vue/no-mutating-props -->
+      <el-button @click="selfVisible = false">取 消</el-button>
+      <el-button type="primary" @click="save">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+  import taskApi from '@/api/plat/task'
+
+  export default {
+    name: 'CommentAdd',
+    props: {
+      selfVisible: {
+        type: Boolean,
+        default: false,
+      },
+      theTask: {
+        type: Object,
+        // eslint-disable-next-line vue/require-valid-default-prop
+        default: {},
+      },
+      doRefresh: {
+        type: Function,
+        default: undefined,
+      },
+    },
+    data() {
+      return {
+        // 新增数据表单
+        form: {
+          taskId: '',
+          progDate: '',
+          progDesc: '',
+          progFile: '',
+          remark: '',
+        },
+        // 校验规则
+        rules: {
+          progDesc: [
+            { required: true, message: '进展说明不能为空', trigger: 'blur' },
+          ],
+        },
+      }
+    },
+    watch: {
+      selfVisible(val) {
+        this.$emit('update:selfVisible', val)
+      },
+    },
+    methods: {
+      // 打开弹窗
+      open() {
+        this.getData()
+        if (this.$refs['form']) {
+          this.$refs['form'].resetFields()
+        }
+        this.form.taskId = ''
+        this.form.progDate = ''
+        this.form.progDesc = ''
+        this.form.progFile = ''
+        this.form.remark = ''
+      },
+      // 关闭弹窗
+      close() {
+        if (this.$refs['form']) {
+          this.$refs['form'].resetFields()
+        }
+        // eslint-disable-next-line vue/no-mutating-props
+        this.selfVisible = false
+      },
+      // 查看附件
+      showFile(path) {
+        const a = document.createElement('a')
+        a.href = path // 文件链接
+        a.download = path // 文件名,跨域资源download无效
+        a.click()
+        a.remove()
+      },
+      // 保存数据
+      save() {
+        this.$refs['form'].validate(async (valid) => {
+          if (valid) {
+            // eslint-disable-next-line vue/no-mutating-props
+            this.selfVisible = false
+            this.form.taskId = this.theTask.id
+            const { msg } = await taskApi.createTaskProgress(this.form)
+            this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+            if (this.doRefresh) {
+              this.doRefresh()
+            }
+          }
+        })
+      },
+    },
+  }
+</script>

+ 339 - 0
src/views/plat/task/components/TaskAdd.vue

@@ -0,0 +1,339 @@
+<!-- eslint-disable vue/no-mutating-props -->
+<template>
+  <el-dialog
+    title="添加"
+    :visible.sync="selfVisible"
+    width="650px"
+    @close="close"
+    @open="open">
+    <el-form ref="form" label-width="80px" :model="form" :rules="rules">
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="标题" prop="taskTitle">
+            <el-input v-model="form.taskTitle" placeholder="请输入标题" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="类型" prop="taskType">
+            <el-select
+              v-model="form.taskType"
+              placeholder="请选择类型"
+              style="width: 100%">
+              <el-option
+                v-for="item in types"
+                :key="item.dictCode"
+                :label="item.dictLabel"
+                :value="item.dictValue" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="开始时间" prop="taskStartDate">
+            <el-date-picker
+              v-model="form.taskStartDate"
+              placeholder="请选择开始时间"
+              style="width: 100%"
+              type="datetime"
+              value-format="yyyy-MM-dd HH:mm:ss" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="结束时间" prop="taskEndDate">
+            <el-date-picker
+              v-model="form.taskEndDate"
+              placeholder="请选择结束时间"
+              style="width: 100%"
+              type="datetime"
+              value-format="yyyy-MM-dd HH:mm:ss" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="说明" prop="taskDesc">
+            <el-input
+              v-model="form.taskDesc"
+              placeholder="请输入说明"
+              type="textarea" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="督办人" prop="supervisorUserId">
+            <el-select
+              v-model="form.supervisorUserId"
+              placeholder="请选择督办人"
+              style="width: 100%">
+              <el-option
+                v-for="item in users"
+                :key="item.id"
+                :label="item.userName"
+                :value="item.id" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="监办人" prop="watchUserId">
+            <el-select
+              v-model="form.watchUserId"
+              clear
+              placeholder="请选择监办人"
+              style="width: 100%">
+              <el-option
+                v-for="item in users"
+                :key="item.id"
+                :label="item.userName"
+                :value="item.id" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="负责人" prop="mainUserId">
+            <el-select
+              v-model="form.mainUserId"
+              placeholder="请选择负责人"
+              style="width: 100%">
+              <el-option
+                v-for="item in users"
+                :key="item.id"
+                :label="item.userName"
+                :value="item.id" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="团队成员">
+            <el-select
+              v-model="teamIds"
+              clear
+              multiple
+              placeholder="请选择团队成员"
+              style="width: 100%">
+              <el-option
+                v-for="item in users"
+                :key="item.id"
+                :label="item.userName"
+                :value="item.id" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="关联类型" prop="targetType">
+            <el-select
+              v-model="form.targetType"
+              placeholder="请选择关联对象类型"
+              style="width: 100%"
+              @change="targetTypeChange">
+              <el-option label="客户" value="10" />
+              <el-option label="项目" value="20" />
+              <el-option label="合同" value="30" />
+              <el-option label="回款" value="40" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="关联对象" prop="targetId">
+            <el-select
+              v-model="form.targetId"
+              placeholder="请选择关联对象"
+              style="width: 100%"
+              @change="targetChange">
+              <el-option
+                v-for="item in targets"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input
+              v-model="form.remark"
+              placeholder="请输入备注"
+              type="textarea" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <template #footer>
+      <!-- eslint-disable-next-line vue/no-mutating-props -->
+      <el-button @click="selfVisible = false">取 消</el-button>
+      <el-button type="primary" @click="save">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+  import taskApi from '@/api/plat/task'
+  import custApi from '@/api/customer/index'
+
+  export default {
+    name: 'TaskAdd',
+    props: {
+      selfVisible: {
+        type: Boolean,
+        default: false,
+      },
+      users: {
+        type: Array,
+        // eslint-disable-next-line vue/require-valid-default-prop
+        default: [],
+      },
+      types: {
+        type: Array,
+        // eslint-disable-next-line vue/require-valid-default-prop
+        default: [],
+      },
+      doRefresh: {
+        type: Function,
+        default: undefined,
+      },
+    },
+    data() {
+      return {
+        // 团队成员
+        teamIds: [],
+        // 客户数据
+        customers: [],
+        // 关联对象
+        targets: [],
+        // 新增数据表单
+        form: {
+          taskTitle: '',
+          taskType: '',
+          taskStatus: '10',
+          isOverdue: '10',
+          taskStartDate: '',
+          taskEndDate: '',
+          taskDesc: '',
+          supervisorUserId: '',
+          watchUserId: '',
+          mainUserId: '',
+          ownerUserId: '',
+          taskLabel: '',
+          targetId: '',
+          targetType: '',
+          targetName: '',
+          remark: '',
+        },
+        // 校验规则
+        rules: {
+          taskTitle: [
+            { required: true, message: '标题不能为空', trigger: 'blur' },
+          ],
+          taskType: [
+            { required: true, message: '类型不能为空', trigger: 'blur' },
+          ],
+          taskStartDate: [
+            { required: true, message: '开始时间不能为空', trigger: 'blur' },
+          ],
+          taskEndDate: [
+            { required: true, message: '结束时间不能为空', trigger: 'blur' },
+          ],
+          supervisorUserId: [
+            { required: true, message: '督办人不能为空', trigger: 'change' },
+          ],
+          mainUserId: [
+            { required: true, message: '负责人不能为空', trigger: 'change' },
+          ],
+        },
+      }
+    },
+    watch: {
+      selfVisible(val) {
+        this.$emit('update:selfVisible', val)
+      },
+    },
+    methods: {
+      // 关联类型变化
+      targetTypeChange() {
+        this.targets.splice(0, this.targets.length)
+        this.form.targetId = ''
+        this.form.targetName = ''
+        if (this.form.targetType == '10') {
+          for (let cust of this.customers) {
+            this.targets.push(cust)
+          }
+        }
+      },
+      // 关联对象变化
+      targetChange() {
+        for (let item of this.targets) {
+          if (item.value == this.form.targetId) {
+            this.form.targetName = item.label
+            break
+          }
+        }
+      },
+      // 获取基本数据
+      getData() {
+        this.customers = []
+        custApi
+          .getList({ targetType: '10' })
+          .then((res) => {
+            if (res.data.list) {
+              for (let cust of res.data.list) {
+                let data = {
+                  value: cust.id,
+                  label: cust.custName,
+                }
+                this.customers.push(data)
+              }
+            }
+          })
+          .catch((err) => {
+            console.error(err)
+          })
+      },
+      // 打开弹窗
+      open() {
+        this.getData()
+        if (this.$refs['form']) {
+          this.$refs['form'].resetFields()
+        }
+        this.teamIds = []
+        this.form.taskTitle = ''
+        this.form.taskType = ''
+        this.form.taskStatus = '10'
+        this.form.isOverdue = '10'
+        this.form.taskStartDate = ''
+        this.form.taskEndDate = ''
+        this.form.taskDesc = ''
+        this.form.supervisorUserId = ''
+        this.form.watchUserId = ''
+        this.form.mainUserId = ''
+        this.form.ownerUserId = ''
+        this.form.taskLabel = ''
+        this.form.targetId = ''
+        this.form.targetType = ''
+        this.form.targetName = ''
+        this.form.remark = ''
+      },
+      // 关闭弹窗
+      close() {
+        if (this.$refs['form']) {
+          this.$refs['form'].resetFields()
+        }
+        // eslint-disable-next-line vue/no-mutating-props
+        this.selfVisible = false
+      },
+      // 保存数据
+      save() {
+        this.$refs['form'].validate(async (valid) => {
+          if (valid) {
+            if (this.teamIds && this.teamIds.length > 0) {
+              this.form.ownerUserId = this.teamIds.join(',')
+            } else {
+              this.form.ownerUserId = ''
+            }
+            // eslint-disable-next-line vue/no-mutating-props
+            this.selfVisible = false
+            const { msg } = await taskApi.createTask(this.form)
+            this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+            if (this.doRefresh) {
+              this.doRefresh()
+            }
+          }
+        })
+      },
+    },
+  }
+</script>

+ 322 - 0
src/views/plat/task/components/TaskDetail.vue

@@ -0,0 +1,322 @@
+<template>
+  <div>
+    <!-- eslint-disable-next-line vue/no-mutating-props, vue/no-mutating-props -->
+    <el-dialog title="督办事项" :visible.sync="selfVisible" @open="open">
+      <el-row :gutter="20">
+        <el-col :span="18">
+          <span style="font-size: 18px; margin-right: 10px">
+            {{ theTask.taskTitle }}
+          </span>
+          <el-button
+            v-show="theTask.isOverdue === '10'"
+            type="primary"
+            @click="changeStatus('IsOverdue', '20')">
+            超期
+          </el-button>
+          <el-button
+            v-show="theTask.taskStatus === '10'"
+            type="primary"
+            @click="changeStatus('TaskStatus', '20')">
+            关闭
+          </el-button>
+          <div style="border-bottom: solid 1px; margin-top: 1px"></div>
+          <el-row :gutter="20" style="margin-top: 10px">
+            <el-col :span="8">督办类型:{{ typeMap[theTask.taskType] }}</el-col>
+            <el-col :span="8">
+              状态:{{ theTask.taskStatus === '10' ? '进行中' : '关闭' }}
+            </el-col>
+            <el-col :span="8">
+              超期:{{ theTask.isOverdue === '10' ? '否' : '是' }}
+            </el-col>
+            <el-col :span="8">
+              开始时间:{{ parseTime(theTask.taskStartDate) }}
+            </el-col>
+            <el-col :span="8">
+              结束时间:{{ parseTime(theTask.taskEndDate) }}
+            </el-col>
+            <el-col :span="24">说明:{{ theTask.taskDesc }}</el-col>
+            <el-col :span="8">
+              督办人:{{ userMap[theTask.supervisorUserId] }}
+            </el-col>
+            <el-col :span="8">
+              监办人:{{ userMap[theTask.watchUserId] }}
+            </el-col>
+            <el-col :span="8">负责人:{{ userMap[theTask.mainUserId] }}</el-col>
+            <el-col :span="16">团队成员:{{ teamNames }}</el-col>
+            <el-col :span="8">
+              关联类型:
+              <span v-show="theTask.targetType == '10'">客户</span>
+              <span v-show="theTask.targetType == '20'">项目</span>
+              <span v-show="theTask.targetType == '30'">合同</span>
+              <span v-show="theTask.targetType == '40'">回款</span>
+            </el-col>
+            <el-col :span="8">关联对象:{{ theTask.targetName }}</el-col>
+            <el-col :span="8">创建人:{{ theTask.createdName }}</el-col>
+            <el-col :span="8">
+              创建时间:{{ parseTime(theTask.createdTime) }}
+            </el-col>
+            <el-col :span="24">备注:{{ theTask.remark }}</el-col>
+            <el-button
+              v-show="theTask.taskStatus === '10'"
+              type="primary"
+              @click="addProgress">
+              添加进展
+            </el-button>
+            <el-table border :data="progressList" height="440">
+              <el-table-column
+                align="center"
+                label="进展说明"
+                prop="progDesc" />
+              <el-table-column align="center" label="时间" prop="progDate">
+                <template #default="{ row }">
+                  {{ parseTime(row.progDate) }}
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="附件" prop="progFile">
+                <template #default="{ row }">
+                  <el-link
+                    v-show="row.progFile != ''"
+                    @click="showFile(row.progFile)">
+                    查看附件
+                  </el-link>
+                </template>
+              </el-table-column>
+              <el-table-column align="center" label="备注" prop="remark" />
+            </el-table>
+          </el-row>
+        </el-col>
+        <el-col :span="6">
+          <el-button
+            round
+            :type="rightInfo == 'log' ? 'primary' : 'none'"
+            @click="changeRightInfo('log')">
+            流程动态
+          </el-button>
+          <el-button
+            round
+            :type="rightInfo == 'comment' ? 'primary' : 'none'"
+            @click="changeRightInfo('comment')">
+            评论
+          </el-button>
+          <div style="border-bottom: solid 1px; margin-top: 1px"></div>
+          <div style="height: 560px; width: 100%">
+            <div
+              v-for="log in logList"
+              v-show="rightInfo == 'log'"
+              :key="log.id"
+              style="margin-top: 10px">
+              {{ log.nodeName }}
+              <div style="margin-top: 5px">
+                <span style="margin-top: 5px">{{ log.createdName }}</span>
+                <span style="float: right; margin-top: 5px">
+                  {{ log.desc }}
+                </span>
+                <div style="margin-top: 5px">开始处理:{{ log.startTime }}</div>
+                <div style="margin-top: 5px">完成处理:{{ log.endTime }}</div>
+              </div>
+            </div>
+            <el-button
+              v-show="rightInfo == 'comment'"
+              style="margin-top: 10px"
+              type="primary"
+              @click="addComment">
+              评论
+            </el-button>
+            <div
+              v-for="comment in commentList"
+              v-show="rightInfo == 'comment'"
+              :key="comment.id"
+              style="margin-top: 10px">
+              {{ comment.createdName }}
+              <div style="margin-top: 5px">
+                <span style="margin-top: 5px">
+                  评论时间:{{ comment.createdTime }}
+                </span>
+                <div style="margin-top: 5px">评论:{{ comment.content }}</div>
+              </div>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-dialog>
+    <!-- 评论添加 -->
+    <commentAdd
+      :do-refresh="getCommentList"
+      :self-visible.sync="commentAddDialogVisible"
+      :the-task="theTask" />
+    <!-- 进展添加 -->
+    <progressAdd
+      :do-refresh="doRefreshProgressAndLogs"
+      :self-visible.sync="progressAddDialogVisible"
+      :the-task="theTask" />
+  </div>
+</template>
+
+<script>
+  import taskApi from '@/api/plat/task'
+  import commentAdd from './CommentAdd.vue'
+  import progressAdd from './ProgressAdd.vue'
+
+  export default {
+    name: 'TaskDetail',
+    components: { commentAdd, progressAdd },
+    props: {
+      selfVisible: {
+        type: Boolean,
+        default: false,
+      },
+      theTask: {
+        type: Object,
+        // eslint-disable-next-line vue/require-valid-default-prop
+        default: {},
+      },
+      userMap: {
+        type: Object,
+        // eslint-disable-next-line vue/require-valid-default-prop
+        default: {},
+      },
+      typeMap: {
+        type: Object,
+        // eslint-disable-next-line vue/require-valid-default-prop
+        default: {},
+      },
+      doRefresh: {
+        type: Function,
+        default: undefined,
+      },
+    },
+    data() {
+      return {
+        // 进展新增弹窗
+        progressAddDialogVisible: false,
+        // 团队成员
+        teamNames: '',
+        // 评论新增弹窗
+        commentAddDialogVisible: false,
+        // 右侧栏显示项目
+        rightInfo: 'log',
+        // 督办进展
+        progressList: [],
+        // 日志
+        logList: [],
+        // 评论
+        commentList: [],
+      }
+    },
+    watch: {
+      selfVisible(val) {
+        this.$emit('update:selfVisible', val)
+      },
+    },
+    methods: {
+      // 改变督办状态
+      changeStatus(type, status) {
+        this.$confirm('确定修改督办状态?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(async () => {
+          // eslint-disable-next-line vue/no-mutating-props
+          this.selfVisible = false
+          let data = {
+            taskId: this.theTask.id + '',
+            type: type,
+            oldStatus: this.theTask.taskStatus,
+            nowStatus: status,
+          }
+          const { msg } = await taskApi.updateTaskStatus(data)
+          this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+          if (this.doRefresh) {
+            this.doRefresh()
+          }
+        })
+      },
+      // 刷新进展和日志信息
+      doRefreshProgressAndLogs() {
+        this.getProgressList()
+        this.getLogList()
+      },
+      // 添加进展
+      addProgress() {
+        this.progressAddDialogVisible = true
+      },
+      // 查看附件
+      showFile(path) {
+        const a = document.createElement('a')
+        a.href = path // 文件链接
+        a.download = path // 文件名,跨域资源download无效
+        a.click()
+        a.remove()
+      },
+      // 新增评论
+      addComment() {
+        this.commentAddDialogVisible = true
+      },
+      // 打开弹窗
+      open() {
+        this.rightInfo = 'log'
+        // 获取数据信息
+        this.getProgressList()
+        this.getLogList()
+        this.getCommentList()
+        this.teamNames = ''
+        if (this.theTask.ownerUserId != '') {
+          let ids = this.theTask.ownerUserId.split(',')
+          for (let id of ids) {
+            if (this.teamNames == '') {
+              this.teamNames = this.userMap[parseInt(id)]
+            } else {
+              this.teamNames += ',' + this.userMap[parseInt(id)]
+            }
+          }
+        }
+      },
+      // 获取进展信息
+      getProgressList() {
+        this.progressList = []
+        taskApi
+          .getTaskProgressList({ taskId: this.theTask.id + '' })
+          .then((res) => {
+            if (res.data.list) {
+              this.progressList = res.data.list
+            }
+          })
+          .catch((err) => {
+            console.error(err)
+          })
+      },
+      // 获取日志信息
+      getLogList() {
+        this.logList = []
+        taskApi
+          .getTaskLogList({ taskId: this.theTask.id + '' })
+          .then((res) => {
+            if (res.data.list) {
+              this.logList = res.data.list
+            }
+          })
+          .catch((err) => {
+            console.error(err)
+          })
+      },
+      // 获取评论信息
+      getCommentList() {
+        this.commentList = []
+        taskApi
+          .getTaskCommentList({ taskId: this.theTask.id + '' })
+          .then((res) => {
+            if (res.data.list) {
+              this.commentList = res.data.list
+            }
+          })
+          .catch((err) => {
+            console.error(err)
+          })
+      },
+      // 右侧操作栏改变状态
+      changeRightInfo(info) {
+        this.rightInfo = info
+      },
+    },
+  }
+</script>

+ 407 - 0
src/views/plat/task/index.vue

@@ -0,0 +1,407 @@
+<template>
+  <div class="user-management-container">
+    <el-row :gutter="20">
+      <el-col :span="2">
+        <span>操作类型</span>
+        <div style="margin-top: 25px; cursor: pointer" @click="search('1')">
+          <i class="el-icon-message-solid" style="margin-right: 10px"></i>
+          我的待办({{ statisticsForm.toDoNumber }})
+        </div>
+        <div style="margin-top: 20px; cursor: pointer" @click="search('2')">
+          <i class="el-icon-video-play" style="margin-right: 10px"></i>
+          我发起的({{ statisticsForm.createNumber }})
+        </div>
+        <div style="margin-top: 20px; cursor: pointer" @click="search('3')">
+          <i class="el-icon-folder-checked" style="margin-right: 10px"></i>
+          我处理的({{ statisticsForm.completedNumber }})
+        </div>
+        <div style="margin-top: 50px">督办类型</div>
+        <div style="margin-top: 20px; cursor: pointer" @click="searchType('')">
+          全部
+        </div>
+        <div
+          v-for="item in types"
+          :key="item.dictCode"
+          style="margin-top: 20px; cursor: pointer"
+          @click="searchType(item.dictValue)">
+          {{ item.dictLabel }}
+        </div>
+      </el-col>
+      <el-col :span="22">
+        <el-row :gutter="10" style="margin-bottom: 10px">
+          <el-col :span="4">
+            <el-input
+              v-model.trim="queryForm.taskTitle"
+              clearable
+              placeholder="请输入督办标题" />
+          </el-col>
+          <el-col :span="12">
+            <el-button icon="el-icon-search" type="primary" @click="queryData">
+              查询
+            </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="handleAdd">
+              添加
+            </el-button>
+          </vab-query-form-left-panel>
+          <vab-query-form-right-panel :span="12">
+            <el-button icon="el-icon-download" @click="exportData" />
+            <table-tool :check-list.sync="checkList" :columns="columns" />
+          </vab-query-form-right-panel>
+        </vab-query-form>
+        <!-- 主页面 -->
+        <el-table v-loading="listLoading" border :data="list" :height="height">
+          <el-table-column
+            v-for="(item, index) in finallyColumns"
+            :key="index"
+            align="center"
+            :label="item.label"
+            :prop="item.prop"
+            show-overflow-tooltip
+            :sortable="item.sortable"
+            :width="item.width">
+            <template #default="{ row }">
+              <span v-if="item.prop === 'taskType'">
+                {{ typeMap[row.taskType] }}
+              </span>
+              <span v-else-if="item.prop === 'taskStatus'">
+                {{ row.taskStatus === '10' ? '进行中' : '关闭' }}
+              </span>
+              <span v-else-if="item.prop === 'isOverdue'">
+                {{ row.isOverdue === '10' ? '否' : '是' }}
+              </span>
+              <span
+                v-else-if="
+                  item.prop === 'mainUserId' || item.prop === 'supervisorUserId'
+                ">
+                {{ userMap[row[item.prop]] }}
+              </span>
+              <span
+                v-else-if="
+                  item.prop === 'taskStartDate' ||
+                  item.prop === 'taskEndDate' ||
+                  item.prop === 'createdTime'
+                ">
+                {{ parseTime(row[item.prop]) }}
+              </span>
+              <span v-else>{{ row[item.prop] }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="操作" width="85">
+            <template #default="{ row }">
+              <el-button type="text" @click="showDetail(row)">查看</el-button>
+            </template>
+          </el-table-column>
+          <template #empty>
+            <el-image
+              class="vab-data-empty"
+              :src="require('@/assets/empty_images/data_empty.png')" />
+          </template>
+        </el-table>
+        <el-pagination
+          background
+          :current-page="queryForm.pageNum"
+          :layout="layout"
+          :page-size="queryForm.pageSize"
+          :total="total"
+          @current-change="handleCurrentChange"
+          @size-change="handleSizeChange" />
+      </el-col>
+    </el-row>
+    <!-- 新建督办 -->
+    <taskAdd
+      :do-refresh="doRefresh"
+      :self-visible.sync="addDialogVisible"
+      :types="types"
+      :users="users" />
+    <!-- 查看详情 -->
+    <taskDetail
+      :do-refresh="doRefresh"
+      :self-visible.sync="detailDialogVisible"
+      :the-task="theTask"
+      :type-map="typeMap"
+      :user-map="userMap" />
+  </div>
+</template>
+
+<script>
+  import taskApi from '@/api/plat/task'
+  import userApi from '@/api/system/user'
+  import dictApi from '@/api/system/dict'
+  import taskAdd from './components/TaskAdd.vue'
+  import taskDetail from './components/TaskDetail.vue'
+  import TableTool from '@/components/table/TableTool'
+  import downloadFileByByte from '@/utils/base64ToFile'
+
+  export default {
+    name: 'Task',
+    components: { taskAdd, taskDetail, TableTool },
+    data() {
+      return {
+        // 各督办数量统计
+        statisticsForm: {
+          toDoNumber: 0,
+          createNumber: 0,
+          completedNumber: 0,
+        },
+        // 督办详情查看
+        detailDialogVisible: false,
+        // 新建弹窗控制
+        addDialogVisible: false,
+        // 展示的督办数据
+        theTask: {},
+        list: [],
+        listLoading: true,
+        layout: 'total, sizes, prev, pager, next, jumper',
+        total: 0,
+        queryForm: {
+          pageNum: 1,
+          pageSize: 10,
+          taskTitle: undefined,
+          taskType: undefined,
+          taskStatus: undefined,
+          mySelf: undefined,
+          isMain: undefined,
+        },
+        // 用户信息
+        userMap: {},
+        users: [],
+        // 类型信息
+        typeMap: {},
+        types: [],
+        // 自定义列表
+        checkList: [],
+        columns: [
+          {
+            label: '督办标题',
+            width: 'auto',
+            prop: 'taskTitle',
+            sortable: true,
+            disableCheck: true,
+          },
+          {
+            label: '督办类型',
+            width: 'auto',
+            prop: 'taskType',
+            sortable: true,
+          },
+          {
+            label: '状态',
+            width: 'auto',
+            prop: 'taskStatus',
+            sortable: true,
+          },
+          {
+            label: '超期',
+            width: 'auto',
+            prop: 'isOverdue',
+            sortable: true,
+          },
+          {
+            label: '督办说明',
+            width: 'auto',
+            prop: 'taskDesc',
+            sortable: true,
+          },
+          {
+            label: '关联对象',
+            width: 'auto',
+            prop: 'targetName',
+            sortable: true,
+          },
+          {
+            label: '负责人',
+            width: 'auto',
+            prop: 'mainUserId',
+            sortable: true,
+          },
+          {
+            label: '督办人',
+            width: 'auto',
+            prop: 'supervisorUserId',
+            sortable: true,
+          },
+          {
+            label: '开始时间',
+            width: 'auto',
+            prop: 'taskStartDate',
+            sortable: true,
+          },
+          {
+            label: '结束时间',
+            width: 'auto',
+            prop: 'taskEndDate',
+            sortable: true,
+          },
+          {
+            label: '创建时间',
+            width: 'auto',
+            prop: 'createdTime',
+            sortable: true,
+          },
+        ],
+      }
+    },
+    computed: {
+      height() {
+        return this.$baseTableHeight(1)
+      },
+      finallyColumns() {
+        return this.columns.filter((item) =>
+          this.checkList.includes(item.label)
+        )
+      },
+    },
+    async created() {
+      this.statistics()
+      await this.initData()
+      this.fetchData()
+    },
+    methods: {
+      // 刷新表数据和数量统计
+      doRefresh() {
+        this.fetchData()
+        this.statistics()
+      },
+      // 统计各类型督办数量
+      statistics() {
+        taskApi
+          .statisticsTaskNumber()
+          .then((res) => {
+            if (res.data.list) {
+              this.statisticsForm = res.data.list
+            }
+          })
+          .catch((err) => {
+            console.error(err)
+          })
+      },
+      // 数据导出
+      exportData() {
+        let exportFrom = JSON.parse(JSON.stringify(this.queryForm))
+        exportFrom.columns = this.finallyColumns.map((item) => item.label)
+        taskApi
+          .exportTasks(exportFrom)
+          .then((res) => {
+            if (res.data.list.content) {
+              downloadFileByByte(res.data.list.content, '督办数据.xlsx')
+            }
+          })
+          .catch((err) => {
+            console.error(err)
+          })
+      },
+      // 重置查询数据
+      reset() {
+        this.queryForm.pageNum = 1
+        this.queryForm.pageSize = 10
+        this.queryForm.taskTitle = undefined
+        this.queryForm.taskType = undefined
+        this.queryForm.taskStatus = undefined
+        this.queryForm.mySelf = undefined
+        this.queryForm.isMain = undefined
+        this.queryData()
+      },
+      // 左侧操作栏搜索
+      search(type) {
+        this.queryForm.taskStatus = undefined
+        this.queryForm.mySelf = undefined
+        this.queryForm.isMain = undefined
+        if (type == '1') {
+          this.queryForm.taskStatus = '10'
+          this.queryForm.isMain = '1'
+        }
+        if (type == '2') {
+          this.queryForm.mySelf = '1'
+        }
+        if (type == '3') {
+          this.queryForm.taskStatus = '20'
+          this.queryForm.isMain = '1'
+        }
+        this.queryData()
+      },
+      // 督办类型搜索
+      searchType(type) {
+        this.queryForm.taskType = type
+        this.queryData()
+      },
+      // 初始化数据
+      async initData() {
+        await dictApi
+          .getDictDataList({ dictType: 'TaskType' })
+          .then((res) => {
+            if (res.data.list) {
+              this.types = res.data.list
+              for (let type of this.types) {
+                this.typeMap[type.dictValue] = type.dictLabel
+              }
+            }
+          })
+          .catch((err) => {
+            console.error(err)
+          })
+        await userApi
+          .getList()
+          .then((res) => {
+            if (res.data.list) {
+              this.users = res.data.list
+              for (let user of this.users) {
+                this.userMap[user.id] = user.userName
+              }
+            }
+          })
+          .catch((err) => {
+            console.error(err)
+          })
+      },
+      // 显示详情数据
+      showDetail(row) {
+        this.theTask = JSON.parse(JSON.stringify(row))
+        this.detailDialogVisible = true
+      },
+      // 处理新增
+      handleAdd() {
+        this.addDialogVisible = true
+      },
+      // 更换页数据大小
+      handleSizeChange(val) {
+        this.queryForm.pageSize = val
+        this.fetchData()
+      },
+      // 更换当前页
+      handleCurrentChange(val) {
+        this.queryForm.pageNum = val
+        this.fetchData()
+      },
+      // 查询
+      queryData() {
+        this.queryForm.pageNum = 1
+        this.fetchData()
+      },
+      // 获取数据
+      fetchData() {
+        this.listLoading = true
+        this.list = []
+        taskApi
+          .getTaskList(this.queryForm)
+          .then((res) => {
+            if (res.data.list) {
+              this.list = res.data.list
+            }
+            this.total = res.data.total
+            this.listLoading = false
+          })
+          .catch((err) => {
+            this.listLoading = false
+            console.error(err)
+          })
+      },
+    },
+  }
+</script>