Преглед на файлове

Merge branch 'feature/合同' into develop

liuzl преди 3 години
родител
ревизия
b067d6bb57

+ 4 - 3
.env.development

@@ -1,11 +1,12 @@
+
 # 租户码
 VUE_APP_TENANT=default
 
 # GateWay地址
-VUE_APP_MicroSrvProxy_API=http://127.0.0.1:9981/
+VUE_APP_MicroSrvProxy_API=http://192.168.0.252:8100/
 
 # 登录验证微服务名称
-VUE_APP_AdminPath=dashoo.opms.admin-0.0.1
+VUE_APP_AdminPath=dashoo.labsop.lims_latest_opms_admin
 
 # 业务接口微服务名称
-VUE_APP_ParentPath=dashoo.opms.parent-0.0.1
+VUE_APP_ParentPath=dashoo.labsop.lims_latest_opms_parent

+ 35 - 0
src/api/contract/collection.js

@@ -0,0 +1,35 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-05 16:34:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-07 16:12:48
+ * @FilePath: \订单全流程管理系统\src\api\contract\index.js
+ */
+import micro_request from '@/utils/micro_request'
+const basePath = process.env.VUE_APP_ParentPath
+export default {
+  // 获取回款列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollection', 'List', query)
+  },
+  // 删除合同
+  delContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollection', 'Delete', query)
+  },
+  // 新增合同
+  addContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollection', 'Add', query)
+  },
+  // 获取合同列表
+  getDetails(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollection', 'Get', query)
+  },
+  // 更新合同
+  updateContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollection', 'Update', query)
+  },
+  // 转移合同
+  transferContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollection', 'Transfer', query)
+  },
+}

+ 35 - 0
src/api/contract/collectionPlan.js

@@ -0,0 +1,35 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-05 16:34:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-09 14:58:26
+ * @FilePath: \订单全流程管理系统\src\api\contract\collectionPlan.js
+ */
+import micro_request from '@/utils/micro_request'
+const basePath = process.env.VUE_APP_ParentPath
+export default {
+  // 获取回款计划列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollectionPlan', 'List', query)
+  },
+  // 删除合同
+  delContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollectionPlan', 'Delete', query)
+  },
+  // 新增合同
+  addContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollectionPlan', 'Add', query)
+  },
+  // 获取合同列表
+  getDetails(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollectionPlan', 'Get', query)
+  },
+  // 更新合同
+  updateContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollectionPlan', 'Update', query)
+  },
+  // 转移合同
+  transferContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContractCollectionPlan', 'Transfer', query)
+  },
+}

+ 35 - 0
src/api/contract/index.js

@@ -0,0 +1,35 @@
+/*
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-05 16:34:58
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-07 16:12:48
+ * @FilePath: \订单全流程管理系统\src\api\contract\index.js
+ */
+import micro_request from '@/utils/micro_request'
+const basePath = process.env.VUE_APP_ParentPath
+export default {
+  // 获取合同列表
+  getList(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'List', query)
+  },
+  // 删除合同
+  delContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'Delete', query)
+  },
+  // 新增合同
+  addContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'Add', query)
+  },
+  // 获取合同列表
+  getDetails(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'Get', query)
+  },
+  // 更新合同
+  updateContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'Update', query)
+  },
+  // 转移合同
+  transferContract(query) {
+    return micro_request.postRequest(basePath, 'CtrContract', 'Transfer', query)
+  },
+}

+ 3 - 0
src/api/proj/business.js

@@ -24,4 +24,7 @@ export default {
   businessTransfer(query) {
     return micro_request.postRequest(basePath, 'Business', 'BusinessTransfer', query)
   },
+  getProductByBusinessId(query) {
+    return micro_request.postRequest(basePath, 'Business', 'GetBusinessProduct', query)
+  },
 }

+ 1 - 0
src/utils/micro_request.js

@@ -91,6 +91,7 @@ service.postRequest = function postRequest(basePath, srvName, funcName, data) {
       'X-RPCX-SerializeType': '1',
       'X-RPCX-ServicePath': srvName,
       'X-RPCX-ServiceMethod': funcName,
+      SrvEnv: 'dev',
     },
     data: data,
   })

+ 183 - 0
src/views/contract/components/DetailsCollection.vue

