소스 검색

feature:增加经销商代理商管理模块

liuzl 2 년 전
부모
커밋
084747090e

+ 60 - 0
src/api/base/distr.js

@@ -1,3 +1,11 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-05-17 10:33:43
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-05-19 17:31:25
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\api\base\distr.js
+ */
 import micro_request from '@/utils/micro_request'
 
 const basePath = process.env.VUE_APP_ParentPath
@@ -29,4 +37,56 @@ export default {
   getDynamicsList(query) {
     return micro_request.postRequest(basePath, 'Distributor', 'DynamicsList', query)
   },
+  // 获取经销商代理商项目记录列表
+  getProjectList(query) {
+    return micro_request.postRequest(basePath, 'Distributor', 'ProjectList', query)
+  },
+  // 获取经销商代理商合同列表
+  getContractList(query) {
+    return micro_request.postRequest(basePath, 'Distributor', 'ContractList', query)
+  },
+  // 获取经销商代理商联系人列表
+  getContactList(query) {
+    return micro_request.postRequest(basePath, 'DistributorContact', 'List', query)
+  },
+  // 新增经销商代理商联系人
+  addContact(query) {
+    return micro_request.postRequest(basePath, 'DistributorContact', 'Add', query)
+  },
+  // 更新经销商代理商联系人
+  updateContact(query) {
+    return micro_request.postRequest(basePath, 'DistributorContact', 'Update', query)
+  },
+  // 删除经销商代理商联系人
+  delContact(query) {
+    return micro_request.postRequest(basePath, 'DistributorContact', 'Delete', query)
+  },
+  // 转代理商
+  changeAgent(query) {
+    return micro_request.postRequest(basePath, 'Distributor', 'ToProxy', query)
+  },
+  // 转经销商
+  changeDistr(query) {
+    return micro_request.postRequest(basePath, 'Distributor', 'ToDist', query)
+  },
+  // 续签
+  renewal(query) {
+    return micro_request.postRequest(basePath, 'Distributor', 'Renew', query)
+  },
+  // 历史代理记录
+  getProxyHistoryList(query) {
+    return micro_request.postRequest(basePath, 'Distributor', 'TransRecord', query)
+  },
+  // 获取代理商指标
+  getProxyIndexList(query) {
+    return micro_request.postRequest(basePath, 'DistributorTarget', 'List', query)
+  },
+  // 新增代理商指标
+  addProxyIndex(query) {
+    return micro_request.postRequest(basePath, 'DistributorTarget', 'Add', query)
+  },
+  // 编辑代理商指标
+  editProxyIndex(query) {
+    return micro_request.postRequest(basePath, 'DistributorTarget', 'Update', query)
+  },
 }

+ 392 - 0
src/views/base/agent/components/AgentEdit.vue

