Browse Source

Merge branch 'master' of http://47.100.37.243:10191/wutt/manHourHousekeeper

Min 11 tháng trước cách đây
mục cha
commit
a8ee8ed66e
15 tập tin đã thay đổi với 571 bổ sung51 xóa
  1. 3 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/api.ts
  2. 1 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/attachment.vue
  3. 1 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/information.vue
  4. 8 2
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/operationRecord.vue
  5. 43 14
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/products.vue
  6. 1 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/stageSetting.vue
  7. 114 17
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/detail/index.vue
  8. 18 12
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/index.vue
  9. 144 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/component/attachment.vue
  10. 146 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/component/information.vue
  11. 53 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/detail/index.vue
  12. 14 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/times.ts
  13. 11 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/BusinessOpportunityController.java
  14. 2 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/BusinessOpportunityService.java
  15. 12 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/BusinessOpportunityServiceImpl.java

+ 3 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/api.ts

@@ -21,6 +21,9 @@ export const UPLOADFILEFILE = `/business-opportunity/uploadFile`
 export const URL_IMPOERBUSINESS = `/business-opportunity/importData`
 export const URL_DETELESTAGE = `/business-opportunity/deleteStage`
 export const URL_SAVECONTACT = `/business-opportunity/saveContactsId`
+export const URL_STAGEIDNEXT = `/business-opportunity/saveStageId`
+export const URL_SAVEREASON = `/business-opportunity/saveReason`
+export const URL_EXPORTBUSINESS = `/business-opportunity/exportData`
 
 
 export const stageStatus = [

+ 1 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/attachment.vue

@@ -121,7 +121,7 @@ async function httpUploadFile(param: UploadRequestOptions) {
 
 watchEffect(() => {
     information.value = props.information
-    attachmenttable.value = props.information.uploadFilePList
+    attachmenttable.value = (props.information.uploadFilePList || [])
 });
 
 // 生命周期钩子

+ 1 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/information.vue

@@ -3,7 +3,7 @@
         <div class="flex justify-between">
             <div class="title">基本信息</div>
             <div>
-                <el-button type="primary" @click="associateContact()" v-if="!information.cuntactsId">关联联系人</el-button>
+                <el-button type="primary" @click="associateContact()" v-if="!information.contactsId">关联联系人</el-button>
                 <el-button type="primary" @click="claimBusiness()" v-if="!information.customerId">认领</el-button>
                 <el-button type="primary" @click="showVisible('transferBusinessVisible')"
                     v-if="information.customerId">转移</el-button>

+ 8 - 2
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/operationRecord.vue

@@ -5,7 +5,7 @@
         </div>
         <div class="flex-1 overflow-auto pt-5">
             <el-table :data="operationRecordtable" border style="width: 100%;height: 278px;">
-                <el-table-column prop="creatTime" label="操作时间" width="140" />
+                <el-table-column prop="newCreatTime" label="操作时间" width="160" />
                 <el-table-column prop="userName" label="操作人" width="120" />
                 <el-table-column prop="name" label="操作内容" />
             </el-table>
@@ -13,6 +13,7 @@
     </div>
 </template>
 <script lang="ts" setup>
+import { formatDateMinutes } from '@/utils/times';
 import { ref, reactive, onMounted, onUnmounted, defineExpose, inject, watchEffect } from 'vue'
 
 const props = defineProps<{
@@ -24,7 +25,12 @@ const information = ref<any>({})
 
 watchEffect(() => {
     information.value = props.information
-    operationRecordtable.value = props.information.actionLogList
+    operationRecordtable.value = (props.information.actionLogList || []).map((item: any) => {
+        return {
+            ...item,
+            newCreatTime: item.creatTime ? formatDateMinutes(new Date(item.creatTime)) : ''
+        }
+    })
 });
 
 // 生命周期钩子

+ 43 - 14
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/products.vue

@@ -3,7 +3,7 @@
         <div class="flex justify-between">
             <div class="title">相关产品</div>
             <div>
-                <el-button type="primary" @click="showVisible('editProductVisible')">编辑产品</el-button>
+                <el-button type="primary" @click="editProductShow()">编辑产品</el-button>
             </div>
         </div>
         <div class="flex-1 overflow-auto pt-3">
@@ -13,22 +13,21 @@
                         {{ scope.$index + 1 }}
                     </template>
                 </el-table-column>
-                <el-table-column prop="taskName" label="产品名称">
+                <el-table-column prop="productName" label="产品名称">
                     <template #default="scope">
                         <el-button link type="primary" size="large">{{
-                            scope.row.taskName
+                            scope.row.productName
                         }}</el-button>
                     </template>
                 </el-table-column>
-                <el-table-column prop="priority" label="产品类别" width="130" />
-                <el-table-column prop="status" label="产品类型" width="130" />
-                <el-table-column prop="executor" label="单位" width="130" />
-                <el-table-column prop="startTime" label="标准价格" width="130" />
-                <el-table-column prop="endTime" label="库存" width="130" />
-                <el-table-column prop="endTime" label="售价" width="130" />
-                <el-table-column prop="endTime" label="数量" width="130" />
-                <el-table-column prop="endTime" label="折扣(%)" width="130" />
-                <el-table-column prop="endTime" label="合计" width="130" />
+                <el-table-column prop="typeName" label="产品类型" width="130" />
+                <el-table-column prop="unitName" label="单位" width="130" />
+                <el-table-column prop="price" label="标准价格" width="130" />
+                <el-table-column prop="inventory" label="库存" width="130" />
+                <el-table-column prop="sellingPrice" label="售价" width="130" />
+                <el-table-column prop="quantity" label="数量" width="130" />
+                <el-table-column prop="discount" label="折扣(%)" width="130" />
+                <el-table-column prop="totalPrice" label="合计" width="130" />
             </el-table>
         </div>
 
@@ -45,7 +44,8 @@
                 </div>
             </template>
             <div class="h-[60vh] overflow-y-auto scroll-bar pt-3">
-                <RelatedProducts ref="relatedProductsRef" :productTableList="productTableList" :height="'420px'" />
+                <RelatedProducts ref="relatedProductsRef" :productTableList="productTableList"
+                    :productTableListValue="productTableListValue" :height="'420px'" />
             </div>
         </el-dialog>
     </div>
@@ -56,14 +56,18 @@ import { GETTABLELIST } from '@/pages/product/api';
 import { post } from '@/utils/request';
 
 import RelatedProducts from '@/components/relatedProducts/relatedProducts.vue'
+import { UPDATEINSET } from '../api';
+import { all } from 'axios';
 
+const emits = defineEmits(['refreshData']);
 const props = defineProps<{
     information: any
 }>()
-
+const globalPopup = inject<GlobalPopup>('globalPopup')
 const information = ref<any>({})
 const relatedTaskstable = ref([])
 const productTableList = ref<any>([])
+const productTableListValue = ref<any>([])
 const relatedProductsRef = ref<typeof RelatedProducts>()
 const allVisible = reactive({
     editProductVisible: false
@@ -74,9 +78,33 @@ const allLoading = reactive({
 
 function editProduct() {
     let productTableListData = relatedProductsRef?.value?.returnData()
+    productTableListData.forEach((item: any) => {
+        delete item.id
+    })
     const { id, name, customerId, contactsId, amountOfMoney, expectedTransactionDate, stageId, inchargerId, remark } = information.value
     const formData = { id, name, customerId, contactsId, amountOfMoney, expectedTransactionDate, stageId, inchargerId, remark }
     console.log(productTableListData, '<===== 将要提交的数据', formData)
+    allLoading.editProductLoading = true
+    post(UPDATEINSET, { ...formData }).then((_res) => {
+        allVisible.editProductVisible = false
+        globalPopup?.showSuccess('操作成功')
+        emits('refreshData')
+    }).finally(() => {
+        allLoading.editProductLoading = false
+    })
+}
+
+function editProductShow() {
+    const productList = information.value.businessItemProducts || []
+    const list = productList.map((item: any) => {
+        const { id, productName, productId, productCode, unit, unitName, typeName, type, price, inventory, orderProductDetail, num, discount, sealPrice, totalPrice, quantity } = item
+        return {
+            id, productId: productId, productName, productCode, unit, unitName, typeName, type, price, inventory,
+            num, discount, sealPrice, totalPrice, quantity
+        }
+    })
+    productTableListValue.value = list
+    showVisible('editProductVisible')
 }
 
 function showVisible(type: keyof typeof allVisible) {
@@ -113,6 +141,7 @@ function getProductTableList() {
 
 watchEffect(() => {
     information.value = props.information
+    relatedTaskstable.value = props.information?.businessItemProducts || []
 });
 
 onMounted(() => {

+ 1 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/stageSetting.vue

@@ -28,7 +28,7 @@
                         </el-table-column>
                         <el-table-column label="操作" fixed="right" width="200">
                             <template #default="scope">
-                                <el-button link type="primary" size="large" @click="addStage(scope.row)">编辑</el-button>
+                                <el-button link type="primary" size="large" @click="addStage(scope.row)" :disabled="scope.row.isFinish == 1">编辑</el-button>
                                 <el-button link type="danger" size="large" @click="deteStage(+scope.$index, scope.row)" :disabled="scope.row.isFinish == 1">删除</el-button>
                                 <el-button link type="primary" size="large" @click="moveStage(+scope.$index, 'up')"
                                     v-if="scope.$index != 0">上移</el-button>

+ 114 - 17
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/detail/index.vue

@@ -7,25 +7,41 @@
         </el-link>
       </div>
       <div class="mr-8">
-        <el-select v-model="optionVal" placeholder="请选择" style="width: 150px" filterable @change="getDetail()">
+        <el-select v-model="optionVal" placeholder="请选择" style="width: 150px" filterable @change="selectChange()">
           <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
         </el-select>
       </div>
       <div class="flex-1 flex h-full justify-end overflow-auto scroll-bar-hide cursor-pointer" @wheel="handleScroll">
-        <div :class="`${index === 0 ? 'startStep' : 'nextStep'} relative rounded-md flex items-center backGray pl-6 pr-6`"
+        <div
+          :class="`${index === 0 ? 'startStep' : 'nextStep'} ${(currentStage >= index || businessInfo.stageValue == '赢单') ? 'selected' : (currentStage >= index || businessInfo.stageValue == '输单') ? 'backOrange' : 'backGray'} relative rounded-md flex items-center pl-6 pr-6`"
           v-for="(item, index) in stageList" :key="index">
           <div class="pr-3 text-nowrap">{{ item.name }}</div>
           <div class="text-nowrap">{{ item.plan }}</div>
         </div>
       </div>
-      <div class="relative rounded-md flex items-center itemPing backGray endStep item pl-6 pr-6 mr-4 resetStyle">
+      <div class="relative rounded-md flex items-center itemPing backGray endStep item pl-6 pr-6 mr-4 resetStyle" v-if="currentStage >= 0">
         <el-select v-model="stageStatusVal" placeholder="结束" style="width: 100px" class="selectClas"
           @change="advanceChange()">
           <el-option v-for="(item, index) in stageListOption" :key="index" :label="item.label" :value="item.value" />
         </el-select>
       </div>
-      <div class="bg-[#075985] rounded-md text itemPing pl-2 pr-2 flex items-center aloneText" @click="advancementStage()">
-        <el-link :underline="false">推进至阶段【验证客户】</el-link>
+      <div class="relative rounded-md flex items-center justify-around backOrange endStep item pl-6 pr-6 mr-4 resetStyle" v-if="businessInfo.stageValue == '输单'" style="padding-top: 6px;padding-bottom: 6px;">
+        <div>{{ businessInfo.stageValue }}</div>
+        <div class="ml-5">0%</div>
+      </div>
+      <div class="relative rounded-md flex items-center justify-around backWan endStep item pl-6 pr-6 mr-4 resetStyle" v-if="businessInfo.stageValue == '赢单'" style="padding-top: 6px;padding-bottom: 6px;">
+        <div>{{ businessInfo.stageValue }}</div>
+        <div class="ml-4"> 100%</div>
+      </div>
+      <div class="relative rounded-md flex items-center justify-around backGray endStep item pl-6 pr-6 mr-4 resetStyle" v-if="businessInfo.stageValue == '无效'" style="padding-top: 6px;padding-bottom: 6px;">
+        <div>{{ businessInfo.stageValue }}</div>
+        <div class="ml-5">0%</div>
+      </div>
+      <div class="bg-[#075985] rounded-md text itemPing pl-2 pr-2 flex items-center aloneText" @click="advancementStage()"
+        v-if="currentStage != -1">
+        <el-link :underline="false" v-if="(currentStage + 1) != stageList.length">推进至下个阶段【{{ stageList[currentStage +
+          1]?.name }}】</el-link>
+        <el-link :underline="false" v-else>赢单</el-link>
       </div>
     </div>
     <!-- 内容 -->
@@ -82,7 +98,7 @@ import type { FormInstance, FormRules } from 'element-plus'
 import { Edit, ArrowLeft as IconView } from '@element-plus/icons-vue'
 import { backPath } from '@/utils/tools'
 import { useRoute } from "vue-router";
-import { BUSIESS_ALL, BUSIESS_GETSATE, BUSIESS_INFO } from '../api'
+import { BUSIESS_ALL, BUSIESS_GETSATE, BUSIESS_INFO, URL_SAVEREASON, URL_STAGEIDNEXT } from '../api'
 
 import Information from '../component/information.vue'
 import Attachment from '../component/attachment.vue'
@@ -94,7 +110,8 @@ import { post } from "@/utils/request";
 
 type stageListType = {
   name: string,
-  plan: string
+  plan: string,
+  id?: number,
 }
 
 const route = useRoute()
@@ -105,6 +122,7 @@ const stageStatusVal = ref('')
 const stageStatusValOriginally = ref('')
 const advanceVal = ref('')
 const options = ref<optionType[]>([])
+const currentStage = ref<any>('') // 当前阶段的下标
 const allLoading = reactive({
   skeletonLoading: false,
   advanceSaveLoading: false
@@ -116,7 +134,7 @@ const allText = reactive({
   advanceText: ''
 })
 
-const businessInfo = ref({})
+const businessInfo = ref<any>({})
 const stageListOption = ref<optionType[]>([])
 const stageList = ref<stageListType[]>([])
 
@@ -139,8 +157,20 @@ function advanceChange() {
     allVisible.advanceVisible = true
     return
   }
-
-  advanceSave(false)
+  console.log(item, '<=====')
+  allLoading.skeletonLoading = true
+  post(URL_STAGEIDNEXT, {
+    id: businessInfo.value.id,
+    stageId: item.value,
+    stageValue: item.label,
+  }).then((_res) => {
+    globalPopup?.showSuccess('操作成功')
+    selectChange()
+  }).finally(() => {
+    setTimeout(() => {
+      allLoading.skeletonLoading = false
+    }, 500)
+  })
 }
 
 function advanceClose() {
@@ -149,16 +179,57 @@ function advanceClose() {
 }
 
 function advanceSave(flag: boolean) {
-  if(!advanceVal && flag) {
+  if (!advanceVal.value && flag) {
     globalPopup?.showError(`请输入${allText.advanceText}原因`)
     return
   }
-  allLoading.advanceSaveLoading = true
-  post('接口', {}).then(() => {
+  console.log(flag)
+  if (flag) {
+    allLoading.advanceSaveLoading = true
+    advancementStageNext(true)
+    return
+  }
+}
+
+function loseOrder() {
+  post(URL_SAVEREASON, {
+    id: businessInfo.value.id,
+    reason: advanceVal.value
+  }).then((_res) => {
     globalPopup?.showSuccess('操作成功')
-    getDetail()
+    selectChange()
   }).finally(() => {
     allLoading.advanceSaveLoading = false
+    allVisible.advanceVisible = false
+  })
+}
+
+function advancementStageNext(flag: boolean = false) {
+  let fromVal = {}
+  if (!flag) {
+    fromVal = {
+      id: businessInfo.value.id,
+      stageId: stageList.value[currentStage.value + 1].id,
+      stageValue: stageList.value[currentStage.value + 1].name,
+    }
+  } else {
+    const item: any = stageListOption.value.find((item) => item.value === stageStatusVal.value)
+    fromVal = {
+      id: businessInfo.value.id,
+      stageId: item.value,
+      stageValue: item.label,
+    }
+  }
+  post(URL_STAGEIDNEXT, { ...fromVal }).then((_res) => {
+    if (!flag) {
+      globalPopup?.showSuccess('操作成功')
+      selectChange()
+    } else {
+      loseOrder()
+    }
+  }).catch(() => {
+    allLoading.advanceSaveLoading = false
+    allVisible.advanceVisible = false
   })
 }
 
@@ -167,6 +238,7 @@ function getDetail() {
   post(BUSIESS_INFO, { id: optionVal.value }).then(({ data }) => {
     businessInfo.value = (data || []);
     detailCompinentsData.value = data.taskList || []
+    setStageIndex()
   }).finally(() => {
     allLoading.skeletonLoading = false
   })
@@ -192,22 +264,39 @@ function getOptionAll() {
 
 function getSatge() {
   post(BUSIESS_GETSATE, {}).then(({ data }) => {
-    stageList.value = (data || []).sort((a: any, b: any) => { return a.seq - b.seq; }).filter((item: any) => !item.isFinish).map((item: any) => ({ name: item.name, plan: item.plan }))
+    stageList.value = (data || []).sort((a: any, b: any) => { return a.seq - b.seq; }).filter((item: any) => !item.isFinish).map((item: any) => ({ name: item.name, plan: item.plan, id: item.id }))
     stageListOption.value = (data || []).sort((a: any, b: any) => { return a.seq - b.seq; }).filter((item: any) => item.isFinish).map((item: any) => ({ label: item.name, value: item.id, plan: item.plan }))
+    setStageIndex()
   })
 }
 
+function selectChange() {
+  getDetail()
+}
+
+function setStageIndex() {
+  currentStage.value = stageList.value.findIndex((item: any) => item.id == businessInfo.value.stageId)
+}
+
 onMounted(() => {
   const { id } = route.query
   optionVal.value = id
-  getSatge()
-  getOptionAll()
   getDetail()
+  getOptionAll()
+  getSatge()
+  setTimeout(() => {
+    setStageIndex()
+  }, 500)
 })
 </script>
   
 <style lang="scss" scoped>
 .businessDetail {
+  .selected {
+    background-color: #075985 !important;
+    color: #fff !important;
+  }
+
   .icon {
     .el-link {
       color: #0052CC;
@@ -230,6 +319,14 @@ onMounted(() => {
     background-color: #F4F5F7;
     color: #000;
   }
+  .backOrange {
+    background-color: #FF5531;
+    color: #fff;
+  }
+  .backWan {
+    background-color: #075985;
+    color: #fff;
+  }
 
   .startStep {
     clip-path: polygon(0% 0%,

+ 18 - 12
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/index.vue

@@ -170,7 +170,7 @@
 import { ref, reactive, onMounted, inject } from "vue";
 import type { ElTable, FormInstance, FormRules, UploadRequestOptions } from 'element-plus'
 import { useRouter, useRoute } from "vue-router";
-import { GETSYSFILED, MOD, GETPERSONNEL, GETGENERATEFOEM, GETBUSINESSLIST, UPDATEINSET, BUSINESSDETELE, BATCHTRANSFER, MODURL, tableColumn, BUSIESS_GETSATE, URL_IMPOERBUSINESS } from './api'
+import { GETSYSFILED, MOD, GETPERSONNEL, GETGENERATEFOEM, GETBUSINESSLIST, UPDATEINSET, BUSINESSDETELE, BATCHTRANSFER, MODURL, tableColumn, BUSIESS_GETSATE, URL_IMPOERBUSINESS, BUSIESS_INFO, URL_EXPORTBUSINESS } from './api'
 import { GETTABLELIST } from '@/pages/product/api'
 import { post, get, uploadFile } from "@/utils/request";
 import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, createTaskFromType, formatDate, confirmAction, downloadTemplate, downloadFile } from '@/utils/tools'
@@ -246,14 +246,17 @@ const productTableListValue = ref([])
 
 function editBusiness(visibles: boolean) {
   businessTemplateRef.value?.getData().then((res: any) => {
-    let productTableListData = relatedProductsRef?.value?.returnData()
+    let productTableListData = relatedProductsRef?.value?.returnData() || []
+    productTableListData.forEach((item: any) => {
+      delete item.id
+    })
     let newForm = {
       ...res,
       expectedTransactionDate: res.expectedTransactionDate ? formatDateTime(new Date(res.expectedTransactionDate)) : '',
       businessItemProductList: productTableListData ? JSON.stringify(productTableListData) : []
     }
     allLoading.businessSaveLading = true
-    post(UPDATEINSET, { ...newForm }).then((_res) => {
+    post(UPDATEINSET, { ...businessTemplateValue.value, ...newForm }).then((_res) => {
       allVisible.newBusinessisible = visibles
       globalPopup?.showSuccess('保存成功')
       getBusinessTableList()
@@ -361,7 +364,7 @@ async function importBusiness(param: UploadRequestOptions) {
 function exportBusinessTableList() {
   allLoading.exoprtLoading = true
   let valueForm = getFromValue(businessOpportunityForm)
-  post('接口名称', { ...valueForm }).then((res) => {
+  post(URL_EXPORTBUSINESS, { ...valueForm }).then((res) => {
     downloadFile(res.data, '商机表导出.xlsx')
   }).finally(() => {
     allLoading.exoprtLoading = false
@@ -378,14 +381,16 @@ function changeBatch(flag: boolean = true) {
 }
 
 function editProduct(row: any) {
-  const list = row.businessItemProductList.map((item: any) => {
-    const { id, productName, productCode, unit, unitName, typeName, type, price, inventory, orderProductDetail, num, discount, sealPrice, totalPrice } = item
-    return {
-      id, productId: id, productName, productCode, unit, unitName, typeName, type, price, inventory,
-      num, discount, sealPrice, totalPrice 
-    }
+  post(BUSIESS_INFO, { id: row.id }).then(({ data }) => {
+    const list = (data.businessItemProducts || []).map((item: any) => {
+      const { id, productName, productId, productCode, unit, unitName, typeName, type, price, inventory, orderProductDetail, num, discount, sealPrice, totalPrice, quantity } = item
+      return {
+        id, productId: productId, productName, productCode, unit, unitName, typeName, type, price, inventory,
+        num, discount, sealPrice, totalPrice, quantity
+      }
+    })
+    productTableListValue.value = list
   })
-  productTableListValue.value = list
 }
 
 function showVisible(type: keyof typeof allVisible) { // 显示弹窗
@@ -510,4 +515,5 @@ onMounted(() => {
     font-size: 18px;
     line-height: 24px;
   }
-}</style>
+}
+</style>

+ 144 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/component/attachment.vue

@@ -0,0 +1,144 @@
+<template>
+    <div class="attachment pl-4 pr-4 pt-3 pb-3 h-full flex flex-col">
+        <div class="flex justify-between">
+            <div class="title">附件</div>
+            <div>
+                <el-upload ref="uploadRef" :http-request="httpUploadFile" :limit="1" :show-file-list="false"
+                    element-loading-text="正在上传" :loading="allLoading.uploadFileLoading">
+                    <template #trigger>
+                        <el-button type="primary">上传</el-button>
+                    </template>
+                </el-upload>
+            </div>
+        </div>
+        <div class="flex-1 overflow-auto pt-3">
+            <el-table :data="attachmenttable" border style="width: 100%;height: 200px;">
+                <el-table-column prop="documentName" label="附件名称" width="180" />
+                <el-table-column prop="size" label="附件大小" width="120" />
+                <el-table-column prop="creatorName" label="上传人" width="120" />
+                <el-table-column prop="indate" label="上传时间" width="180" />
+                <el-table-column label="操作" width="180" fixed="right">
+                    <template #default="scope">
+                        <el-button link type="primary" size="large" @click="fileDownload(scope.row)">下载</el-button>
+                        <el-button link type="primary" size="large" @click="operation(scope.row)">重命名</el-button>
+                        <el-button link type="danger" size="large" @click="fileDetele(scope.row)">删除</el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+
+        <!-- 弹窗 -->
+        <el-dialog v-model="allVisible.renameDialogVisible" width="800" :show-close="false" top="10vh">
+            <template #header="{ close, titleId, titleClass }">
+                <div class="flex justify-between items-center border-b pb-3 dialog-header">
+                    <h4 :id="titleId">{{ '文件重命名' }}</h4>
+                    <div>
+                        <el-button type="primary" @click="saveEditClue()" :loading="allLoading.saveLoading">保存</el-button>
+                        <el-button @click="allVisible.renameDialogVisible = false">取消</el-button>
+                    </div>
+                </div>
+            </template>
+            <div class="pt-3">
+                <el-input v-model.trim="fileFormVal.name" style="width: 100%" class="pb-3" clearable />
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script lang="ts" setup>
+import { post, uploadFile } from '@/utils/request';
+import { UploadRequestOptions } from 'element-plus';
+import { ref, reactive, onMounted, onUnmounted, defineExpose, inject, watchEffect } from 'vue'
+import { URLFILEDETELE, URL_REFNAME, URL_UPLOADFILE } from '@/pages/api';
+import { downloadFile } from '@/utils/tools';
+
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const emits = defineEmits(['refreshData']);
+const props = defineProps<{
+    data?: any
+}>()
+
+type fileFormVal = {
+    id?: string,
+    name?: string
+}
+
+const uploadRef = ref<any>()
+const information = ref<any>({})
+const attachmenttable = ref([])
+const fileTypeStr = ref('') // 文件重命名的类型
+const fileFormVal = ref<fileFormVal>({})
+const allLoading = reactive({
+    uploadFileLoading: false,
+    saveLoading: false
+})
+const allVisible = reactive({
+    renameDialogVisible: false
+})
+
+function saveEditClue() {
+    if(!fileFormVal.value.name) {
+        globalPopup?.showWarning('请输入文件名称')
+        return
+    }
+    allLoading.saveLoading = true
+    post(URL_REFNAME, {
+        fileId: fileFormVal.value.id,
+        newName: fileFormVal.value.name + '.' + fileTypeStr.value
+    }).then(() => {
+        allVisible.renameDialogVisible = false
+        globalPopup?.showSuccess('重命名成功')
+        emits('refreshData');
+    }).finally(() => {
+        allLoading.saveLoading = false
+    })
+}
+
+function operation(item: any) {
+    fileTypeStr.value = item.documentName.split('.').pop()
+    fileFormVal.value = {
+        id: item.id,
+        name: item.documentName.replace(/\.[^/.]+$/, '')
+    }
+    allVisible.renameDialogVisible = true
+}
+
+function fileDownload(item: any) {
+    downloadFile(`${item.url}`, item.documentName)
+}
+
+function fileDetele(item: any) {
+    post(URLFILEDETELE, { fileIds: item.id }).then(() => {
+        globalPopup?.showSuccess('删除成功')
+        emits('refreshData');
+    })
+}
+
+// 上传附件
+async function httpUploadFile(param: UploadRequestOptions) {
+    const id = information.value.id
+    const formData = new FormData();
+    formData.append('file', param.file)
+    formData.append('contactsId', id)
+    allLoading.uploadFileLoading = true
+    const res = await uploadFile(URL_UPLOADFILE, formData).finally(() => {
+        allLoading.uploadFileLoading = false
+        uploadRef.value.clearFiles()
+    })
+    if (res.code == 'ok') {
+        globalPopup?.showSuccess(res.msg || '')
+        emits('refreshData');
+        return
+    }
+    globalPopup?.showError(res.msg || '')
+    return res
+}
+
+watchEffect(() => {
+    const { data } = props
+    // information.value = data
+    // attachmenttable.value = data.contactsDocumentList || []
+    information.value = {}
+    attachmenttable.value = []
+});
+</script>
+<style scoped lang="scss"></style>

+ 146 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/component/information.vue

@@ -0,0 +1,146 @@
+<template>
+    <div class="information pl-4 pr-4 pt-3 pb-3">
+        <div class="flex justify-between">
+            <div class="title">基本信息</div>
+            <div>
+                <el-button type="primary">认领</el-button>
+                <el-button type="primary">转移</el-button>
+                <el-button type="primary">编辑</el-button>
+            </div>
+        </div>
+        <div class="form flex flex-wrap justify-between">
+            <div v-for="item in formItems" :key="item.label" class="formItem flex" :style="{ width: item.width }">
+                <div :class="item.labelClass">{{ item.label }}:</div>
+                <div class="flex-1 overflow-hidden text-ellipsis whitespace-nowrap ml-1">{{ item.value }}</div>
+            </div>
+        </div>
+
+        <el-dialog v-model="allVisible.editContactsVisible" width="1000" :show-close="false" top="10vh">
+            <template #header="{ close, titleId, titleClass }">
+                <div class="flex justify-between items-center border-b pb-3 dialog-header">
+                    <h4 :id="titleId">编辑联系人</h4>
+                    <div>
+                        <el-button type="primary" :loading="allLoading.editContactsSaveLoading">保存</el-button>
+                        <el-button @click="closeVisible('editContactsVisible')">取消</el-button>
+                    </div>
+                </div>
+            </template>
+            <div class="h-[60vh] overflow-y-auto scroll-bar pt-3">
+                <div class="ml-4 mr-4">
+                    <GenerateForm ref="contactsTemplateRef" :data="contactsTemplate" :value="contactsTemplateValue"
+                        :key="contactsTemplateRefKey" v-loading="allLoading.contactsTemplateRefLoading" />
+                </div>
+            </div>
+        </el-dialog>
+
+        <el-dialog v-model="allVisible.transferVisible" width="600" :show-close="false" top="10vh">
+            <template #header="{ close, titleId, titleClass }">
+                <div class="flex justify-between items-center border-b pb-3 dialog-header">
+                    <h4 :id="titleId">{{ allText.operationText }}</h4>
+                    <div>
+                        <el-button type="primary" :loading="allLoading.transferLoading">转移</el-button>
+                        <el-button @click="allVisible.transferVisible = false">取消</el-button>
+                    </div>
+                </div>
+            </template>
+            <div class="scroll-bar m-6">
+                <div class="flex mb-4">
+                    <div class="w-20 flex items-center justify-end pr-4">转移至:</div>
+                    <el-select v-model="transferValue" placeholder="请选择" class="flex1">
+                        <el-option v-for="item in transferOptions" :key="item.value" :label="item.label"
+                            :value="item.value" />
+                    </el-select>
+                </div>
+                <div class="pl-3 text-[#e94a4a]">转移后,将看不到此联系人</div>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script lang="ts" setup>
+import { ref, reactive, onMounted, onUnmounted, defineExpose, inject, watchEffect } from 'vue'
+import { GenerateForm } from '@zmjs/form-design';
+import { getFromValue, getTemplateKey } from '@/utils/tools';
+import { get, post } from '@/utils/request';
+
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const emits = defineEmits(['refreshData']);
+const props = defineProps<{
+    data?: any
+}>()
+const allLoading = reactive({
+    contactsTemplateRefLoading: false,
+    editContactsSaveLoading: false,
+    transferLoading: false
+})
+const allVisible = reactive({
+    editContactsVisible: false,
+    transferVisible: false
+})
+const allText = reactive({
+    operationText: '认领联系人'
+})
+const contactsTemplate = ref({
+    list: [],
+    config: {}
+})
+const contactsTemplateValue = ref({})
+const contactsTemplateRef = ref<typeof GenerateForm>()
+const contactsTemplateRefKey = ref(1)
+const info: any = ref({})
+const transferValue = ref('')
+const transferOptions = ref<optionType[]>([])
+
+function showVisible(type: keyof typeof allVisible) {
+    allVisible[type] = true
+}
+
+function closeVisible(type: keyof typeof allVisible) {
+    allVisible[type] = false
+}
+
+const formItems = reactive([
+    { label: '联系人', key: 'name', value: '', labelClass: 'w-20 text-right text-gray-500', width: '48%' },
+    { label: '客户', key: 'productName', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '电话', key: 'phone', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '邮箱', key: 'email', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '职务', key: 'position', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '性别', key: 'sexValue', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '地址', key: 'address', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '负责人', key: 'inchargerName', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '创建人', key: 'creatorName', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '创建时间', key: 'createTime', value: '', labelClass: 'w-22 text-right text-gray-500', width: '48%' },
+    { label: '备注', key: 'remark', value: '', labelClass: 'w-22 text-right text-gray-500', width: '100%' },
+])
+
+// 生命周期钩子
+onMounted(async () => {
+
+});
+</script>
+<style scoped lang="scss">
+.information {
+    .title {
+        font-size: 18px;
+        color: #000
+    }
+
+    .form {
+        .formItem {
+            .text {
+                display: -webkit-box;
+                /* Safari */
+                -webkit-line-clamp: 2;
+                /* number of lines to show */
+                -webkit-box-orient: vertical;
+                overflow: hidden;
+                line-height: 1.5;
+            }
+        }
+
+        .w-20,
+        .w-22 {
+            width: 80px;
+        }
+    }
+}
+</style>

+ 53 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/detail/index.vue

@@ -1,7 +1,59 @@
 <template>
-    <div>销售订单详情</div>
+    <div class="h-full flex p-3 flex-col businessDetail" v-loading="pageLoading">
+        <div class="w-full bg-white p-2 mb-2 shadow-md rounded-md flex items-center">
+            <div class="icon mr-4">
+                <el-link :underline="false" @click="backPath()">
+                    <el-icon class="el-icon--right"><icon-view /></el-icon> 返回销售订单列表
+                </el-link>
+            </div>
+            <div class="mr-8">
+                <el-select v-model="values" placeholder="请选择" style="width: 300px">
+                    <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+                </el-select>
+            </div>
+        </div>
+
+        <div class="flex-1 flex flex-col overflow-y-auto overflow-x-hidden scroll-bar" v-loading="pageLoading">
+            <div class="w-full h-auto flex justify-between">
+                <div class="bg-white shadow-md rounded-md" style="width: 46%;">
+                    <Information />
+                </div>
+                <div class="bg-white ml-2 shadow-md rounded-md flex-1">
+                    <Attachment />
+                </div>
+            </div>
+        </div>
+    </div>
 </template>
 
 <script lang="ts" setup>
+import { ref, reactive, onMounted, inject } from "vue";
+import { Edit, ArrowLeft as IconView } from '@element-plus/icons-vue'
+import { backPath } from '../../../utils/tools'
+import { useRoute } from "vue-router";
+import { post } from "@/utils/request";
+import { GETTABLELIST } from "../api";
+
+import Information from '../component/information.vue'
+import Attachment from '../component/attachment.vue'
+
+const route = useRoute()
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const pageLoading = ref(false)
+const rowId = ref(+(route.query.id || ''))
+const values = ref<number | string>('')
+const options = ref<optionType[]>([])
+
+function getAllContacts() {
+    post(GETTABLELIST, { pageIndex: -1, pageSize: -1 }).then(({ data }) => {
+        options.value = (data.record || []).map((item: any) => ({ value: item.id, label: item.orderName }))
+    }).catch((err) => {
+        globalPopup?.showError(err.message)
+    })
+}
+
+onMounted(() => {
+    getAllContacts()
+})
 
 </script>

+ 14 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/times.ts

@@ -44,3 +44,17 @@ export function formatDateTime(date: Date) {
   const second = date.getSeconds().toString().padStart(2, "0");
   return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
 }
+
+/**
+ * 将 Date 对象格式化为 "YYYY-MM-DD HH:mm" 的形式
+ * @param date 日期 new Date()
+ * @returns
+ */
+export function formatDateMinutes(date: Date) {
+  const year = date.getFullYear();
+  const month = (1 + date.getMonth()).toString().padStart(2, "0");
+  const day = date.getDate().toString().padStart(2, "0");
+  const hour = date.getHours().toString().padStart(2, "0");
+  const minute = date.getMinutes().toString().padStart(2, "0");
+  return `${year}-${month}-${day} ${hour}:${minute}`;
+}

+ 11 - 1
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/BusinessOpportunityController.java

@@ -236,7 +236,8 @@ public class BusinessOpportunityController {
         msg.setMsg("操作成功");
         bOservice.saveReason(bo, user);
         return msg;
-    } @RequestMapping("saveContactsId")
+    }
+    @RequestMapping("saveContactsId")
     public Object saveContactsId(BusinessOpportunity bo, HttpServletRequest request) {
         User user = userMapper.selectById(request.getHeader("Token"));
         HttpRespMsg msg = new HttpRespMsg();
@@ -244,6 +245,15 @@ public class BusinessOpportunityController {
         bOservice.saveContactsId(bo, user);
         return msg;
     }
+    @RequestMapping("saveStageId")
+    public Object getStageValue(BusinessOpportunity bo, HttpServletRequest request) {
+        User user = userMapper.selectById(request.getHeader("Token"));
+        HttpRespMsg msg = new HttpRespMsg();
+        msg.setMsg("操作成功");
+        bOservice.saveStage(bo, user);
+        return msg;
+    }
+
     @RequestMapping("list")
     public HttpRespMsg list(BusinessOpportunity bo, HttpServletRequest request) {
         HashMap<Object, Object> r = new HashMap<>();

+ 2 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/BusinessOpportunityService.java

@@ -65,4 +65,6 @@ public interface BusinessOpportunityService extends IService<BusinessOpportunity
     void saveContactsId(BusinessOpportunity bo, User user);
 
     void saveReason(BusinessOpportunity bo, User user);
+
+    void saveStage(BusinessOpportunity bo, User user);
 }

+ 12 - 1
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/BusinessOpportunityServiceImpl.java

@@ -581,7 +581,18 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
         bOMapper.update(bo, new UpdateWrapper<BusinessOpportunity>().eq("id", bo.getId()).set("reason", bo.getReason()));
         ActionLog al = new ActionLog();
         al.setCode("business");
-        al.setName("编辑了 结束理由");
+        al.setName("编辑了结束理由");
+        al.setUserId(user.getId());
+        al.setItemId(bo.getId());
+        actionLogMapper.insert(al);
+    }
+
+    @Override
+    public void saveStage(BusinessOpportunity bo, User user) {
+        bOMapper.update(bo, new UpdateWrapper<BusinessOpportunity>().eq("id", bo.getId()).set("Stage_Id", bo.getStageId()));
+        ActionLog al = new ActionLog();
+        al.setCode("business");
+        al.setName("推进了阶段至"+bo.getStageValue());
         al.setUserId(user.getId());
         al.setItemId(bo.getId());
         actionLogMapper.insert(al);