@@ -0,0 +1,183 @@
+<!--
+ * @Author: liuzl liuzl0802
+ * @Date: 2023-01-09 13:54:40
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-09 15:28:38
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\contract\components\DetailsCollection.vue
+-->
+<template>
+  <div class="collection-container">
+    <div class="collection-plan mb10">
+      <el-row class="mb10">
+        <el-col class="text-right" :span="24">
+          <el-button icon="el-icon-plus" size="mini" type="primary">新建回款计划</el-button>
+        </el-col>
+      </el-row>
+      <el-table border :data="collectionPlanData" height="calc(100% - 40px)">
+        <el-table-column
+          v-for="(item, index) in planColumns"
+          :key="index"
+          align="center"
+          :label="item.label"
+          :min-width="item.width"
+          :prop="item.prop"
+          show-overflow-tooltip>
+          <template #default="{ row }">
+            <span v-if="item.label == '合计'">
+              {{ calculatedDiscount(row.tranPrice, row.prodNum) }}
+            </span>
+            <span v-else>{{ row[item.prop] }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+    <div class="collection">
+      <el-row class="mb10">
+        <el-col class="text-right" :span="24">
+          <el-button icon="el-icon-plus" size="mini" type="primary">新建回款</el-button>
+        </el-col>
+      </el-row>
+      <el-table border :data="collectionData" height="calc(100% - 40px)">
+        <el-table-column
+          v-for="(item, index) in collectionColumns"
+          :key="index"
+          align="center"
+          :label="item.label"
+          :min-width="item.width"
+          :prop="item.prop"
+          show-overflow-tooltip>
+          <template #default="{ row }">
+            <span v-if="item.label == '合计'">
+              {{ calculatedDiscount(row.tranPrice, row.prodNum) }}
+            </span>
+            <span v-else>{{ row[item.prop] }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script>
+  import collectionApi from '@/api/contract/collection'
+  import collectionPlanApi from '@/api/contract/collectionPlan'
+  import to from 'await-to-js'
+  export default {
+    name: 'DetailsCollection',
+    props: {
+      details: {
+        type: Object,
+        default: () => {},
+      },
+    },
+    data() {
+      return {
+        collectionData: [],
+        collectionPlanData: [],
+        planColumns: [
+          {
+            label: '产品名称',
+            width: 'auto',
+            prop: 'prodName',
+          },
+          {
+            label: '产品类别',
+            width: '100px',
+            prop: 'prodClass',
+          },
+          {
+            label: '建议成交价',
+            width: '100px',
+            prop: 'sugSalesPrice',
+          },
+          {
+            label: '售价',
+            width: '160px',
+            prop: 'tranPrice',
+          },
+          {
+            label: '数量',
+            width: '160px',
+            prop: 'prodNum',
+          },
+          {
+            label: '合计',
+            width: '120px',
+          },
+        ],
+        collectionColumns: [
+          {
+            label: '产品名称',
+            width: 'auto',
+            prop: 'prodName',
+          },
+          {
+            label: '产品类别',
+            width: '100px',
+            prop: 'prodClass',
+          },
+          {
+            label: '建议成交价',
+            width: '100px',
+            prop: 'sugSalesPrice',
+          },
+          {
+            label: '售价',
+            width: '160px',
+            prop: 'tranPrice',
+          },
+          {
+            label: '数量',
+            width: '160px',
+            prop: 'prodNum',
+          },
+          {
+            label: '合计',
+            width: '120px',
+          },
+        ],
+      }
+    },
+
+    mounted() {
+      this.getCollectionData()
+      this.getCollectionPlaneData()
+    },
+
+    methods: {
+      // 获取回款和回款计划列表
+      async getCollectionData() {
+        let params = { contractId: this.details.id, pageNum: 0 }
+        const [err, res] = await to(collectionApi.getList(params))
+        if (err) return
+        this.collectionData = res.data.list
+      },
+      async getCollectionPlaneData() {
+        let params = { contractId: this.details.id, pageNum: 0 }
+        const [err, res] = await to(collectionPlanApi.getList(params))
+        if (err) return
+        this.collectionPlanData = res.data.list
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .collection-container {
+    .mb10 {
+      margin-bottom: 10px;
+    }
+    height: 100%;
+    .collection-plan,
+    .collection {
+      height: 50%;
+      .el-row {
+        height: 30px;
+      }
+    }
+    .text-right {
+      text-align: right;
+    }
+  }
+</style>

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


+ 73 - 0
src/views/contract/components/DetailsInfo.vue

@@ -0,0 +1,73 @@
+<!--
+ * @Author: liuzl liuzl0802
+ * @Date: 2023-01-09 13:54:36
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-09 14:38:24
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\contract\components\DetailsInfo.vue
+-->
+<template>
+  <div class="details-container">
+    <el-descriptions border :column="2" size="medium" title="基本信息">
+      <el-descriptions-item content-class-name="my-content" label="合同编号" label-class-name="my-label">
+        {{ details.contractCode }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="合同名称" label-class-name="my-label">
+        {{ details.contractName }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="客户名称" label-class-name="my-label">
+        {{ details.custName }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="项目名称" label-class-name="my-label">
+        {{ details.nboName }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="合同金额" label-class-name="my-label">
+        {{ formatPrice(details.contractAmount) }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="合同开始时间" label-class-name="my-label">
+        {{ details.contractStartTime }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="合同结束时间" label-class-name="my-label">
+        {{ details.contractEndTime }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="负责人" label-class-name="my-label">
+        {{ details.inchargeName }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="公司签约人" label-class-name="my-label">
+        {{ details.signatoryName }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="备注" label-class-name="my-label">
+        {{ details.remark }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="合同类型" label-class-name="my-label">
+        {{ details.contractType }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="创建人" label-class-name="my-label">
+        {{ details.createdName }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="创建时间" label-class-name="my-label">
+        {{ details.createdTime }}
+      </el-descriptions-item>
+      <el-descriptions-item content-class-name="my-content" label="更新时间" label-class-name="my-label">
+        {{ details.updatedTime }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </div>
+</template>
+<script>
+  export default {
+    name: 'DetailsInfo',
+    props: {
+      details: {
+        type: Object,
+        default: () => {},
+      },
+    },
+    data() {
+      return {}
+    },
+    mounted() {},
+    methods: {},
+  }
+</script>
+<style lang="scss" scoped></style>

+ 0 - 0
src/views/contract/components/DetailsInvoice.vue


+ 90 - 0
src/views/contract/components/DetailsProduct.vue

@@ -0,0 +1,90 @@
+<!--
+ * @Author: liuzl liuzl0802
+ * @Date: 2023-01-09 13:54:40
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-09 14:35:38
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\contract\components\DetailsProduct.vue
+-->
+<template>
+  <div>
+    <el-table border :data="product">
+      <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>
+        <template #default="{ row }">
+          <span v-if="item.label == '合计'">
+            {{ calculatedDiscount(row.tranPrice, row.prodNum) }}
+          </span>
+          <span v-else>{{ row[item.prop] }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: 'DetailsProduct',
+    props: {
+      product: {
+        type: Array,
+        default: () => [],
+      },
+    },
+    data() {
+      return {
+        columns: [
+          {
+            label: '产品名称',
+            width: 'auto',
+            prop: 'prodName',
+          },
+          {
+            label: '产品类别',
+            width: '100px',
+            prop: 'prodClass',
+          },
+          {
+            label: '建议成交价',
+            width: '100px',
+            prop: 'sugSalesPrice',
+          },
+          {
+            label: '售价',
+            width: '160px',
+            prop: 'tranPrice',
+          },
+          {
+            label: '数量',
+            width: '160px',
+            prop: 'prodNum',
+          },
+          {
+            label: '合计',
+            width: '120px',
+          },
+        ],
+      }
+    },
+
+    mounted() {},
+
+    methods: {
+      // 计算总价
+      calculatedDiscount(price, count) {
+        let intPrice = null
+        if (typeof price === 'string') intPrice = this.delcommafy(price) * 100
+        else intPrice = price * 100
+        return this.formatPrice((intPrice * count) / 100)
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped></style>

+ 432 - 0
src/views/contract/components/Edit.vue

@@ -0,0 +1,432 @@
+<template>
+  <el-dialog :title="title" :visible.sync="editVisible" @close="handleClose">
+    <el-form ref="editForm" :model="editForm" :rules="editRules">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="合同编号" prop="contractCode">
+            <el-input v-model="editForm.contractCode" placeholder="请输入合同编号" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同名称" prop="contractName">
+            <el-input v-model="editForm.contractName" placeholder="请输入合同名称" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="关联项目" prop="nboName">
+            <el-input
+              ref="businessInput"
+              v-model="editForm.nboName"
+              placeholder="请选择关联项目"
+              @focus="openProject" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="客户名称" prop="nboId">
+            <el-input v-model="editForm.custName" disabled />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="合同开始时间" prop="contractStartTime">
+            <el-date-picker
+              v-model="editForm.contractStartTime"
+              :picker-options="pickerOptionsStart"
+              placeholder="选择开始日期"
+              style="width: 100%"
+              type="date"
+              value-format="yyyy-MM-dd" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同结束时间" prop="contractEndTime">
+            <el-date-picker
+              v-model="editForm.contractEndTime"
+              :picker-options="pickerOptionsEnd"
+              placeholder="选择结束日期"
+              style="width: 100%"
+              type="date"
+              value-format="yyyy-MM-dd" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="负责人" prop="inchargeId">
+            <el-select
+              v-model="editForm.inchargeId"
+              placeholder="请选择负责人"
+              style="width: 100%"
+              @change="changeIncharge">
+              <el-option v-for="item in usersList" :key="item.id" :label="item.userName" :value="item.id" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="公司签约人" prop="signatoryName">
+            <el-input
+              ref="userInput"
+              v-model="editForm.signatoryName"
+              placeholder="请选择公司签约人"
+              @focus="openUser" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="经销商" prop="distributorName">
+            <el-input
+              ref="distributorInput"
+              v-model="editForm.distributorName"
+              placeholder="请选择经销商"
+              @focus="openDistributor" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同类型" prop="contractType">
+            <el-select v-model="editForm.contractType" placeholder="合同类型" style="width: 100%">
+              <el-option v-for="item in contractOptions" :key="item.value" :label="item.value" :value="item.value" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input v-model="editForm.remark" placeholder="请输入备注" :rows="5" type="textarea" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <!-- 产品分类 -->
+    <el-row>
+      <el-row class="mb10" :gutter="20">
+        <el-col :span="12">
+          <p>产品:</p>
+        </el-col>
+        <el-col class="proj-col" :span="12">
+          <el-button type="primary" @click="$refs.product.open()">选择产品</el-button>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="24">
+          <product-table
+            ref="productTable"
+            :product-data="productData"
+            @changeProductData="changeProductData"
+            @delProductData="delProductData" />
+        </el-col>
+      </el-row>
+    </el-row>
+
+    <span slot="footer">
+      <el-button v-show="!editForm.id" type="primary" @click="contractSave">保存</el-button>
+      <el-button v-show="editForm.id" type="primary" @click="contractEdit">保存</el-button>
+      <el-button @click="editVisible = false">取消</el-button>
+    </span>
+    <!-- 选择项目 -->
+    <select-business ref="project" :multiple="false" @save="getBusinessInfo" />
+    <!-- 选择经销商 -->
+    <select-distributor ref="distributor" :multiple="false" @save="getDistributor" />
+    <!-- 选择公司签约人 -->
+    <select-user ref="user" :multiple="false" @save="getUser" />
+    <!-- 选择产品 -->
+    <select-product ref="product" :multiple="true" @save="getProduct" />
+  </el-dialog>
+</template>
+
+<script>
+  import { mapGetters } from 'vuex'
+  import to from 'await-to-js'
+  import contractApi from '@/api/contract'
+  import businessApi from '@/api/proj/business'
+  import ProductTable from './ProductTable'
+  import SelectBusiness from '@/components/select/SelectBusiness'
+  import SelectDistributor from '@/components/select/SelectDistributor'
+  import SelectUser from '@/components/select/SelectUser'
+  import SelectProduct from '@/components/select/SelectProduct'
+  export default {
+    components: {
+      ProductTable,
+      SelectBusiness,
+      SelectDistributor,
+      SelectUser,
+      SelectProduct,
+    },
+    props: {
+      usersList: {
+        type: Array,
+        default: () => [],
+      },
+    },
+    data() {
+      return {
+        title: '新增客户信息',
+        editVisible: false,
+        editForm: {
+          contractCode: '', //合同编号
+          contractName: '', //合同名称
+          contractType: '', //合同类型
+          nboId: null, //项目id
+          nboName: '', //项目名称
+          custName: '', // 客户名称
+          inchargeId: null, //负责人
+          inchargeName: '', //负责人姓名
+          contractStartTime: '', //合同开始时间
+          contractEndTime: '', //合同结束时间
+          signatoryName: '', //公司签约人
+          signatoryId: null, //公司签约人id
+          distributorId: null, //经销商id
+          distributorName: '', //经销商name
+          remark: '', //备注
+        },
+        editRules: {
+          contractCode: [{ required: true, trigger: 'blur', message: '请输入合同编号' }],
+          contractName: [{ required: true, trigger: 'blur', message: '请输入合同名称' }],
+          contractType: [{ required: true, trigger: 'change', message: '请选择合同类型' }],
+          nboName: [{ required: true, trigger: 'change', message: '请选择关联项目' }],
+          contractStartTime: [
+            { required: true, trigger: 'change', validator: this.pickerOptionsStart, message: '请选择开始时间' },
+          ],
+          contractEndTime: [
+            { required: true, trigger: 'change', validator: this.pickerOptionsEnd, message: '请选择结束时间' },
+          ],
+          inchargeId: [{ required: true, trigger: 'change', message: '请选择负责人' }],
+          signatoryName: [{ required: true, trigger: 'change', message: '请选择公司签约人' }],
+          distributorName: [{ required: true, trigger: 'change', message: '请选择经销商' }],
+        },
+        pickerOptionsStart: {
+          disabledDate: (time) => {
+            let endDateVal = this.editForm.contractEndTime
+            if (endDateVal) {
+              return time.getTime() > new Date(endDateVal).getTime()
+            }
+          },
+        },
+        pickerOptionsEnd: {
+          disabledDate: (time) => {
+            let beginDateVal = this.editForm.contractStartTime
+            if (beginDateVal) {
+              return time.getTime() < new Date(beginDateVal).getTime() - 1 * 24 * 60 * 60 * 1000
+            }
+          },
+        },
+        contractOptions: [], //合同类型
+        productData: [],
+      }
+    },
+    computed: {
+      ...mapGetters({
+        userId: 'user/id',
+      }),
+    },
+    mounted() {
+      this.getOptions()
+    },
+    methods: {
+      getOptions() {
+        Promise.all([this.getDicts('contract_type')])
+          .then(([contract]) => {
+            this.contractOptions = contract.data.values || []
+            // 设置负责人默认为当前用户
+            this.editForm.inchargeId = this.userId
+            this.editForm.inchargeName = this.usersList.filter((item) => item.id == this.userId)[0]['userName']
+          })
+          .catch((err) => console.log(err))
+      },
+      async init(id) {
+        if (!id) {
+          this.title = '新增合同信息'
+        } else {
+          this.title = '编辑合同'
+          const [err, res] = await to(contractApi.getDetails({ id }))
+          if (err) return
+          if (res.data) {
+            // eslint-disable-next-line no-unused-vars
+            let { product, ...data } = res.data
+            this.editForm = data
+            this.productData =
+              product.length > 0
+                ? product.map((item) => ({
+                    id: item.prodId,
+                    prodName: item.prodName,
+                    prodClass: item.prodClass,
+                    guidPrice: item.sugSalesPrice,
+                    price: item.tranPrice,
+                    count: item.prodNum,
+                  }))
+                : []
+          }
+        }
+        this.editVisible = true
+      },
+      // 修改责任人
+      changeIncharge(selectedId) {
+        this.editForm.inchargeName = this.usersList.filter((item) => item.id == selectedId)[0]['userName']
+      },
+      // 打开选择项目
+      openProject() {
+        this.$refs.project.open()
+        this.$refs.businessInput.blur()
+      },
+      // 关闭选择项目获取项目信息
+      getBusinessInfo(data) {
+        let business = data[0] || null
+        if (!business) return
+        this.editForm.custName = business.custName
+        this.editForm.nboId = business.id
+        this.editForm.nboName = business.nboName
+        // 获取产品信息
+        this.getProjectInfo(business.id)
+      },
+      // 打开选择经销商
+      openDistributor() {
+        this.$refs.distributor.open()
+        this.$refs.distributorInput.blur()
+      },
+      // 关闭经销商获取经销商信息
+      getDistributor(data) {
+        let distributor = data[0] || null
+        if (!distributor) return
+        this.editForm.distributorName = distributor.distName
+        this.editForm.distributorId = distributor.id
+      },
+      // 打开选择公司签约人
+      openUser() {
+        this.$refs.user.open()
+        this.$refs.userInput.blur()
+      },
+      // 获取签约人信息
+      getUser(data) {
+        let user = data[0] || null
+        if (!user) return
+        this.editForm.signatoryName = user.userName
+        this.editForm.signatoryId = user.id
+      },
+      // 获取产品信息
+      getProduct(data) {
+        // 重构产品数据结构
+        if (data.length > 0) this.setProductData(data)
+      },
+      // 根据项目id获取产品信息
+      getProjectInfo(id) {
+        let params = { id }
+        businessApi.getProductByBusinessId(params).then((res) => {
+          if (res.data && res.data.length > 0) this.setProductData(res.data)
+        })
+      },
+      setProductData(data) {
+        let projData = data.map((item) => ({
+          id: item.id,
+          prodName: item.prodName,
+          prodClass: item.prodClass,
+          guidPrice: item.guidPrice,
+          price: item.guidPrice,
+          count: 1,
+        }))
+        this.productData.push(...projData)
+        this.productData = this.removeDuplicateObj(this.productData)
+      },
+      // 数组对象去重
+      removeDuplicateObj(arr) {
+        let obj = {}
+        arr = arr.reduce((newArr, next) => {
+          obj[next.id] ? '' : (obj[next.id] = true && newArr.push(next))
+          return newArr
+        }, [])
+        return arr
+      },
+      // 修改产品列表数据
+      changeProductData(data) {
+        this.productData = this.productData.map((item) => {
+          return item.id === data.id ? data : item
+        })
+      },
+      delProductData(data) {
+        this.productData = this.productData.filter((item) => item.id != data.id)
+      },
+      // 保存合同
+      async contractSave() {
+        let product = this.productData.map((item) => ({
+          prodId: item.id,
+          prodNum: item.count,
+          maintTerm: 1,
+          sugSalesPrice: item.guidPrice,
+          tranPrice: this.delcommafy(item.price),
+          remark: '',
+        }))
+        let params = Object.assign({ ...this.editForm }, { product })
+        const [valid] = await to(this.$refs.editForm.validate())
+        if (valid == false) return
+        if (product.length == 0) return this.$message.warning('请选择产品信息')
+        const [err, res] = await to(contractApi.addContract(params))
+        if (err) return
+        if (res.code == 200) this.$message.success(res.msg)
+        else return
+        this.editVisible = false
+        this.$emit('contractSave')
+      },
+      // 编辑合同
+      async contractEdit() {
+        let product = this.productData.map((item) => ({
+          prodId: item.id,
+          prodNum: item.count,
+          maintTerm: 1,
+          sugSalesPrice: item.guidPrice,
+          tranPrice: this.delcommafy(item.price),
+          remark: '',
+        }))
+        let params = Object.assign({ ...this.editForm }, { product })
+        const [valid] = await to(this.$refs.editForm.validate())
+        if (valid == false) return
+        if (product.length == 0) return this.$message.warning('请选择产品信息')
+        const [err, res] = await to(contractApi.updateContract(params))
+        if (err) return
+        if (res.code == 200) this.$message.success(res.msg)
+        else return
+        this.editVisible = false
+        this.$emit('contractSave')
+      },
+      handleClose() {
+        this.editForm = {
+          contractCode: '', //合同编号
+          contractName: '', //合同名称
+          contractType: '', //合同类型
+          nboId: null, //项目id
+          nboName: '', //项目名称
+          custName: '', // 客户名称
+          inchargeId: null, //负责人
+          inchargeName: '', //负责人名称
+          contractStartTime: '', //合同开始时间
+          contractEndTime: '', //合同结束时间
+          signatoryName: '', //公司签约人
+          signatoryId: null, //公司签约人id
+          distributorId: null, //经销商id
+          distributorName: '', //经销商name
+          remark: '', //备注
+        }
+        this.productData = []
+        this.$refs.editForm.resetFields()
+      },
+      delcommafy(num) {
+        if (!num) return 0
+        num = num.toString()
+        num = num.replace(/,/gi, '')
+        if (num.indexOf('.00') > 0) num = parseInt(num)
+        return Number(num)
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .mb10 {
+    margin-bottom: 10px;
+  }
+  .proj-col {
+    text-align: right;
+  }
+</style>

+ 8 - 0
src/views/contract/components/EditCollection.vue

@@ -0,0 +1,8 @@
+<!--
+ * @Author: liuzl liuzl0802
+ * @Date: 2023-01-09 15:34:06
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-09 15:35:04
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\contract\components\EditCollection.vue
+-->

+ 190 - 0
src/views/contract/components/EditPlane.vue

@@ -0,0 +1,190 @@
+<!--
+ * @Author: liuzl liuzl0802
+ * @Date: 2023-01-09 15:34:20
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-09 15:39:32
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\contract\components\EditPlane.vue
+-->
+<template>
+  <el-dialog :title="title" :visible.sync="editVisible" @close="handleClose">
+    <el-form ref="editForm" :model="editForm" :rules="editRules">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="客户名称" prop="contractCode">
+            <el-input v-model="editForm.contractCode" placeholder="请输入合同编号" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同编号" prop="contractName">
+            <el-input v-model="editForm.contractName" placeholder="请输入合同名称" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="合同开始时间" prop="contractStartTime">
+            <el-date-picker
+              v-model="editForm.contractStartTime"
+              :picker-options="pickerOptionsStart"
+              placeholder="选择开始日期"
+              style="width: 100%"
+              type="date"
+              value-format="yyyy-MM-dd" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同结束时间" prop="contractEndTime">
+            <el-date-picker
+              v-model="editForm.contractEndTime"
+              :picker-options="pickerOptionsEnd"
+              placeholder="选择结束日期"
+              style="width: 100%"
+              type="date"
+              value-format="yyyy-MM-dd" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <span slot="footer">
+      <el-button v-show="!editForm.id" type="primary" @click="contractSave">保存</el-button>
+      <el-button v-show="editForm.id" type="primary" @click="contractEdit">保存</el-button>
+      <el-button @click="editVisible = false">取消</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import contractApi from '@/api/contract'
+  export default {
+    props: {},
+    data() {
+      return {
+        title: '新建回款计划',
+        editVisible: false,
+        editForm: {
+          contractCode: '', //合同编号
+          contractName: '', //合同名称
+          contractType: '', //合同类型
+        },
+        editRules: {
+          contractCode: [{ required: true, trigger: 'blur', message: '请输入合同编号' }],
+          contractName: [{ required: true, trigger: 'blur', message: '请输入合同名称' }],
+          contractType: [{ required: true, trigger: 'change', message: '请选择合同类型' }],
+          nboName: [{ required: true, trigger: 'change', message: '请选择关联项目' }],
+          contractStartTime: [
+            { required: true, trigger: 'change', validator: this.pickerOptionsStart, message: '请选择开始时间' },
+          ],
+          contractEndTime: [
+            { required: true, trigger: 'change', validator: this.pickerOptionsEnd, message: '请选择结束时间' },
+          ],
+          inchargeId: [{ required: true, trigger: 'change', message: '请选择负责人' }],
+          signatoryName: [{ required: true, trigger: 'change', message: '请选择公司签约人' }],
+          distributorName: [{ required: true, trigger: 'change', message: '请选择经销商' }],
+        },
+      }
+    },
+    mounted() {},
+    methods: {
+      async init(id) {
+        if (!id) {
+          this.title = '新建回款计划'
+        } else {
+          this.title = '编辑合同'
+          const [err, res] = await to(contractApi.getDetails({ id }))
+          if (err) return
+          if (res.data) {
+            // eslint-disable-next-line no-unused-vars
+            let { product, ...data } = res.data
+            this.editForm = data
+            this.productData =
+              product.length > 0
+                ? product.map((item) => ({
+                    id: item.prodId,
+                    prodName: item.prodName,
+                    prodClass: item.prodClass,
+                    guidPrice: item.sugSalesPrice,
+                    price: item.tranPrice,
+                    count: item.prodNum,
+                  }))
+                : []
+          }
+        }
+        this.editVisible = true
+      },
+      // 保存合同
+      async contractSave() {
+        let product = this.productData.map((item) => ({
+          prodId: item.id,
+          prodNum: item.count,
+          maintTerm: 1,
+          sugSalesPrice: item.guidPrice,
+          tranPrice: this.delcommafy(item.price),
+          remark: '',
+        }))
+        let params = Object.assign({ ...this.editForm }, { product })
+        const [valid] = await to(this.$refs.editForm.validate())
+        if (valid == false) return
+        if (product.length == 0) return this.$message.warning('请选择产品信息')
+        const [err, res] = await to(contractApi.addContract(params))
+        if (err) return
+        if (res.code == 200) this.$message.success(res.msg)
+        else return
+        this.editVisible = false
+        this.$emit('contractSave')
+      },
+      // 编辑合同
+      async contractEdit() {
+        let product = this.productData.map((item) => ({
+          prodId: item.id,
+          prodNum: item.count,
+          maintTerm: 1,
+          sugSalesPrice: item.guidPrice,
+          tranPrice: this.delcommafy(item.price),
+          remark: '',
+        }))
+        let params = Object.assign({ ...this.editForm }, { product })
+        const [valid] = await to(this.$refs.editForm.validate())
+        if (valid == false) return
+        if (product.length == 0) return this.$message.warning('请选择产品信息')
+        const [err, res] = await to(contractApi.updateContract(params))
+        if (err) return
+        if (res.code == 200) this.$message.success(res.msg)
+        else return
+        this.editVisible = false
+        this.$emit('contractSave')
+      },
+      handleClose() {
+        this.editForm = {
+          contractCode: '', //合同编号
+          contractName: '', //合同名称
+          contractType: '', //合同类型
+          nboId: null, //项目id
+          nboName: '', //项目名称
+          custName: '', // 客户名称
+          inchargeId: null, //负责人
+          inchargeName: '', //负责人名称
+          contractStartTime: '', //合同开始时间
+          contractEndTime: '', //合同结束时间
+          signatoryName: '', //公司签约人
+          signatoryId: null, //公司签约人id
+          distributorId: null, //经销商id
+          distributorName: '', //经销商name
+          remark: '', //备注
+        }
+        this.productData = []
+        this.$refs.editForm.resetFields()
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .mb10 {
+    margin-bottom: 10px;
+  }
+  .proj-col {
+    text-align: right;
+  }
+</style>

+ 209 - 0
src/views/contract/components/ProductTable.vue

@@ -0,0 +1,209 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-05 17:43:46
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-07 14:36:08
+ * @FilePath: \订单全流程管理系统\src\components\table\ProductTable.vue
+-->
+<template>
+  <div class="table-container">
+    <el-table border :data="data">
+      <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>
+        <template #default="{ row }">
+          <span v-if="item.prop == 'price'">
+            <el-input
+              v-model.trim="row.price"
+              placeholder="请输入金额"
+              @blur="inputMoney($event, row, 'price')"
+              @change="handleChange(row)"
+              @focus="uninputMoney($event, row, 'price')">
+              <!-- <template slot="append">元</template> -->
+            </el-input>
+          </span>
+          <span v-else-if="item.prop == 'count'">
+            <el-input-number v-model="row.count" :min="0" size="mini" @change="handleChange(row)" />
+          </span>
+          <span v-else-if="item.label == '合计'">
+            {{ calculatedDiscount(row.price, row.count) }}
+          </span>
+          <span v-else>{{ row[item.prop] }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="操作">
+        <template slot-scope="scope">
+          <el-button type="text" @click="handleDel(scope.row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+  // import AmountInput from '@/components/AmountInput'
+
+  export default {
+    name: 'ProductTable',
+    components: {
+      // AmountInput,
+    },
+    props: {
+      productData: {
+        type: Array,
+        default() {
+          return []
+        },
+      },
+      checkList: {
+        type: Array,
+        default() {
+          return []
+        },
+      },
+    },
+    data() {
+      return {
+        data: [],
+        columns: [
+          {
+            label: '产品名称',
+            width: 'auto',
+            prop: 'prodName',
+          },
+          {
+            label: '产品类别',
+            width: '100px',
+            prop: 'prodClass',
+          },
+          {
+            label: '建议成交价',
+            width: '100px',
+            prop: 'guidPrice',
+          },
+          {
+            label: '售价',
+            width: '160px',
+            prop: 'price',
+          },
+          {
+            label: '数量',
+            width: '160px',
+            prop: 'count',
+          },
+          {
+            label: '合计',
+            width: '120px',
+          },
+        ],
+      }
+    },
+    watch: {
+      productData() {
+        this.data = []
+        this.data = this.productData.map((item) => {
+          return { ...item, price: this.setPriceFormat(item['price']) }
+        })
+        console.log(this.data)
+      },
+    },
+    created() {
+      this.data = []
+      this.data = this.productData.map((item) => {
+        return { ...item, price: this.setPriceFormat(item['price']) }
+      })
+    },
+    methods: {
+      // input修改
+      handleChange(item) {
+        this.$emit('changeProductData', item)
+      },
+      // 删除
+      handleDel(item) {
+        this.$emit('delProductData', item)
+      },
+      // 失焦显示数字类型
+      inputMoney(el, row, key) {
+        let temp = Number(el.target.value) || null
+        row[key] = this.priceFormat(temp)
+      },
+      // 获得焦点金额去掉格式
+      uninputMoney(el, row, key) {
+        if (el.target.value) {
+          row[key] = this.delcommafy(el.target.value)
+        } else {
+          row[key] = null
+        }
+      },
+      setPriceFormat(num) {
+        if (!num) {
+          return (num = 0)
+        } else if (typeof num == 'number') {
+          return this.priceFormat(num)
+        } else {
+          return num
+        }
+      },
+      // 金额格式化
+      priceFormat(num, n) {
+        n = n || 2
+        let symbol = ','
+        if (num === null) return num
+        if (typeof num !== 'number') throw new TypeError('num参数应该是一个number类型')
+        if (n < 0) throw new Error('参数n不应该小于0')
+        let hasDot = parseInt(num) != num //这里检测num是否为小数,true表示小数
+        let m = n != undefined && n != null ? n : 1
+        num = m == 0 ? num.toFixed(m) + '.' : hasDot ? (n ? num.toFixed(n) : num) : num.toFixed(m)
+        symbol = symbol || ','
+        num = num.toString().replace(/(\d)(?=(\d{3})+\.)/g, function (match, p1) {
+          return p1 + symbol
+        })
+        if (n == 0 || (!hasDot && !n)) {
+          //如果n为0或者传入的num是整数并且没有指定整数的保留位数,则去掉前面操作中的小数位
+          num = num.substring(0, num.indexOf('.'))
+        }
+        return num
+      },
+      //去除千分位中的‘,’
+      delcommafy(num) {
+        if (!num) return num
+        num = num.toString()
+        num = num.replace(/,/gi, '')
+        if (num.indexOf('.00') > 0) num = parseInt(num)
+        return num
+      },
+      // 计算总价
+      calculatedDiscount(price, count) {
+        let intPrice = null
+        if (typeof price === 'string') intPrice = this.delcommafy(price) * 100
+        else intPrice = price * 100
+        return this.priceFormat((intPrice * count) / 100)
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .custom-table-container {
+    :deep() {
+      i {
+        cursor: pointer;
+      }
+    }
+
+    .stripe-panel,
+    .border-panel {
+      margin: 0 10px #{$base-margin/2} 10px !important;
+    }
+  }
+</style>
+<style lang="scss">
+  .table-container {
+    width: 100%;
+  }
+</style>

+ 92 - 0
src/views/contract/components/Transfer.vue

@@ -0,0 +1,92 @@
+<!--
+ * @Author: liuzl liuzl0802
+ * @Date: 2023-01-07 15:44:08
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-07 16:11:45
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\contract\components\Transfer.vue
+-->
+<template>
+  <el-dialog append-to-body title="选择转移负责人" :visible.sync="innerVisible" width="40%" @close="handleClose">
+    <el-form ref="userForm" :model="userForm" :rules="userRules">
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="负责人" prop="inchargeId">
+            <el-select
+              v-model="userForm.inchargeId"
+              placeholder="请选择负责人"
+              style="width: 100%"
+              @change="changeIncharge">
+              <el-option v-for="item in usersList" :key="item.id" :label="item.userName" :value="item.id" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <span slot="footer">
+      <el-button size="mini" type="primary" @click="save">确认</el-button>
+      <el-button size="mini" @click="handleClose">取消</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import contractApi from '@/api/contract'
+  export default {
+    props: {
+      contractId: {
+        type: Number,
+        default: 0,
+      },
+      usersList: {
+        type: Array,
+        default: () => [],
+      },
+    },
+    data() {
+      return {
+        userForm: {
+          inchargeId: null,
+          inchargeName: '',
+        },
+        innerVisible: false,
+        userRules: {
+          inchargeId: [{ required: true, trigger: 'change', message: '请选择负责人' }],
+        },
+      }
+    },
+    mounted() {},
+    methods: {
+      // 修改责任人
+      changeIncharge(selectedId) {
+        this.userForm.inchargeName = this.usersList.filter((item) => item.id == selectedId)[0]['userName']
+      },
+      open() {
+        this.innerVisible = true
+      },
+      async save() {
+        let params = { ...this.userForm, id: this.contractId }
+        console.log(params)
+        const [valid] = await to(this.$refs.userForm.validate())
+        if (valid == false) return
+        const [err, res] = await to(contractApi.transferContract(params))
+        if (err) return
+        if (res.code == 200) this.$message.success(res.msg)
+        else return
+        this.handleClose()
+      },
+      handleClose() {
+        this.innerVisible = false
+        this.$emit('transferSave')
+        this.userForm = {
+          inchargeId: null,
+          inchargeName: '',
+        }
+        this.$refs.userForm.resetFields()
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped></style>

+ 413 - 0
src/views/contract/detail.vue

@@ -0,0 +1,413 @@
+<!--
+ * @Author: liuzl liuzl0802
+ * @Date: 2023-01-05 16:29:01
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-09 14:43:21
+ * @Description: file content
+ * @FilePath: \订单全流程管理系统\src\views\contract\detail.vue
+-->
+<template>
+  <div class="details">
+    <el-row :gutter="10">
+      <el-col :span="16">
+        <div class="title">
+          <p>合同</p>
+          <h3>
+            {{ details.contractName }}
+          </h3>
+        </div>
+        <header>
+          <el-descriptions :colon="false" :column="6" direction="vertical">
+            <el-descriptions-item content-class-name="my-content" label="客户名称" label-class-name="my-label">
+              {{ details.custName }}
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="合同金额(元)" label-class-name="my-label">
+              {{ details.contractAmount }}
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="下单时间" label-class-name="my-label">
+              {{ details.createdTime }}
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="回款金额(元)" label-class-name="my-label">
+              {{ details.collectedAmount }}
+            </el-descriptions-item>
+            <el-descriptions-item content-class-name="my-content" label="负责人" label-class-name="my-label">
+              {{ details.inchargeName }}
+            </el-descriptions-item>
+          </el-descriptions>
+        </header>
+        <el-tabs v-model="activeName" @tab-click="handleClick">
+          <el-tab-pane label="详细信息" name="details">
+            <details-info :details="details" />
+          </el-tab-pane>
+          <el-tab-pane label="产品" name="product">
+            <details-product :product="product" />
+          </el-tab-pane>
+          <el-tab-pane label="回款" name="collection">
+            <details-collection v-if="activeName == 'collection'" :details="details" />
+          </el-tab-pane>
+          <el-tab-pane label="发票" name="invoice">发票</el-tab-pane>
+          <el-tab-pane label="附件" name="enclosure">附件</el-tab-pane>
+        </el-tabs>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+  import { mapGetters } from 'vuex'
+  import contractApi from '@/api/contract'
+  import DetailsInfo from './components/DetailsInfo'
+  import DetailsProduct from './components/DetailsProduct'
+  import DetailsCollection from './components/DetailsCollection'
+
+  export default {
+    name: 'ContractDetail',
+    components: { DetailsInfo, DetailsProduct, DetailsCollection },
+    data() {
+      return {
+        id: '',
+        details: {},
+        product: [],
+        abstract: {},
+        activeName: 'details',
+      }
+    },
+    computed: {
+      ...mapGetters({
+        avatar: 'user/avatar',
+        username: 'user/username',
+      }),
+    },
+    mounted() {
+      this.id = this.$route.query.id
+      this.init()
+      this.handleClick({ name: 'details' })
+    },
+    methods: {
+      init() {
+        Promise.all([contractApi.getDetails({ id: parseInt(this.id) })]).then(([details]) => {
+          if (details.data) {
+            let { product, ...data } = details.data
+            this.product = product
+            this.details = data
+          }
+        })
+      },
+      async handleClick() {},
+      back() {
+        this.$router.go(-1)
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  $base: '.details';
+  #{$base} {
+    height: calc(100vh - 60px - 50px - 12px * 2 - 40px);
+    display: flex;
+    padding: 20px 40px;
+
+    > .el-row {
+      flex: 1;
+
+      > .el-col {
+        height: 100%;
+      }
+    }
+
+    .title {
+      p,
+      h3 {
+        margin: 0;
+      }
+
+      p {
+        font-size: 14px;
+        font-weight: 400;
+        line-height: 22px;
+      }
+
+      h3 {
+        font-size: 24px;
+        font-weight: 500;
+        line-height: 36px;
+        color: #333;
+        display: flex;
+        justify-content: space-between;
+      }
+    }
+
+    header {
+      height: 74px;
+      background: rgba(196, 196, 196, 0.5);
+      border-radius: 4px;
+      display: flex;
+      align-items: center;
+      padding: 0 20px;
+      margin-top: 16px;
+
+      ::v-deep .el-descriptions__body {
+        background: transparent;
+      }
+
+      ::v-deep .my-label {
+        font-size: 14px;
+        font-weight: 600;
+        color: #1d66dc;
+      }
+
+      ::v-deep .my-content {
+        font-size: 14px;
+        font-weight: 600;
+        color: #333;
+      }
+    }
+
+    .el-tabs {
+      height: calc(100% - 148px);
+      display: flex;
+      flex-direction: column;
+
+      ::v-deep .el-tabs__content {
+        flex: 1;
+
+        .el-tab-pane {
+          height: 100%;
+        }
+      }
+    }
+
+    .buttons {
+      padding-top: 28px;
+      text-align: right;
+    }
+
+    .records {
+      margin: 0;
+      padding: 10px 20px;
+      list-style: none;
+      height: calc(100% - 60px);
+      overflow-y: auto;
+
+      > li {
+        display: flex;
+
+        & + li {
+          margin-top: 10px;
+        }
+      }
+
+      .date {
+        width: 100px;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+
+        h2,
+        h3 {
+          margin: 0;
+        }
+
+        h2 {
+          font-size: 26px;
+          line-height: 32px;
+        }
+      }
+
+      .content {
+        flex: 1;
+        list-style: none;
+
+        li {
+          display: flex;
+
+          & + li {
+            margin-top: 10px;
+          }
+        }
+
+        .user-avatar {
+          font-size: 40px;
+        }
+
+        .text {
+          flex: 1;
+          padding-left: 20px;
+
+          p {
+            font-weight: 500;
+            margin: 0;
+            line-height: 20px;
+
+            span {
+              color: #1d66dc;
+            }
+          }
+
+          p:nth-child(2) {
+            margin-bottom: 10px;
+          }
+
+          .action {
+            font-weight: bold;
+            color: #333;
+          }
+        }
+      }
+    }
+
+    .follow {
+      height: 100%;
+      padding: 10px 20px;
+      overflow: auto;
+
+      > li {
+        display: flex;
+
+        + li {
+          margin-top: 10px;
+        }
+      }
+
+      .date {
+        width: 100px;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+
+        h2,
+        h3 {
+          margin: 0;
+        }
+
+        h2 {
+          font-size: 26px;
+          line-height: 32px;
+        }
+      }
+
+      .content {
+        flex: 1;
+        list-style: none;
+
+        > li {
+          border: 1px solid rgb(215, 232, 244);
+          background: rgb(247, 251, 254);
+          border-radius: 4px;
+          padding: 8px;
+          overflow: hidden;
+
+          .text-container {
+            display: flex;
+          }
+
+          .comments {
+            padding-left: 60px;
+            margin-top: 10px;
+            max-height: 190px;
+            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;
+          }
+        }
+      }
+    }
+
+    .no-follow {
+      height: 100%;
+      width: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 12px;
+      color: rgba(0, 0, 0, 0.65);
+    }
+  }
+
+  .height-enter-active,
+  .height-leave-active {
+    transition: all 0.5s;
+  }
+
+  .height-enter-to,
+  .height-leave {
+    height: 190px;
+  }
+
+  .height-enter, .height-leave-to /* .fade-leave-active below version 2.1.8 */ {
+    height: 0;
+  }
+
+  ::v-deep .el-descriptions__table tbody {
+    td,
+    th {
+      width: 25%;
+    }
+  }
+</style>

+ 338 - 0
src/views/contract/manage.vue

@@ -0,0 +1,338 @@
+<!--
+ * @Author: liuzhenlin 461480418@qq.ocm
+ * @Date: 2023-01-05 11:28:51
+ * @LastEditors: liuzhenlin
+ * @LastEditTime: 2023-01-09 14:03:40
+ * @FilePath: \订单全流程管理系统\src\views\contract\manage.vue
+-->
+<template>
+  <div class="open-sea-container">
+    <vab-query-form>
+      <vab-query-form-top-panel>
+        <el-form :inline="true" label-width="0px" :model="queryForm" @submit.native.prevent>
+          <el-form-item prop="contractCode">
+            <el-input
+              v-model="queryForm.contractCode"
+              clearable
+              placeholder="合同编号"
+              @keyup.enter.native="queryData" />
+          </el-form-item>
+          <el-form-item prop="contractName">
+            <el-input
+              v-model="queryForm.contractName"
+              clearable
+              placeholder="合同名称"
+              @keyup.enter.native="queryData" />
+          </el-form-item>
+          <el-form-item prop="custId">
+            <el-input v-model="queryForm.custName" clearable placeholder="客户名称" @keyup.enter.native="queryData" />
+          </el-form-item>
+          <el-form-item prop="custId">
+            <el-input v-model="queryForm.nboName" clearable placeholder="项目名称" @keyup.enter.native="queryData" />
+          </el-form-item>
+          <el-form-item prop="custId">
+            <el-select v-model="queryForm.approStatus" clearable placeholder="审批状态">
+              <el-option v-for="item in approStatusOption" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button icon="el-icon-search" type="primary" @click="queryData">查询</el-button>
+          </el-form-item>
+        </el-form>
+      </vab-query-form-top-panel>
+      <vab-query-form-left-panel :span="12">
+        <el-button icon="el-icon-plus" size="mini" type="primary" @click="handleEdit()">新建</el-button>
+        <el-button icon="el-icon-delete" type="danger" @click="handleDelete()">删除</el-button>
+      </vab-query-form-left-panel>
+      <vab-query-form-right-panel :span="12">
+        <table-tool :check-list.sync="checkList" :columns="columns" />
+      </vab-query-form-right-panel>
+    </vab-query-form>
+    <el-table
+      v-loading="listLoading"
+      border
+      :data="list"
+      height="calc(100vh - 340px)"
+      @selection-change="setSelectRows">
+      <el-table-column align="center" show-overflow-tooltip type="selection" />
+      <el-table-column
+        v-for="(item, index) in finallyColumns"
+        :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 === 'custName'" class="link-button" type="text" @click="handleCustDetail(row)">
+            {{ row.custName }}
+          </el-button>
+          <el-button
+            v-else-if="item.prop === 'contractName'"
+            style="font-size: 14px"
+            type="text"
+            @click="handleContractDetail(row)">
+            {{ row.contractName }}
+          </el-button>
+          <span v-else>{{ row[item.prop] }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="操作">
+        <template slot-scope="scope">
+          <el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
+          <el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
+          <el-button type="text" @click="handleTransfer(scope.row)">转移</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      background
+      :current-page="queryForm.pageNum"
+      :layout="layout"
+      :page-size="queryForm.pageSize"
+      :total="total"
+      @current-change="handleCurrentChange"
+      @size-change="handleSizeChange" />
+    <!-- 新增编辑客户弹窗 -->
+    <Edit ref="edit" :users-list="usersList" @contractSave="contractSave" @createContact="createContact" />
+    <!-- 转移合同 -->
+    <Transfer
+      ref="transfer"
+      :contract-id="contractId"
+      :users-list="usersList"
+      @createContact="createContact"
+      @transferSave="contractSave" />
+  </div>
+</template>
+
+<script>
+  import to from 'await-to-js'
+  import contractApi from '@/api/contract'
+  import userApi from '@/api/system/user'
+  import Edit from './components/Edit'
+  import TableTool from '@/components/table/TableTool'
+  import Transfer from './components/Transfer'
+  export default {
+    name: 'OpenSea',
+    components: {
+      Edit,
+      TableTool,
+      Transfer,
+    },
+    data() {
+      return {
+        usersList: [], //负责人列表
+        approStatusOption: [
+          { id: 1, label: '审批中' },
+          { id: 2, label: '已拒绝' },
+          { id: 3, label: '已通过' },
+        ],
+        contractId: 0, //当前合同id
+        listLoading: false,
+        layout: 'total, sizes, prev, pager, next, jumper',
+        list: [],
+        total: 0,
+        queryForm: {
+          pageNum: 1,
+          pageSize: 10,
+          contractCode: '', // 合同编号
+          contractName: '', //合同名称
+          custName: '', // 客户名称  ()
+          nboName: '', //项目名称
+          approStatus: '', //审批状态
+        },
+        selectRows: [], //选择的表格数据
+        industryOptions: [], //客户行业
+        levelOptions: [], //客户级别
+        // 自定义列表
+        checkList: [],
+        columns: [
+          {
+            label: '合同编号',
+            width: '120px',
+            prop: 'contractCode',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '合同名称',
+            width: '120px',
+            prop: 'contractName',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '客户名称',
+            width: '120px',
+            prop: 'custName',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '项目名称',
+            width: 'auto',
+            prop: 'nboName',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '审批状态',
+            width: 'auto',
+            prop: 'approStatus',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '合同类型',
+            width: 'auto',
+            prop: 'contractType',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '合同金额',
+            width: 'auto',
+            prop: 'contractAmount',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '负责人',
+            width: 'auto',
+            prop: 'inchargeName',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '公司签约人',
+            width: '140px',
+            prop: 'signatoryName',
+            sortable: false,
+            disableCheck: false,
+          },
+          {
+            label: '经销商',
+            width: '140px',
+            prop: 'distributorName',
+            sortable: false,
+            disableCheck: false,
+          },
+        ],
+      }
+    },
+    computed: {
+      finallyColumns() {
+        return this.columns.filter((item) => this.checkList.includes(item.label))
+      },
+    },
+    mounted() {
+      this.queryData()
+      this.getOptions()
+    },
+    methods: {
+      getOptions() {
+        Promise.all([userApi.getList()])
+          .then(([user]) => {
+            this.usersList = user.data.list || []
+          })
+          .catch((err) => console.log(err))
+      },
+      async queryData() {
+        this.listLoading = true
+        const params = { ...this.queryForm }
+        const [err, res] = await to(contractApi.getList(params))
+        if (err) return (this.listLoading = false)
+        this.list = res.data.list || []
+        this.total = res.data.total
+        this.listLoading = false
+      },
+      reset() {
+        this.queryForm = {
+          pageNum: 1,
+          pageSize: 10,
+          custCode: '', // 客户编码
+          custName: '', //客户名称
+          custIndustry: '', // 客户行业  ()
+          custLevel: '', //客户级别
+        }
+        this.queryData()
+      },
+      handleSizeChange(val) {
+        this.queryForm.pageSize = val
+        this.queryData()
+      },
+      handleCurrentChange(val) {
+        this.queryForm.pageNum = val
+        this.queryData()
+      },
+      setSelectRows(val) {
+        this.selectRows = val.map((item) => item.id)
+      },
+      // 客户编辑
+      async handleEdit(row = null) {
+        row ? this.$refs.edit.init(row.id) : this.$refs.edit.init()
+      },
+      // 合同详情
+      handleContractDetail(row) {
+        this.$router.push({
+          path: '/contract/detail',
+          query: {
+            id: row.id,
+          },
+        })
+      },
+      // 客户详情
+      handleCustDetail(row) {
+        this.$router.push({
+          path: '/customer/detail',
+          query: {
+            id: row.custId,
+          },
+        })
+      },
+      // 转移合同
+      handleTransfer(row) {
+        this.contractId = row.id
+        this.$refs.transfer.open()
+      },
+      // 合同删除
+      handleDelete(row = null) {
+        let ids = row ? [row.id] : this.selectRows
+        if (!ids[0]) {
+          return
+        }
+        this.$confirm('确认删除?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        })
+          .then(async () => {
+            const [err, res] = await to(contractApi.delContract({ id: ids }))
+            if (err) return
+            if (res.code == 200) {
+              this.$message({
+                type: 'success',
+                message: '删除成功!',
+              })
+              this.queryData()
+            }
+          })
+          .catch(() => {})
+      },
+      // 联系人弹窗
+      createContact(res) {
+        this.$refs.contact.contactForm.custId = res.id
+        this.$refs.contact.contactForm.custName = res.name
+        this.$refs.contact.contactVisible = true
+      },
+      contractSave() {
+        this.queryData()
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  $base: '.open-sea';
+</style>