Browse Source

CRM-合同:完善合同表单

puhui999 1 year ago
parent
commit
ed94205fa7

+ 6 - 0
src/api/crm/business/index.ts

@@ -1,4 +1,5 @@
 import request from '@/config/axios'
+import { TransferReqVO } from '@/api/crm/customer'
 
 export interface BusinessVO {
   id: number
@@ -70,3 +71,8 @@ export const getBusinessPageByContact = async (params) => {
 export const getBusinessListByIds = async (val: number[]) => {
   return await request.get({ url: '/crm/business/list-by-ids', params: { ids: val.join(',') } })
 }
+
+// 商机转移
+export const transfer = async (data: TransferReqVO) => {
+  return await request.put({ url: '/crm/business/transfer', data })
+}

+ 2 - 0
src/api/crm/contract/index.ts

@@ -1,4 +1,5 @@
 import request from '@/config/axios'
+import { ProductExpandVO } from '@/api/crm/product'
 
 export interface ContractVO {
   id: number
@@ -20,6 +21,7 @@ export interface ContractVO {
   signUserId: number
   contactLastTime: Date
   remark: string
+  productItems: ProductExpandVO[]
 }
 
 // 查询 CRM 合同列表

+ 6 - 0
src/api/crm/product/index.ts

@@ -12,6 +12,12 @@ export interface ProductVO {
   ownerUserId: number
 }
 
+export interface ProductExpandVO extends ProductVO {
+  count: number
+  discountPercent: number
+  totalPrice: number
+}
+
 // 查询产品列表
 export const getProductPage = async (params) => {
   return await request.get({ url: `/crm/product/page`, params })

+ 2 - 1
src/components/Table/index.ts

@@ -1,6 +1,7 @@
 import Table from './src/Table.vue'
 import { ElTable } from 'element-plus'
 import { TableSetPropsType } from '@/types/table'
+import TableSelectForm from './src/TableSelectForm.vue'
 
 export interface TableExpose {
   setProps: (props: Recordable) => void
@@ -9,4 +10,4 @@ export interface TableExpose {
   elTableRef: ComponentRef<typeof ElTable>
 }
 
-export { Table }
+export { Table, TableSelectForm }

+ 90 - 0
src/components/Table/src/TableSelectForm.vue

@@ -0,0 +1,90 @@
+<template>
+  <Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%">
+    <el-table
+      ref="multipleTableRef"
+      v-loading="loading"
+      :data="list"
+      :show-overflow-tooltip="true"
+      :stripe="true"
+      @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" />
+      <slot></slot>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+
+defineOptions({ name: 'TableSelectForm' })
+withDefaults(
+  defineProps<{
+    modelValue: any[]
+    title: string
+  }>(),
+  { modelValue: () => [], title: '选择' }
+)
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const loading = ref(false) // 列表的加载中
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false)
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+// 确认选择时的触发事件
+const emits = defineEmits<{
+  (e: 'update:modelValue', v: number[]): void
+}>()
+const multipleTableRef = ref<InstanceType<typeof ElTable>>()
+const multipleSelection = ref<any[]>([])
+const handleSelectionChange = (val: any[]) => {
+  multipleSelection.value = val
+}
+/** 触发 */
+const submitForm = () => {
+  formLoading.value = true
+  try {
+    emits('update:modelValue', multipleSelection.value) // 返回选择的原始数据由使用方处理
+  } finally {
+    formLoading.value = false
+    // 关闭弹窗
+    dialogVisible.value = false
+  }
+}
+
+const getList = async (getListFunc: Function) => {
+  loading.value = true
+  try {
+    const data = await getListFunc(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 打开弹窗 */
+const open = async (getListFunc: Function) => {
+  dialogVisible.value = true
+  await nextTick()
+  if (multipleSelection.value.length > 0) {
+    multipleTableRef.value!.clearSelection()
+  }
+  await getList(getListFunc)
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

+ 156 - 113
src/views/crm/contract/ContractForm.vue

@@ -1,54 +1,108 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog v-model="dialogVisible" :title="dialogTitle" width="70%">
     <el-form
       ref="formRef"
+      v-loading="formLoading"
       :model="formData"
       :rules="formRules"
       label-width="100px"
-      v-loading="formLoading"
     >
       <el-row>
+        <el-col :span="24" class="mb-10px">
+          <CardTitle title="基本信息" />
+        </el-col>
         <el-col :span="12">
           <el-form-item label="合同名称" prop="name">
             <el-input v-model="formData.name" placeholder="请输入合同名称" />
           </el-form-item>
         </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同编号" prop="no">
+            <el-input v-model="formData.no" placeholder="请输入合同编号" />
+          </el-form-item>
+        </el-col>
         <el-col :span="12">
           <el-form-item label="客户" prop="customerId">
-            <el-input v-model="formData.customerId" placeholder="请选择对应客户" />
+            <el-select v-model="formData.customerId">
+              <el-option
+                v-for="item in customerList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="客户签约人" prop="contactId">
+            <el-select v-model="formData.contactId" :disabled="!formData.customerId">
+              <el-option
+                v-for="item in getContactOptions"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="公司签约人" prop="signUserId">
+            <el-select v-model="formData.signUserId">
+              <el-option
+                v-for="item in userList"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="负责人" prop="ownerUserId">
+            <el-select v-model="formData.ownerUserId">
+              <el-option
+                v-for="item in userList"
+                :key="item.id"
+                :label="item.nickname"
+                :value="item.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="商机名称" prop="businessId">
+            <el-select v-model="formData.businessId">
+              <el-option
+                v-for="item in businessList"
+                :key="item.id"
+                :label="item.name"
+                :value="item.id!"
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="合同金额(元)" prop="price">
+            <el-input v-model="formData.price" placeholder="请输入合同金额" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="下单日期" prop="orderDate">
+            <el-date-picker
+              v-model="formData.orderDate"
+              placeholder="选择下单日期"
+              type="date"
+              value-format="x"
+            />
           </el-form-item>
         </el-col>
-      </el-row>
-
-      <el-form-item label="商机名称" prop="businessId">
-        <el-input v-model="formData.businessId" placeholder="请选择对应商机" />
-      </el-form-item>
-      <el-form-item label="工作流" prop="processInstanceId">
-        <el-input v-model="formData.processInstanceId" placeholder="请选择工作流" />
-      </el-form-item>
-      <el-form-item label="下单日期" prop="orderDate">
-        <el-date-picker
-          v-model="formData.orderDate"
-          type="date"
-          value-format="x"
-          placeholder="选择下单日期"
-        />
-      </el-form-item>
-      <el-form-item label="负责人" prop="ownerUserId">
-        <el-input v-model="formData.ownerUserId" placeholder="请选择负责人" />
-      </el-form-item>
-      <el-form-item label="合同编号" prop="no">
-        <el-input v-model="formData.no" placeholder="请输入合同编号" />
-      </el-form-item>
-
-      <el-row>
         <el-col :span="12">
           <el-form-item label="开始时间" prop="startTime">
             <el-date-picker
               v-model="formData.startTime"
+              placeholder="选择开始时间"
               type="date"
               value-format="x"
-              placeholder="选择开始时间"
             />
           </el-form-item>
         </el-col>
@@ -56,72 +110,60 @@
           <el-form-item label="结束时间" prop="endTime">
             <el-date-picker
               v-model="formData.endTime"
+              placeholder="选择结束时间"
               type="date"
               value-format="x"
-              placeholder="选择结束时间"
             />
           </el-form-item>
         </el-col>
-      </el-row>
-
-      <el-row>
-        <el-col :span="8">
-          <el-form-item label="合同金额" prop="price">
-            <el-input v-model="formData.price" placeholder="请输入合同金额" />
+        <el-col :span="24">
+          <el-form-item label="备注" prop="remark">
+            <el-input
+              v-model="formData.remark"
+              :rows="3"
+              placeholder="请输入备注"
+              type="textarea"
+            />
           </el-form-item>
         </el-col>
-        <el-col :span="8">
-          <el-form-item label="整单折扣" prop="discountPercent">
-            <el-input v-model="formData.discountPercent" placeholder="请输入整单折扣" />
+        <el-col :span="24">
+          <el-form-item label="产品列表" prop="productList">
+            <ProductList v-model="formData.productItems" />
           </el-form-item>
         </el-col>
-        <el-col :span="8">
-          <el-form-item label="产品总金额" prop="productPrice">
-            <el-input v-model="formData.productPrice" placeholder="请输入产品总金额" />
+        <el-col :span="12">
+          <el-form-item label="整单折扣(%)" prop="discountPercent">
+            <el-input v-model="formData.discountPercent" placeholder="请输入整单折扣" />
           </el-form-item>
         </el-col>
-      </el-row>
-
-      <el-form-item label="只读权限的用户" prop="roUserIds">
-        <el-input v-model="formData.roUserIds" placeholder="请输入只读权限的用户" />
-      </el-form-item>
-      <el-form-item label="读写权限的用户" prop="rwUserIds">
-        <el-input v-model="formData.rwUserIds" placeholder="请输入读写权限的用户" />
-      </el-form-item>
-
-      <el-row>
         <el-col :span="12">
-          <el-form-item label="联系人编号" prop="contactId">
-            <el-input v-model="formData.contactId" placeholder="请输入联系人编号" />
+          <el-form-item label="产品总金额(元)" prop="productPrice">
+            <el-input v-model="formData.productPrice" placeholder="请输入产品总金额" />
           </el-form-item>
         </el-col>
+        <el-col :span="24">
+          <CardTitle class="mb-10px" title="审批信息" />
+        </el-col>
         <el-col :span="12">
-          <el-form-item label="公司签约人" prop="signUserId">
-            <el-input v-model="formData.signUserId" placeholder="请输入公司签约人" />
+          <el-form-item label="工作流" prop="processInstanceId">
+            <el-input v-model="formData.processInstanceId" placeholder="请选择工作流" />
           </el-form-item>
         </el-col>
       </el-row>
-
-      <el-form-item label="最后跟进时间" prop="contactLastTime">
-        <el-date-picker
-          v-model="formData.contactLastTime"
-          type="date"
-          value-format="x"
-          placeholder="选择最后跟进时间"
-        />
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入备注" />
-      </el-form-item>
     </el-form>
     <template #footer>
-      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
 </template>
-<script setup lang="ts">
+<script lang="ts" setup>
+import * as CustomerApi from '@/api/crm/customer'
 import * as ContractApi from '@/api/crm/contract'
+import * as UserApi from '@/api/system/user'
+import * as ContactApi from '@/api/crm/contact'
+import * as BusinessApi from '@/api/crm/business'
+import ProductList from './components/ProductList.vue'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -130,38 +172,34 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  name: undefined,
-  customerId: undefined,
-  businessId: undefined,
-  processInstanceId: undefined,
-  orderDate: undefined,
-  ownerUserId: undefined,
-  no: undefined,
-  startTime: undefined,
-  endTime: undefined,
-  price: undefined,
-  discountPercent: undefined,
-  productPrice: undefined,
-  roUserIds: undefined,
-  rwUserIds: undefined,
-  contactId: undefined,
-  signUserId: undefined,
-  contactLastTime: undefined,
-  remark: undefined
-})
+const formData = ref<ContractApi.ContractVO>({} as ContractApi.ContractVO)
 const formRules = reactive({
   name: [{ required: true, message: '合同名称不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-
+watch(
+  () => formData.value.productItems,
+  (val) => {
+    if (!val || val.length === 0) {
+      formData.value.productPrice = 0
+      return
+    }
+    // 使用reduce函数进行累加
+    formData.value.productPrice = val.reduce(
+      (accumulator, currentValue) =>
+        isNaN(accumulator + currentValue.totalPrice) ? 0 : accumulator + currentValue.totalPrice,
+      0
+    )
+  },
+  { deep: true }
+)
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
   dialogVisible.value = true
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
+  await getAllApi()
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
@@ -172,6 +210,9 @@ const open = async (type: string, id?: number) => {
     }
   }
 }
+const getAllApi = async () => {
+  await Promise.all([getCustomerList(), getUserList(), getContactListList(), getBusinessList()])
+}
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
@@ -199,30 +240,32 @@ const submitForm = async () => {
     formLoading.value = false
   }
 }
-
+const customerList = ref<CustomerApi.CustomerVO[]>([])
+/** 获取客户 */
+const getCustomerList = async () => {
+  customerList.value = await CustomerApi.getSimpleCustomerList()
+}
+const contactList = ref<ContactApi.ContactVO[]>([])
+/** 动态获取客户联系人 */
+const getContactOptions = computed(() =>
+  contactList.value.filter((item) => item.customerId === formData.value.customerId)
+)
+const getContactListList = async () => {
+  contactList.value = await ContactApi.getSimpleContactList()
+}
+const userList = ref<UserApi.UserVO[]>([])
+/** 获取用户列表 */
+const getUserList = async () => {
+  userList.value = await UserApi.getSimpleUserList()
+}
+const businessList = ref<BusinessApi.BusinessVO[]>([])
+/** 获取商机 */
+const getBusinessList = async () => {
+  businessList.value = await BusinessApi.getSimpleBusinessList()
+}
 /** 重置表单 */
 const resetForm = () => {
-  formData.value = {
-    id: undefined,
-    name: undefined,
-    customerId: undefined,
-    businessId: undefined,
-    processInstanceId: undefined,
-    orderDate: undefined,
-    ownerUserId: undefined,
-    no: undefined,
-    startTime: undefined,
-    endTime: undefined,
-    price: undefined,
-    discountPercent: undefined,
-    productPrice: undefined,
-    roUserIds: undefined,
-    rwUserIds: undefined,
-    contactId: undefined,
-    signUserId: undefined,
-    contactLastTime: undefined,
-    remark: undefined
-  }
+  formData.value = {} as ContractApi.ContractVO
   formRef.value?.resetFields()
 }
 </script>

+ 112 - 0
src/views/crm/contract/components/ProductList.vue

@@ -0,0 +1,112 @@
+<template>
+  <el-row justify="end">
+    <el-button plain type="primary" @click="openForm">添加产品</el-button>
+  </el-row>
+  <el-table :data="list" :show-overflow-tooltip="true" :stripe="true">
+    <el-table-column align="center" label="产品名称" prop="name" width="160" />
+    <el-table-column align="center" label="产品类型" prop="categoryName" width="160" />
+    <el-table-column align="center" label="产品单位" prop="unit">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="产品编码" prop="no" />
+    <el-table-column
+      :formatter="fenToYuanFormat"
+      align="center"
+      label="价格(元)"
+      prop="price"
+      width="100"
+    />
+    <el-table-column align="center" label="数量" prop="count" width="200">
+      <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
+        <el-input-number v-model="row.count" class="!w-100%" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="折扣(%)" prop="discountPercent" width="200">
+      <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
+        <el-input-number v-model="row.discountPercent" class="!w-100%" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="合计" prop="totalPrice" width="100">
+      <template #default="{ row }: { row: ProductApi.ProductExpandVO }">
+        {{ getTotalPrice(row) }}
+      </template>
+    </el-table-column>
+    <el-table-column align="center" fixed="right" label="操作" width="130">
+      <template #default="scope">
+        <el-button link type="danger" @click="handleDelete(scope.row.id)"> 移除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+
+  <!-- table 选择表单 -->
+  <TableSelectForm ref="tableSelectFormRef" v-model="multipleSelection" title="选择商品">
+    <el-table-column align="center" label="产品名称" prop="name" width="160" />
+    <el-table-column align="center" label="产品类型" prop="categoryName" width="160" />
+    <el-table-column align="center" label="产品单位" prop="unit">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
+      </template>
+    </el-table-column>
+    <el-table-column align="center" label="产品编码" prop="no" />
+    <el-table-column
+      :formatter="fenToYuanFormat"
+      align="center"
+      label="价格(元)"
+      prop="price"
+      width="100"
+    />
+  </TableSelectForm>
+</template>
+
+<script lang="ts" setup>
+import * as ProductApi from '@/api/crm/product'
+import { DICT_TYPE } from '@/utils/dict'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { TableSelectForm } from '@/components/Table/index'
+
+defineOptions({ name: 'ProductList' })
+withDefaults(defineProps<{ modelValue: any[] }>(), { modelValue: () => [] })
+const emits = defineEmits<{
+  (e: 'update:modelValue', v: any[]): void
+}>()
+const list = ref<ProductApi.ProductExpandVO[]>([])
+const handleDelete = (id: number) => {
+  const index = list.value.findIndex((item) => item.id === id)
+  if (index !== -1) {
+    list.value.splice(index, 1)
+  }
+}
+const tableSelectFormRef = ref<InstanceType<typeof TableSelectForm>>()
+const multipleSelection = ref<ProductApi.ProductExpandVO[]>([])
+const openForm = () => {
+  tableSelectFormRef.value?.open(ProductApi.getProductPage)
+}
+const getTotalPrice = computed(() => (row: ProductApi.ProductExpandVO) => {
+  const totalPrice = (row.price * row.count * row.discountPercent) / 100
+  row.totalPrice = isNaN(totalPrice) ? 0 : totalPrice
+  return isNaN(totalPrice) ? 0 : totalPrice
+})
+watch(
+  list,
+  (val) => {
+    if (!val || val.length === 0) {
+      return
+    }
+    emits('update:modelValue', list.value)
+  },
+  { deep: true }
+)
+watch(
+  multipleSelection,
+  (val) => {
+    if (!val || val.length === 0) {
+      return
+    }
+    const ids = list.value.map((item) => item.id)
+    list.value.push(...multipleSelection.value.filter((item) => ids.indexOf(item.id) === -1))
+  },
+  { deep: true }
+)
+</script>