@@ -0,0 +1,392 @@
+<template>
+  <el-dialog append-to-body :title="title" :visible.sync="dialogFormVisible" @close="close">
+    <el-form ref="form" :model="form" :rules="dist">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="代理商名称" prop="distName">
+            <el-input v-model="form.distName" placeholder="请输入代理商名称" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="助记名" prop="abbrName">
+            <el-input v-model="form.abbrName" placeholder="请输入助记名" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="选择省份" prop="provinceDesc">
+            <el-select
+              ref="optionRef"
+              v-model="form.provinceDesc"
+              placeholder="请选择"
+              style="width: 100%"
+              @change="selectDistrict">
+              <el-option
+                v-for="item in district"
+                :key="item.regionCode"
+                :label="item.regionDesc"
+                :value="item.regionCode" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="省份" prop="provinceId" style="display: none">
+            <el-input v-model.trim="form.provinceId" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <select-user
+            ref="selectUser"
+            :query-params="{ roles: ['SalesEngineer', 'ProductLineManager'] }"
+            @save="selectUser" />
+          <el-form-item label="销售人员" prop="belongSale">
+            <el-input v-model="form.belongSale" readonly suffix-icon="el-icon-search" @focus="choose" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="注册资金/万元" prop="capital">
+            <el-input v-model.number="form.capital" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="注册地" prop="registerDistrict">
+            <el-input v-model.trim="form.registerDistrict" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="业务范围" prop="businessScope">
+            <el-input v-model.trim="form.businessScope" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="现有销售人数" prop="saleNum">
+            <el-input v-model.number="form.saleNum" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="授权客户类型" prop="customerType">
+            <el-select v-model="form.customerType" multiple placeholder="授权客户类型" style="width: 100%">
+              <el-option v-for="item in customerOptions" :key="item.value" :label="item.value" :value="item.key" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="授权代理区域" prop="proxyDistrict">
+            <el-input v-model="form.proxyDistrict" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="代理签约有效期" prop="date">
+            <el-date-picker
+              v-model="form.date"
+              end-placeholder="结束日期"
+              range-separator="至"
+              start-placeholder="开始日期"
+              style="width: 100%"
+              type="daterange"
+              value-format="yyyy-MM-dd" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="代理合同" prop="contractUrl">
+            <div style="width: 100%">
+              <span style="color: #999">支持格式:.rar .zip .doc .docx .pdf ,单个文件不能超过20MB</span>
+              <el-upload
+                ref="uploadRef"
+                action="#"
+                :before-upload="
+                  (file) => {
+                    return beforeAvatarUpload(file)
+                  }
+                "
+                :file-list="fileList"
+                :http-request="uploadrequest"
+                :limit="1"
+                :on-exceed="handleExceed"
+                :on-remove="handleRemove">
+                <el-button size="mini" type="primary">点击上传</el-button>
+              </el-upload>
+            </div>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="已有代理品牌和产品" prop="existedProduct">
+        <el-input
+          v-model="form.existedProduct"
+          maxlength="500"
+          placeholder="请输入备注"
+          resize="none"
+          :rows="5"
+          show-word-limit
+          type="textarea" />
+      </el-form-item>
+      <el-form-item label="历史合作的终端客户名称" prop="historyCustomer">
+        <el-input
+          v-model="form.historyCustomer"
+          maxlength="500"
+          placeholder="请输入备注"
+          resize="none"
+          :rows="5"
+          show-word-limit
+          type="textarea" />
+      </el-form-item>
+    </el-form>
+    <span slot="footer">
+      <el-button @click="close">取 消</el-button>
+      <el-button type="primary" @click="save">确 定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+  import distrApi from '@/api/base/distr'
+  import '@riophae/vue-treeselect/dist/vue-treeselect.css'
+  import regionAuthApi from '@/api/base/regionAuth'
+  import SelectUser from '@/components/select/SelectUser'
+  import asyncUploadFile from '@/utils/uploadajax'
+  import axios from 'axios'
+
+  export default {
+    name: 'UserEdit',
+    components: {
+      SelectUser,
+    },
+    data() {
+      return {
+        form: {
+          distName: '',
+          abbrName: '',
+          provinceDesc: '',
+          provinceId: 0,
+          belongSale: '',
+          belongSaleId: 0,
+          capital: 0,
+          registerDistrict: '',
+          businessScope: '',
+          saleNum: 0,
+          customerType: '',
+          existedProduct: '',
+          historyCustomer: '',
+          proxyDistrict: '',
+          date: [],
+          proxyEndTime: '',
+          proxyStartTime: '',
+          contractUrl: '',
+        },
+        dist: {
+          distName: [{ required: true, trigger: 'blur', message: '请输入经销商名称' }],
+          provinceDesc: [{ required: true, trigger: 'blur', message: '请输入省份' }],
+          provinceId: [{ required: true, trigger: 'blur', message: '请输入省份' }],
+          belongSale: [{ required: true, trigger: 'blur', message: '请输入销售人员' }],
+          capital: [{ required: true, trigger: 'blur', message: '请输入注册资金' }],
+          registerDistrict: [{ required: true, trigger: 'blur', message: '请输入注册地' }],
+          businessScope: [{ required: true, trigger: 'blur', message: '请输入业务范围' }],
+          saleNum: [{ required: true, trigger: 'blur', message: '请输入现有销售人数' }],
+          customerType: [{ required: true, trigger: 'blur', message: '请输入授权客户类型' }],
+          proxyDistrict: [{ required: true, trigger: 'blur', message: '请输入授权代理区域' }],
+          date: [{ required: true, trigger: 'blur', message: '请选择代理签约有效期' }],
+          existedProduct: [{ required: true, trigger: 'blur', message: '请输入已有代理品牌和产品' }],
+          historyCustomer: [{ required: true, trigger: 'blur', message: '请输入历史合作的终端客户名称' }],
+        },
+        //省份
+        district: [],
+        title: '',
+        dialogFormVisible: false,
+        customerOptions: [],
+        fileList: [],
+        fileSettings: {
+          // 文件配置信息
+          fileSize: 20971520,
+          fileTypes: '.rar,.zip,.doc,.docx,.pdf',
+          pictureSize: 20971520,
+          pictureTypes: '.jpg,.jpeg,.gif,.png,.jfif,.txt',
+          types: '.rar,.zip,.doc,.docx,.pdf',
+          videoSize: 104857600,
+          videoType: '.mp4',
+        },
+      }
+    },
+    created() {
+      //省份
+      // this.getProvinceInfo()
+      this.getUserSalesProvince()
+    },
+    mounted() {
+      this.getOptions()
+    },
+    methods: {
+      getOptions() {
+        Promise.all([this.getDicts('cust_idy')])
+          .then(([data]) => {
+            this.customerOptions = data.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
+      showEdit(row) {
+        if (!row) {
+          this.title = '新建'
+        } else {
+          this.title = '编辑'
+          this.form = Object.assign({}, row)
+          this.form.customerType = row.customerType.split(',')
+          this.form.date = [this.form.proxyStartTime, this.form.proxyEndTime]
+          if (row.contractUrl) {
+            this.fileList = [{ name: '代理合同', url: row.contractUrl }]
+          }
+        }
+        this.dialogFormVisible = true
+      },
+      close() {
+        this.$refs['form'].resetFields()
+        this.form = {
+          distName: '',
+          abbrName: '',
+          provinceDesc: '',
+          provinceId: 0,
+          belongSale: '',
+          belongSaleId: 0,
+          capital: 0,
+          registerDistrict: '',
+          businessScope: '',
+          saleNum: 0,
+          customerType: '',
+          existedProduct: '',
+          historyCustomer: '',
+          proxyDistrict: '',
+          date: [],
+          proxyEndTime: '',
+          proxyStartTime: '',
+          contractUrl: '',
+        }
+        this.fileList = []
+        this.dialogFormVisible = false
+      },
+      choose() {
+        // this.$refs.transfer.innerVisible = true
+        this.$refs.selectUser.open()
+      },
+      async getProvinceInfo() {
+        const { data: data } = await distrApi.getProvinceInfo({})
+        this.district = data.list
+      },
+      async getUserSalesProvince() {
+        const { data: data } = await regionAuthApi.getUserSalesProvince({})
+        if (data && data.list) {
+          this.district = data.list.children
+        }
+      },
+      selectDistrict(code) {
+        let obj = {}
+        obj = this.district.find((item) => {
+          return item.regionCode === code //筛选出匹配数据
+        })
+        console.log('省份名称', obj)
+        this.provinceDesc = obj.regionDesc
+        this.form.provinceId = obj.regionCode
+        this.form.provinceDesc = obj.regionDesc
+      },
+      save() {
+        this.$refs['form'].validate(async (valid) => {
+          if (valid) {
+            console.log(this.form)
+            let params = { ...this.form }
+            params.provinceId = parseInt(params.provinceId)
+            params.distType = '20'
+            params.proxyStartTime = params.date[0]
+            params.proxyEndTime = params.date[1]
+            params.customerType = params.customerType.join(',')
+            if (this.form.id) {
+              console.log('表单修改提交内容:', params)
+              const { msg } = await distrApi.doEdit(params)
+              this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+            } else {
+              console.log('表单提交内容:', params)
+              const { msg } = await distrApi.doAdd(params)
+              this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+            }
+            this.$emit('fetch-data')
+            this.close()
+          }
+        })
+      },
+      selectUser(userList) {
+        this.userList = userList
+        if (userList && userList.length > 0) {
+          this.form.belongSaleId = userList[0].id
+        }
+        this.form.belongSale = userList.map((item) => item.nickName).join()
+      },
+      async handleSubmit() {
+        this.form.belongSaleId = this.userList[0].id
+        this.form.belongSale = this.userList[0].nickName
+      },
+      // 上传图片
+      beforeAvatarUpload(file) {
+        let flag1 = file.size < this.fileSettings.fileSize
+        if (!flag1) {
+          this.$message.warning('文件过大,请重新选择!')
+          return false
+        }
+        let flag2 = this.fileSettings.fileTypes.split(',').includes('.' + file.name.split('.').pop())
+        if (!flag2) {
+          this.$message.warning('文件类型不符合,请重新选择!')
+          return false
+        }
+        return true
+      },
+      // 图片删除
+      handleRemove() {
+        this.form.curmCover = ''
+        this.fileList = []
+      },
+      handleExceed() {
+        this.$message.warning(`当前限制只能上传一个附件`)
+      },
+      // 上传
+      uploadrequest(option) {
+        let _this = this
+        let url = process.env.VUE_APP_UPLOAD_WEED
+        axios
+          .post(url)
+          .then(function (res) {
+            if (res.data && res.data.fid && res.data.fid !== '') {
+              option.action = `${process.env.VUE_APP_PROTOCOL}${res.data.publicUrl}/${res.data.fid}`
+              let file_name = option.file.name
+              let index = file_name.lastIndexOf('.')
+              let file_extend = ''
+              if (index > 0) {
+                // 截取名称中的扩展名
+                file_extend = file_name.substr(index + 1)
+              }
+              let uploadform = {
+                fileName: file_name, // 资料名称
+                fileUrl: `${process.env.VUE_APP_PROTOCOL}${res.data.publicUrl}/${res.data.fid}`, // 资料存储url
+                size: option.file.size.toString(), // 资料大小
+                fileType: file_extend, // 资料文件类型
+              }
+              asyncUploadFile(option).then(() => {
+                _this.form.contractUrl = uploadform.fileUrl
+              })
+            } else {
+              _this.$message({
+                type: 'warning',
+                message: '未上传成功!请刷新界面重新上传!',
+              })
+            }
+          })
+          .catch(function () {
+            _this.$message({
+              type: 'warning',
+              message: '未上传成功!请重新上传!',
+            })
+          })
+      },
+    },
+  }
+</script>

+ 234 - 0
src/views/base/agent/components/BusinessTarget.vue

@@ -0,0 +1,234 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-05-17 14:03:04
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-05-19 17:18:12
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\base\agent\components\BusinessTarget.vue
+-->
+<template>
+  <div class="pane-wrapper">
+    <el-row align="middle" class="mb10" justify="end" type="flex">
+      <el-button icon="el-icon-plus" size="mini" type="primary" @click="$refs.target.open()">新增</el-button>
+    </el-row>
+    <el-table ref="table" v-loading="listLoading" :data="list" height="calc(100% - 94px)">
+      <el-table-column
+        v-for="(item, index) in columns"
+        :key="index"
+        align="center"
+        :label="item.label"
+        :min-width="item.width"
+        :prop="item.prop"
+        show-overflow-tooltip
+        :sortable="item.sortable">
+        <template #default="{ row }">
+          <span v-if="item.prop === 'q1Amount'">
+            {{ row.q1Amount.toFixed(2) }}
+          </span>
+          <span v-else-if="item.prop === 'q1Rate'">
+            {{ row.q1Amount == 0 ? 0 : ((row.q1Amount.toFixed(2) / row.q1) * 100).toFixed(2) }} %
+          </span>
+          <span v-else-if="item.prop === 'q2Amount'">
+            {{ row.q2Amount.toFixed(2) }}
+          </span>
+          <span v-else-if="item.prop === 'q2Rate'">
+            {{ row.q2Amount == 0 ? 0 : ((row.q2Amount.toFixed(2) / row.q2) * 100).toFixed(2) }} %
+          </span>
+          <span v-else-if="item.prop === 'q3Amount'">
+            {{ row.q3Amount.toFixed(2) }}
+          </span>
+          <span v-else-if="item.prop === 'q3Rate'">
+            {{ row.q3Amount == 0 ? 0 : ((row.q3Amount.toFixed(2) / row.q3) * 100).toFixed(2) }} %
+          </span>
+          <span v-else-if="item.prop === 'q4Amount'">
+            {{ row.q4Amount.toFixed(2) }}
+          </span>
+          <span v-else-if="item.prop === 'q4Rate'">
+            {{ row.q4Amount == 0 ? 0 : ((row.q4Amount.toFixed(2) / row.q4) * 100).toFixed(2) }} %
+          </span>
+          <span v-else-if="item.prop === 'totalAmount'">
+            {{ row.totalAmount.toFixed(2) }}
+          </span>
+          <span v-else-if="item.prop === 'totalRate'">
+            {{ row.totalAmount == 0 ? 0 : ((row.totalAmount.toFixed(2) / row.q4) * 100).toFixed(2) }} %
+          </span>
+          <span v-else>{{ row[item.prop] }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" show-overflow-tooltip width="85">
+        <template #default="{ row }">
+          <el-button v-permissions="['base:distributor:edit']" type="text">编辑</el-button>
+          <el-button v-permissions="['base:distributor:delete']" type="text" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <edit-target ref="target" @fetchData="queryData" />
+  </div>
+</template>
+
+<script>
+  import distrApi from '@/api/base/distr'
+  import to from 'await-to-js'
+  import EditTarget from './EditTarget'
+  export default {
+    name: 'ProjectRecords',
+    components: { EditTarget },
+    data() {
+      return {
+        listLoading: false,
+        list: [],
+        columns: [
+          {
+            label: '年份',
+            width: '160px',
+            prop: 'year',
+            sortable: false,
+          },
+          {
+            label: 'Q1代理指标',
+            width: '160px',
+            prop: 'q1',
+            sortable: false,
+          },
+          {
+            label: 'Q1完成指标',
+            width: '100px',
+            prop: 'q1Amount',
+            sortable: false,
+          },
+          {
+            label: 'Q1完成比率',
+            width: '160px',
+            prop: 'q1Rate',
+            sortable: false,
+          },
+          {
+            label: 'Q2代理指标',
+            width: '100px',
+            prop: 'q2',
+            sortable: false,
+          },
+          {
+            label: 'Q2完成指标',
+            width: '100px',
+            prop: 'q2Amount',
+            sortable: false,
+          },
+          {
+            label: 'Q2完成比率',
+            width: '100px',
+            prop: 'q2Rate',
+            sortable: false,
+          },
+          {
+            label: 'Q3代理指标',
+            width: '100px',
+            prop: 'q3',
+            sortable: false,
+          },
+          {
+            label: 'Q3完成指标',
+            width: '100px',
+            prop: 'q3Amount',
+            sortable: false,
+          },
+          {
+            label: 'Q3完成比率',
+            width: '100px',
+            prop: 'q3Rate',
+            sortable: false,
+          },
+          {
+            label: 'Q4代理指标',
+            width: '100px',
+            prop: 'q4',
+            sortable: false,
+          },
+          {
+            label: 'Q4完成指标',
+            width: '100px',
+            prop: 'q4Amount',
+            sortable: false,
+          },
+          {
+            label: 'Q4完成比率',
+            width: '100px',
+            prop: 'q4Rate',
+            sortable: false,
+          },
+          {
+            label: '年度代理指标',
+            width: '160px',
+            prop: 'total',
+            sortable: false,
+          },
+          {
+            label: '年度完成指标',
+            width: '160px',
+            prop: 'totalAmount',
+            sortable: false,
+          },
+          {
+            label: '年度完成比率',
+            width: '160px',
+            prop: 'totalRate',
+            sortable: false,
+          },
+        ],
+      }
+    },
+
+    mounted() {
+      this.queryData()
+    },
+
+    methods: {
+      async queryData() {
+        this.listLoading = true
+        const params = {
+          distId: this.$route.query.id * 1,
+        }
+        const [err, res] = await to(distrApi.getProxyIndexList(params))
+        this.listLoading = false
+        if (err) return
+        if (res.code == 200) {
+          console.log(res.data)
+          this.list = res.data.list
+        }
+      },
+      handleDelete(row) {
+        if (row.id) {
+          this.$baseConfirm('你确定要删除当前项吗', null, async () => {
+            const { msg } = await distrApi.doDelete({ ids: [row.id] })
+            this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+            await this.fetchData()
+          })
+        } else {
+          if (this.selectRows.length > 0) {
+            const ids = this.selectRows.map((item) => parseInt(item.id))
+            console.log(ids)
+            this.$baseConfirm('你确定要删除选中项吗', null, async () => {
+              const { msg } = await distrApi.doDelete({ ids })
+              this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+              await this.fetchData()
+            })
+          } else {
+            this.$baseMessage('未选中任何行', 'error', 'vab-hey-message-error')
+          }
+        }
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .pane-wrapper {
+    height: 100%;
+  }
+  .mb10 {
+    margin-bottom: 10px;
+  }
+  .mr10 {
+    margin-right: 10px;
+  }
+</style>

+ 127 - 0
src/views/base/agent/components/DetailsRecords.vue

@@ -0,0 +1,127 @@
+<!--
+ * @Author: liuzl 461480418@qq.com
+ * @Date: 2023-01-09 17:42:13
+ * @LastEditors: wanglj
+ * @LastEditTime: 2023-02-21 17:14:52
+ * @Description: file content
+ * @FilePath: \opms_frontend\src\views\contract\components\DetailsRecords.vue
+-->
+<template>
+  <ul class="records">
+    <li v-for="(value, key) in dynamicsList" :key="key">
+      <div class="date">
+        {{ key }}
+        <h2>{{ key.split('-')[2] }}</h2>
+        <h3>{{ key.split('-').splice(0, 2).join('.') }}</h3>
+      </div>
+      <ul class="content">
+        <li v-for="(item, index) in dynamicsList[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>{{ parseTime(item.opnDate, '{y}-{m}-{d}') }}</p>
+          </div>
+        </li>
+      </ul>
+    </li>
+  </ul>
+</template>
+
+<script>
+  export default {
+    name: 'Records',
+    props: {
+      dynamicsList: {
+        // eslint-disable-next-line vue/require-prop-type-constructor
+        type: Array | Object,
+        default: () => [],
+      },
+    },
+    data() {
+      return {}
+    },
+
+    mounted() {},
+
+    methods: {},
+  }
+</script>
+
+<style lang="scss" scoped>
+  .records {
+    margin: 0;
+    padding: 10px 20px;
+    list-style: none;
+    height: calc(100% - 60px);
+    margin-top: 6px;
+    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;
+        }
+      }
+    }
+  }
+</style>

+ 146 - 0
src/views/base/agent/components/EditTarget.vue

@@ -0,0 +1,146 @@
+<template>
+  <el-dialog append-to-body :title="title" :visible.sync="visible" @close="close">
+    <el-form ref="form" :model="form" :rules="rules">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="年份" prop="year">
+            <el-date-picker
+              v-model="form.year"
+              placeholder="选择年"
+              style="width: 100%"
+              type="year"
+              value-format="yyyy" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="Q1代理指标(万元)" prop="q1">
+            <el-input v-model.number="form.q1" style="width: 100%" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="Q2代理指标(万元)" prop="q2">
+            <el-input v-model.number="form.q2" style="width: 100%" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="Q3代理指标(万元)" prop="q3">
+            <el-input v-model.number="form.q3" style="width: 100%" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="Q4代理指标(万元)" prop="q4">
+            <el-input v-model.number="form.q4" style="width: 100%" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="年度代理指标(万元)">
+            <el-input v-model="totalTarget" readonly style="width: 100%" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <span slot="footer">
+      <el-button @click="close">取 消</el-button>
+      <el-button type="primary" @click="save">确 定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+  import to from 'await-to-js'
+  import distrApi from '@/api/base/distr'
+  import '@riophae/vue-treeselect/dist/vue-treeselect.css'
+  export default {
+    name: 'UserEdit',
+    data() {
+      return {
+        visible: false,
+        title: '',
+        form: {
+          distId: 0,
+          id: null,
+          year: '',
+          q1: 0,
+          q2: 0,
+          q3: 0,
+          q4: 0,
+        },
+        rules: {
+          year: [{ required: true, trigger: 'blur', message: '请选择年份' }],
+          proxyDistrict: [{ required: true, trigger: 'blur', message: '请输入授权代理区域' }],
+          date: [{ required: true, trigger: 'blur', message: '请选择代理签约有效期' }],
+        },
+      }
+    },
+    computed: {
+      totalTarget: function () {
+        return this.form.q1 + this.form.q2 + this.form.q3 + this.form.q4
+      },
+    },
+    methods: {
+      open(row) {
+        this.form.distId = this.$route.query.id * 1
+        if (!row) {
+          this.title = '新增指标'
+        } else {
+          this.form.id = row.id
+          this.title = '编辑指标'
+        }
+        this.visible = true
+      },
+      close() {
+        this.form = {
+          id: null,
+          year: '',
+          q1: 0,
+          q2: 0,
+          q3: 0,
+          q4: 0,
+        }
+        this.visible = false
+      },
+      /**
+       * @param {string} value - 输入的值
+       * @param {string} name - 匹配的对象属性 [mkPrice | slPrice]
+       */
+      limitInput(value, name) {
+        let num =
+          ('' + value) // 第一步:转成字符串
+            .replace(/[^\d^.]+/g, '') // 第二步:把不是数字,不是小数点的过滤掉
+            .replace(/^0+(\d)/, '$1') // 第三步:第一位0开头,0后面为数字,则过滤掉,取后面的数字
+            .replace(/^\./, '0.') // 第四步:如果输入的第一位为小数点,则替换成 0. 实现自动补全
+            .match(/^\d*(\.?\d{0,2})/g)[0] || '' // 第五步:最终匹配得到结果 以数字开头,只有一个小数点,而且小数点后面只能有0到2位小数
+        this.form[name] = Number(num)
+      },
+      save() {
+        this.$refs['form'].validate(async (valid) => {
+          if (valid) {
+            console.log(this.form)
+            let params = { ...this.form }
+            params.year = Number(params.year)
+            // params.q1 = Number(params.q1)
+            // params.q2 = Number(params.q2)
+            // params.q3 = Number(params.q3)
+            // params.q4 = Number(params.q4)
+            console.log(params)
+            let [err, res] = []
+            if (!this.form.id) {
+              ;[err, res] = await to(distrApi.addProxyIndex(params))
+            } else {
+              ;[err, res] = await to(distrApi.editProxyIndex(params))
+            }
+            if (err) return
+            if (res.code == 200) {
+              this.$baseMessage(`${this.title}成功`, 'success', 'vab-hey-message-success')
+              this.$emit('fetchData')
+              this.close()
+            }
+          }
+        })
+      },
+    },
+  }
+</script>

+ 557 - 0
src/views/base/agent/detail.vue

@@ -0,0 +1,557 @@
+<template>
+  <div class="detail">
+    <div class="side-layout">
+      <div class="info">
+        <div class="title">
+          <p>代理商</p>
+          <h3>
+            {{ detail.distName }}
+          </h3>
+        </div>
+        <header>
+          <el-descriptions :colon="false" :column="6" direction="vertical" style="padding-top: 15px">
+            <el-descriptions-item content-class-name="my-content" label="签约有效期" label-class-name="my-label">
+              <el-tooltip
+                class="item"
+                :content="
+                  parseTime(detail.proxyStartTime, '{y}-{m}-{d}') + '-' + parseTime(detail.proxyEndTime, '{y}-{m}-{d}')
+                "
+                effect="dark"
+                placement="top-start">
+                <span>
+                  {{
+                    parseTime(detail.proxyStartTime, '{y}-{m}-{d}') +
+                    '-' +
+                    parseTime(detail.proxyEndTime, '{y}-{m}-{d}')
+                  }}
+                </span>
+              </el-tooltip>
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="年度代理指标" label-class-name="my-label">
+              {{ detail.yearTarget }}
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="ABC项目总数量" label-class-name="my-label">
+              {{ detail.projectNum }}
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="成交项目数量" label-class-name="my-label">
+              {{ detail.saledProjectNum }}
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="成交总金额" label-class-name="my-label">
+              {{ formatPrice(detail.saledAmount) }}
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="归属人员" label-class-name="my-label">
+              {{ detail.belongSale }}
+            </el-descriptions-item>
+          </el-descriptions>
+        </header>
+        <el-tabs v-model="activeName">
+          <el-tab-pane label="详细信息" name="details">
+            <el-descriptions border :column="2" size="medium" title="代理商详情">
+              <el-descriptions-item content-class-name="my-content" label="代理商名称" label-class-name="my-label">
+                {{ detail.distName }}
+              </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.provinceDesc }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="业务范围" label-class-name="my-label">
+                {{ detail.businessScope }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="注册资金" label-class-name="my-label">
+                {{ detail.capital }}万元
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="注册地" label-class-name="my-label">
+                {{ detail.registerDistrict }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="现有销售人数:" label-class-name="my-label">
+                {{ detail.saleNum }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="授权客户类型" label-class-name="my-label">
+                {{ setCustomerType(detail.customerType) }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="授权区域代理" label-class-name="my-label">
+                {{ detail.proxyDistrict }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="代理合同" label-class-name="my-label">
+                <el-link v-show="detail.contractUrl != ''" @click="showFile(detail.contractUrl)">查看附件</el-link>
+              </el-descriptions-item>
+              <el-descriptions-item
+                content-class-name="my-content"
+                label="ABC项目出货总金额"
+                label-class-name="my-label">
+                {{ formatPrice(detail.allProductAmount) }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="未回款金额" label-class-name="my-label">
+                {{ formatPrice(detail.unpaidAmount) }}
+              </el-descriptions-item>
+              <el-descriptions-item
+                content-class-name="my-content"
+                label="已有代理品牌和产品"
+                label-class-name="my-label">
+                {{ detail.existedProduct }}
+              </el-descriptions-item>
+              <el-descriptions-item
+                content-class-name="my-content"
+                label="历史合作终端客户名称"
+                label-class-name="my-label">
+                {{ detail.historyCustomer }}
+              </el-descriptions-item>
+              <!-- <el-descriptions-item content-class-name="my-content" label="归属销售" label-class-name="my-label">
+                {{ detail.belongSale }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="销售人数" label-class-name="my-label">
+                {{ detail.saleNum }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="创建时间" label-class-name="my-label">
+                {{ parseTime(detail.createdTime, '{y}-{m}-{d}') }}
+              </el-descriptions-item>
+              <el-descriptions-item content-class-name="my-content" label="更新时间" label-class-name="my-label">
+                {{ parseTime(detail.updatedTime, '{y}-{m}-{d}') }}
+              </el-descriptions-item> -->
+            </el-descriptions>
+          </el-tab-pane>
+          <el-tab-pane label="业务指标" name="business">
+            <business-target v-if="activeName == 'business'" />
+          </el-tab-pane>
+          <el-tab-pane label="跟进记录" name="follow">
+            <follow v-if="activeName == 'follow'" target-type="60" />
+          </el-tab-pane>
+          <el-tab-pane label="联系人" name="contacts">
+            <contacts v-if="activeName == 'contacts'" @initRecords="getRecord" />
+          </el-tab-pane>
+          <el-tab-pane label="项目记录" name="projectRecords">
+            <project-records v-if="activeName == 'projectRecords'" />
+          </el-tab-pane>
+          <el-tab-pane label="合同记录" name="contractRecords">
+            <contract-records v-if="activeName == 'contractRecords'" />
+          </el-tab-pane>
+          <el-tab-pane label="历史代理记录" name="historyProxy">
+            <history-proxy v-if="activeName == 'historyProxy'" />
+          </el-tab-pane>
+        </el-tabs>
+      </div>
+      <div class="info-side">
+        <div class="buttons">
+          <!-- <el-button v-permissions="['contract:manage:edit']" type="primary" @click="handleEdit">编辑</el-button>
+          <el-button v-permissions="['contract:manage:delete']" @click="handleDelete">删除</el-button> -->
+          <el-button @click="back">返回</el-button>
+        </div>
+        <details-records :dynamics-list="dynamicsList" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import { mapGetters } from 'vuex'
+  import api from '@/api/base/distr'
+  import Contacts from '../components/Contacts'
+  import ProjectRecords from '../components/ProjectRecords'
+  import ContractRecords from '../components/ContractRecords'
+  import HistoryProxy from '../components/HistoryProxy'
+  import DetailsRecords from './components/DetailsRecords'
+  import BusinessTarget from './components/BusinessTarget'
+  import Follow from '../components/Follow'
+  export default {
+    name: 'DistributorDetail',
+    components: { DetailsRecords, Contacts, ProjectRecords, ContractRecords, HistoryProxy, BusinessTarget, Follow },
+    data() {
+      return {
+        id: 0,
+        privateCus: '',
+        list: [],
+        activeName: 'details',
+        detail: {
+          distCode: '', //代理商编码
+          distName: '', //代理商名称
+          abbrName: '', //助记名
+          distBoss: '', //负责人
+          distBossPhone: '', //负责人电话
+          belongSale: '', //销售人
+          provinceDesc: '', //归属省份
+          businessScope: '', //业务范围
+          createdName: '', //创建人名字
+          createdTime: '', //创建时间
+        },
+        customerOptions: [],
+        dynamicsList: [],
+      }
+    },
+    computed: {
+      ...mapGetters({
+        avatar: 'user/avatar',
+        username: 'user/username',
+      }),
+    },
+    mounted() {
+      this.id = parseInt(this.$route.query.id)
+      this.privateCus = this.$route.query.privateCus
+      this.init()
+      this.getOptions()
+      this.getRecord()
+      //this.getDynamics()
+    },
+    methods: {
+      setCustomerType(type) {
+        if (this.customerOptions.length == 0) return
+        if (!type) return
+        let arr = []
+        let typeArr = type.split(',')
+        console.log(type)
+        console.log(this.customerOptions)
+        typeArr.map((item) => {
+          console.log(item)
+          arr.push(this.customerOptions.find((e) => e.key == item).value)
+        })
+        return arr.join(',')
+      },
+      getOptions() {
+        Promise.all([this.getDicts('cust_idy')])
+          .then(([data]) => {
+            this.customerOptions = data.data.values
+            // data.data.values.filter((i) => {
+            //   this.customerOptions[i.key] = i.value
+            // })
+          })
+          .catch((err) => console.log(err))
+      },
+      async init() {
+        Promise.all([api.getEntity({ id: this.id })]).then(([detail]) => {
+          console.log('detail', detail)
+          this.detail = detail.data.list
+        })
+      },
+      async getRecord() {
+        const [err, res] = await to(api.getDynamicsList({ distId: this.id }))
+        if (err) return
+        if (res.data.list) {
+          let obj = res.data.list
+          const keys = Object.keys(obj).reverse()
+          let records = {}
+          for (const item of keys) {
+            records[item] = obj[item]
+          }
+          this.dynamicsList = records
+        }
+      },
+      // 查看附件
+      showFile(path) {
+        const a = document.createElement('a')
+        a.href = path // 文件链接
+        a.download = path // 文件名,跨域资源download无效
+        a.click()
+        a.remove()
+      },
+      setSelectRows(val) {
+        this.selectRows = val
+      },
+      back() {
+        this.$router.go(-1)
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  $base: '.detail';
+
+  #{$base} {
+    height: calc(100vh - 60px - 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: #1d66dc;
+      }
+
+      ::v-deep .my-content {
+        font-size: 14px;
+        font-weight: 600;
+        color: #333;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
+    }
+
+    .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>

+ 401 - 0
src/views/base/agent/index.vue

@@ -0,0 +1,401 @@
+<template>
+  <div class="user-management-container">
+    <div class="side-layout">
+      <div class="tree-side">
+        <span style="font-size: 25px">所属区域</span>
+        <!--        我负责的区域-->
+        <!--        所有区域-->
+        <el-tree
+          ref="tree"
+          :data="regionOptions"
+          default-expand
+          :default-expand-all="false"
+          :expand-on-click-node="true"
+          :filter-node-method="filterNode"
+          highlight-current
+          node-key="id"
+          :props="defaultProps"
+          @node-click="handleNodeClick">
+          <span slot-scope="{ node }" class="custom-tree-node">
+            <span>{{ node.label }}</span>
+            <span>
+              <i class="el-icon-more"></i>
+            </span>
+          </span>
+        </el-tree>
+      </div>
+      <div class="tree-table">
+        <vab-query-form>
+          <vab-query-form-top-panel>
+            <el-form :inline="true" :model="queryForm" @submit.native.prevent>
+              <!-- <el-form-item>
+                <el-input
+                  v-model.trim="queryForm.distCode"
+                  clearable
+                  placeholder="经销商编码"
+                  @keyup.enter.native="queryData" />
+              </el-form-item> -->
+              <el-form-item>
+                <el-input
+                  v-model.trim="queryForm.distName"
+                  clearable
+                  placeholder="经销商名称"
+                  @keyup.enter.native="queryData" />
+              </el-form-item>
+              <el-form-item>
+                <el-input
+                  v-model.trim="queryForm.belongSale"
+                  clearable
+                  placeholder="所属销售"
+                  @keyup.enter.native="queryData" />
+              </el-form-item>
+              <el-form-item>
+                <el-button icon="el-icon-search" type="primary" @click="queryData">查询</el-button>
+                <el-button icon="el-icon-refresh-right" @click="reset">重置</el-button>
+              </el-form-item>
+            </el-form>
+          </vab-query-form-top-panel>
+        </vab-query-form>
+
+        <vab-query-form-left-panel>
+          <el-button v-permissions="['base:agent:add']" icon="el-icon-plus" type="primary" @click="handleEdit($event)">
+            新建
+          </el-button>
+        </vab-query-form-left-panel>
+        <vab-query-form-right-panel>
+          <table-tool :columns="columns" :show-columns.sync="showColumns" table-type="agentTable" />
+        </vab-query-form-right-panel>
+        <el-table
+          :key="tableKey"
+          ref="table"
+          v-loading="listLoading"
+          border
+          :data="list"
+          :height="$baseTableHeight(2)"
+          @selection-change="setSelectRows">
+          <el-table-column
+            v-for="(item, index) in showColumns"
+            :key="index"
+            align="center"
+            :label="item.label"
+            :prop="item.prop"
+            show-overflow-tooltip
+            :sortable="item.sortable"
+            :width="item.width">
+            <template #default="{ row }">
+              <el-button v-if="item.prop === 'distName'" style="font-size: 14px" type="text" @click="handleDetail(row)">
+                {{ row.distName }}
+              </el-button>
+              <span v-else-if="item.prop === 'allProductAmount'">
+                {{ formatPrice(row.allProductAmount) }}
+              </span>
+              <span v-else-if="item.prop === 'saledAmount'">
+                {{ formatPrice(row.saledAmount) }}
+              </span>
+              <span v-else-if="item.prop === 'unpaidAmount'">
+                {{ formatPrice(row.unpaidAmount) }}
+              </span>
+              <span v-else-if="item.prop === 'invoicedAmount'">
+                {{ formatPrice(row.invoicedAmount) }}
+              </span>
+              <span v-else>{{ row[item.prop] }}</span>
+            </template>
+          </el-table-column>
+
+          <el-table-column align="center" fixed="right" label="操作" show-overflow-tooltip width="180">
+            <template #default="{ row }">
+              <el-button type="text" @click="changeDistr(row)">转为经销</el-button>
+              <el-button type="text" @click="$refs.changeAgent.open(row, 'renewal')">续签</el-button>
+              <el-button v-permissions="['base:distributor:edit']" type="text" @click="handleEdit(row)">编辑</el-button>
+              <el-button v-permissions="['base:distributor:delete']" type="text" @click="handleDelete(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" />
+      </div>
+    </div>
+    <edit ref="edit" @fetch-data="fetchData" />
+    <change-agent ref="changeAgent" @fetch-data="fetchData" />
+  </div>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import distrApi from '@/api/base/distr'
+  import regionApi from '@/api/base/region'
+  import regionAuthApi from '@/api/base/regionAuth'
+  import Edit from './components/AgentEdit'
+  import TableTool from '@/components/table/TableTool'
+  import ChangeAgent from '@/views/base/distributor/components/ChangeAgent'
+
+  export default {
+    name: 'Distributor',
+    components: { Edit, TableTool, ChangeAgent },
+    data() {
+      return {
+        tableKey: 0,
+        list: [],
+        listLoading: true,
+        layout: 'total, sizes, prev, pager, next, jumper',
+        total: 0,
+        selectRows: '',
+        queryForm: {
+          distType: '20', // 10 经销商 20 代理商
+          pageNum: 1,
+          pageSize: 10,
+          userName: '',
+        },
+        showColumns: [],
+        columns: [
+          {
+            label: '代理商名称',
+            width: 'auto',
+            prop: 'distName',
+            sortable: false,
+          },
+          {
+            label: '所在省份',
+            width: '100px',
+            prop: 'provinceDesc',
+            sortable: false,
+          },
+          {
+            label: 'ABC项目总数量',
+            width: '100px',
+            prop: 'projectNum',
+            sortable: false,
+          },
+          {
+            label: 'ABC项目出货总金额',
+            width: '100px',
+            prop: 'allProductAmount',
+            sortable: false,
+          },
+          {
+            label: '成交项目数量',
+            width: '100px',
+            prop: 'saledProjectNum',
+            sortable: false,
+          },
+          {
+            label: '成交总金额',
+            width: '100px',
+            prop: 'saledAmount',
+            sortable: false,
+          },
+          {
+            label: '未回款总金额',
+            width: '100px',
+            prop: 'unpaidAmount',
+            sortable: false,
+          },
+          {
+            label: '开票总金额',
+            width: '100px',
+            prop: 'invoicedAmount',
+            sortable: false,
+          },
+          {
+            label: '归属销售',
+            width: 'auto',
+            prop: 'belongSale',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '业务范围',
+            width: 'auto',
+            prop: 'businessScope',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '创建时间',
+            width: '100px',
+            prop: 'createdTime',
+            sortable: false,
+            disableCheck: false,
+          },
+        ],
+
+        regionOptions: [],
+        userSalesProvince: undefined,
+        defaultProps: {
+          id: 'id',
+          children: 'children',
+          label: 'regionDesc',
+        },
+        treeDefaultExpandAll: true,
+      }
+    },
+    watch: {
+      showColumns: function () {
+        this.tableKey++
+        this.$nextTick(() => this.$refs.table.doLayout())
+      },
+    },
+    activated() {
+      this.fetchData()
+    },
+    created() {
+      this.fetchData()
+      this.getRegionTree()
+      this.getUserSalesProvince()
+    },
+    methods: {
+      // 转经销商
+      changeDistr(row) {
+        console.log(row)
+        this.$prompt('请输入转经销商原因', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          inputPattern: /\S+/,
+          inputErrorMessage: '原因不能为空',
+        })
+          .then(async ({ value }) => {
+            // 当用户点击确定按钮时,执行的逻辑
+            console.log('输入的值为:', value)
+            const [err, res] = await to(distrApi.changeDistr({ id: row.id, customerType: value }))
+            if (err) return
+            if (res.code == 200) {
+              this.$baseMessage('转经销商成功', 'success', 'vab-hey-message-success')
+              this.handleCurrentChange(1)
+            }
+          })
+          .catch(() => {
+            // 当用户点击取消按钮时,执行的逻辑
+            console.log('取消输入')
+          })
+      },
+      async getRegionTree() {
+        const { data: data } = await regionApi.getRegionTree({})
+        this.regionOptions.push(...data.list)
+      },
+      async getUserSalesProvince() {
+        const { data: data } = await regionAuthApi.getUserSalesProvince({})
+        if (data && data.list) {
+          this.regionOptions.unshift(data.list)
+        }
+      },
+      // 筛选节点
+      filterNode(value, data) {
+        if (!value) return true
+        return data[this.defaultProps.label].indexOf(value) !== -1
+      },
+      // 节点单击事件
+      handleNodeClick(data) {
+        if (data.children && data.children.length) {
+          this.queryForm.provinceId = data.children.map((item) => item.regionCode)
+        } else {
+          this.queryForm.provinceId = [data.regionCode]
+        }
+        this.fetchData()
+      },
+      setSelectRows(val) {
+        this.selectRows = val
+      },
+      handleEdit(row) {
+        if (row.id) {
+          this.$refs['edit'].showEdit(row)
+        } else {
+          this.$refs['edit'].showEdit()
+        }
+      },
+      handleDelete(row) {
+        if (row.id) {
+          this.$baseConfirm('你确定要删除当前项吗', null, async () => {
+            const { msg } = await distrApi.doDelete({ ids: [row.id] })
+            this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+            await this.fetchData()
+          })
+        } else {
+          if (this.selectRows.length > 0) {
+            const ids = this.selectRows.map((item) => parseInt(item.id))
+            console.log(ids)
+            this.$baseConfirm('你确定要删除选中项吗', null, async () => {
+              const { msg } = await distrApi.doDelete({ ids })
+              this.$baseMessage(msg, 'success', 'vab-hey-message-success')
+              await this.fetchData()
+            })
+          } else {
+            this.$baseMessage('未选中任何行', 'error', 'vab-hey-message-error')
+          }
+        }
+      },
+      handleSizeChange(val) {
+        this.queryForm.pageSize = val
+        this.fetchData()
+      },
+      handleCurrentChange(val) {
+        this.queryForm.pageNum = val
+        this.fetchData()
+      },
+      queryData() {
+        this.queryForm.pageNum = 1
+        this.fetchData()
+      },
+      async fetchData() {
+        this.listLoading = true
+        const params = Object.assign(this.queryForm, { withStatistic: true, distType: '20' })
+        const {
+          data: { list, total },
+        } = await distrApi.getList(params)
+        this.list = list
+        this.total = total
+        this.listLoading = false
+        this.tableKey++
+        this.$nextTick(() => this.$refs.table.doLayout())
+      },
+      reset() {
+        this.queryForm = {
+          pageNum: 1,
+          pageSize: 10,
+          distName: '',
+          belongSale: '',
+        }
+        this.fetchData()
+      },
+      //详情
+      handleDetail(row) {
+        this.$router.push({
+          path: '/base/agentDetails',
+          query: {
+            id: row.id,
+          },
+        })
+      },
+    },
+  }
+</script>
+
+<style>
+  .el-tree-node:focus > .el-tree-node__content {
+    /*设置选中的样式 */
+    background-color: #dde9ff !important;
+  }
+
+  .el-tree-node__content:hover {
+    /*设置鼠标飘过的颜色 */
+    background: #eaf9ff !important;
+    color: #007bff;
+  }
+
+  .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
+    /*current选中的样式 */
+    color: #4d95fd;
+    font-weight: bold;
+    background-color: #dde9ff !important;
+  }
+</style>

+ 201 - 0
src/views/base/components/Contacts.vue

@@ -0,0 +1,201 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-05-17 14:03:04
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-05-17 18:19:20
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\base\distributor\components\Contacts.vue
+-->
+<template>
+  <div class="pane-wrapper">
+    <el-row align="middle" class="mb10" justify="end" type="flex">
+      <el-button icon="el-icon-plus" size="mini" type="primary" @click="$refs.editContacts.open()">添加</el-button>
+      <!-- <el-button icon="el-icon-bottom" size="mini" type="primary">导出</el-button> -->
+      <el-button
+        :disabled="selectRows.length == 0"
+        icon="el-icon-delete"
+        size="mini"
+        type="danger"
+        @click="delContacts()">
+        删除
+      </el-button>
+    </el-row>
+    <el-table
+      ref="table"
+      v-loading="listLoading"
+      :data="list"
+      height="calc(100% - 94px)"
+      @selection-change="setSelectRows">
+      <el-table-column align="center" show-overflow-tooltip type="selection" />
+      <el-table-column
+        v-for="(item, index) in columns"
+        :key="index"
+        align="center"
+        :label="item.label"
+        :min-width="item.width"
+        :prop="item.prop"
+        show-overflow-tooltip
+        :sortable="item.sortable">
+        <template #default="{ row }">
+          <el-button v-if="item.prop === 'nboCode'" class="link-button" type="text">
+            {{ row.nboCode }}
+          </el-button>
+          <span v-else>{{ row[item.prop] }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" show-overflow-tooltip width="85">
+        <template #default="{ row }">
+          <el-button type="text" @click="$refs.editContacts.open(row)">编辑</el-button>
+          <el-button type="text" @click="delContacts(row.id)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      background
+      :current-page="pageNum"
+      :layout="layout"
+      :page-size="pageSize"
+      :total="total"
+      @current-change="handleCurrentChange"
+      @size-change="handleSizeChange" />
+    <!-- 新增联系人 -->
+    <edit-contact ref="editContacts" :dist-id="distId" @initData="initDataAndRecords()" />
+  </div>
+</template>
+
+<script>
+  import EditContact from './EditContact'
+  import distrApi from '@/api/base/distr'
+  import to from 'await-to-js'
+  export default {
+    name: 'Contacts',
+    components: { EditContact },
+    data() {
+      return {
+        listLoading: false,
+        layout: 'total, sizes, prev, pager, next, jumper',
+        list: [],
+        total: 0,
+        pageNum: 1,
+        pageSize: 10,
+        columns: [
+          {
+            label: '联系人',
+            width: '160px',
+            prop: 'name',
+            sortable: false,
+          },
+          {
+            label: '手机号码',
+            width: '160px',
+            prop: 'phone',
+            sortable: false,
+          },
+          {
+            label: '职位',
+            width: '160px',
+            prop: 'post',
+            sortable: false,
+          },
+          {
+            label: '微信',
+            width: '160px',
+            prop: 'wechat',
+            sortable: false,
+          },
+          {
+            label: '电子邮箱',
+            width: '160px',
+            prop: 'mail',
+            sortable: false,
+          },
+          {
+            label: '负责区域/业务线',
+            width: '160',
+            prop: 'territory',
+            sortable: false,
+          },
+        ],
+        selectRows: [],
+        distId: this.$route.query.id * 1,
+      }
+    },
+
+    mounted() {
+      this.queryData()
+    },
+
+    methods: {
+      handleSizeChange(val) {
+        this.pageSize = val
+        this.queryData()
+      },
+      resetQueryData() {
+        this.pageNum = 1
+        this.queryData()
+      },
+      async queryData() {
+        const params = {
+          distId: this.distId,
+          pageNum: this.pageNum,
+          pageSize: this.pageSize,
+        }
+        this.listLoading = true
+        const [err, res] = await to(distrApi.getContactList(params))
+        this.listLoading = false
+        if (err) return
+        if (res.code == 200) {
+          console.log(res.data)
+          this.list = res.data.list
+          this.total = res.data.total
+        }
+      },
+      handleCurrentChange(val) {
+        this.pageNum = val
+        this.queryData()
+      },
+      setSelectRows(val) {
+        this.selectRows = val.map((item) => item.id)
+        console.log(this.selectRows)
+      },
+      // 删除联系人
+      delContacts(id = null) {
+        this.$confirm('确认删除?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        })
+          .then(async () => {
+            console.log('id', id)
+            const params = { id: id ? [id] : [...this.selectRows] }
+            console.log('selectRows', this.selectRows)
+            console.log('params', params)
+            const [err, res] = await to(distrApi.delContact(params))
+            if (err) return
+            if (res.code == 200) {
+              this.$message.success('删除成功')
+              this.handleCurrentChange(1)
+              this.$emit('initRecords')
+            }
+          })
+          .catch(() => {})
+      },
+      initDataAndRecords() {
+        this.handleCurrentChange(1)
+        this.$emit('initRecords')
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .pane-wrapper {
+    height: 100%;
+  }
+  .mb10 {
+    margin-bottom: 10px;
+  }
+  .mr10 {
+    margin-right: 10px;
+  }
+</style>

+ 183 - 0
src/views/base/components/ContractRecords.vue

@@ -0,0 +1,183 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-05-17 14:03:04
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-05-17 16:39:14
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\base\distributor\components\ContractRecords.vue
+-->
+<template>
+  <div class="pane-wrapper">
+    <el-table ref="table" v-loading="listLoading" :data="list" height="calc(100% - 94px)">
+      <el-table-column
+        v-for="(item, index) in columns"
+        :key="index"
+        align="center"
+        :label="item.label"
+        :min-width="item.width"
+        :prop="item.prop"
+        show-overflow-tooltip
+        :sortable="item.sortable">
+        <template #default="{ row }">
+          <el-button
+            v-if="item.prop === 'contractCode'"
+            class="link-button"
+            type="text"
+            @click="toContractDetails(row)">
+            {{ row.contractCode }}
+          </el-button>
+          <el-button v-else-if="item.prop === 'custName'" class="link-button" type="text" @click="toCustomDetails(row)">
+            {{ row.custName }}
+          </el-button>
+          <span v-else-if="item.prop === 'contractAmount'">
+            {{ formatPrice(row.contractAmount) }}
+          </span>
+          <span v-else-if="item.prop === 'collectedAmount'">
+            {{ formatPrice(row.collectedAmount) }}
+          </span>
+          <span v-else-if="item.prop === 'invoiceAmount'">
+            {{ formatPrice(row.invoiceAmount) }}
+          </span>
+          <span v-else-if="item.prop === 'contractSignTime'">
+            {{ parseTime(row.contractSignTime, '{y}-{m}-{d}') }}
+          </span>
+          <span v-else>{{ row[item.prop] }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+  import distrApi from '@/api/base/distr'
+  import to from 'await-to-js'
+  export default {
+    name: 'ContractRecords',
+
+    data() {
+      return {
+        listLoading: false,
+        list: [],
+        columns: [
+          {
+            label: '合同编号',
+            width: '160px',
+            prop: 'contractCode',
+            sortable: false,
+          },
+          {
+            label: '合同名称',
+            width: '160px',
+            prop: 'contractName',
+            sortable: false,
+          },
+          {
+            label: '客户名称',
+            width: '160px',
+            prop: 'custName',
+            sortable: false,
+          },
+          {
+            label: '所在省',
+            width: '160px',
+            prop: 'custProvince',
+            sortable: false,
+          },
+          {
+            label: '所在市',
+            width: '160px',
+            prop: 'custCity',
+            sortable: false,
+          },
+          {
+            label: '合同签订单位',
+            width: '160px',
+            prop: 'distributorName',
+            sortable: false,
+          },
+          {
+            label: '合同金额(元)',
+            width: '160px',
+            prop: 'contractAmount',
+            sortable: false,
+          },
+          {
+            label: '回款金额(元)',
+            width: '160px',
+            prop: 'collectedAmount',
+            sortable: false,
+          },
+          {
+            label: '开票金额(元)',
+            width: '160px',
+            prop: 'invoiceAmount',
+            sortable: false,
+          },
+          {
+            label: '合同签订时间',
+            width: '160px',
+            prop: 'contractSignTime',
+            sortable: false,
+          },
+          {
+            label: '所属工程师',
+            width: '160px',
+            prop: 'incharge_name',
+            sortable: false,
+          },
+        ],
+      }
+    },
+
+    mounted() {
+      this.queryData()
+    },
+
+    methods: {
+      async queryData() {
+        const params = {
+          distId: this.$route.query.id * 1,
+        }
+        this.listLoading = true
+        const [err, res] = await to(distrApi.getContractList(params))
+        this.listLoading = false
+        if (err) return
+        if (res.code == 200) {
+          console.log(res.data)
+          this.list = res.data.list
+          this.total = res.data.total
+        }
+      },
+      // 跳转合同详情
+      toContractDetails(row) {
+        this.$router.push({
+          path: '/contract/detail',
+          query: {
+            id: row.id,
+          },
+        })
+      },
+      // 客户详情
+      toCustomDetails(row) {
+        this.$router.push({
+          path: '/customer/detail',
+          query: {
+            id: row.custId,
+          },
+        })
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .pane-wrapper {
+    height: 100%;
+  }
+  .mb10 {
+    margin-bottom: 10px;
+  }
+  .mr10 {
+    margin-right: 10px;
+  }
+</style>

+ 162 - 0
src/views/base/components/EditContact.vue

@@ -0,0 +1,162 @@
+<template>
+  <!-- 新建联系人弹窗 -->
+  <el-dialog append-to-body :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="name">
+            <el-input v-model="contactForm.name" placeholder="请输入联系人姓名" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="尊称" prop="honorific">
+            <el-radio-group v-model="contactForm.honorific">
+              <el-radio label="未知">未知</el-radio>
+              <el-radio label="女士">女士</el-radio>
+              <el-radio label="先生">先生</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="职位" prop="post">
+            <el-input v-model="contactForm.post" placeholder="请输入职位" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="手机号码" prop="phone">
+            <el-input v-model="contactForm.phone" maxlength="11" placeholder="请输入手机号码" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="电子邮箱" prop="mail">
+            <el-input v-model="contactForm.mail" 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="territory">
+            <el-input v-model="contactForm.territory" placeholder="请输入负责区域/业务线" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </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 distrApi from '@/api/base/distr'
+
+  export default {
+    props: {
+      distId: {
+        type: Number,
+        default: () => 0,
+      },
+    },
+    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('请输入手机号码'))
+          callback()
+        } else if (!reg.test(value)) {
+          callback(new Error('请输入正确手机号码'))
+        } else {
+          callback()
+        }
+      }
+      var validatemail = (rule, value, callback) => {
+        const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/
+        if (!value) {
+          callback()
+        } else if (!reg.test(value)) {
+          callback(new Error('请输入正确邮箱'))
+        } else {
+          callback()
+        }
+      }
+      return {
+        title: '新增联系人',
+        contactVisible: false,
+        contactForm: {
+          id: null,
+          name: '', //联系人名字
+          honorific: '未知',
+          phone: '', //电话
+          wechat: '', //微信
+          mail: '', //邮箱
+          post: '', //部门
+          territory: '', //负责区域、业务线
+        },
+        contactRules: {
+          name: [{ required: true, trigger: 'blur', message: '请输入联系人姓名' }],
+          territory: [{ required: true, trigger: 'blur', message: '请输入负责区域/业务线' }],
+          honorific: [{ trigger: 'blur', message: '请选择尊称' }],
+          post: [{ trigger: 'blur', message: '请输入部门名称' }],
+          wechat: [{ trigger: 'blur', message: '请输入微信号' }],
+          phone: [{ trigger: 'blur', validator: validateTel }],
+          mail: [{ trigger: 'blur', validator: validatemail }],
+        },
+      }
+    },
+    methods: {
+      open(row = null) {
+        if (row) {
+          this.contactForm = { ...row }
+        }
+        this.contactVisible = true
+      },
+      // 联系人新建
+      async contactSave() {
+        this.$refs.contactForm.validate(async (valid) => {
+          if (valid) {
+            let params = { ...this.contactForm, distId: this.distId }
+            console.log(params)
+            const [err, res] = await to(distrApi.addContact(params))
+            if (err) return
+            if (res.code == 200) {
+              this.$message.success('新建联系人成功')
+              this.contactVisible = false
+              this.$emit('initData')
+            }
+          }
+        })
+      },
+      // 联系人编辑
+      async contactEdit() {
+        this.$refs.contactForm.validate(async (valid) => {
+          if (valid) {
+            let params = { ...this.contactForm, distId: this.distId }
+            console.log(params)
+            const [err, res] = await to(distrApi.updateContact(params))
+            if (err) return
+            if (res.code == 200) {
+              this.$message.success('更新联系人成功')
+              this.contactVisible = false
+              this.$emit('initData')
+            }
+          }
+        })
+      },
+      contactClose() {
+        this.$refs['contactForm'].resetFields()
+      },
+    },
+  }
+</script>

+ 237 - 0
src/views/base/components/Follow.vue

@@ -0,0 +1,237 @@
+<template>
+  <div>
+    <ul v-if="followList.length" 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.createdName }} 跟进({{ selectDictLabel(followTypeOptions, 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>
+    <div v-else class="no-follow">暂无跟进记录</div>
+    <!-- 跟进详情 -->
+    <FollowDetail ref="followDetail" />
+  </div>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import FollowDetail from './FollowDetail.vue'
+  import followApi from '@/api/customer/follow'
+  export default {
+    components: { FollowDetail },
+    props: {
+      targetType: {
+        type: String,
+        required: true,
+      },
+    },
+    data() {
+      return {
+        followList: [], //跟进记录
+        followTypeOptions: [],
+      }
+    },
+    mounted() {
+      this.getOptions()
+      this.getFollowList()
+    },
+    methods: {
+      getOptions() {
+        Promise.all([this.getDicts('plat_follow_type')]).then(([followType]) => {
+          this.followTypeOptions = followType.data.values || []
+        })
+      },
+      async getFollowList() {
+        let params = {
+          targetId: this.$route.query.id,
+          targetType: this.targetType,
+          DaysBeforeToday: 9999,
+        }
+        const [err, res] = await to(followApi.getListByDay(params))
+        if (err) return
+        this.followList = res.data.list || []
+      },
+      // 跟进记录详情
+      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>
+  .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;
+        }
+      }
+    }
+  }
+</style>

+ 118 - 0
src/views/base/components/FollowDetail.vue

@@ -0,0 +1,118 @@
+<!--
+ * @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"
+      :content-style="{ width: '85%', 'word-break': 'break-all' }"
+      :label-style="{ width: '15%' }"
+      size="medium">
+      <el-descriptions-item label="跟进类型">
+        {{ formatType(form.followType) }}
+      </el-descriptions-item>
+      <el-descriptions-item label="跟进对象">
+        {{ form.targetName }}
+      </el-descriptions-item>
+      <el-descriptions-item label="跟进时间">
+        {{ form.followDate }}
+      </el-descriptions-item>
+      <el-descriptions-item label="跟进描述">
+        {{ form.followContent }}
+      </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="相关附件">
+        <a v-for="item in form.files" :key="item.id" :href="item.fileUrl">
+          {{ item.fileName }}
+          <br />
+        </a>
+      </el-descriptions-item>
+      <el-descriptions-item label="评论数量">
+        {{ form.commentNumber }}
+      </el-descriptions-item>
+      <el-descriptions-item label="跟进人">
+        {{ form.createdName }}
+      </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>
+  import api from '@/api/customer/follow'
+  import to from 'await-to-js'
+
+  export default {
+    data() {
+      return {
+        visible: false,
+        form: {
+          id: '',
+          followType: '',
+          followDate: '',
+          followContent: '',
+          targetId: '',
+          targetType: '',
+          targetName: '',
+          custId: '',
+          custName: '',
+          contactsId: 0,
+          contactsName: '',
+          reminders: '',
+          nextTime: '',
+          files: [],
+          remark: '',
+          createdBy: '',
+          createdName: '',
+          createdTime: '',
+          updatedBy: '',
+          updatedName: '',
+          updatedTime: '',
+          deletedTime: '',
+        },
+      }
+    },
+    methods: {
+      init(form) {
+        form.files = []
+        this.form = form
+        this.visible = true
+        this.getFollowupFileList()
+      },
+      async getFollowupFileList() {
+        const [err, res] = await to(api.getFollowupFileList({ followId: this.form.id + '' }))
+        if (err) return
+        this.form.files = res.data.list || []
+      },
+
+      formatType(val) {
+        let str = ''
+        if (val == 10) str = '电话'
+        else if (val == 20) str = '邮件'
+        else if (val == 30) str = '拜访'
+        return str
+      },
+    },
+  }
+</script>
+
+<style></style>

+ 163 - 0
src/views/base/components/HistoryProxy.vue

@@ -0,0 +1,163 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-05-17 14:03:04
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-05-19 16:11:06
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\base\agent\components\HistoryProxy.vue
+-->
+<template>
+  <div class="pane-wrapper">
+    <el-table ref="table" v-loading="listLoading" :data="list" height="calc(100% - 94px)">
+      <el-table-column
+        v-for="(item, index) in columns"
+        :key="index"
+        align="center"
+        :label="item.label"
+        :min-width="item.width"
+        :prop="item.prop"
+        show-overflow-tooltip
+        :sortable="item.sortable">
+        <template #default="{ row }">
+          <span v-if="item.prop === 'distType'">
+            {{ row.distType == '10' ? '经销商' : '代理商' }}
+          </span>
+          <span v-else-if="item.prop === 'customerType'">
+            {{ setCustomerType(row.customerType) }}
+          </span>
+          <span v-else-if="item.prop === 'proxyEndTime'">
+            {{ parseTime(row.proxyStartTime, '{y}-{m}-{d}') }} - {{ parseTime(row.proxyEndTime, '{y}-{m}-{d}') }}
+          </span>
+          <span v-else-if="item.prop === 'contractUrl'">
+            <el-link v-show="row.contractUrl != ''" @click="showFile(row.contractUrl)">查看附件</el-link>
+          </span>
+          <span v-else>{{ row[item.prop] }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+  import distrApi from '@/api/base/distr'
+  import to from 'await-to-js'
+  export default {
+    name: 'ProjectRecords',
+
+    data() {
+      return {
+        listLoading: false,
+        list: [],
+        columns: [
+          {
+            label: '类型',
+            width: '100px',
+            prop: 'distType',
+            sortable: false,
+          },
+          {
+            label: '业务范围',
+            width: 'auto',
+            prop: 'businessScope',
+            sortable: false,
+          },
+          {
+            label: '合作客户类型',
+            width: '200px',
+            prop: 'customerType',
+            sortable: false,
+          },
+          {
+            label: '授权代理区域',
+            width: '160px',
+            prop: 'proxyDistrict',
+            sortable: false,
+          },
+          {
+            label: '代理签约有效期',
+            width: '200px',
+            prop: 'proxyEndTime',
+            sortable: false,
+          },
+          {
+            label: '代理合同',
+            width: '160px',
+            prop: 'contractUrl',
+            sortable: false,
+          },
+          {
+            label: '已有代理品牌和产品',
+            width: '160px',
+            prop: 'existedProduct',
+            sortable: false,
+          },
+          {
+            label: '历史合作的终端客户名称',
+            width: '160px',
+            prop: 'historyCustomer',
+            sortable: false,
+          },
+        ],
+        customerOptions: [],
+      }
+    },
+
+    mounted() {
+      this.getOptions()
+      this.queryData()
+    },
+
+    methods: {
+      // 获取参数配置
+      setCustomerType(type) {
+        if (this.customerOptions.length == 0) return
+        if (!type) return
+        let arr = []
+        let typeArr = type.split(',')
+        typeArr.map((item) => {
+          arr.push(this.customerOptions.find((e) => e.key == item).value)
+        })
+        return arr.join(',')
+      },
+      getOptions() {
+        Promise.all([this.getDicts('cust_idy')])
+          .then(([data]) => {
+            this.customerOptions = data.data.values
+          })
+          .catch((err) => console.log(err))
+      },
+      async queryData() {
+        this.listLoading = true
+        const params = {
+          distId: this.$route.query.id * 1,
+        }
+        const [err, res] = await to(distrApi.getProxyHistoryList(params))
+        this.listLoading = false
+        if (err) return
+        if (res.code == 200) {
+          this.list = res.data.list
+        }
+      },
+      // 查看附件
+      showFile(path) {
+        const a = document.createElement('a')
+        a.href = path // 文件链接
+        a.download = path // 文件名,跨域资源download无效
+        a.click()
+        a.remove()
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .pane-wrapper {
+    height: 100%;
+  }
+  .mb10 {
+    margin-bottom: 10px;
+  }
+  .mr10 {
+    margin-right: 10px;
+  }
+</style>

+ 244 - 0
src/views/base/components/ProjectRecords.vue

@@ -0,0 +1,244 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-05-17 14:03:04
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-05-17 16:13:59
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\base\distributor\components\projectRecords.vue
+-->
+<template>
+  <div class="pane-wrapper">
+    <el-row align="middle" class="mb10" justify="end" type="flex">
+      <el-col class="mr10" :span="10">
+        <el-date-picker
+          v-model="time"
+          end-placeholder="结束日期"
+          range-separator="至"
+          start-placeholder="开始日期"
+          style="width: 100%"
+          type="daterange"
+          value-format="yyyy-MM-dd"
+          @change="resetQueryData" />
+      </el-col>
+      <el-col class="mr10" :span="6">
+        <el-input
+          v-model="searchText"
+          placeholder="搜索关键词"
+          prefix-icon="el-icon-search"
+          style="width: 100%"
+          @blur="resetQueryData"
+          @keyup.enter.native="resetQueryData" />
+      </el-col>
+      <!-- <el-button icon="el-icon-plus" size="mini" type="primary">导出</el-button> -->
+    </el-row>
+    <el-table ref="table" v-loading="listLoading" :data="list" height="calc(100% - 94px)">
+      <el-table-column
+        v-for="(item, index) in columns"
+        :key="index"
+        align="center"
+        :label="item.label"
+        :min-width="item.width"
+        :prop="item.prop"
+        show-overflow-tooltip
+        :sortable="item.sortable">
+        <template #default="{ row }">
+          <el-button v-if="item.prop === 'nboCode'" class="link-button" type="text" @click="toPorjectDetails(row)">
+            {{ row.nboCode }}
+          </el-button>
+          <el-button v-else-if="item.prop === 'custName'" class="link-button" type="text" @click="toCustomDetails(row)">
+            {{ row.custName }}
+          </el-button>
+          <span v-else-if="item.prop === 'productLine'">
+            {{ selectDictLabel(productLineOptions, row.productLine) }}
+          </span>
+          <span v-else-if="item.prop === 'nboType'">
+            {{ selectDictLabel(nboTypeOptions, row.nboType) }}
+          </span>
+          <span v-else-if="item.prop === 'nboBudget'">
+            {{ formatPrice(row.nboBudget) }}
+          </span>
+          <span v-else-if="item.prop === 'isBig'">{{ row[item.prop] === '10' ? '是' : '否' }}</span>
+          <span v-else>{{ row[item.prop] }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      background
+      :current-page="pageNum"
+      :layout="layout"
+      :page-size="pageSize"
+      :total="total"
+      @current-change="handleCurrentChange"
+      @size-change="handleSizeChange" />
+  </div>
+</template>
+
+<script>
+  import distrApi from '@/api/base/distr'
+  import to from 'await-to-js'
+  export default {
+    name: 'ProjectRecords',
+
+    data() {
+      return {
+        searchText: '', //关键字
+        time: [], //开始结束时间
+        listLoading: false,
+        layout: 'total, sizes, prev, pager, next, jumper',
+        list: [],
+        total: 0,
+        pageNum: 1,
+        pageSize: 10,
+        columns: [
+          {
+            label: '项目编号',
+            width: '160px',
+            prop: 'nboCode',
+            sortable: false,
+          },
+          {
+            label: '项目名称',
+            width: '160px',
+            prop: 'nboName',
+            sortable: false,
+          },
+          {
+            label: '客户名称',
+            width: '160px',
+            prop: 'custName',
+            sortable: false,
+          },
+          {
+            label: '所在省',
+            width: '160px',
+            prop: 'custProvince',
+            sortable: false,
+          },
+          {
+            label: '所在市',
+            width: '160px',
+            prop: 'custCity',
+            sortable: false,
+          },
+          {
+            label: '产品线',
+            width: '160px',
+            prop: 'productLine',
+            sortable: false,
+          },
+          {
+            label: '项目级别',
+            width: '160px',
+            prop: 'nboType',
+            sortable: false,
+          },
+          {
+            label: '项目预算',
+            width: '160px',
+            prop: 'nboBudget',
+            sortable: false,
+          },
+          {
+            label: '大项目',
+            width: '160px',
+            prop: 'isBig',
+            sortable: false,
+          },
+          {
+            label: '项目备案时间',
+            width: '160px',
+            prop: 'filingTime',
+            sortable: false,
+          },
+          {
+            label: '所属工程师',
+            width: '160px',
+            prop: 'saleName',
+            sortable: false,
+          },
+        ],
+        nboTypeOptions: [], //项目级别
+        productLineOptions: [], //产品线
+        selectRows: [],
+      }
+    },
+
+    mounted() {
+      this.getOptions()
+      this.queryData()
+    },
+
+    methods: {
+      // 获取参数配置
+      getOptions() {
+        Promise.all([this.getDicts('proj_nbo_type'), this.getDicts('sys_product_line')])
+          .then(([nboType, productLine]) => {
+            this.nboTypeOptions = nboType.data.values || []
+            this.productLineOptions = productLine.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
+      handleSizeChange(val) {
+        this.pageSize = val
+        this.queryData()
+      },
+      resetQueryData() {
+        this.pageNum = 1
+        this.queryData()
+      },
+      async queryData() {
+        const params = {
+          distId: this.$route.query.id * 1,
+          pageNum: this.pageNum,
+          pageSize: this.pageSize,
+          beginTime: this.time[0] ? this.time[0] : null,
+          endTime: this.time[1] ? this.time[1] : null,
+          searchText: this.searchText,
+        }
+        this.listLoading = true
+        const [err, res] = await to(distrApi.getProjectList(params))
+        this.listLoading = false
+        if (err) return
+        if (res.code == 200) {
+          console.log(res.data)
+          this.list = res.data.list
+          this.total = res.data.total
+        }
+      },
+      handleCurrentChange(val) {
+        this.pageNum = val
+        this.queryData()
+      },
+      // 跳转客户详情
+      toPorjectDetails(row) {
+        this.$router.push({
+          name: 'BusinessDetail',
+          query: {
+            id: row.id,
+          },
+        })
+      },
+      // 客户详情
+      toCustomDetails(row) {
+        this.$router.push({
+          path: '/customer/detail',
+          query: {
+            id: row.custId,
+          },
+        })
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .pane-wrapper {
+    height: 100%;
+  }
+  .mb10 {
+    margin-bottom: 10px;
+  }
+  .mr10 {
+    margin-right: 10px;
+  }
+</style>

+ 228 - 0
src/views/base/distributor/components/ChangeAgent.vue

@@ -0,0 +1,228 @@
+<template>
+  <el-dialog append-to-body :title="title" :visible.sync="visible" @close="close">
+    <el-form ref="form" :model="form" :rules="dist">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="授权客户类型" prop="customerType">
+            <el-select v-model="form.customerType" multiple placeholder="授权客户类型" style="width: 100%">
+              <el-option v-for="item in customerOptions" :key="item.value" :label="item.value" :value="item.key" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="授权代理区域" prop="proxyDistrict">
+            <el-input v-model="form.proxyDistrict" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="代理签约有效期" prop="date">
+            <el-date-picker
+              v-model="form.date"
+              end-placeholder="结束日期"
+              range-separator="至"
+              start-placeholder="开始日期"
+              style="width: 100%"
+              type="daterange"
+              value-format="yyyy-MM-dd" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="代理合同" prop="contractUrl">
+            <div style="width: 100%">
+              <span style="color: #999">支持格式:.rar .zip .doc .docx .pdf ,单个文件不能超过20MB</span>
+              <el-upload
+                ref="uploadRef"
+                action="#"
+                :before-upload="
+                  (file) => {
+                    return beforeAvatarUpload(file)
+                  }
+                "
+                :file-list="fileList"
+                :http-request="uploadrequest"
+                :limit="1"
+                :on-exceed="handleExceed"
+                :on-remove="handleRemove">
+                <el-button size="mini" type="primary">点击上传</el-button>
+              </el-upload>
+            </div>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <span slot="footer">
+      <el-button @click="close">取 消</el-button>
+      <el-button type="primary" @click="save">确 定</el-button>
+    </span>
+  </el-dialog>
+</template>
+<script>
+  import to from 'await-to-js'
+  import distrApi from '@/api/base/distr'
+  import '@riophae/vue-treeselect/dist/vue-treeselect.css'
+  import asyncUploadFile from '@/utils/uploadajax'
+  import axios from 'axios'
+
+  export default {
+    name: 'UserEdit',
+    data() {
+      return {
+        pageType: '',
+        title: '',
+        form: {
+          id: 0,
+          customerType: '',
+          proxyDistrict: '',
+          date: [],
+          proxyEndTime: '',
+          proxyStartTime: '',
+          contractUrl: '',
+        },
+        dist: {
+          customerType: [{ required: true, trigger: 'blur', message: '请输入授权客户类型' }],
+          proxyDistrict: [{ required: true, trigger: 'blur', message: '请输入授权代理区域' }],
+          date: [{ required: true, trigger: 'blur', message: '请选择代理签约有效期' }],
+        },
+        //省份
+        visible: false,
+        customerOptions: [],
+        fileList: [],
+        fileSettings: {
+          // 文件配置信息
+          fileSize: 20971520,
+          fileTypes: '.rar,.zip,.doc,.docx,.pdf',
+          pictureSize: 20971520,
+          pictureTypes: '.jpg,.jpeg,.gif,.png,.jfif,.txt',
+          types: '.rar,.zip,.doc,.docx,.pdf',
+          videoSize: 104857600,
+          videoType: '.mp4',
+        },
+      }
+    },
+    created() {},
+    mounted() {
+      this.getOptions()
+    },
+    methods: {
+      getOptions() {
+        Promise.all([this.getDicts('cust_idy')])
+          .then(([data]) => {
+            this.customerOptions = data.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
+      open(row, type) {
+        this.pageType = type
+        if (type == 'change') {
+          this.title = '转为代理商'
+        } else if (type == 'renewal') {
+          this.title = '续签'
+        }
+        this.form.id = row.id
+        this.visible = true
+      },
+      close() {
+        this.form = {
+          id: 0,
+          customerType: '',
+          proxyDistrict: '',
+          date: [],
+          proxyEndTime: '',
+          proxyStartTime: '',
+          contractUrl: '',
+        }
+        this.fileList = []
+        this.visible = false
+      },
+
+      save() {
+        this.$refs['form'].validate(async (valid) => {
+          if (valid) {
+            console.log(this.form)
+            let params = { ...this.form }
+            params.distType = '20'
+            params.proxyStartTime = params.date[0]
+            params.proxyEndTime = params.date[1]
+            params.customerType = params.customerType.join(',')
+            console.log('表单修改提交内容:', params)
+            let [err, res] = []
+            if (this.pageType == 'change') {
+              ;[err, res] = await to(distrApi.changeAgent(params))
+            } else {
+              ;[err, res] = await to(distrApi.renewal(params))
+            }
+            if (err) return
+            if (res.code == 200) {
+              this.$baseMessage(`${this.title}成功`, 'success', 'vab-hey-message-success')
+              this.$emit('fetchData')
+              this.close()
+            }
+          }
+        })
+      },
+      // 上传图片
+      beforeAvatarUpload(file) {
+        let flag1 = file.size < this.fileSettings.fileSize
+        if (!flag1) {
+          this.$message.warning('文件过大,请重新选择!')
+          return false
+        }
+        let flag2 = this.fileSettings.fileTypes.split(',').includes('.' + file.name.split('.').pop())
+        if (!flag2) {
+          this.$message.warning('文件类型不符合,请重新选择!')
+          return false
+        }
+        return true
+      },
+      // 图片删除
+      handleRemove() {
+        this.form.curmCover = ''
+        this.fileList = []
+      },
+      handleExceed() {
+        this.$message.warning(`当前限制只能上传一个附件`)
+      },
+      // 上传
+      uploadrequest(option) {
+        let _this = this
+        let url = process.env.VUE_APP_UPLOAD_WEED
+        axios
+          .post(url)
+          .then(function (res) {
+            if (res.data && res.data.fid && res.data.fid !== '') {
+              option.action = `${process.env.VUE_APP_PROTOCOL}${res.data.publicUrl}/${res.data.fid}`
+              let file_name = option.file.name
+              let index = file_name.lastIndexOf('.')
+              let file_extend = ''
+              if (index > 0) {
+                // 截取名称中的扩展名
+                file_extend = file_name.substr(index + 1)
+              }
+              let uploadform = {
+                fileName: file_name, // 资料名称
+                fileUrl: `${process.env.VUE_APP_PROTOCOL}${res.data.publicUrl}/${res.data.fid}`, // 资料存储url
+                size: option.file.size.toString(), // 资料大小
+                fileType: file_extend, // 资料文件类型
+              }
+              asyncUploadFile(option).then(() => {
+                _this.form.contractUrl = uploadform.fileUrl
+              })
+            } else {
+              _this.$message({
+                type: 'warning',
+                message: '未上传成功!请刷新界面重新上传!',
+              })
+            }
+          })
+          .catch(function () {
+            _this.$message({
+              type: 'warning',
+              message: '未上传成功!请重新上传!',
+            })
+          })
+      },
+    },
+  }
+</script>

+ 25 - 9
src/views/base/distributor/components/DistrEdit.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-dialog append-to-body :title="title" :visible.sync="dialogFormVisible">
+  <el-dialog append-to-body :title="title" :visible.sync="dialogFormVisible" @close="close">
     <el-form ref="form" :model="form" :rules="dist">
       <el-row :gutter="20">
         <el-col :span="12">
@@ -70,7 +70,7 @@
       <el-row :gutter="20">
         <el-col :span="12">
           <el-form-item label="授权客户类型" prop="customerType">
-            <el-select v-model="form.customerType" placeholder="授权客户类型" style="width: 100%">
+            <el-select v-model="form.customerType" multiple placeholder="授权客户类型" style="width: 100%">
               <el-option v-for="item in customerOptions" :key="item.value" :label="item.value" :value="item.key" />
             </el-select>
           </el-form-item>
@@ -174,6 +174,7 @@
           this.title = '编辑'
 
           this.form = Object.assign({}, row)
+          this.form.customerType = row.customerType.split(',')
           //  this.pid = this.form.provinceId
           // this.form.provinceId = this.form.provinceDesc
         }
@@ -181,7 +182,21 @@
       },
       close() {
         this.$refs['form'].resetFields()
-        this.form = this.$options.data().form
+        this.form = {
+          distName: '',
+          abbrName: '',
+          provinceDesc: '',
+          provinceId: 0,
+          belongSale: '',
+          belongSaleId: 0,
+          capital: 0,
+          registerDistrict: '',
+          businessScope: '',
+          saleNum: 0,
+          customerType: '',
+          existedProduct: '',
+          historyCustomer: '',
+        }
         this.dialogFormVisible = false
       },
       choose() {
@@ -212,16 +227,17 @@
         this.$refs['form'].validate(async (valid) => {
           if (valid) {
             console.log(this.form)
-            this.form.provinceId = parseInt(this.form.provinceId)
-            this.form.distType = '10'
 
+            let params = { ...this.form }
+            params.distType = '10'
+            params.provinceId = parseInt(params.provinceId)
+            params.distType = '10'
+            params.customerType = params.customerType.join(',')
             if (this.form.id) {
-              console.log('表单修改提交内容:', this.form)
-              const { msg } = await distrApi.doEdit(this.form)
+              const { msg } = await distrApi.doEdit(params)
               this.$baseMessage(msg, 'success', 'vab-hey-message-success')
             } else {
-              console.log('表单提交内容:', this.form)
-              const { msg } = await distrApi.doAdd(this.form)
+              const { msg } = await distrApi.doAdd(params)
               this.$baseMessage(msg, 'success', 'vab-hey-message-success')
             }
             this.$emit('fetch-data')

+ 164 - 0
src/views/base/distributor/components/EditContact.vue

@@ -0,0 +1,164 @@
+<template>
+  <!-- 新建联系人弹窗 -->
+  <el-dialog append-to-body :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="name">
+            <el-input v-model="contactForm.name" placeholder="请输入联系人姓名" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="尊称" prop="honorific">
+            <el-radio-group v-model="contactForm.honorific">
+              <el-radio label="未知">未知</el-radio>
+              <el-radio label="女士">女士</el-radio>
+              <el-radio label="先生">先生</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="职位" prop="post">
+            <el-input v-model="contactForm.post" placeholder="请输入职位" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="手机号码" prop="phone">
+            <el-input v-model="contactForm.phone" maxlength="11" placeholder="请输入手机号码" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="电子邮箱" prop="mail">
+            <el-input v-model="contactForm.mail" 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="territory">
+            <el-input v-model="contactForm.territory" placeholder="请输入负责区域/业务线" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </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 distrApi from '@/api/base/distr'
+
+  export default {
+    props: {
+      distId: {
+        type: Number,
+        default: () => 0,
+      },
+    },
+    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('请输入手机号码'))
+          callback()
+        } else if (!reg.test(value)) {
+          callback(new Error('请输入正确手机号码'))
+        } else {
+          callback()
+        }
+      }
+      var validatemail = (rule, value, callback) => {
+        const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(.[a-zA-Z0-9_-]+)+$/
+        if (!value) {
+          callback()
+        } else if (!reg.test(value)) {
+          callback(new Error('请输入正确邮箱'))
+        } else {
+          callback()
+        }
+      }
+      return {
+        title: '新增联系人',
+        contactVisible: false,
+        contactForm: {
+          id: null,
+          name: '', //联系人名字
+          honorific: '未知',
+          phone: '', //电话
+          wechat: '', //微信
+          mail: '', //邮箱
+          post: '', //部门
+          territory: '', //负责区域、业务线
+        },
+        contactRules: {
+          name: [{ required: true, trigger: 'blur', message: '请输入联系人姓名' }],
+          territory: [{ required: true, trigger: 'blur', message: '请输入负责区域/业务线' }],
+          honorific: [{ trigger: 'blur', message: '请选择尊称' }],
+          post: [{ trigger: 'blur', message: '请输入部门名称' }],
+          wechat: [{ trigger: 'blur', message: '请输入微信号' }],
+          phone: [{ trigger: 'blur', validator: validateTel }],
+          mail: [{ trigger: 'blur', validator: validatemail }],
+        },
+      }
+    },
+    methods: {
+      open(row = null) {
+        if (row) {
+          this.contactForm = { ...row }
+        }
+        this.contactVisible = true
+      },
+      // 联系人新建
+      async contactSave() {
+        this.$refs.contactForm.validate(async (valid) => {
+          if (valid) {
+            let params = { ...this.contactForm, distId: this.distId }
+            console.log(params)
+            const [err, res] = await to(distrApi.addContact(params))
+            if (err) return
+            if (res.code == 200) {
+              this.$message.success('新建联系人成功')
+              this.contactVisible = false
+              this.$emit('initData')
+            }
+          }
+        })
+      },
+      // 联系人编辑
+      async contactEdit() {
+        this.$refs.contactForm.validate(async (valid) => {
+          if (valid) {
+            let params = { ...this.contactForm, distId: this.distId }
+            console.log(params)
+            const [err, res] = await to(distrApi.updateContact(params))
+            if (err) return
+            if (res.code == 200) {
+              this.$message.success('更新联系人成功')
+              this.contactVisible = false
+              this.$emit('initData')
+            }
+          }
+        })
+      },
+      contactClose() {
+        this.$refs['contactForm'].resetFields()
+      },
+    },
+  }
+</script>
+
+<style></style>

+ 42 - 130
src/views/base/distributor/detail.vue

@@ -43,13 +43,13 @@
                 {{ detail.businessScope }}
               </el-descriptions-item>
               <el-descriptions-item content-class-name="my-content" label="注册资金" label-class-name="my-label">
-                {{ detail.capital }}
+                {{ detail.capital }}万元
               </el-descriptions-item>
               <el-descriptions-item content-class-name="my-content" label="注册地" label-class-name="my-label">
                 {{ detail.registerDistrict }}
               </el-descriptions-item>
               <el-descriptions-item content-class-name="my-content" label="授权客户类型" label-class-name="my-label">
-                {{ customerOptions[detail.customerType] }}
+                {{ setCustomerType(detail.customerType) }}
               </el-descriptions-item>
               <el-descriptions-item
                 content-class-name="my-content"
@@ -86,6 +86,21 @@
               </el-descriptions-item>
             </el-descriptions>
           </el-tab-pane>
+          <el-tab-pane label="跟进记录" name="follow">
+            <follow v-if="activeName == 'follow'" target-type="50" />
+          </el-tab-pane>
+          <el-tab-pane label="联系人" name="contacts">
+            <contacts v-if="activeName == 'contacts'" @initRecords="getRecord" />
+          </el-tab-pane>
+          <el-tab-pane label="项目记录" name="projectRecords">
+            <project-records v-if="activeName == 'projectRecords'" />
+          </el-tab-pane>
+          <el-tab-pane label="合同记录" name="contractRecords">
+            <contract-records v-if="activeName == 'contractRecords'" />
+          </el-tab-pane>
+          <el-tab-pane label="历史代理记录" name="historyProxy">
+            <history-proxy v-if="activeName == 'historyProxy'" />
+          </el-tab-pane>
         </el-tabs>
       </div>
       <div class="info-side">
@@ -104,10 +119,15 @@
   import to from 'await-to-js'
   import { mapGetters } from 'vuex'
   import api from '@/api/base/distr'
+  import Contacts from '../components/Contacts.vue'
+  import ProjectRecords from '../components/ProjectRecords'
+  import ContractRecords from '../components/ContractRecords'
+  import HistoryProxy from '../components/HistoryProxy'
   import DetailsRecords from './components/DetailsRecords'
+  import Follow from '../components/Follow'
   export default {
     name: 'DistributorDetail',
-    components: { DetailsRecords },
+    components: { DetailsRecords, Contacts, ProjectRecords, ContractRecords, HistoryProxy, Follow },
     data() {
       return {
         id: 0,
@@ -126,7 +146,7 @@
           createdName: '', //创建人名字
           createdTime: '', //创建时间
         },
-        customerOptions: {},
+        customerOptions: [],
         dynamicsList: [],
       }
     },
@@ -145,12 +165,26 @@
       //this.getDynamics()
     },
     methods: {
+      setCustomerType(type) {
+        if (this.customerOptions.length == 0) return
+        if (!type) return
+        let arr = []
+        let typeArr = type.split(',')
+        console.log(type)
+        console.log(this.customerOptions)
+        typeArr.map((item) => {
+          console.log(item)
+          arr.push(this.customerOptions.find((e) => e.key == item).value)
+        })
+        return arr.join(',')
+      },
       getOptions() {
         Promise.all([this.getDicts('cust_idy')])
           .then(([data]) => {
-            data.data.values.filter((i) => {
-              this.customerOptions[i.key] = i.value
-            })
+            this.customerOptions = data.data.values
+            // data.data.values.filter((i) => {
+            //   this.customerOptions[i.key] = i.value
+            // })
           })
           .catch((err) => console.log(err))
       },
@@ -188,7 +222,7 @@
   $base: '.detail';
 
   #{$base} {
-    height: calc(100vh - 60px - 50px - 12px * 2 - 40px);
+    height: calc(100vh - 60px - 12px * 2 - 40px);
     display: flex;
     padding: 20px 40px;
 
@@ -343,128 +377,6 @@
         }
       }
     }
-
-    .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,

+ 8 - 5
src/views/base/distributor/index.vue

@@ -106,8 +106,9 @@
             </template>
           </el-table-column>
 
-          <el-table-column align="center" fixed="right" label="操作" show-overflow-tooltip width="85">
+          <el-table-column align="center" fixed="right" label="操作" show-overflow-tooltip width="160">
             <template #default="{ row }">
+              <el-button type="text" @click="$refs.changeAgent.open(row, 'change')">转为代理</el-button>
               <el-button v-permissions="['base:distributor:edit']" type="text" @click="handleEdit(row)">编辑</el-button>
               <el-button v-permissions="['base:distributor:delete']" type="text" @click="handleDelete(row)">
                 删除
@@ -129,6 +130,7 @@
       </div>
     </div>
     <edit ref="edit" @fetch-data="fetchData" />
+    <change-agent ref="changeAgent" @fetch-data="fetchData" />
   </div>
 </template>
 
@@ -136,13 +138,14 @@
   import distrApi from '@/api/base/distr'
   import regionApi from '@/api/base/region'
   import regionAuthApi from '@/api/base/regionAuth'
+  import ChangeAgent from '@/views/base/distributor/components/ChangeAgent'
 
   import Edit from './components/DistrEdit'
   import TableTool from '@/components/table/TableTool'
 
   export default {
     name: 'Distributor',
-    components: { Edit, TableTool },
+    components: { Edit, TableTool, ChangeAgent },
     data() {
       return {
         tableKey: 0,
@@ -161,7 +164,7 @@
         columns: [
           {
             label: '经销商名称',
-            width: '200px',
+            width: 'auto',
             prop: 'distName',
             sortable: false,
           },
@@ -324,9 +327,10 @@
       },
       async fetchData() {
         this.listLoading = true
+        const params = Object.assign(this.queryForm, { withStatistic: true, distType: '10' })
         const {
           data: { list, total },
-        } = await distrApi.getList(this.queryForm)
+        } = await distrApi.getList(params)
         this.list = list
         this.total = total
         this.listLoading = false
@@ -337,7 +341,6 @@
         this.queryForm = {
           pageNum: 1,
           pageSize: 10,
-          distCode: '',
           distName: '',
           belongSale: '',
         }

+ 1 - 1
src/views/contract/components/DetailsEnclosure.vue

@@ -2,7 +2,7 @@
  * @Author: liuzhenlin 461480418@qq.ocm
  * @Date: 2023-01-10 15:03:27
  * @LastEditors: liuzhenlin
- * @LastEditTime: 2023-01-11 14:45:28
+ * @LastEditTime: 2023-05-18 15:09:27
  * @Description: file content
  * @FilePath: \订单全流程管理系统\src\views\contract\components\DetailsEnclosure.vue
 -->