Переглянути джерело

Merge remote-tracking branch 'origin/master'

yusm 1 рік тому
батько
коміт
a81d70417d
76 змінених файлів з 4107 додано та 813 видалено
  1. 1 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/App.vue
  2. BIN
      fhKeeper/formulahousekeeper/customerBuler-crm/src/assets/line.png
  3. BIN
      fhKeeper/formulahousekeeper/customerBuler-crm/src/assets/trend.png
  4. 133 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/components/detailcompinents/relatedTasks.vue
  5. 50 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/components/detailcompinents/type.d.ts
  6. 5 2
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/relatedProducts.vue
  7. 94 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/api.ts
  8. 13 3
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/components/SimpleCard.vue
  9. 24 6
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/components/TrendCard.vue
  10. 376 46
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/index.vue
  11. 5 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/api.ts
  12. 13 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/api.ts
  13. 105 47
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/attachment.vue
  14. 160 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/deteleTables.vue
  15. 165 13
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/information.vue
  16. 16 33
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/operationRecord.vue
  17. 82 46
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/products.vue
  18. 230 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/stageSetting.vue
  19. 154 39
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/detail/index.vue
  20. 90 25
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/index.vue
  21. 2 2
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/type.d.ts
  22. 29 10
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/api.ts
  23. 160 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/component/deteleTables.vue
  24. 33 8
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/detail/index.vue
  25. 309 44
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/index.vue
  26. 74 39
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/corpreport/index.vue
  27. 13 6
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/customer/api.ts
  28. 163 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/customer/component/deteleTables.vue
  29. 38 35
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/customer/detail/index.vue
  30. 323 60
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/customer/index.vue
  31. 29 22
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/api.ts
  32. 34 25
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/index.vue
  33. 3 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/index.vue
  34. 3 2
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/system/customForm/index.vue
  35. 1 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/api.ts
  36. 4 4
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/index.vue
  37. 4 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/constant.ts
  38. 86 22
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/information.vue
  39. 85 51
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/relatedTasks.vue
  40. 2 2
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/index.vue
  41. 63 10
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/index.vue
  42. 11 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/type.d.ts
  43. 22 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/tools.ts
  44. 16 16
      fhKeeper/formulahousekeeper/customerBuler-crm/vite.config.ts
  45. 5 0
      fhKeeper/formulahousekeeper/management-crm/pom.xml
  46. 20 2
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/BusinessOpportunityController.java
  47. 12 6
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/ClueController.java
  48. 20 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/CustomController.java
  49. 9 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/SalesOrderController.java
  50. 4 2
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/BusinessOpportunity.java
  51. 1 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/Custom.java
  52. 9 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/SalesOrder.java
  53. 1 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/BusinessOpportunityMapper.java
  54. 3 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/ClueMapper.java
  55. 3 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/BusinessOpportunityService.java
  56. 4 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/ClueService.java
  57. 2 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/CustomService.java
  58. 2 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/StageService.java
  59. 16 8
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/BusinessOpportunityServiceImpl.java
  60. 215 42
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/ClueServiceImpl.java
  61. 10 4
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/CustomServiceImpl.java
  62. 86 38
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/ProductServiceImpl.java
  63. 213 34
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/SalesOrderServiceImpl.java
  64. 2 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/StageServiceImpl.java
  65. 15 2
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/SysFormServiceImpl.java
  66. 31 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/util/BusinessItemProductListDeserializer.java
  67. 15 0
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/CustomMapper.xml
  68. 2 1
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/SalesOrderMapper.xml
  69. 1 0
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectController.java
  70. 1 1
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ReportController.java
  71. 153 26
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/UserWithBeisenController.java
  72. 16 9
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectServiceImpl.java
  73. 2 2
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ReportServiceImpl.java
  74. 3 4
      fhKeeper/formulahousekeeper/management-platform/src/main/resources/mapper/ProjectMapper.xml
  75. 6 4
      fhKeeper/formulahousekeeper/timesheet/src/components/vueMultipleDept.vue
  76. 2 1
      fhKeeper/formulahousekeeper/timesheet/src/views/corpreport/list.vue

+ 1 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/App.vue

@@ -47,6 +47,7 @@ provide<GlobalPopup>('globalPopup', {
 })
 
 const notificationTiop = (options: NotificationParamsTyped) => {
+  ElNotification.closeAll()
   ElNotification(options)
 }
  

BIN
fhKeeper/formulahousekeeper/customerBuler-crm/src/assets/line.png


BIN
fhKeeper/formulahousekeeper/customerBuler-crm/src/assets/trend.png


+ 133 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/components/detailcompinents/relatedTasks.vue

@@ -0,0 +1,133 @@
+<template>
+    <div class="relatedTasks pl-4 pr-4 pt-3 pb-3 h-full flex flex-col">
+        <div class="flex justify-between">
+            <div class="title">相关任务</div>
+            <div>
+                <el-button type="primary" @click="newTask()">新建任务</el-button>
+            </div>
+        </div>
+        <div class="flex-1 overflow-auto pt-3">
+            <el-table :data="relatedTaskstable" border style="width: 100%;height: 100%;">
+                <el-table-column prop="taskName" label="任务名称">
+                    <template #default="scope">
+                        <el-button link type="primary" size="large" @click="toTask()">{{
+                            scope.row.taskName
+                        }}</el-button>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="priorityStr" label="优先级" width="130" />
+                <el-table-column prop="statusStr" label="状态" width="130" />
+                <el-table-column prop="executorNamesStr" label="执行人" width="130" />
+                <el-table-column prop="startTimes" label="开始时间" width="130" />
+                <el-table-column prop="endTimes" label="截至时间" width="130" />
+            </el-table>
+        </div>
+    </div>
+
+    <!-- 新建任务 -->
+    <TaskModal :visible="allVisible.taskModalVisible" :edit-form="taskModalForm" :save-loading="taskLoading"
+        @close="closeTaskModal" @submit="submitForm" :title="'新建任务'" :disabled-list="props.disabledList" />
+</template>
+<script lang="ts" setup>
+import { PRIORITY } from '../TaskModal/api';
+import { STATUS } from '../../pages/tasks/api';
+import { formatDate } from '../../utils/times';
+import { createTaskFromType } from '../../utils/tools';
+import { ElNotification, NotificationParamsTyped } from 'element-plus'
+import { ref, reactive, onMounted, watchEffect } from 'vue'
+import { useRouter } from "vue-router";
+import TaskModal from '../TaskModal/index.vue'
+import { createTask } from '../TaskModal/taskFunction';
+import { ITEM_RENDER_EVT } from 'element-plus/es/components/virtual-list/src/defaults';
+import { Props, Emits } from './type';
+
+const props = defineProps<Props>()
+const emits = defineEmits<Emits>();
+
+type saveLoadingType = "1" | "2" | "3" | "4"; //1是没有保存, 2是正在保存, 3是保存成功, 4是保存失败
+
+const relatedTaskstable = ref([])
+const information = ref<any>({})
+const router = useRouter()
+const taskLoading = ref<saveLoadingType>('1')
+const taskModalForm = ref({}) // 任务弹窗表单
+const allVisible = reactive({
+    taskModalVisible: false
+})
+
+function submitForm(submitData: any, isClose: boolean) { // 任务提交
+    taskLoading.value = '2'
+    createTask(submitData, isClose).then((res) => {
+        const { saveLoading, isClose } = res
+        taskLoading.value = saveLoading
+        allVisible.taskModalVisible = isClose
+        globalPopup('新建成功', 'success')
+        emits('refreshData')
+    }).catch((err) => {
+        const { saveLoading, isClose, message } = err
+        taskLoading.value = saveLoading
+        allVisible.taskModalVisible = isClose
+        globalPopup(message, 'error')
+    })
+}
+
+function globalPopup(str: string, type: string) {
+    notificationTiop({
+      message: str || '成功',
+      type: type,
+      title: "提示",
+      duration: 2000
+    })
+}
+
+const notificationTiop = (options: NotificationParamsTyped) => {
+  ElNotification(options)
+}
+
+function newTask() {
+    const { id } = information.value
+    taskModalForm.value = { ...createTaskFromType(props.formTaskType), [props.filed as any]: id, }
+    allVisible.taskModalVisible = true
+}
+
+function toTask() {
+    router.push({
+        path: `/tasks`
+    })
+}
+
+function closeTaskModal() {
+    allVisible.taskModalVisible = false
+}
+
+// 接收参数赋值
+function receiveAssignment(item: any) {
+    console.log(item, '<==== 过来的值')
+    information.value = item.information
+    const dataVal = item.data
+    for (let i in dataVal) {
+        dataVal[i].executorNamesStr = (dataVal[i].executorNames || []).join(','),
+            dataVal[i].startTimes = dataVal[i].startDate ? formatDate(new Date(dataVal[i].startDate)) : '',
+            dataVal[i].endTimes = dataVal[i].endDate ? formatDate(new Date(dataVal[i].endDate)) : '',
+            dataVal[i].priorityStr = PRIORITY.find(item => item.value == dataVal[i].priority)?.label || '',
+            dataVal[i].statusStr = STATUS.find(item => item.value == dataVal[i].status)?.label || ''
+    }
+    relatedTaskstable.value = dataVal
+}
+
+watchEffect(() => {
+    receiveAssignment(props)
+});
+
+// 生命周期钩子
+onMounted(() => {
+});
+</script>
+<style scoped lang="scss">
+.relatedTasks {
+    .title {
+        font-size: 18px;
+        color: #000
+    }
+}
+</style>

+ 50 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/components/detailcompinents/type.d.ts

@@ -0,0 +1,50 @@
+type TASK_VALUE_TYPE = 0 | 1 | 2 | 3;
+type tableTypeFiled = {
+    id: number,
+    status: number,
+    priority: number,
+    executorNames: Array,
+    startDate: string,
+    endDate: string,
+}
+export interface Props {
+    /**
+     * data: 任务 Table 数据
+     */
+    data: tableTypeFiled[];
+    /**
+     * information: 详情数据
+     */
+    information: any;
+    /**
+     * disabledList:新建任务需要禁用的字段(参考 TaskModal 文件)
+     */
+    disabledList?: (
+        | "taskName"
+        | "priority"
+        | "taskType"
+        | "customId"
+        | "businessOpportunityId"
+        | "orderId"
+        | "clueId"
+        | "contactsId"
+        | "executorId"
+        | "startDate"
+        | "endDate"
+    )[];
+    /** 
+     * formTaskType: 任务类型  0是客户, 1是商机, 2是销售订单 ,3是线索
+     */
+    formTaskType: TASK_VALUE_TYPE,
+    /**
+     * 详情数据中需要关联的任务类型字段
+     */
+    filed?: 'customId' | 'businessOpportunityId' | 'orderId' | 'clueId' | 'contactsId'
+}
+
+export interface Emits {
+    /**
+     *  新建任务后触发的更新数据事件
+     */
+    (event: "refreshData"): void;
+}

+ 5 - 2
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/relatedProducts.vue

@@ -1,7 +1,7 @@
 <template>
     <div>
         <el-table ref="productTableRef" :data="productTable" border :row-class-name="tableRowClassName"
-            @row-click="tableRowItem" style="width: 100%;height: 200px">
+            @row-click="tableRowItem" :style="{ width: '100%', height: heightClass }">
             <el-table-column label="序号" width="60" align="center">
                 <template #default="scope">
                     <span>{{ scope.$index + 1 }}</span>
@@ -63,11 +63,13 @@ import { ref, reactive, onMounted, inject, watchEffect } from "vue";
 
 const props = defineProps<{
     productTableList: any,
+    height?: string,
 }>()
 
 const productTable: any = ref([{}])
 const productTableIndex = ref(0) // 可以编辑索引
 const productArrar: any = ref([])
+const heightClass = ref('200px') // 表格高度
 
 type ProductTableField = 'sellingPrice' | 'quantity' | 'discount'
 
@@ -119,8 +121,9 @@ function returnData() {
 }
 
 watchEffect(() => {
-    const { productTableList } = props
+    const { productTableList, height } = props
     productArrar.value = productTableList || []
+    heightClass.value = !height ? '200px' : height
 });
 
 defineExpose({

+ 94 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/api.ts

@@ -0,0 +1,94 @@
+import { post, get } from '@/utils/request';
+
+export type RequestProps = {
+  /**
+   * @description 0-本月 1-本周 2-本年
+   */
+  dateType?: 0 | 1 | 2;
+  /**
+   * @description 0-仅本人 1-本人及下属 2-仅本部门 3-本部门及下属部门
+   */
+  queryType?: 0 | 1 | 2 | 3;
+  startDate?: string;
+  endDate?: string;
+};
+
+export type SummaryData = {
+  code: string;
+  data: {
+    clueDataSummary: {
+      changeNum: number;
+      newNum: number;
+    };
+    businessOpportunityDataSummary: {
+      winning: number;
+      allAmountOfMoney: number;
+      losting: number;
+      newNum: number;
+    };
+    customDataSummary: {
+      closeDealNum: number;
+      newNum: number;
+    };
+  };
+};
+
+export async function getSummaryData(
+  payload?: RequestProps
+): Promise<SummaryData> {
+  return await post('/order/dataSummary', payload);
+}
+
+export type StageData = {
+  code: string;
+  data: {
+    dataMap: {
+      stageName: string;
+      num: number;
+    }[];
+  };
+};
+
+export async function getStageData(payload?: RequestProps): Promise<StageData> {
+  return await post('/order/businessOpportunityStage', payload);
+}
+
+export type BulletinData = {
+  code: string;
+  data: {
+    custom: {
+      customPromote: string;
+      customCount: number;
+    };
+    businessOpportunity: {
+      businessOpportunityCount: number;
+      businessOpportunityPromote: string;
+    };
+    contacts: {
+      contactsCount: number;
+      contactsPromote: string;
+    };
+    salesOrder: {
+      salesOrderCount: number;
+      salesOrderPromote: string;
+    };
+    salesOrdersPrice: {
+      salesOrderPricePromote: string;
+      salesOrdersPrice: number;
+    };
+    clue: {
+      cluePromote: string;
+      clueCount: number;
+    };
+    businessOpportunityPrice: {
+      businessOpportunityPromote: string;
+      businessOpportunityPrice: number;
+    };
+  };
+};
+
+export async function getBulletinData(
+  payload?: RequestProps
+): Promise<BulletinData> {
+  return await post('/order/salesKit', payload);
+}

+ 13 - 3
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/components/SimpleCard.vue

@@ -2,10 +2,20 @@
   <div
     class="bg-gray-200 rounded-sm p-2 flex flex-col gap-1 text-xs text-gray-600"
   >
-    <span>转成交易</span>
+    <span>{{ title }}</span>
     <span class="text-gray-900">
-      <span>{{ 1 }}</span>
-      {{ '个' }}
+      <span>{{ number === null || number === undefined ? '--' : number }}</span>
+      {{ unit }}
     </span>
   </div>
 </template>
+
+<script lang="ts" setup>
+type Props = {
+  title: string;
+  number?: number;
+  unit: '个' | '人' | '元';
+};
+
+defineProps<Props>();
+</script>

+ 24 - 6
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/components/TrendCard.vue

@@ -3,25 +3,43 @@
     <div class="flex flex-col justify-between text-gray-500 text-xs gap-1">
       <div class="">{{ title }}</div>
       <div class="text-gray-800">
-        <span class="text-sm">{{ number }}</span>
+        <span class="text-sm">{{
+          number === null || number === undefined ? '--' : number
+        }}</span>
         {{ unit }}
       </div>
       <div>
         <span>较上月 </span>
-        <span class="text-xs text-red-600 inline-flex items-center">
-          <span>100%</span>
-          <el-icon><Top /></el-icon>
+        <span
+          class="text-xs text-red-600 inline-flex items-center"
+          v-if="compare"
+        >
+          <span>{{ compare.includes('-') ? compare.slice(1) : compare }}</span>
+          <el-icon class="mt-0.5">
+            <Bottom v-if="compare.includes('-')" />
+            <Top v-else />
+          </el-icon>
         </span>
+        <span v-else>--</span>
       </div>
     </div>
-    <div>11</div>
+    <div>
+      <img
+        width="60"
+        :style="{
+          transform: compare?.includes('-') ? 'rotate(66deg)' : 'rotate(180deg)'
+        }"
+        src="../../../assets/line.png"
+      />
+    </div>
   </div>
 </template>
 
 <script lang="ts" setup>
 type Props = {
+  compare?: string;
   title: string;
-  number: number;
+  number?: number;
   unit: '个' | '人' | '元';
 };
 

+ 376 - 46
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/analysis/index.vue

@@ -1,18 +1,90 @@
 <script lang="ts" setup>
-import { ref, reactive } from 'vue';
+import { ref, reactive, onMounted, watchEffect } from 'vue';
 import TrendCard from './components/TrendCard.vue';
 import SimpleCard from './components/SimpleCard.vue';
 import Divider from './components/Divider.vue';
 import Echarts from '@/components/ReEcharts/index.vue';
 import { EChartsOption } from 'echarts';
+import { dayjs } from 'element-plus';
+import {
+  getSummaryData,
+  getBulletinData,
+  BulletinData,
+  getStageData,
+  RequestProps,
+  StageData,
+  SummaryData
+} from './api';
 
-const prompt = reactive({ summary: {}, stage: {}, bulletin: {} });
+const permissionOptions = [
+  {
+    label: '仅本人',
+    value: 0
+  },
+  {
+    label: '本人及下属',
+    value: 1
+  },
+  {
+    label: '仅本部门',
+    value: 2
+  },
+  {
+    label: '本部门及下属部门',
+    value: 3
+  }
+];
+const dateOptions = [
+  {
+    label: '本月',
+    value: 0
+  },
+  {
+    label: '本周',
+    value: 1
+  },
+  {
+    label: '本年',
+    value: 2
+  }
+];
 
-const option: EChartsOption = {
+type PromptType = {
+  permission?: 0 | 1 | 2 | 3;
+  date?: 0 | 1 | 2;
+  sliceDate: [Date, Date];
+};
+
+const bulletinPrompt = reactive<PromptType>({
+  permission: undefined,
+  date: undefined,
+  sliceDate: [new Date(), new Date()]
+});
+const summaryPrompt = reactive<PromptType>({
+  permission: undefined,
+  date: undefined,
+  sliceDate: [new Date(), new Date()]
+});
+const stagePrompt = reactive<PromptType>({
+  permission: undefined,
+  date: undefined,
+  sliceDate: [new Date(), new Date()]
+});
+
+const requestData = reactive<{
+  bulletin: BulletinData['data'] | null;
+  summary: SummaryData['data'] | null;
+  stage: StageData['data'] | null;
+}>({
+  summary: null,
+  stage: null,
+  bulletin: null
+});
+
+const chartOptions: EChartsOption = reactive({
   grid: { top: 0, bottom: 20, left: 60 },
   yAxis: {
-    type: 'category',
-    data: ['验证客户', '赢单', '输单']
+    type: 'category'
   },
   xAxis: {
     type: 'value'
@@ -20,39 +92,188 @@ const option: EChartsOption = {
   series: [
     {
       barWidth: 20,
-      data: [0, 20, 40, 60, 80, 100],
+      data: [10, 30, 30, 30, 30],
       type: 'bar'
     }
   ]
+});
+
+const queryBulletin = async (payload?: RequestProps) => {
+  const bulletinResult = await getBulletinData(payload);
+  requestData.bulletin = bulletinResult.data;
 };
+
+const querySummary = async (payload?: RequestProps) => {
+  const summaryResult = await getSummaryData(payload);
+  requestData.summary = summaryResult.data;
+};
+
+const queryStage = async (payload?: RequestProps) => {
+  const stageResult = await getStageData(payload);
+  const data = stageResult.data;
+
+  // @ts-ignore
+  chartOptions.yAxis.data = data.dataMap.map((map) => map.stageName);
+  // @ts-ignore
+  chartOptions.series[0].data = data.dataMap.map((map) => map.num);
+};
+
+watchEffect(() => {
+  queryBulletin({
+    ...(bulletinPrompt.date === ('ignore' as any)
+      ? {
+          startDate: dayjs(bulletinPrompt.sliceDate[0]).format('YYYY-MM-DD'),
+          endData: dayjs(bulletinPrompt.sliceDate[1]).format('YYYY-MM-DD')
+        }
+      : { dateType: bulletinPrompt.date }),
+    queryType: bulletinPrompt.permission
+  });
+});
+
+watchEffect(() => {
+  queryStage({
+    ...(stagePrompt.date === ('ignore' as any)
+      ? {
+          startDate: dayjs(stagePrompt.sliceDate[0]).format('YYYY-MM-DD'),
+          endData: dayjs(stagePrompt.sliceDate[1]).format('YYYY-MM-DD')
+        }
+      : { dateType: stagePrompt.date }),
+    queryType: stagePrompt.permission
+  });
+});
+
+watchEffect(() => {
+  querySummary({
+    ...(summaryPrompt.date === ('ignore' as any)
+      ? {
+          startDate: dayjs(summaryPrompt.sliceDate[0]).format('YYYY-MM-DD'),
+          endData: dayjs(summaryPrompt.sliceDate[1]).format('YYYY-MM-DD')
+        }
+      : { dateType: summaryPrompt.date }),
+    queryType: summaryPrompt.permission
+  });
+});
+
+watchEffect(() => {
+  console.log(
+    bulletinPrompt.date,
+    '-----',
+    bulletinPrompt.sliceDate,
+    '88888888---watchEffect'
+  );
+});
 </script>
 
 <template>
-  <div
-    class="m-5 bg-white min-h-full p-4 rounded relative flex gap-12 items-start"
-  >
-    <section class="flex-[4]">
+  <div class="m-5 bg-white min-h-full p-4 rounded">
+    <section>
       <div class="flex gap-3 mb-4">
         <div class="w-40">
-          <el-select size="small"></el-select>
+          <el-select
+            clearable
+            size="small"
+            :model-value="bulletinPrompt.permission"
+            @change="(value) => (bulletinPrompt.permission = value)"
+          >
+            <el-option
+              v-for="permission in permissionOptions"
+              :label="permission.label"
+              :value="permission.value"
+            />
+          </el-select>
         </div>
         <div class="w-40">
-          <el-select size="small"></el-select>
+          <el-select
+            clearable
+            size="small"
+            :model-value="bulletinPrompt.date"
+            @change="(value) => (bulletinPrompt.date = value)"
+          >
+            <el-option
+              v-for="date in dateOptions"
+              :label="date.label"
+              :value="date.value"
+            />
+            <el-option label="自定义" value="ignore">
+              <div class="flex gap-2 w-80">
+                <el-date-picker
+                  size="small"
+                  :clearable="false"
+                  type="daterange"
+                  class="w-12"
+                  v-model="bulletinPrompt.sliceDate"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                />
+                <el-button size="small" type="primary">确认</el-button>
+              </div>
+            </el-option>
+          </el-select>
         </div>
       </div>
       <div class="border-gray-200 border rounded p-3">
         <div class="flex gap-1.5 items-center mb-3">
+          <img width="16" src="../../assets/trend.png" />
           <div class="text-sm font-medium">销售简报</div>
           <el-icon><QuestionFilled class="text-gray-500 text-sm" /></el-icon>
         </div>
         <div class="grid xl:grid-cols-4 lg:grid-cols-3 grid-cols-2 gap-4">
-          <TrendCard title="新增客户" :number="4" unit="人" />
-          <TrendCard title="新增联系人" :number="4" unit="人" />
-          <TrendCard title="新增商机" :number="4" unit="个" />
-          <TrendCard title="新增销售订单" :number="4" unit="个" />
-          <TrendCard title="销售订单金额" :number="4" unit="元" />
-          <TrendCard title="商机金额" :number="4" unit="元" />
-          <TrendCard title="新增线索" :number="4" unit="个" />
+          <TrendCard
+            title="新增客户"
+            unit="人"
+            :number="requestData?.bulletin?.custom.customCount"
+            :compare="requestData?.bulletin?.custom.customPromote"
+          />
+          <TrendCard
+            title="新增联系人"
+            unit="人"
+            :number="requestData?.bulletin?.contacts.contactsCount"
+            :compare="requestData?.bulletin?.contacts.contactsPromote"
+          />
+          <TrendCard
+            title="新增商机"
+            unit="个"
+            :number="
+              requestData?.bulletin?.businessOpportunity
+                .businessOpportunityCount
+            "
+            :compare="
+              requestData?.bulletin?.businessOpportunity
+                .businessOpportunityPromote
+            "
+          />
+          <TrendCard
+            title="新增销售订单"
+            unit="个"
+            :number="requestData?.bulletin?.salesOrder.salesOrderCount"
+            :compare="requestData?.bulletin?.salesOrder.salesOrderPromote"
+          />
+          <TrendCard
+            title="销售订单金额"
+            unit="元"
+            :number="requestData?.bulletin?.salesOrdersPrice.salesOrdersPrice"
+            :compare="
+              requestData?.bulletin?.salesOrdersPrice.salesOrderPricePromote
+            "
+          />
+          <TrendCard
+            title="商机金额"
+            unit="元"
+            :number="
+              requestData?.bulletin?.businessOpportunityPrice
+                .businessOpportunityPrice
+            "
+            :compare="
+              requestData?.bulletin?.businessOpportunityPrice
+                .businessOpportunityPromote
+            "
+          />
+          <TrendCard
+            title="新增线索"
+            unit="个"
+            :number="requestData?.bulletin?.clue.clueCount"
+            :compare="requestData?.bulletin?.clue.cluePromote"
+          />
         </div>
       </div>
       <div class="my-8 flex gap-4 items-start">
@@ -60,69 +281,178 @@ const option: EChartsOption = {
           <div class="text-sm font-medium">数据汇总</div>
           <div class="flex gap-3 mb-8 mt-2">
             <div class="w-40">
-              <el-select size="small"></el-select>
+              <el-select
+                clearable
+                size="small"
+                :model-value="summaryPrompt.permission"
+                @change="(value) => (summaryPrompt.permission = value)"
+              >
+                <el-option
+                  v-for="permission in permissionOptions"
+                  :label="permission.label"
+                  :value="permission.value"
+                />
+              </el-select>
             </div>
             <div class="w-40">
-              <el-select size="small"></el-select>
+              <el-select
+                clearable
+                size="small"
+                :model-value="summaryPrompt.date"
+                @change="(value) => (summaryPrompt.date = value)"
+              >
+                <el-option
+                  v-for="date in dateOptions"
+                  :label="date.label"
+                  :value="date.value"
+                />
+                <el-option label="自定义" value="ignore">
+                  <div class="flex gap-2 w-80">
+                    <el-date-picker
+                      size="small"
+                      :clearable="false"
+                      type="daterange"
+                      class="w-12"
+                      v-model="summaryPrompt.sliceDate"
+                      start-placeholder="开始日期"
+                      end-placeholder="结束日期"
+                    />
+                    <el-button size="small" type="primary">确认</el-button>
+                  </div>
+                </el-option>
+              </el-select>
             </div>
           </div>
           <Divider title="客户汇总" />
           <div class="my-6 grid grid-cols-4 gap-2">
-            <SimpleCard />
-            <SimpleCard />
-            <SimpleCard />
-            <SimpleCard />
+            <SimpleCard
+              title="新增客户"
+              unit="个"
+              :number="requestData.summary?.customDataSummary.newNum"
+            />
+            <SimpleCard
+              title="转成交客户"
+              unit="个"
+              :number="requestData.summary?.customDataSummary.closeDealNum"
+            />
           </div>
           <Divider title="商机汇总" />
           <div class="my-6 grid grid-cols-4 gap-2">
-            <SimpleCard />
-            <SimpleCard />
-            <SimpleCard />
-            <SimpleCard />
+            <SimpleCard
+              title="新增商机"
+              unit="个"
+              :number="
+                requestData.summary?.businessOpportunityDataSummary.newNum
+              "
+            />
+            <SimpleCard
+              title="赢单商机"
+              unit="个"
+              :number="
+                requestData.summary?.businessOpportunityDataSummary.winning
+              "
+            />
+            <SimpleCard
+              title="输单商机"
+              unit="个"
+              :number="
+                requestData.summary?.businessOpportunityDataSummary.losting
+              "
+            />
+            <SimpleCard
+              title="商机总金额"
+              unit="元"
+              :number="
+                requestData.summary?.businessOpportunityDataSummary
+                  .allAmountOfMoney
+              "
+            />
           </div>
           <Divider title="线索汇总" />
           <div class="my-6 grid grid-cols-4 gap-2">
-            <SimpleCard />
-            <SimpleCard />
-            <SimpleCard />
-            <SimpleCard />
+            <SimpleCard
+              title="新增线索"
+              unit="个"
+              :number="requestData.summary?.clueDataSummary.newNum"
+            />
+            <SimpleCard
+              title="线索转商机"
+              unit="个"
+              :number="requestData.summary?.clueDataSummary.changeNum"
+            />
           </div>
         </div>
         <div class="border-gray-200 border rounded p-3 flex-1">
           <div class="text-sm font-medium">商机阶段</div>
           <div class="flex gap-3 mb-8 mt-2">
             <div class="w-40">
-              <el-select size="small"></el-select>
+              <el-select
+                clearable
+                size="small"
+                :model-value="stagePrompt.permission"
+                @change="(value) => (stagePrompt.permission = value)"
+              >
+                <el-option
+                  v-for="permission in permissionOptions"
+                  :label="permission.label"
+                  :value="permission.value"
+                />
+              </el-select>
             </div>
             <div class="w-40">
-              <el-select size="small"></el-select>
+              <el-select
+                clearable
+                size="small"
+                :model-value="stagePrompt.date"
+                @change="(value) => (stagePrompt.date = value)"
+              >
+                <el-option
+                  v-for="date in dateOptions"
+                  :label="date.label"
+                  :value="date.value"
+                />
+                <el-option label="自定义" value="ignore">
+                  <div class="flex gap-2 w-80">
+                    <el-date-picker
+                      size="small"
+                      :clearable="false"
+                      type="daterange"
+                      class="w-12"
+                      v-model="stagePrompt.sliceDate"
+                      start-placeholder="开始日期"
+                      end-placeholder="结束日期"
+                    />
+                    <el-button size="small" type="primary">确认</el-button>
+                  </div>
+                </el-option>
+              </el-select>
             </div>
           </div>
           <div class="h-60">
-            <Echarts :option="option"></Echarts>
+            <Echarts :option="chartOptions"></Echarts>
           </div>
         </div>
       </div>
     </section>
-    <nav class="flex-1 min-w-60 sticky right-0 top-9">
+    <!-- <nav class="flex-1 min-w-60 sticky right-0 top-9 max-lg:hidden">
       <div class="border border-gray-200 rounded w-3/4 text-sm text-gray-500">
-        <div class="p-2 bg-sky-100 border-l-2 border-blue-700">仅本人</div>
-        <div class="p-2">本人及下属</div>
-        <div class="p-2">仅本部门</div>
-        <div class="p-2">本部门及下属</div>
+        <div class="p-2" v-for="primission in permissionOptions">
+          {{ primission.label }}
+        </div>
       </div>
       <div class="border border-gray-200 rounded text-sm text-gray-500 mt-4">
-        <div class="p-2 bg-sky-100 border-l-2 border-blue-700">本月</div>
-        <div class="p-2">本周</div>
-        <div class="p-2">本年</div>
+        <div class="p-2" v-for="date in dateOptions">{{ date.label }}</div>
         <div class="p-2">
           <div>自定义</div>
           <el-date-picker
             type="daterange"
             class="max-w-full mt-2"
-          ></el-date-picker>
+            v-model="defineDate"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+          />
         </div>
       </div>
-    </nav>
+    </nav> -->
   </div>
 </template>

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

@@ -2,3 +2,8 @@ export const SENDVCODE = "/user/sendVcode";     //发送验证码
 export const REGISTER = "/user/insertCompany";  //注册
 export const LOGIN = "/user/loginAdmin";        //登录
 export const IMPORTTIMELIST = "/sys-form/getExportTemplate" // 下载模板
+
+export const SEX: sexTYpe[] = [
+    { label: "男", value: '1' },
+    { label: "女", value: '0' },
+];

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

@@ -1,4 +1,5 @@
 export const MOD = "/business";
+export const MODURL = "Business";
 export const prefix = "/clue";
 export const GETSYSFILED = "/sys-dict/getListByCode";
 export const GETPERSONNEL = "/user/getSimpleActiveUserList";
@@ -7,6 +8,18 @@ export const GETBUSINESSLIST = `/business-opportunity/list`
 export const UPDATEINSET = `/business-opportunity/insertAndUpdate`
 export const BUSINESSDETELE = `/business-opportunity/delete`
 export const BATCHTRANSFER = `/business-opportunity/claim`
+export const BUSIESS_DETELELIST = `/business-opportunity/deleterList`
+export const BUSIESS_ROWBACK = `/business-opportunity/rollBack`
+export const BUSIESS_PERDETELE = `/business-opportunity/deleterDelete`
+export const BUSIESS_GETSATE = `/business-opportunity/getStage`
+export const BUSIESS_SAVESAIE = `/business-opportunity/saveStage`
+export const BUSIESS_ALL = `/business-opportunity/getAll`
+export const BUSIESS_INFO = `/business-opportunity/getInfo`
+export const DETELEFILEFILE = `/business-opportunity/deleteFile`
+export const REFIENAMEFILE = `/business-opportunity/reFileName`
+export const UPLOADFILEFILE = `/business-opportunity/uploadFile`
+export const URL_IMPOERBUSINESS = `/business-opportunity/importData`
+
 
 export const stageStatus = [
     { id: 1, name: "赢单", progress: "100%" },

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

@@ -3,68 +3,126 @@
         <div class="flex justify-between">
             <div class="title">附件</div>
             <div>
-                <el-button type="primary">上传</el-button>
+                <el-upload ref="uploadRef" :http-request="httpUploadFile" :limit="1" :show-file-list="false"
+                    element-loading-text="正在上传">
+                    <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="fileName" label="附件名称" width="180" />
-                <el-table-column prop="fileSize" label="附件大小" width="120" />
-                <el-table-column prop="uploader" label="上传人" width="120" />
-                <el-table-column prop="uploadTime" label="上传时间" width="180" />
+                <el-table-column prop="name" label="附件名称" width="180" />
+                <el-table-column prop="size" label="附件大小" width="120" />
+                <el-table-column prop="userName" label="上传人" width="120" />
+                <el-table-column prop="createTime" label="上传时间" width="180" />
                 <el-table-column label="操作" width="180" fixed="right">
                     <template #default="scope">
-                        <el-button link type="primary" size="large">下载</el-button>
-                        <el-button link type="primary" size="large">重命名</el-button>
-                        <el-button link type="danger" size="large">删除</el-button>
+                        <el-button link type="primary" size="large" @click="fileDownload(scope.row)">下载</el-button>
+                        <el-button link type="primary" size="large" @click="showVisible(scope.row)">重命名</el-button>
+                        <el-button link type="danger" size="large" @click="deteleFile(scope.row)">删除</el-button>
                     </template>
                 </el-table-column>
             </el-table>
         </div>
+
+        <!-- 弹窗 -->
+        <el-dialog v-model="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()">保存</el-button>
+                        <el-button @click="renameDialogVisible = false">取消</el-button>
+                    </div>
+                </div>
+            </template>
+            <div class="pt-3">
+                <el-input v-model.trim="renameVal" style="width: 100%" class="pb-3" clearable />
+            </div>
+        </el-dialog>
     </div>
 </template>
 <script lang="ts" setup>
-import { ref, reactive, onMounted, onUnmounted, defineExpose, inject } from 'vue'
+import { ElMessageBox, UploadRequestOptions } from 'element-plus';
+import { post, uploadFile } from '@/utils/request';
+import { confirmAction, downloadFile } from '@/utils/tools';
+import { ref, reactive, onMounted, onUnmounted, defineEmits, defineExpose, inject, watchEffect } from 'vue'
+import { DETELEFILEFILE, REFIENAMEFILE, UPLOADFILEFILE } from '../api';
 
-const attachmenttable = ref([{
-    fileName: '文件附件',
-    fileSize: '3.1MB',
-    uploader: '张三',
-    uploadTime: '2024-04-01',
-}, {
-    fileName: '文件附件',
-    fileSize: '3.1MB',
-    uploader: '张三',
-    uploadTime: '2024-04-01',
-}, {
-    fileName: '文件附件',
-    fileSize: '3.1MB',
-    uploader: '张三',
-    uploadTime: '2024-04-01',
-}, {
-    fileName: '文件附件',
-    fileSize: '3.1MB',
-    uploader: '张三',
-    uploadTime: '2024-04-01',
-}, {
-    fileName: '文件附件',
-    fileSize: '3.1MB',
-    uploader: '张三',
-    uploadTime: '2024-04-01',
-}, {
-    fileName: '文件附件',
-    fileSize: '3.1MB',
-    uploader: '张三',
-    uploadTime: '2024-04-01',
-}, {
-    fileName: '文件附件',
-    fileSize: '3.1MB',
-    uploader: '张三',
-    uploadTime: '2024-04-01',
-}])
-// 生命周期钩子
-onMounted(() => {
+const emits = defineEmits(['refreshData']);
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const props = defineProps<{
+    information: any
+}>()
+
+const attachmenttable = ref([])
+const information = ref<any>({})
+const renameDialogVisible = ref(false)
+const renameVal = ref('')
+
+// 下载文件
+function fileDownload(item: any) {
+    downloadFile(`${item.path}`, item.name)
+}
+
+// 删除文件
+function deteleFile(item: any) {
+    const id = item.id
+    confirmAction(`确定删除【${item.name}】文件吗?`).then(() => {
+        post(DETELEFILEFILE, { id }).then((_res) => {
+            globalPopup?.showSuccess('删除成功')
+            emits('refreshData')
+        })
+    })
+}
+
+// 保存重命名
+function saveEditClue() {
+    if (!renameVal.value) {
+        globalPopup?.showWarning('请输入文件名称')
+        return
+    }
+    const bussinessId = information.value.id
+    post(REFIENAMEFILE, { name: renameVal.value, id: bussinessId }).then((res) => {
+        if (res.code == 'ok') {
+            renameDialogVisible.value = false
+            globalPopup?.showSuccess(res.msg || '')
+            emits('refreshData')
+        }
+    }).catch((_err) => { })
+}
+
+// 显示弹窗
+function showVisible(item: any) {
+    renameVal.value = JSON.parse(JSON.stringify(item.name))
+    renameDialogVisible.value = true
+}
+
+// 上传附件
+async function httpUploadFile(param: UploadRequestOptions) {
+    const bussinessId = information.value.id
+    const formData = new FormData();
+    formData.append('file', param.file)
+    formData.append('id', bussinessId)
+    const res = await uploadFile(UPLOADFILEFILE, formData)
+    if (res.code == 'ok') {
+        globalPopup?.showSuccess(res.msg || '')
+        emits('refreshData');
+        return
+    }
+    globalPopup?.showError(res.msg || '')
+    return res
+}
+
+watchEffect(() => {
+    information.value = props.information
+    attachmenttable.value = props.information.uploadFilePList
 });
+
+// 生命周期钩子
+onMounted(() => { });
 </script>
 <style scoped lang="scss">
 .attachment {

+ 160 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/deteleTables.vue

@@ -0,0 +1,160 @@
+<template>
+    <el-dialog v-model="deteleBusinessDialogVisible" width="1000" :before-close="beForeCancel" :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" v-loading="allLoading.batchRecoveryLoading" :disabled="batchTableData.length == 0"
+                        @click="batchOperation('恢复')">批量恢复</el-button>
+                    <el-button type="primary" v-loading="allLoading.batchDeteleLoading" :disabled="batchTableData.length == 0"
+                        @click="batchOperation('删除')">批量删除</el-button>
+                    <el-button @click="cancel()">取消</el-button>
+                </div>
+            </div>
+        </template>
+        <div class="h-[60vh] flex flex-col">
+            <div class="flex-1 w-full overflow-hidden">
+                <el-table ref="busiessTableRef" :data="deteleBusinessTable" border v-loading="allLoading.tableLoading"
+                    @selection-change="changeBatch" style="width: 100%;height: 100%;">
+                    <el-table-column type="selection" width="55" />
+                    <el-table-column v-for="(item, index) in tableColumn" :prop="item.prop" :label="item.label" :key="index"
+                        :width="item.width">
+                        <template #default="scope">
+                            <span>{{ scope.row[item.prop] }}</span>
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="操作" fixed="right" width="120">
+                        <template #default="scope">
+                            <el-button link type="primary" size="large"
+                                @click="businessOperationItem(scope.row.id, scope.row.name, '恢复')">恢复</el-button>
+                            <el-button link type="danger" size="large"
+                                @click="businessOperationItem(scope.row.id, scope.row.name, '删除')">删除</el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+            <div class="flex justify-end pt-3">
+                <el-pagination layout="total, prev, pager, next, sizes" :page-size="tableForm.pageFrom"
+                    @size-change="handleSizeChange" @current-change="handleCurrentChange" :total="businessTotalTable"
+                    :hide-on-single-page="true" />
+            </div>
+        </div>
+    </el-dialog>
+</template>
+<script lang="ts" setup>
+import { post } from '@/utils/request';
+import { ref, reactive, onMounted, watchEffect, watch, inject } from 'vue'
+import { BUSIESS_DETELELIST, BUSIESS_PERDETELE, BUSIESS_ROWBACK, tableColumn } from '../api';
+import { ElTable } from 'element-plus';
+import { confirmAction } from '@/utils/tools';
+import { formatDate } from '@/utils/times';
+
+type operationType = '恢复' | '删除'
+
+const emits = defineEmits(['closeVisible']);
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const deteleBusinessTable = ref([])
+const deteleBusinessDialogVisible = ref(false)
+const businessTotalTable = ref(0)
+const batchTableData = ref([])
+const allLoading = reactive({
+    batchRecoveryLoading: false,
+    batchDeteleLoading: false,
+    tableLoading: false
+})
+
+const tableForm = reactive({
+    pageIndex: 1,
+    pageFrom: 10
+})
+
+const busiessTableRef = ref<InstanceType<typeof ElTable>>() // 线索table dom
+
+const props = defineProps<{
+    visibles: boolean
+}>()
+
+watch(() => props.visibles, (newVal) => {
+    deteleBusinessDialogVisible.value = newVal
+    if (newVal) {
+        getTableList()
+    }
+})
+
+function batchOperation(type: operationType) {
+    const value = batchTableData.value.map((item: any) => item.id).join(',')
+    const label = batchTableData.value.map((item: any) => item.name).join(',')
+    businessOperationItem(value, label, type, true)
+}
+
+function businessOperationItem(value: string | number, label: string, type: operationType, batch: boolean = false) {
+    confirmAction(`确定${batch ? '批量' : ''}${type}【${label}】商机吗?`).then(() => {
+        let url = type == '恢复' ? BUSIESS_ROWBACK : BUSIESS_PERDETELE
+        post(url, { ids: value }).then(res => {
+            if (res.code != 'ok') {
+                globalPopup?.showError(res.msg)
+                return
+            }
+            globalPopup?.showSuccess(`${type}成功`)
+            getTableList()
+            changeBatch(false)
+        }).catch((err) => {
+            globalPopup?.showError(err.message)
+        })
+    })
+}
+
+function changeBatch(flag: boolean = true) {
+    if (flag) {
+        batchTableData.value = busiessTableRef.value && busiessTableRef.value.getSelectionRows()
+    } else {
+        batchTableData.value = []
+        busiessTableRef.value && busiessTableRef.value.clearSelection()
+    }
+}
+
+function getTableList() {
+    allLoading.tableLoading = true
+    post(BUSIESS_DETELELIST, { ...tableForm }).then((res) => {
+        if (res.code == 'ok') {
+            const { data, total } = res.data
+            deteleBusinessTable.value = data.map((item: any) => {
+                return {
+                    ...item,
+                    expectedTransactionDate: formatDate(new Date(item.expectedTransactionDate))
+                }
+            })
+            businessTotalTable.value = total
+        }
+    }).finally(() => {
+        allLoading.tableLoading = false
+    })
+}
+
+function handleSizeChange(val: number) {
+    tableForm.pageIndex = 1
+    tableForm.pageFrom = val
+    getTableList()
+}
+
+function handleCurrentChange(val: number) {
+    tableForm.pageIndex = val
+    getTableList()
+}
+
+function cancel() {
+    emits('closeVisible', 'deteleBusinessVisible')
+}
+
+function beForeCancel(done: () => void) {
+    emits('closeVisible', 'deteleBusinessVisible')
+    done()
+}
+
+onMounted(() => {
+
+})
+
+</script>
+<style lang="scss" scoped></style>

Різницю між файлами не показано, бо вона завелика
+ 165 - 13
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/information.vue


+ 16 - 33
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/operationRecord.vue

@@ -5,45 +5,28 @@
         </div>
         <div class="flex-1 overflow-auto pt-5">
             <el-table :data="operationRecordtable" border style="width: 100%;height: 278px;">
-                <el-table-column prop="operatingTime" label="操作时间" width="140" />
-                <el-table-column prop="operator" label="操作人" width="120" />
-                <el-table-column prop="operationContent" label="操作内容" />
+                <el-table-column prop="creatTime" label="操作时间" width="140" />
+                <el-table-column prop="userName" label="操作人" width="120" />
+                <el-table-column prop="name" label="操作内容" />
             </el-table>
         </div>
     </div>
 </template>
 <script lang="ts" setup>
-import { ref, reactive, onMounted, onUnmounted, defineExpose, inject } from 'vue'
+import { ref, reactive, onMounted, onUnmounted, defineExpose, inject, watchEffect } from 'vue'
+
+const props = defineProps<{
+    information: any
+}>()
+
+const operationRecordtable = ref([])
+const information = ref<any>({})
+
+watchEffect(() => {
+    information.value = props.information
+    operationRecordtable.value = props.information.actionLogList
+});
 
-const operationRecordtable = ref([{
-    operationContent: '转移线索',
-    operator: '张三',
-    operatingTime: '2024-04-01',
-}, {
-    operationContent: '转移线索',
-    operator: '张三',
-    operatingTime: '2024-04-01',
-}, {
-    operationContent: '转移线索',
-    operator: '张三',
-    operatingTime: '2024-04-01',
-}, {
-    operationContent: '转移线索',
-    operator: '张三',
-    operatingTime: '2024-04-01',
-}, {
-    operationContent: '转移线索',
-    operator: '张三',
-    operatingTime: '2024-04-01',
-}, {
-    operationContent: '转移线索',
-    operator: '张三',
-    operatingTime: '2024-04-01',
-}, {
-    operationContent: '转移线索',
-    operator: '张三',
-    operatingTime: '2024-04-01',
-},])
 // 生命周期钩子
 onMounted(() => {
 });

+ 82 - 46
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">编辑产品</el-button>
+                <el-button type="primary" @click="showVisible('editProductVisible')">编辑产品</el-button>
             </div>
         </div>
         <div class="flex-1 overflow-auto pt-3">
@@ -31,56 +31,92 @@
                 <el-table-column prop="endTime" label="合计" width="130" />
             </el-table>
         </div>
+
+        <!-- 弹窗 -->
+        <el-dialog v-model="allVisible.editProductVisible" 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" @click="editProduct()"
+                            :loading="allLoading.editProductLoading">保存</el-button>
+                        <el-button @click="closeVisible('editProductVisible')">取消</el-button>
+                    </div>
+                </div>
+            </template>
+            <div class="h-[60vh] overflow-y-auto scroll-bar pt-3">
+                <RelatedProducts ref="relatedProductsRef" :productTableList="productTableList" :height="'420px'" />
+            </div>
+        </el-dialog>
     </div>
 </template>
 <script lang="ts" setup>
-import { ref, reactive, onMounted, onUnmounted, defineExpose, inject } from 'vue'
+import { ref, reactive, onMounted, onUnmounted, defineExpose, inject, watchEffect } from 'vue'
+import { GETTABLELIST } from '@/pages/product/api';
+import { post } from '@/utils/request';
+
+import RelatedProducts from '@/components/relatedProducts/relatedProducts.vue'
+
+const props = defineProps<{
+    information: any
+}>()
+
+const information = ref<any>({})
+const relatedTaskstable = ref([])
+const productTableList = ref<any>([])
+const relatedProductsRef = ref<typeof RelatedProducts>()
+const allVisible = reactive({
+    editProductVisible: false
+})
+const allLoading = reactive({
+    editProductLoading: false
+})
+
+function editProduct() {
+    let productTableListData = relatedProductsRef?.value?.returnData()
+    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)
+}
+
+function showVisible(type: keyof typeof allVisible) {
+    allVisible[type] = true
+}
+function closeVisible(type: keyof typeof allVisible) {
+    allVisible[type] = false
+}
+function getProductTableList() {
+    post(GETTABLELIST, { pageIndex: -1, pageSize: -1 }).then((res) => {
+        if (res.code == 'ok') {
+            const { record, total } = res.data
+            productTableList.value = record.map((item: any) => {
+                const { id, productName, productCode, unit, unitName, typeName, type, price, inventory } = item
+                return {
+                    id,
+                    productId: id,
+                    productName,
+                    productCode,
+                    unit,
+                    unitName,
+                    price,
+                    type,
+                    typeName,
+                    inventory,
+                    quantity: '',
+                    discount: '',
+                    totalPrice: ''
+                }
+            })
+        }
+    })
+}
+
+watchEffect(() => {
+    information.value = props.information
+});
 
-const relatedTaskstable = ref([{
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}])
-// 生命周期钩子
 onMounted(() => {
+    getProductTableList()
 });
 </script>
 <style scoped lang="scss">

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

@@ -0,0 +1,230 @@
+<template>
+    <div>
+        <el-dialog v-model="stageVisible" width="1000" :before-close="beForeCancel" :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="addStage(false)">新增</el-button>
+                        <el-button type="primary" @click="saveState()" v-loading="allLoading.saveLoading">保存</el-button>
+                        <el-button @click="cancel()">取消</el-button>
+                    </div>
+                </div>
+            </template>
+            <div class="h-[60vh] flex flex-col">
+                <div class="flex-1 w-full overflow-hidden">
+                    <el-table :data="stageTableList" border v-loading="allLoading.tableLoading"
+                        style="width: 100%;height: 100%;">
+                        <el-table-column :prop="'seq'" :label="'顺序'" width="100">
+                            <template #default="scope">
+                                {{ scope.$index + 1 }}
+                            </template>
+                        </el-table-column>
+                        <el-table-column :prop="'name'" :label="'阶段名称'"></el-table-column>
+                        <el-table-column :prop="'plan'" :label="'进度'" width="100">
+                            <template #default="scope">
+                                {{ scope.row.plan }} %
+                            </template>
+                        </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="danger" size="large" @click="deteStage(+scope.$index)">删除</el-button>
+                                <el-button link type="primary" size="large" @click="moveStage(+scope.$index, 'up')"
+                                    v-if="scope.$index != 0">上移</el-button>
+                                <el-button link type="primary" size="large" @click="moveStage(+scope.$index, 'down')"
+                                    v-if="scope.$index < stageTableList.length - 1">下移</el-button>
+                            </template>
+                        </el-table-column>
+                    </el-table>
+                </div>
+            </div>
+
+            <!-- 内层弹窗 -->
+            <el-dialog width="700px" v-model="allVisible.editVisible" append-to-body :show-close="false">
+                <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="editState(true)">保存并新增</el-button>
+                            <el-button type="primary" @click="editState(false)">保存</el-button>
+                            <el-button @click="allVisible.editVisible = false">取消</el-button>
+                        </div>
+                    </div>
+                </template>
+                <div class="h-[120px] flex flex-col pt-5">
+                    <div class="flex flex-row w-full items-center">
+                        <div class="w-[100px] mr-2 text-right">阶段名称:</div>
+                        <div class="flex-1">
+                            <el-input v-model="stageForm.name" placeholder="请输入阶段名称" clearable></el-input>
+                        </div>
+                    </div>
+                    <div class="flex flex-row w-full items-center pt-3">
+                        <div class="w-[100px] mr-2 text-right">进度:</div>
+                        <div class="flex-1">
+                            <el-input-number v-model="stageForm.plan" controls-position="right" :min="0"
+                                :max="100"></el-input-number>
+                            <span class="inline-block ml-2">%</span>
+                        </div>
+                    </div>
+                </div>
+            </el-dialog>
+        </el-dialog>
+    </div>
+</template>
+<script lang="ts" setup>
+import { post } from '@/utils/request';
+import { ref, reactive, onMounted, watch, inject } from 'vue'
+import { BUSIESS_GETSATE, BUSIESS_SAVESAIE } from '../api';
+import { List } from 'echarts';
+
+type moveStageType = 'up' | 'down';
+type stageFormType = {
+    name: string,
+    plan: number,
+    seq: number,
+    id?: number,
+    isFinish?: number
+}
+
+const emits = defineEmits(['closeVisible']);
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const stageVisible = ref(false)
+const stageTableList = ref<stageFormType[]>([])
+const allLoading = reactive({
+    editLoading: false,
+    saveLoading: false,
+    tableLoading: false
+})
+const allVisible = reactive({
+    editVisible: false,
+})
+const stageForm = reactive<stageFormType>({
+    name: '',
+    plan: 0,
+    seq: 0,
+})
+
+const props = defineProps<{
+    visibles: boolean
+}>()
+
+watch(() => props.visibles, (newVal) => {
+    stageVisible.value = newVal
+    if (newVal) {
+        getTableList()
+    }
+})
+
+function saveState() {
+    let newList = JSON.parse(JSON.stringify(stageTableList.value))
+    let data = newList.map((item: stageFormType, index: number) => {
+        return {
+            ...item,
+            plan: item.plan + '%',
+            seq: index
+        }
+    })
+    allLoading.saveLoading = true
+    post(BUSIESS_SAVESAIE, { list: JSON.stringify(data) }).then(() => {
+        globalPopup?.showSuccess('保存成功')
+        cancel()
+    }).finally(() => {
+        allLoading.saveLoading = false
+    })
+}
+
+function editState(flag: boolean) {
+    if (!stageForm.name) {
+        globalPopup?.showWarning('请输入阶段名称')
+        return
+    }
+    const listIndex = stageTableList.value.findIndex((item: stageFormType) => item.name == stageForm.name)
+    const newStage = {
+        ...stageForm,
+        plan: stageForm.plan,
+    }
+    if (listIndex != -1) {
+        stageTableList.value.splice(listIndex, 1, newStage)
+    } else {
+        stageTableList.value.push(newStage)
+    }
+
+    if (flag) {
+        resetStage()
+    }
+    allVisible.editVisible = flag
+}
+
+function addStage(item: any) {
+    const row = JSON.parse(JSON.stringify(item))
+    if (!item) {
+        resetStage()
+    } else {
+        Object.assign(stageForm, {
+            name: row.name,
+            plan: row.plan,
+            seq: row.seq
+        })
+    }
+    allVisible.editVisible = true
+}
+
+function resetStage() {
+    let newData = JSON.parse(JSON.stringify(stageTableList.value))
+    let maxnum = newData.sort((a: any, b: any) => { return b.seq - a.seq; })[0];
+    Object.assign(stageForm, {
+        name: '',
+        plan: 0,
+        seq: +maxnum + 1
+    })
+}
+
+function moveStage(index: number, stageType: moveStageType) {
+    const tableList = stageTableList.value
+    if (stageType == 'up') {
+        stageTableList.value = tableList.slice(0, index - 1).concat(tableList[index], tableList[index - 1], tableList.slice(index + 1))
+    }
+
+    if (stageType == 'down') {
+        stageTableList.value = tableList.slice(0, index).concat(tableList[index + 1], tableList[index], tableList.slice(index + 2))
+    }
+}
+
+function deteStage(index: number) {
+    stageTableList.value.splice(index, 1)
+}
+
+function getTableList() {
+    allLoading.tableLoading = true
+    post(BUSIESS_GETSATE, {}).then((res) => {
+        const { data } = res
+        let newData = (data || []).map((item: any) => {
+            let plans = item.plan.indexOf('%') != -1 ? Number(item.plan.replace(/%/g, '')) : Number(item.plan)
+            return {
+                ...item,
+                plan: plans
+            }
+        })
+        stageTableList.value = newData.sort(function (a: any, b: any) {
+            return a.seq - b.seq;
+        });
+    }).finally(() => {
+        allLoading.tableLoading = false
+    })
+}
+
+function cancel() {
+    emits('closeVisible', 'stageSetVisible')
+}
+
+function beForeCancel(done: () => void) {
+    emits('closeVisible', 'stageSetVisible')
+    done()
+}
+
+onMounted(() => {
+});
+
+</script>
+<style scoped lang="scss"></style>

+ 154 - 39
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/detail/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="h-full flex p-3 flex-col businessDetail">
+  <div class="h-full flex p-3 flex-col businessDetail" v-loading="allLoading.skeletonLoading">
     <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()">
@@ -7,23 +7,24 @@
         </el-link>
       </div>
       <div class="mr-8">
-        <el-select v-model="value" placeholder="请选择" style="width: 150px">
+        <el-select v-model="optionVal" placeholder="请选择" style="width: 150px" filterable @change="getDetail()">
           <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`"
-          v-for="(item, index) in 2" :key="index">
-          <div class="pr-3 text-nowrap">验证客户</div>
-          <div class="text-nowrap">30%</div>
+          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">
-        <el-select v-model="stageStatusVal" placeholder="结束" style="width: 100px" class="selectClas">
-          <el-option v-for="item in stageStatus" :key="item.id" :label="item.name" :value="item.id" />
+      <div class="relative rounded-md flex items-center itemPing backGray endStep item pl-6 pr-6 mr-4 resetStyle">
+        <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-[#0052CC] rounded-md text itemPing pl-2 pr-2 flex items-center">
+      <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>
     </div>
@@ -31,28 +32,47 @@
     <div class="flex-1 flex flex-col overflow-y-auto overflow-x-hidden scroll-bar">
       <div class="w-full h-auto flex justify-between">
         <div class="bg-white shadow-md rounded-md" style="width: 46%;">
-          <Information />
+          <Information :information="businessInfo" @refreshData="refreshData" />
         </div>
         <div class="bg-white ml-2 shadow-md rounded-md flex-1">
-          <Attachment />
+          <Attachment :information="businessInfo" @refreshData="refreshData" />
         </div>
       </div>
 
       <div class="w-full h-auto flex justify-between mt-2">
         <div class="bg-white shadow-md rounded-md" style="width: 65%;">
-          <RelatedTasks />
+          <DetailCompinents :data="detailCompinentsData" :information="businessInfo" :formTaskType="1"
+            :filed="'businessOpportunityId'" :disabledList="['taskType', 'businessOpportunityId']"
+            @refreshData="getDetail" />
         </div>
         <div class="bg-white ml-2 shadow-md rounded-md flex-1">
-          <OperationRecord />
+          <OperationRecord :information="businessInfo" />
         </div>
       </div>
 
       <div class="w-full h-auto flex justify-between mt-2">
         <div class="bg-white shadow-md rounded-md w-full">
-          <Products />
+          <Products :information="businessInfo" />
         </div>
       </div>
     </div>
+
+    <!-- 弹窗 -->
+    <el-dialog v-model="allVisible.advanceVisible" 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">{{ allText.advanceText }}原因</h4>
+          <div>
+            <el-button type="primary" :loading="allLoading.advanceSaveLoading" @click="advanceSave(true)">保存</el-button>
+            <el-button @click="advanceClose">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="pt-3">
+        <div>请输入{{ allText.advanceText }}原因</div>
+        <el-input v-model.trim="advanceVal" style="width: 100%" class="pb-3" :placeholder="'请输入'" clearable />
+      </div>
+    </el-dialog>
   </div>
 </template>
   
@@ -61,39 +81,96 @@ import { ref, reactive, onMounted, inject } from "vue";
 import type { FormInstance, FormRules } from 'element-plus'
 import { Edit, ArrowLeft as IconView } from '@element-plus/icons-vue'
 import { backPath } from '@/utils/tools'
-import { stageStatus } from '../api'
+import { useRoute } from "vue-router";
+import { BUSIESS_ALL, BUSIESS_GETSATE, BUSIESS_INFO } from '../api'
 
 import Information from '../component/information.vue'
 import Attachment from '../component/attachment.vue'
-import RelatedTasks from '../component/relatedTasks.vue';
+// import RelatedTasks from '../component/relatedTasks.vue';
+import DetailCompinents from '@/components/detailcompinents/relatedTasks.vue'
 import OperationRecord from '../component/operationRecord.vue';
 import Products from '../component/products.vue';
+import { post } from "@/utils/request";
+
+type stageListType = {
+  name: string,
+  plan: string
+}
 
-const value = ref('')
+const route = useRoute()
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const detailCompinentsData = ref([])
+const optionVal = ref<any>('')
 const stageStatusVal = ref('')
-const options = [
-  {
-    value: 'Option1',
-    label: 'Option1',
-  },
-  {
-    value: 'Option2',
-    label: 'Option2',
-  },
-  {
-    value: 'Option3',
-    label: 'Option3',
-  },
-  {
-    value: 'Option4',
-    label: 'Option4',
-  },
-  {
-    value: 'Option5',
-    label: 'Option5',
-  },
-]
+const stageStatusValOriginally = ref('')
+const advanceVal = ref('')
+const options = ref<optionType[]>([])
+const allLoading = reactive({
+  skeletonLoading: false,
+  advanceSaveLoading: false
+})
+const allVisible = reactive({
+  advanceVisible: false
+})
+const allText = reactive({
+  advanceText: ''
+})
+
+const businessInfo = ref({})
+const stageListOption = ref<optionType[]>([])
+const stageList = ref<stageListType[]>([])
+
+function refreshData() {
+  getDetail()
+}
+
+function advancementStage() {
+  console.log('点击了推进阶段')
+  advanceSave(false)
+}
+
+function advanceChange() {
+  stageStatusValOriginally.value = JSON.parse(JSON.stringify(stageStatusVal.value))
+  const item: any = stageListOption.value.find((item) => item.value === stageStatusVal.value)
+  console.log(item)
+  if (item.plan == '0%') {
+    advanceVal.value = ''
+    allText.advanceText = item.label
+    allVisible.advanceVisible = true
+    return
+  }
+
+  advanceSave(false)
+}
+
+function advanceClose() {
+  stageStatusVal.value = stageStatusValOriginally.value
+  allVisible.advanceVisible = false
+}
+
+function advanceSave(flag: boolean) {
+  if(!advanceVal && flag) {
+    globalPopup?.showError(`请输入${allText.advanceText}原因`)
+    return
+  }
+  allLoading.advanceSaveLoading = true
+  post('接口', {}).then(() => {
+    globalPopup?.showSuccess('操作成功')
+    getDetail()
+  }).finally(() => {
+    allLoading.advanceSaveLoading = false
+  })
+}
 
+function getDetail() {
+  allLoading.skeletonLoading = true
+  post(BUSIESS_INFO, { id: optionVal.value }).then(({ data }) => {
+    businessInfo.value = (data || []);
+    detailCompinentsData.value = data.taskList || []
+  }).finally(() => {
+    allLoading.skeletonLoading = false
+  })
+}
 
 function handleScroll(event: any) { // 滚表横向滚动
   if (event.deltaY) {
@@ -102,6 +179,31 @@ function handleScroll(event: any) { // 滚表横向滚动
     element.scrollLeft += event.deltaY;
   }
 }
+
+function getOptionAll() {
+  post(BUSIESS_ALL, {}).then((res) => {
+    const { data } = res
+    options.value = (data || []).map((item: any) => ({
+      value: item.id + '',
+      label: item.name
+    }))
+  })
+}
+
+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 }))
+    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 }))
+  })
+}
+
+onMounted(() => {
+  const { id } = route.query
+  optionVal.value = id
+  getSatge()
+  getOptionAll()
+  getDetail()
+})
 </script>
   
 <style lang="scss" scoped>
@@ -159,5 +261,18 @@ function handleScroll(event: any) { // 滚表横向滚动
     padding-top: 4px;
     padding-bottom: 4px;
   }
+
+  .aloneText {
+    padding-top: 9px;
+    padding-bottom: 9px;
+  }
+}
+</style>
+<style lang="scss">
+.resetStyle {
+  .el-select__wrapper {
+    background-color: #F4F5F7;
+    box-shadow: none !important;
+  }
 }
 </style>

+ 90 - 25
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/index.vue

@@ -51,10 +51,10 @@
           <el-button type="primary" @click="showVisible('batchTransferVisible')"
             :disabled="batchTableData.length <= 0">批量转移</el-button>
           <el-button type="primary" @click="batchDeteleItem()" :disabled="batchTableData.length <= 0">批量删除</el-button>
-          <el-button type="primary">阶段设置</el-button>
-          <el-button type="primary">回收站</el-button>
-          <el-button type="primary">导入</el-button>
-          <el-button type="primary">导出</el-button>
+          <el-button type="primary" @click="showVisible('stageSetVisible')">阶段设置</el-button>
+          <el-button type="primary" @click="showVisible('deteleBusinessVisible')">回收站</el-button>
+          <el-button type="primary" @click="showVisible('importVisible')">导入</el-button>
+          <el-button type="primary" @click="exportBusinessTableList()" :loading="allLoading.exoprtLoading">导出</el-button>
         </div>
         <div class="flex-1 w-full overflow-hidden">
           <el-table ref="businessTableRef" :data="businessTable" border v-loading="allLoading.businessTableLading"
@@ -105,11 +105,6 @@
       </div>
     </el-dialog>
 
-    <!-- 新建任务 -->
-    <TaskModal :visible="allVisible.taskModalVisible" :edit-form="taskModalForm" :save-loading="taskLoading"
-      @close="closeVisible('taskModalVisible')" @submit="submitForm" :title="'新建任务'"
-      :disabled-list="['taskType', 'businessOpportunityId']" />
-
     <!-- 批量转移 -->
     <el-dialog v-model="allVisible.batchTransferVisible" width="600" :show-close="false" top="10vh">
       <template #header="{ close, titleId, titleClass }">
@@ -131,22 +126,56 @@
         <div class="pl-3 text-[#e94a4a]">转移后,将看不到此商机</div>
       </div>
     </el-dialog>
+
+    <!-- 导入 -->
+    <el-dialog v-model="allVisible.importVisible" width="680" :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 class="flex">
+            <el-upload class="upload-demo mr-3" :limit="1" :show-file-list="false" accept=".xlsx" :http-request="importBusiness">
+              <el-button type="primary" :loading="allLoading.importLoading">导入</el-button>
+            </el-upload>
+            <el-button @click="allVisible.importVisible = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="p-8">
+        <div class="ml-4 mr-4">
+          <div class="flex items-center">1、点击下载 <el-link type="primary" @click="downloadTemplate(MODURL, '商机导入模板.xlsx')">商机导入模板.xlsx</el-link></div>
+          <div class="mt-4">2、填写excel文件、商机名称、商机金额、商机阶段必填</div>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 新建任务 -->
+    <TaskModal :visible="allVisible.taskModalVisible" :edit-form="taskModalForm" :save-loading="taskLoading"
+      @close="closeVisible('taskModalVisible')" @submit="submitForm" :title="'新建任务'"
+      :disabled-list="['taskType', 'businessOpportunityId']" />
+
+    <!-- 回收站 -->
+    <DeteleBusiness :visibles="allVisible.deteleBusinessVisible" @closeVisible="closeVisible" />
+
+    <!-- 阶段设置 -->
+    <StageSetting :visibles="allVisible.stageSetVisible" @closeVisible="closeVisible" />
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref, reactive, onMounted, inject } from "vue";
-import type { ElTable, FormInstance, FormRules } from 'element-plus'
+import type { ElTable, FormInstance, FormRules, UploadRequestOptions } from 'element-plus'
 import { useRouter, useRoute } from "vue-router";
-import { GETSYSFILED, MOD, GETPERSONNEL, GETGENERATEFOEM, GETBUSINESSLIST, UPDATEINSET, BUSINESSDETELE, BATCHTRANSFER, tableColumn } from './api'
+import { GETSYSFILED, MOD, GETPERSONNEL, GETGENERATEFOEM, GETBUSINESSLIST, UPDATEINSET, BUSINESSDETELE, BATCHTRANSFER, MODURL, tableColumn, BUSIESS_GETSATE, URL_IMPOERBUSINESS } from './api'
 import { GETTABLELIST } from '@/pages/product/api'
-import { post, get } from "@/utils/request";
-import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, createTaskFromType, formatDate, confirmAction } from '@/utils/tools'
+import { post, get, uploadFile } from "@/utils/request";
+import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, createTaskFromType, formatDate, confirmAction, downloadTemplate, downloadFile } from '@/utils/tools'
 import { createTask } from '@/components/TaskModal/taskFunction'
 import { formatDateTime } from '@/utils/times'
 import { GenerateForm } from '@zmjs/form-design';
-import RelatedProducts from './component/relatedProducts.vue'
+import RelatedProducts from '@/components/relatedProducts/relatedProducts.vue'
 import TaskModal from '@/components/TaskModal/index.vue'
+import DeteleBusiness from './component/deteleTables.vue'
+import StageSetting from './component/stageSetting.vue'
 
 const route = useRoute()
 const router = useRouter()
@@ -164,13 +193,18 @@ const allLoading = reactive({
   businessTableLading: false,
   businessSaveLading: false,
   newBusinessSaveLading: false,
-  transferLoading: false
+  transferLoading: false,
+  importLoading: false,
+  exoprtLoading: false,
 })
 const allVisible = reactive({
   newBusinessisible: false,
   recycleVisible: false,
   taskModalVisible: false,
-  batchTransferVisible: false
+  batchTransferVisible: false,
+  deteleBusinessVisible: false,
+  stageSetVisible: false,
+  importVisible: false
 })
 const allText = reactive({
   newBusinessisibleText: '新建商机',
@@ -284,6 +318,31 @@ function businessDeteleItem(value: string | number, label: string, batch: boolea
   })
 }
 
+async function importBusiness(param: UploadRequestOptions) {
+  allLoading.importLoading = true
+  const formData = new FormData();
+  formData.append('multipartFile', param.file)
+  const res = await uploadFile(URL_IMPOERBUSINESS, formData).finally(() => {
+    allLoading.importLoading = false
+  })
+  if (res.code == 'ok') {
+    globalPopup?.showSuccess('导入成功' || '')
+    getBusinessTableList()
+    return
+  }
+  globalPopup?.showError(res.msg || '')
+}
+
+function exportBusinessTableList() {
+  allLoading.exoprtLoading = true
+  let valueForm = getFromValue(businessOpportunityForm)
+  post('接口名称', {...valueForm}).then((res) => {
+    downloadFile(res.data, '商机表导出.xlsx')
+  }).finally(() => {
+    allLoading.exoprtLoading = false
+  })
+}
+
 function changeBatch(flag: boolean = true) {
   if (flag) {
     batchTableData.value = businessTableRef.value && businessTableRef.value.getSelectionRows()
@@ -335,15 +394,21 @@ function resetForm() {
 }
 
 async function getSystemField() {
-  const systemField = getAllListByCode(['商机阶段'])
-  for (let i in systemField) {
-    const { data } = await get(`${GETSYSFILED}?code=${systemField[i]}`)
-    for (let key of Object.keys(fixedData)) {
-      if (systemField[i] == key) {
-        Object.assign(fixedData, { [key]: data })
-      }
-    }
-  }
+  // const systemField = getAllListByCode(['商机阶段'])
+  // for (let i in systemField) {
+  //   const { data } = await get(`${GETSYSFILED}?code=${systemField[i]}`)
+  //   for (let key of Object.keys(fixedData)) {
+  //     if (systemField[i] == key) {
+  //       Object.assign(fixedData, { [key]: data })
+  //     }
+  //   }
+  // }
+
+  const row = await post(BUSIESS_GETSATE, {})
+  fixedData.BusinessStage = (row.data || []).map((item: any) => {
+    const { name, id, seq } = item
+    return { name, id, seq }
+  }).sort(function (a: any, b: any) {return a.seq - b.seq;});
 
   const { data } = await post(GETPERSONNEL, {})
   fixedData.Personnel = data.map((item: any) => {

+ 2 - 2
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/type.d.ts

@@ -14,8 +14,8 @@ interface businessOpportunityFormType {
 
 interface fixedDataInterface {
   id: string | number;
-  companyId: string | number;
-  code: string;
+  companyId?: string | number;
+  code?: string;
   name: string;
   seq: string | number;
 }

+ 29 - 10
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/api.ts

@@ -1,11 +1,25 @@
+import { SEX } from '../api.ts'
 export const MOD = 'contacts'
+export const URL = '/contacts'
+export const IMPORTMOD = 'Contacts'
 
 export const GETSYSFILED = "/sys-dict/getListByCode";
 export const GETPERSONNEL = "/user/getSimpleActiveUserList";
-export const GETGENERATEFOEM = `sys-form/getListByCode${MOD}`
+export const GETGENERATEFOEM = `sys-form/getListByCode/${MOD}`
+
+export const URL_PAGECONTACTS = `${URL}/pageContacts`
+export const URL_ADD = `${URL}/addContacts`
+export const URL_UPLOAD = `${URL}/updateContacts`
+export const URL_DETELERECYCLE =  `${URL}/deleteContacts`
+export const URL_BATCHDETELE = `${URL}/confirmDeleteContacts`
+export const URL_RECYCLELIST = `${URL}/getDeletedContacts`
+export const URL_DETELEITEM = `${URL}/confirmDeleteContacts`
+export const URL_RESTORE = `${URL}/returnContacts`
+export const URL_GETALL = `${URL}/getAllContacts`
+export const URL_GETDETAIL = `${URL}/getContactsDetail`
 
 export const actionButtons: any[] = [
-    { text: '批量转移' },
+    { text: '新建联系人' },
     { text: '批量删除' },
     { text: '导入' },
     { text: '导出' },
@@ -13,12 +27,17 @@ export const actionButtons: any[] = [
 
 export const tableColumns: TableColumn[] = [
     { prop: 'name', label: '联系人', event: 'toDetali', width: '150' },
-    { prop: 'mobile', label: '客户名称', width: '150' },
-    { prop: 'email', label: '电话号码', width: '200' },
-    { prop: 'wechat', label: '邮箱', width: '200' },
+    { prop: 'customName', label: '客户名称', width: '150' },
+    { prop: 'phone', label: '电话号码', width: '200' },
+    { prop: 'email', label: '邮箱', width: '200' },
     { prop: 'position', label: '职务', width: '100' },
-    { prop: 'company', label: '性别', width: '100' },
-    { prop: 'companya', label: '负责人', width: '100' },
-    { prop: 'companyb', label: '创建人', width: '100' },
-    { prop: 'companyc', label: '创建时间', width: '200' },
-]
+    { prop: 'sex', label: '性别', width: '100', event: 'getSex' },
+    { prop: 'ownerName', label: '负责人', width: '100' },
+    { prop: 'creatorName', label: '创建人', width: '100' },
+    { prop: 'createTime', label: '创建时间', width: '200' },
+]
+
+export const getSex = (val: number) => {
+    let sexItem = SEX.filter((item: sexTYpe) => item.value == val)
+    return sexItem.length > 0 ? sexItem[0].label : ''
+}

+ 160 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/component/deteleTables.vue

@@ -0,0 +1,160 @@
+<template>
+    <el-dialog v-model="deteleBusinessDialogVisible" width="1000" :before-close="beForeCancel" :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" v-loading="allLoading.batchRecoveryLoading" :disabled="batchTableData.length == 0"
+                        @click="batchOperation('恢复')">批量恢复</el-button>
+                    <el-button type="primary" v-loading="allLoading.batchDeteleLoading" :disabled="batchTableData.length == 0"
+                        @click="batchOperation('删除')">批量删除</el-button>
+                    <el-button @click="cancel()">取消</el-button>
+                </div>
+            </div>
+        </template>
+        <div class="h-[60vh] flex flex-col">
+            <div class="flex-1 w-full overflow-hidden">
+                <el-table ref="busiessTableRef" :data="deteleBusinessTable" border v-loading="allLoading.tableLoading"
+                    @selection-change="changeBatch" style="width: 100%;height: 100%;">
+                    <el-table-column type="selection" width="55" />
+                    <el-table-column v-for="(item, index) in tableColumns" :prop="item.prop" :label="item.label" :key="index"
+                        :width="item.width">
+                        <template #default="scope">
+                            <span>{{ scope.row[item.prop] }}</span>
+                        </template>
+                    </el-table-column>
+                    <el-table-column label="操作" fixed="right" width="120">
+                        <template #default="scope">
+                            <el-button link type="primary" size="large"
+                                @click="businessOperationItem(scope.row.id, scope.row.name, '恢复')">恢复</el-button>
+                            <el-button link type="danger" size="large"
+                                @click="businessOperationItem(scope.row.id, scope.row.name, '删除')">删除</el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+            <div class="flex justify-end pt-3">
+                <el-pagination layout="total, prev, pager, next, sizes" :page-size="tableForm.pageSize"
+                    @size-change="handleSizeChange" @current-change="handleCurrentChange" :total="businessTotalTable"
+                    :hide-on-single-page="true" />
+            </div>
+        </div>
+    </el-dialog>
+</template>
+<script lang="ts" setup>
+import { post } from '@/utils/request';
+import { ref, reactive, onMounted, watchEffect, watch, inject } from 'vue'
+import { URL_RECYCLELIST, URL_DETELEITEM, URL_RESTORE, tableColumns } from '../api';
+import { ElTable } from 'element-plus';
+import { confirmAction } from '@/utils/tools';
+import { formatDate } from '@/utils/times';
+
+type operationType = '恢复' | '删除'
+
+const emits = defineEmits(['closeVisible']);
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const deteleBusinessTable = ref([])
+const deteleBusinessDialogVisible = ref(false)
+const businessTotalTable = ref(0)
+const batchTableData = ref([])
+const allLoading = reactive({
+    batchRecoveryLoading: false,
+    batchDeteleLoading: false,
+    tableLoading: false
+})
+
+const tableForm = reactive({
+    pageIndex: 1,
+    pageSize: 10
+})
+
+const busiessTableRef = ref<InstanceType<typeof ElTable>>() // 线索table dom
+
+const props = defineProps<{
+    visibles: boolean
+}>()
+
+watch(() => props.visibles, (newVal) => {
+    deteleBusinessDialogVisible.value = newVal
+    if (newVal) {
+        getTableList()
+    }
+})
+
+function batchOperation(type: operationType) {
+    const value = batchTableData.value.map((item: any) => item.id).join(',')
+    const label = batchTableData.value.map((item: any) => item.name).join(',')
+    businessOperationItem(value, label, type, true)
+}
+
+function businessOperationItem(value: string | number, label: string, type: operationType, batch: boolean = false) {
+    confirmAction(`确定${batch ? '批量' : ''}${type}【${label}】商机吗?`).then(() => {
+        let url = type == '恢复' ? URL_RESTORE : URL_DETELEITEM
+        post(url, { ids: value }).then(res => {
+            if (res.code != 'ok') {
+                globalPopup?.showError(res.msg)
+                return
+            }
+            globalPopup?.showSuccess(`${type}成功`)
+            getTableList()
+            changeBatch(false)
+        }).catch((err) => {
+            globalPopup?.showError(err.message)
+        })
+    })
+}
+
+function changeBatch(flag: boolean = true) {
+    if (flag) {
+        batchTableData.value = busiessTableRef.value && busiessTableRef.value.getSelectionRows()
+    } else {
+        batchTableData.value = []
+        busiessTableRef.value && busiessTableRef.value.clearSelection()
+    }
+}
+
+function getTableList() {
+    allLoading.tableLoading = true
+    post(URL_RECYCLELIST, { ...tableForm }).then((res) => {
+        if (res.code == 'ok') {
+            const { data, total } = res.data
+            deteleBusinessTable.value = data.map((item: any) => {
+                return {
+                    ...item,
+                    expectedTransactionDate: formatDate(new Date(item.expectedTransactionDate))
+                }
+            })
+            businessTotalTable.value = total
+        }
+    }).finally(() => {
+        allLoading.tableLoading = false
+    })
+}
+
+function handleSizeChange(val: number) {
+    tableForm.pageIndex = 1
+    tableForm.pageSize = val
+    getTableList()
+}
+
+function handleCurrentChange(val: number) {
+    tableForm.pageIndex = val
+    getTableList()
+}
+
+function cancel() {
+    emits('closeVisible', 'deteleContactsVisible')
+}
+
+function beForeCancel(done: () => void) {
+    emits('closeVisible', 'deteleContactsVisible')
+    done()
+}
+
+onMounted(() => {
+
+})
+
+</script>
+<style lang="scss" scoped></style>

+ 33 - 8
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/detail/index.vue

@@ -1,5 +1,5 @@
 <template>
-    <div class="h-full flex p-3 flex-col businessDetail">
+    <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()">
@@ -7,8 +7,8 @@
                 </el-link>
             </div>
             <div class="mr-8">
-                <el-select v-model="value" placeholder="请选择" style="width: 150px">
-                    <el-option v-for="item in options" :key="item.id" :label="item.productName" :value="item.id" />
+                <el-select v-model="values" placeholder="请选择" style="width: 150px">
+                    <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
                 </el-select>
             </div>
         </div>
@@ -25,7 +25,8 @@
 
             <div class="w-full h-auto flex justify-between mt-2">
                 <div class="bg-white shadow-md rounded-md" style="width: 65%;">
-                    <RelatedTasks :data="relatedTasks" :information="information" />
+                    <!-- <RelatedTasks :data="relatedTasks" :information="information" /> -->
+                    <Detailcompinents :data="relatedTasks" :information="information" :formTaskType="0" :filed="'contactsId'" :disabledList="['contactsId']" @refreshData="getDetail" />
                 </div>
                 <div class="bg-white ml-2 shadow-md rounded-md flex-1">
                     <OperationRecord :data="operationRecord" />
@@ -45,26 +46,50 @@
 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 { URL_GETALL, URL_GETDETAIL } from "../api";
 
 import Information from '../component/information.vue'
 import Attachment from '../component/attachment.vue'
 import RelatedTasks from '../component/relatedTasks.vue'
 import OperationRecord from '../component/operationRecord.vue'
+import Detailcompinents from '@/components/detailcompinents/relatedTasks.vue'
 import RelatedBusiness from '../component/relatedBusiness.vue'
 
+const route = useRoute()
+const globalPopup = inject<GlobalPopup>('globalPopup')
 const information = ref({}) // 基本信息
 const attachment = ref([]) // 附件
 const relatedTasks = ref([]) // 相关任务
 const operationRecord = ref([]) // 操作记录
 const relatedBusiness = ref([]) // 相关商机
-
+const rowId = ref(+(route.query.id || ''))
+const values = ref<number | string>('')
+const options = ref<optionType[]>([])
 const pageLoading = ref(false)
 
-const value = ref('')
-const options: any = ref([])
+function getDetail() {
+    pageLoading.value = true
+    post(URL_GETDETAIL, {}).then((res) => {
+        console.log(res)
+    }).finally(() => {
+        pageLoading.value = false
+    })
+}
+
+function getAllContacts() {
+    post(URL_GETALL, {}).then(({ data }) => {
+        options.value = (data || []).map((item: any) => ({ value: item.id, label: item.name }))
+    }).catch((err) => {
+        globalPopup?.showError(err.message)
+    })
+}
 
 onMounted(() => {
-    
+    values.value = rowId.value || ''
+    getAllContacts()
+    getDetail()
 })
 </script>
     

+ 309 - 44
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/index.vue

@@ -6,59 +6,139 @@
           <!-- 筛选条件 -->
           <el-form :model="filterForm" label-width="70px" style="max-width: 600px">
             <el-form-item v-for="(item, index) in filterItems" :key="index" :label="item.label">
-              <el-input v-if="item.type === 'input'" v-model="filterForm[item.key as keyof FilterForm]" clearable placeholder="请输入"></el-input>
+              <el-input v-if="item.type === 'input'" v-model="filterForm[item.key as keyof FilterForm]" clearable
+                placeholder="请输入"></el-input>
               <el-select v-else v-model="filterForm[item.key as keyof FilterForm]" placeholder="请选择" clearable>
                 <el-option v-for="option in item.options" :key="option.id" :label="option.name" :value="option.id" />
               </el-select>
             </el-form-item>
           </el-form>
         </div>
+        <div class="w-full flex p-3 shadow-[0_-3px_5px_0px_rgba(0,0,0,0.2)]">
+          <El-button class="w-full" @click="resetForm()" :loading="allLoading.formTableLading">重置</El-Button>
+          <El-button type="primary" class="w-full" :loading="allLoading.formTableLading"
+            @click="getContactPerson()">搜索</El-Button>
+        </div>
       </div>
     </div>
     <div class="flex-1 p-5 overflow-auto">
       <div class="bg-white w-full h-full p-3 shadow-md rounded-md flex flex-col">
         <div class="flex justify-end pb-3">
           <!-- 操作按钮 -->
-          <el-button v-for="(button, index) in actionButtons" :key="index" type="primary">{{ button.text }}</el-button>
+          <!-- <el-button v-for="(button, index) in actionButtons" :key="index" type="primary">{{ button.text }}</el-button> -->
+          <el-button type="primary" @click="editContacts(false)">新建联系人</el-button>
+          <el-button type="primary" @click="batchDeteleItem" :disabled="batchTableData.length <= 0">批量删除</el-button>
+          <el-button type="primary" @click="showVisible('deteleContactsVisible')">回收站</el-button>
+          <el-button type="primary" @click="showVisible('importVisible')">导入</el-button>
+          <el-button type="primary" @click="exportCustomerTableList()" :loading="allLoading.exoprtLoading">导出</el-button>
         </div>
         <div class="flex-1 w-full overflow-hidden">
           <!-- 表格 -->
-          <el-table ref="clueTableRef" :data="formTable" border v-loading="allLoading.formTableLading"
-            style="width: 100%;height: 100%;">
-            <el-table-column v-for="(column, index) in tableColumns" :key="index" :prop="column.prop" :label="column.label" :width="column.width">
+          <el-table ref="contactsTableRef" :data="formTable" border v-loading="allLoading.formTableLading"
+            style="width: 100%;height: 100%;" @selection-change="changeBatch">
+            <el-table-column type="selection" width="55" />
+            <el-table-column v-for="(column, index) in tableColumns" :key="index" :prop="column.prop"
+              :label="column.label" :width="column.width">
               <template #default="scope">
                 <template v-if="column.event === 'toDetali'">
-                  <el-button link type="primary" size="large" @click="toDetali(scope.row)">{{ scope.row.name }}</el-button>
+                  <el-button link type="primary" size="large" @click="toDetali(scope.row)">{{ scope.row.name
+                  }}</el-button>
+                </template>
+                <template v-if="column.event === 'getSex'">
+                  {{ getSex(scope.row.sex) }}
                 </template>
               </template>
             </el-table-column>
             <el-table-column :label="'操作'" :width="'200px'" fixed="right">
               <template #default="scope">
-                <el-button link type="primary" size="large">编辑</el-button>
-                <el-button link type="primary" size="large">新建任务</el-button>
-                <el-button link type="danger" size="large">删除</el-button>
+                <el-button link type="primary" size="large" @click="editContacts(scope.row)">编辑</el-button>
+                <el-button link type="primary" size="large" @click="newTask(scope.row)">新建任务</el-button>
+                <el-button link type="danger" size="large"
+                  @click="contactsDeteleItem(scope.row.id, scope.row.customName)">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
         </div>
         <div class="flex justify-end pt-3">
           <!-- 分页 -->
-          <el-pagination layout="total, prev, pager, next, sizes" :total="formTablePaging.total"
-            :hide-on-single-page="true" />
+          <el-pagination layout="total, prev, pager, next, sizes" @size-change="handleSizeChange"
+            @current-change="handleCurrentChange" :total="tableTotal" :hide-on-single-page="true" />
         </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">{{ allText.editContactsText }}</h4>
+          <div>
+            <el-button type="primary" :loading="allLoading.editContactsSaveLoading"
+              @click="editContactsSave(true)">保存并新建</el-button>
+            <el-button type="primary" :loading="allLoading.editContactsSaveLoading"
+              @click="editContactsSave(false)">保存</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>
+
+    <!-- 回收站 -->
+    <DeteleTables :visibles="allVisible.deteleContactsVisible" @closeVisible="closeVisible" />
+
+    <!-- 导入 -->
+    <el-dialog v-model="allVisible.importVisible" width="680" :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 class="flex">
+            <el-upload class="upload-demo mr-3" :limit="1" :show-file-list="false" accept=".xlsx"
+              :http-request="importBusiness">
+              <el-button type="primary" :loading="allLoading.importLoading">导入</el-button>
+            </el-upload>
+            <el-button @click="allVisible.importVisible = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="p-8">
+        <div class="ml-4 mr-4">
+          <div class="flex items-center">1、点击下载 <el-link type="primary"
+              @click="downloadTemplate(IMPORTMOD, allText.importText)">{{ allText.importText }}</el-link></div>
+          <div class="mt-4">2、填写excel文件、联系人、客户名称必填</div>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 任务 -->
+    <TaskModal :visible="allVisible.taskModalVisible" :edit-form="taskModalForm" :save-loading="taskLoading"
+      @close="closeVisible('taskModalVisible')" @submit="submitForm" :title="'新建任务'"
+      :disabled-list="['contactsId']" />
+
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref, reactive, onMounted, inject } from "vue";
-import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, getLastDayOfMonth, formatDate } from '@/utils/tools'
-import { post, get } from "@/utils/request";
-import { actionButtons, tableColumns, GETSYSFILED, GETPERSONNEL, GETGENERATEFOEM, MOD } from "./api";
+import { getAllListByCode, getFromValue, resetFromValue, getTemplateKey, confirmAction, downloadTemplate, downloadFile, createTaskFromType } from '@/utils/tools'
+import { post, get, uploadFile } from "@/utils/request";
+import { actionButtons, tableColumns, GETSYSFILED, GETPERSONNEL, GETGENERATEFOEM, MOD, URL_PAGECONTACTS, getSex, URL_ADD, URL_UPLOAD, URL_BATCHDETELE, URL_DETELERECYCLE, IMPORTMOD } from "./api";
 import { useRouter, useRoute } from "vue-router";
+import { GenerateForm } from '@zmjs/form-design';
+import { URL_FETALL } from "../customer/api";
+import { ElTable, UploadRequestOptions } from "element-plus";
+
+import DeteleTables from './component/deteleTables.vue'
+import TaskModal from '@/components/TaskModal/index.vue'
+import { createTask } from "@/components/TaskModal/taskFunction";
 
 const router = useRouter()
+const globalPopup = inject<GlobalPopup>('globalPopup')
 
 const filterForm = reactive<FilterForm>({ // 筛选条件
   contactPerson: "",
@@ -68,53 +148,209 @@ const filterForm = reactive<FilterForm>({ // 筛选条件
   createId: '',
   email: '',
 });
+const formTablePaging = reactive({ // 分页条件
+  pageIndex: 1,
+  pageSize: 10,
+})
+const tableTotal = ref(0)
 const selectData = reactive({ // 下拉数据
   Personnel: [] as personnelInterface[],
   Customer: [] as any[],
 })
 const filterItems = ref<FilterItem[]>([
-    { label: '联系人', key: 'contactPerson', type: 'input' },
-    { label: '客户名称', key: 'customerId', type: 'select', options: selectData.Customer },
-    { label: '电话号码', key: 'phoneNumber', type: 'input' },
-    { label: '邮箱', key: 'email', type: 'input' },
-    { label: '负责人', key: 'responsibleId', type: 'select', options: selectData.Personnel },
-    { label: '创建人', key: 'createId', type: 'select', options: selectData.Personnel },
+  { label: '联系人', key: 'contactPerson', type: 'input' },
+  { label: '客户名称', key: 'customerId', type: 'select', options: selectData.Customer },
+  { label: '电话号码', key: 'phoneNumber', type: 'input' },
+  { label: '邮箱', key: 'email', type: 'input' },
+  { label: '负责人', key: 'responsibleId', type: 'select', options: selectData.Personnel },
+  { label: '创建人', key: 'createId', type: 'select', options: selectData.Personnel },
 ])
-const formTablePaging = reactive({ // 分页条件
-  currentPage: 1,
-  pageSize: 10,
-  total: 0,
+const contactsTemplate = ref({
+  list: [],
+  config: {}
 })
-const generateFormData = ref([]) // 自定义表单数据
-
-const formTable = ref([
-  {name: '联系人', mobile: '13311112222', email: '123@qq.com', responsible: '张三', createId: '张三'}
-]) // 表格数据
+const contactsTemplateValue = ref({})
+const contactsTemplateRefKey = ref(1)
+const contactsTemplateRef = ref<typeof GenerateForm>()
+const contactsTableRef = ref<InstanceType<typeof ElTable>>()
+const formTable = ref([]) // 表格数据
+const batchTableData = ref([])
+const taskModalForm = ref({})
+const taskLoading = ref<saveLoadingType>('1')
 const allLoading = reactive({ // 按钮加载 Loading
   formTableLading: false,
+  editContactsSaveLoading: false,
+  contactsTemplateRefLoading: false,
+  importLoading: false,
+  exoprtLoading: false
 })
-const dialogVisible = reactive({
-
+const allVisible = reactive({
+  editContactsVisible: false,
+  taskModalVisible: false,
+  deteleContactsVisible: false,
+  importVisible: false
+})
+const allText = reactive({
+  editContactsText: '新建联系人',
+  importText: '联系人导入模板.xlsx',
+  exportText: '联系人导出.xlsx'
 })
 
-function toDetali(row: any) {
-  router.push({
-    path: `${MOD}/detail`,
-    query: { id: row.name }
+// 方法
+function submitForm(submitData: any, isClose: boolean) {
+  taskLoading.value = '2'
+  createTask(submitData, isClose).then((res) => {
+    const { saveLoading, isClose } = res
+    taskLoading.value = saveLoading
+    allVisible.taskModalVisible = isClose
+    globalPopup?.showSuccess('新增成功')
+  }).catch((err) => {
+    const { saveLoading, isClose, message } = err
+    taskLoading.value = saveLoading
+    allVisible.taskModalVisible = isClose
+    globalPopup?.showError(message)
   })
 }
 
-async function getSystemField() {
-  const systemField = getAllListByCode([])
-  for (let i in systemField) {
-    const { data } = await get(`${GETSYSFILED}?code=${systemField[i]}`)
-    for (let key of Object.keys(selectData)) {
-      if (systemField[i] == key) {
-        Object.assign(selectData, { [key]: data })
+function newTask(item: any) {
+  const { id } = item
+  taskModalForm.value = { ...createTaskFromType(0), contactsId: id, }
+  showVisible('taskModalVisible')
+}
+
+function exportCustomerTableList() {
+  allLoading.exoprtLoading = true
+  let valueForm = getFromValue(filterForm)
+  post('接口名称', { ...valueForm }).then((res) => {
+    downloadFile(res.data, allText.exportText)
+  }).finally(() => {
+    allLoading.exoprtLoading = false
+  })
+}
+
+async function importBusiness(param: UploadRequestOptions) {
+  allLoading.importLoading = true
+  const formData = new FormData();
+  formData.append('multipartFile', param.file)
+  const res = await uploadFile('接口名称', formData).finally(() => {
+    allLoading.importLoading = false
+  })
+  if (res.code == 'ok') {
+    globalPopup?.showSuccess('导入成功' || '')
+    getContactPerson()
+    return
+  }
+  globalPopup?.showError(res.msg || '')
+}
+
+function batchDeteleItem() {
+  const value = batchTableData.value.map((item: any) => item.id).join(',')
+  const label = batchTableData.value.map((item: any) => item.customName).join(',')
+  contactsDeteleItem(value, label, true)
+}
+
+function contactsDeteleItem(value: string | number, label: string, batch: boolean = false) {
+  confirmAction(`确定${batch ? '批量' : ''}删除【${label}】客户吗?`).then(() => {
+    let url = batch ? URL_BATCHDETELE : URL_DETELERECYCLE
+    post(url, { ids: value }).then(res => {
+      if (res.code != 'ok') {
+        globalPopup?.showError(res.msg)
+        return
+      }
+      globalPopup?.showSuccess('删除成功')
+      changeBatch(false)
+      getContactPerson()
+    }).catch((err) => {
+      globalPopup?.showError(err.message)
+    })
+  })
+}
+
+function changeBatch(flag: boolean = true) {
+  if (flag) {
+    batchTableData.value = contactsTableRef.value && contactsTableRef.value.getSelectionRows()
+  } else {
+    batchTableData.value = []
+    contactsTableRef.value && contactsTableRef.value.clearSelection()
+  }
+}
+
+
+function editContactsSave(flag: boolean) {
+  contactsTemplateRef.value?.getData().then((res: any) => {
+    allLoading.editContactsSaveLoading = true
+    let url = allText.editContactsText == '新建联系人' ? URL_ADD : URL_UPLOAD
+    post(url, { ...contactsTemplateValue.value, ...res }).then((_res) => {
+      allVisible.editContactsVisible = flag
+      globalPopup?.showSuccess('保存成功')
+      if (flag) {
+        contactsTemplateRef.value?.reset()
+        allText.editContactsText = '新建联系人'
+      }
+      getContactPerson()
+    }).finally(() => {
+      allLoading.editContactsSaveLoading = false
+    })
+  }).catch((_err: any) => {
+    console.log(_err)
+    globalPopup?.showError('请填写完整')
+  })
+}
+
+function editContacts(row: any) { // row 有数据代表编辑
+  showVisible('editContactsVisible')
+  if (row) {
+    const templateKey = getTemplateKey(contactsTemplate.value.list)
+    const formVal: templateKey = { id: row.id }
+    for (let i in templateKey) {
+      if(row[templateKey[i]]) {
+        formVal[templateKey[i]] = templateKey[i] == 'sex' ? row[templateKey[i]] + '' : row[templateKey[i]]
       }
     }
+    setTemplateVal(formVal)
+    allText.editContactsText = '编辑联系人'
+  } else {
+    setTemplateVal()
+    allText.editContactsText = '新建联系人'
   }
+}
+
+function setTemplateVal(val: any = {}) {
+  console.log(val)
+  contactsTemplateValue.value = val
+  allLoading.contactsTemplateRefLoading = true
+  setTimeout(() => {
+    contactsTemplateRefKey.value++
+    allLoading.contactsTemplateRefLoading = false
+  }, 1000);
+}
 
+function toDetali(row: any) {
+  router.push({
+    path: `${MOD}/detail`,
+    query: { id: row.id }
+  })
+}
+
+function getContactPerson() {
+  allLoading.formTableLading = true
+  const formVal = getFromValue(filterForm)
+  post(URL_PAGECONTACTS, { ...formVal, ...formTablePaging }).then((res) => {
+    const { records, total } = res.data
+    formTable.value = records
+    tableTotal.value = total
+  }).finally(() => {
+    allLoading.formTableLading = false
+  })
+}
+
+function resetForm() {
+  let newResetForm = resetFromValue(filterForm)
+  Object.assign(filterForm, newResetForm)
+  getContactPerson()
+}
+
+async function getSystemField() {
   const { data } = await post(GETPERSONNEL, {})
   selectData.Personnel = data.map((item: any) => {
     const { id, name, phone, jobNumber } = item
@@ -123,12 +359,40 @@ async function getSystemField() {
     }
   })
 
-  // const res = await get(GETGENERATEFOEM)
-  // generateFormData.value = JSON.parse(res.data[0].config)
+  const res = await get(URL_FETALL, {})
+  selectData.Customer = (res.data || []).map((item: any) => {
+    const { id, customName } = item
+    return {
+      id,
+      name: customName
+    }
+  })
+
+  const datas = await get(GETGENERATEFOEM)
+  contactsTemplate.value = JSON.parse(datas.data[0].config)
 
   setFilterItems()
 }
 
+function handleSizeChange(val: number) {
+  formTablePaging.pageIndex = 1
+  formTablePaging.pageSize = val
+  getContactPerson()
+}
+
+function handleCurrentChange(val: number) {
+  formTablePaging.pageIndex = val
+  getContactPerson()
+}
+
+function showVisible(type: keyof typeof allVisible) {
+  allVisible[type] = true
+}
+
+function closeVisible(type: keyof typeof allVisible) {
+  allVisible[type] = false
+}
+
 function setFilterItems() {
   console.log(selectData)
   filterItems.value = [
@@ -143,6 +407,7 @@ function setFilterItems() {
 
 onMounted(() => {
   getSystemField()
+  getContactPerson()
 })
 </script>
 

+ 74 - 39
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/corpreport/index.vue

@@ -1,42 +1,33 @@
-<template>
-  <div class="m-5 bg-white h-full p-4 rounded">
-    <div class="flex justify-start">
-      <span class="mr-12 font-bold text-gray-900">客户总量分析</span>
-      <el-form class="flex gap-4">
-        <el-form-item class="w-52">
-          <el-select>
-            <el-option
-              v-for="date in dateOptions"
-              :key="date.value"
-              :label="date.label"
-              :value="date.value"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item class="w-52">
-          <el-select>
-            <el-option label="按部门" value="1" />
-            <el-option label="按员工" value="2" />
-          </el-select>
-        </el-form-item>
-        <el-form-item class="w-52">
-          <el-select></el-select>
-        </el-form-item>
-        <el-button type="primary">搜索</el-button>
-      </el-form>
-    </div>
-    <el-table :data="dataSource">
-      <el-table-column prop="date" label="员工姓名" />
-      <el-table-column prop="date" label="新增客户数" />
-      <el-table-column prop="date" label="成交客户数" />
-      <el-table-column prop="date" label="客户成交率(%)" />
-      <el-table-column prop="date" label="合同总金额" />
-      <el-table-column prop="date" label="回款金额" />
-    </el-table>
-  </div>
-</template>
-
 <script lang="ts" setup>
+import { ref, reactive, onMounted, watchEffect } from 'vue';
+
+import Echarts from '@/components/ReEcharts/index.vue';
+import { EChartsOption } from 'echarts';
+
+const chartOptions: EChartsOption = {
+  grid: { bottom: 30 },
+  legend: {},
+  tooltip: {},
+  dataset: {
+    dimensions: ['product', '2015', '2016', '2017'],
+    source: [
+      { product: 'Matcha Latte', 2015: 43.3, 2016: 85.8, 2017: 93.7 },
+      { product: 'Milk Tea', 2015: 83.1, 2016: 73.4, 2017: 55.1 },
+      { product: 'Cheese Cocoa', 2015: 86.4, 2016: 65.2, 2017: 82.5 },
+      { product: 'Walnut Brownie', 2015: 72.4, 2016: 53.9, 2017: 39.1 }
+    ]
+  },
+  xAxis: { type: 'category' },
+  yAxis: {},
+  // Declare several bar series, each will be mapped
+  // to a column of dataset.source by default.
+  series: [
+    { type: 'bar', barWidth: 20 },
+    { type: 'bar', barWidth: 20 },
+    { type: 'bar', barWidth: 20 }
+  ]
+};
+
 const dateOptions = [
   { label: '今天', value: '1' },
   { label: '昨天', value: '2' },
@@ -71,4 +62,48 @@ const dataSource = [
 ];
 </script>
 
-<style lang="scss" scoped></style>
+<template>
+  <div class="m-5 bg-white h-full p-4 rounded">
+    <div class="flex justify-between">
+      <el-form class="flex gap-4">
+        <el-form-item class="w-28">
+          <el-select>
+            <el-option
+              v-for="date in dateOptions"
+              :key="date.value"
+              :label="date.label"
+              :value="date.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item class="w-28">
+          <el-select>
+            <el-option label="按部门" value="1" />
+            <el-option label="按员工" value="2" />
+          </el-select>
+        </el-form-item>
+        <el-form-item class="w-52">
+          <el-select></el-select>
+        </el-form-item>
+        <el-button type="primary">搜索</el-button>
+      </el-form>
+      <el-tabs type="card">
+        <el-tab-pane label="客户总量分析" name="first" />
+        <el-tab-pane label="客户转化率分析" name="first" />
+        <el-tab-pane label="合同数量分析" name="first" />
+      </el-tabs>
+      <!-- <span class="mr-12 font-bold text-gray-900">客户总量分析</span> -->
+    </div>
+    <div class="h-96">
+      <Echarts :option="chartOptions"></Echarts>
+    </div>
+    <el-table :data="dataSource">
+      <el-table-column prop="date" label="员工姓名" />
+      <el-table-column prop="date" label="新增客户数" />
+      <el-table-column prop="date" label="成交客户数" />
+      <el-table-column prop="date" label="客户成交率(%)" />
+      <el-table-column prop="date" label="合同总金额" />
+      <el-table-column prop="date" label="回款金额" />
+    </el-table>
+  </div>
+</template>

+ 13 - 6
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/customer/api.ts

@@ -1,9 +1,16 @@
 export const MOD = '/customer'
+export const IMPORTMOD = 'Custom'
+export const PREFIX = '/custom'
 export const GETSYSFILED = '/sys-dict/getListByCode'
 export const GETPERSONNEL = '/user/getSimpleActiveUserList'
-
-export const stageStatus = [
-    { id: 1, name: "赢单", progress: "100%" },
-    { id: 2, name: "输单", progress: "0%" },
-    { id: 3, name: "无效", progress: "0%" }
-]
+export const GETALLCLUE = '/clue/getAll'
+export const URL_TEMPLALE = `/sys-form/getListByCode${MOD}`
+export const URL_TABLELIST = `${PREFIX}/list`
+export const URL_EDITSAVE = `${PREFIX}/insertAndUpdate`
+export const URL_DETELER = `${PREFIX}/deleter`
+export const URL_RECYCLELIST = `${PREFIX}/deleteList`
+export const URL_ROWBACK = `${PREFIX}/rollback`
+export const URL_THOROUGHLYDETELE = `${PREFIX}/reallyDelete`
+export const URL_CLAIM = `${PREFIX}/claim`
+export const URL_FETALL = `${PREFIX}/getAll`
+export const URL_GETDETAIL = `${PREFIX}/getInfo`

+ 163 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/customer/component/deteleTables.vue

@@ -0,0 +1,163 @@
+<template>
+    <el-dialog v-model="deteleBusinessDialogVisible" width="1000" :before-close="beForeCancel" :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" v-loading="allLoading.batchRecoveryLoading"
+                        :disabled="batchTableData.length == 0" @click="batchOperation('恢复')">批量恢复</el-button>
+                    <el-button type="primary" v-loading="allLoading.batchDeteleLoading"
+                        :disabled="batchTableData.length == 0" @click="batchOperation('删除')">批量删除</el-button>
+                    <el-button @click="cancel()">取消</el-button>
+                </div>
+            </div>
+        </template>
+        <div class="h-[60vh] flex flex-col">
+            <div class="flex-1 w-full overflow-hidden">
+                <el-table ref="deteleTableRef" :data="deteleBusinessTable" border v-loading="allLoading.tableLoading"
+                    @selection-change="changeBatch" style="width: 100%;height: 100%;">
+                    <el-table-column type="selection" width="55" />
+                    <el-table-column prop="customName" label="客户名称" width="180"></el-table-column>
+                    <el-table-column prop="customSourceValue" label="客户来源" width="180"></el-table-column>
+                    <el-table-column prop="companyPhone" label="公司电话" width="180"></el-table-column>
+                    <el-table-column prop="email" label="邮箱" width="200"></el-table-column>
+                    <el-table-column prop="customerIndustryValue" label="客户行业" width="180"></el-table-column>
+                    <el-table-column prop="customerLevelValue" label="客户级别" width="180"></el-table-column>
+                    <el-table-column prop="inchargerName" label="负责人" width="190"></el-table-column>
+                    <el-table-column prop="creatorName" label="创建人" width="180"></el-table-column>
+                    <el-table-column prop="newCreateTime" label="创建时间" width="180"></el-table-column>
+                    <el-table-column label="操作" fixed="right" width="120">
+                        <template #default="scope">
+                            <el-button link type="primary" size="large"
+                                @click="operationItem(scope.row.id, scope.row.name, '恢复')">恢复</el-button>
+                            <el-button link type="danger" size="large"
+                                @click="operationItem(scope.row.id, scope.row.name, '删除')">删除</el-button>
+                        </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+            <div class="flex justify-end pt-3">
+                <el-pagination layout="total, prev, pager, next, sizes" :page-size="tableForm.pageFrom"
+                    @size-change="handleSizeChange" @current-change="handleCurrentChange" :total="businessTotalTable"
+                    :hide-on-single-page="true" />
+            </div>
+        </div>
+    </el-dialog>
+</template>
+<script lang="ts" setup>
+import { post } from '@/utils/request';
+import { ref, reactive, onMounted, watchEffect, watch, inject } from 'vue'
+import { URL_RECYCLELIST, URL_ROWBACK, URL_THOROUGHLYDETELE } from '../api';
+import { ElTable } from 'element-plus';
+import { confirmAction } from '@/utils/tools';
+import { formatDate } from '@/utils/times';
+
+type operationType = '恢复' | '删除'
+
+const emits = defineEmits(['closeVisible']);
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const deteleBusinessTable = ref([])
+const deteleBusinessDialogVisible = ref(false)
+const businessTotalTable = ref(0)
+const batchTableData = ref([])
+const allLoading = reactive({
+    batchRecoveryLoading: false,
+    batchDeteleLoading: false,
+    tableLoading: false
+})
+
+const tableForm = reactive({
+    pageIndex: 1,
+    pageFrom: 10
+})
+
+const deteleTableRef = ref<InstanceType<typeof ElTable>>() // 线索table dom
+
+const props = defineProps<{
+    visibles: boolean
+}>()
+
+watch(() => props.visibles, (newVal) => {
+    deteleBusinessDialogVisible.value = newVal
+    if (newVal) {
+        getTableList()
+    }
+})
+
+function batchOperation(type: operationType) {
+    const value = batchTableData.value.map((item: any) => item.id).join(',')
+    const label = batchTableData.value.map((item: any) => item.name).join(',')
+    operationItem(value, label, type, true)
+}
+
+function operationItem(value: string | number, label: string, type: operationType, batch: boolean = false) {
+    confirmAction(`确定${batch ? '批量' : ''}${type}【${label}】客户吗?`).then(() => {
+        let url = type == '恢复' ? URL_ROWBACK : URL_THOROUGHLYDETELE
+        post(url, { ids: value }).then(res => {
+            if (res.code != 'ok') {
+                globalPopup?.showError(res.msg)
+                return
+            }
+            globalPopup?.showSuccess(`${type}成功`)
+            getTableList()
+            changeBatch(false)
+        }).catch((err) => {
+            globalPopup?.showError(err.message)
+        })
+    })
+}
+
+function changeBatch(flag: boolean = true) {
+    if (flag) {
+        batchTableData.value = deteleTableRef.value && deteleTableRef.value.getSelectionRows()
+    } else {
+        batchTableData.value = []
+        deteleTableRef.value && deteleTableRef.value.clearSelection()
+    }
+}
+
+function getTableList() {
+    allLoading.tableLoading = true
+    post(URL_RECYCLELIST, { ...tableForm }).then((res) => {
+        if (res.code == 'ok') {
+            const { data, total } = res.data
+            deteleBusinessTable.value = data.map((item: any) => {
+                return {
+                    ...item,
+                    expectedTransactionDate: formatDate(new Date(item.expectedTransactionDate))
+                }
+            })
+            businessTotalTable.value = total
+        }
+    }).finally(() => {
+        allLoading.tableLoading = false
+    })
+}
+
+function handleSizeChange(val: number) {
+    tableForm.pageIndex = 1
+    tableForm.pageFrom = val
+    getTableList()
+}
+
+function handleCurrentChange(val: number) {
+    tableForm.pageIndex = val
+    getTableList()
+}
+
+function cancel() {
+    emits('closeVisible', 'deteleCustomerVisible')
+}
+
+function beForeCancel(done: () => void) {
+    emits('closeVisible', 'deteleCustomerVisible')
+    done()
+}
+
+onMounted(() => {
+
+})
+
+</script>
+<style lang="scss" scoped></style>

+ 38 - 35
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/customer/detail/index.vue

@@ -7,13 +7,13 @@
         </el-link>
       </div>
       <div class="mr-8">
-        <el-select v-model="value" placeholder="请选择" style="width: 150px">
+        <el-select v-model="values" placeholder="请选择" style="width: 150px" @change="getDetail()">
           <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">
+    <div class="flex-1 flex flex-col overflow-y-auto overflow-x-hidden scroll-bar" v-loading="allLoading.customerDetailLoading">
       <div class="w-full h-auto flex justify-between">
         <div class="bg-white shadow-md rounded-md" style="width: 46%;">
           <Information />
@@ -58,7 +58,9 @@ import { ref, reactive, onMounted, inject } from "vue";
 import type { FormInstance, FormRules } from 'element-plus'
 import { backPath } from '@/utils/tools'
 import { Edit, ArrowLeft as IconView } from '@element-plus/icons-vue'
-import { stageStatus } from '../api'
+import { URL_FETALL, URL_GETDETAIL } from '../api'
+import { post, get } from "@/utils/request";
+import { useRoute } from "vue-router";
 
 import Information from '../component/information.vue'
 import Attachment from '../component/attachment.vue'
@@ -67,40 +69,41 @@ import OperationRecord from '../component/operationRecord.vue';
 import RelatedBusiness from '../component/relatedBusiness.vue';
 import RelatedContacts from "../component/relatedContacts.vue";
 import RelatedOrders from "../component/relatedOrders.vue"
+import { number } from "echarts";
+
+const route = useRoute()
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const rowId = ref(+(route.query.id || ''))
+const values = ref<any>('')
+const options = ref<optionType[]>([])
+const information = ref<any>({})
+const allLoading = reactive({
+  customerDetailLoading: false
+})
+
+function getDetail() {
+  allLoading.customerDetailLoading = true
+  post(URL_GETDETAIL, { id: values.value }).then((res) => {
+    information.value = res.data
+  }).finally(() => {
+    allLoading.customerDetailLoading = false
+  })
+}
 
-const value = ref('')
-const stageStatusVal = ref('')
-const options = [
-  {
-    value: 'Option1',
-    label: 'Option1',
-  },
-  {
-    value: 'Option2',
-    label: 'Option2',
-  },
-  {
-    value: 'Option3',
-    label: 'Option3',
-  },
-  {
-    value: 'Option4',
-    label: 'Option4',
-  },
-  {
-    value: 'Option5',
-    label: 'Option5',
-  },
-]
-
-
-function handleScroll(event: any) { // 滚表横向滚动
-  if (event.deltaY) {
-    event.preventDefault();
-    const element = event.currentTarget;
-    element.scrollLeft += event.deltaY;
-  }
+function getAllCustomer() {
+  get(URL_FETALL).then(({ data }) => {
+      options.value = data.map((item: any) => ({
+          value: item.id,
+          label: item.customName
+      }))
+  })
 }
+
+onMounted(() => {
+  values.value = rowId.value || ''
+  getAllCustomer()
+  getDetail()
+})
 </script>
   
 <style lang="scss" scoped>

+ 323 - 60
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/customer/index.vue

@@ -5,15 +5,15 @@
         <div class="flex-1 p-3 overflow-y-auto">
           <el-form :model="customerCriteriaForm" label-width="70px" style="max-width: 600px">
             <el-form-item label="客户名称">
-              <el-input v-model="customerCriteriaForm.clueName" clearable placeholder="请输入"></el-input>
+              <el-input v-model="customerCriteriaForm.customName" clearable placeholder="请输入"></el-input>
             </el-form-item>
             <el-form-item label="客户来源">
-              <el-select v-model="customerCriteriaForm.clueSourceId" placeholder="请选择">
-                <el-option v-for="item in fixedData.CustomSources" :key="item.id" :label="item.name" :value="item.id" />
+              <el-select v-model="customerCriteriaForm.customSourceId" placeholder="请选择">
+                <el-option v-for="item in fixedData.ClueSources" :key="item.id" :label="item.name" :value="item.id" />
               </el-select>
             </el-form-item>
             <el-form-item label="电话号码">
-              <el-input v-model="customerCriteriaForm.phone" clearable placeholder="请输入"></el-input>
+              <el-input v-model="customerCriteriaForm.telPhone" clearable placeholder="请输入"></el-input>
             </el-form-item>
             <el-form-item label="邮箱">
               <el-input v-model="customerCriteriaForm.email" clearable placeholder="请输入"></el-input>
@@ -52,57 +52,138 @@
     <div class="flex-1 p-5 overflow-auto">
       <div class="bg-white w-full h-full p-3 shadow-md rounded-md flex flex-col">
         <div class="flex justify-end pb-3">
-          <el-button type="primary">新建客户</el-button>
-          <el-button type="primary" @click="batchTransfer()">批量转移</el-button>
-          <el-button type="primary" @clicl="batchDelete()">批量删除</el-button>
-          <el-button type="primary">回收站</el-button>
-          <el-button type="primary">导入</el-button>
-          <el-button type="primary">导出</el-button>
+          <el-button type="primary" @click="editCustomer(false)">新建客户</el-button>
+          <el-button type="primary" @click="showVisible('batchTransferVisible')"
+            :disabled="batchTableData.length <= 0">批量转移</el-button>
+          <el-button type="primary" @click="batchDeteleItem()" :disabled="batchTableData.length <= 0">批量删除</el-button>
+          <el-button type="primary" @click="showVisible('deteleCustomerVisible')">回收站</el-button>
+          <el-button type="primary" @click="showVisible('importVisible')">导入</el-button>
+          <el-button type="primary" @click="exportCustomerTableList()" :loading="allLoading.exoprtLoading">导出</el-button>
         </div>
         <div class="flex-1 w-full overflow-hidden">
-          <el-table ref="clueTableRef" :data="clueTable" border v-loading="allLoading.clueTableLading"
-            style="width: 100%;height: 100%;">
+          <el-table ref="customerTableRef" :data="customerTable" border v-loading="allLoading.customerTableLading"
+            style="width: 100%;height: 100%;" @selection-change="changeBatch">
             <el-table-column type="selection" width="55" />
-            <el-table-column prop="clueName" label="客户名称" width="180">
+            <el-table-column prop="customName" label="客户名称" width="180">
               <template #default="scope">
-                <el-button link type="primary" size="large" @click.prevent="toCustomerTableDetail(scope.row)">{{ scope.row.clueName
+                <el-button link type="primary" size="large" @click.prevent="toCustomerTableDetail(scope.row)">{{
+                  scope.row.customName
                 }}</el-button>
               </template>
             </el-table-column>
-            <el-table-column prop="clueSourceId" label="客户来源" width="180"></el-table-column>
-            <el-table-column prop="phone" label="公司电话" width="180"></el-table-column>
-            <el-table-column prop="email" label="邮箱" width="180"></el-table-column>
-            <el-table-column prop="customerIndustryId" label="客户行业" width="180"></el-table-column>
-            <el-table-column prop="customerLevelId" label="客户级别" width="180"></el-table-column>
-            <el-table-column prop="inchargerId" label="负责人" width="190"></el-table-column>
-            <el-table-column prop="createName" label="创建人" width="180"></el-table-column>
-            <el-table-column prop="createTime" label="创建时间" width="180"></el-table-column>
+            <el-table-column prop="customSourceValue" label="客户来源" width="180"></el-table-column>
+            <el-table-column prop="companyPhone" label="公司电话" width="180"></el-table-column>
+            <el-table-column prop="email" label="邮箱" width="200"></el-table-column>
+            <el-table-column prop="customerIndustryValue" label="客户行业" width="180"></el-table-column>
+            <el-table-column prop="customerLevelValue" label="客户级别" width="180"></el-table-column>
+            <el-table-column prop="inchargerName" label="负责人" width="190"></el-table-column>
+            <el-table-column prop="creatorName" label="创建人" width="180"></el-table-column>
+            <el-table-column prop="newCreateTime" label="创建时间" width="180"></el-table-column>
             <el-table-column label="操作" fixed="right" width="200">
               <template #default="scope">
-                <el-button link type="primary" size="large">编辑</el-button>
-                <el-button link type="primary" size="large">新建客户</el-button>
-                <el-button link type="danger" size="large" @click.prevent="deleteRow(scope.$index)">删除</el-button>
+                <el-button link type="primary" size="large" @click="editCustomer(scope.row)">编辑</el-button>
+                <el-button link type="primary" size="large" @click="newTask(scope.row)">新建任务</el-button>
+                <el-button link type="danger" size="large"
+                  @click="customerDeteleItem(scope.row.id, scope.row.customName)">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
         </div>
         <div class="flex justify-end pt-3">
-          <el-pagination layout="total, prev, pager, next, sizes" :total="clueTotalTable" :hide-on-single-page="true" />
+          <el-pagination layout="total, prev, pager, next, sizes" :total="customerTotalTable" :hide-on-single-page="true"
+            @size-change="handleSizeChange" @current-change="handleCurrentChange" />
         </div>
       </div>
     </div>
 
-    <!-- 弹窗 -->
-    
+    <!-- 新建客户 -->
+    <el-dialog v-model="allVisible.editCustomerVisible" 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">{{ allText.editCustomerText }}</h4>
+          <div>
+            <el-button type="primary" :loading="allLoading.editCustomerSaveLoading"
+              @click="editCustomerSave(true)">保存并新建</el-button>
+            <el-button type="primary" :loading="allLoading.editCustomerSaveLoading"
+              @click="editCustomerSave(false)">保存</el-button>
+            <el-button @click="closeVisible('editCustomerVisible')">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="h-[60vh] overflow-y-auto scroll-bar pt-3">
+        <div class="ml-4 mr-4">
+          <GenerateForm ref="customerTemplateRef" :data="customerTemplate" :value="customerTemplateValue"
+            :key="customerTemplateRefKey" v-loading="allLoading.customerTemplateRefLoading" />
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 批量转移 -->
+    <el-dialog v-model="allVisible.batchTransferVisible" 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.transferText }}</h4>
+          <div>
+            <el-button type="primary" v-loading="allLoading.transferLoading" @click="transferBusiness()">转移</el-button>
+            <el-button @click="allVisible.batchTransferVisible = 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="transferPersonnel" placeholder="请选择" class="flex1">
+            <el-option v-for="item in fixedData.Personnel" :key="item.id" :label="item.name" :value="item.id" />
+          </el-select>
+        </div>
+        <div class="pl-3 text-[#e94a4a]">转移后,将看不到此客户</div>
+      </div>
+    </el-dialog>
+
+    <el-dialog v-model="allVisible.importVisible" width="680" :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 class="flex">
+            <el-upload class="upload-demo mr-3" :limit="1" :show-file-list="false" accept=".xlsx"
+              :http-request="importBusiness">
+              <el-button type="primary" :loading="allLoading.importLoading">导入</el-button>
+            </el-upload>
+            <el-button @click="allVisible.importVisible = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="p-8">
+        <div class="ml-4 mr-4">
+          <div class="flex items-center">1、点击下载 <el-link type="primary"
+              @click="downloadTemplate(IMPORTMOD, allText.importText)">{{ allText.importText }}</el-link></div>
+          <div class="mt-4">2、填写excel文件、客户名称必填</div>
+        </div>
+      </div>
+    </el-dialog>
+
+    <!-- 任务 -->
+    <TaskModal :visible="allVisible.taskModalVisible" :edit-form="taskModalForm" :save-loading="taskLoading"
+      @close="closeVisible('taskModalVisible')" @submit="submitForm" :title="'新建任务'"
+      :disabled-list="['taskType', 'customId']" />
+
+    <!-- 回收站 -->
+    <DeteleBusiness :visibles="allVisible.deteleCustomerVisible" @closeVisible="closeVisible" />
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref, reactive, onMounted, inject } from "vue";
-import { MOD, GETSYSFILED, GETPERSONNEL } from './api.ts'
-import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, getLastDayOfMonth, formatDate } from '@/utils/tools'
-import { post, get } from "@/utils/request";
+import { MOD, GETSYSFILED, GETPERSONNEL, URL_TABLELIST, URL_TEMPLALE, URL_EDITSAVE, URL_DETELER, URL_CLAIM, IMPORTMOD, GETALLCLUE } from './api.ts'
+import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, getLastDayOfMonth, formatDate, getTemplateKey, createTaskFromType, confirmAction, downloadFile, downloadTemplate } from '@/utils/tools'
+import { post, get, uploadFile } from "@/utils/request";
 import { useRouter, useRoute } from "vue-router";
+import { GenerateForm } from '@zmjs/form-design';
+import { createTask } from "@/components/TaskModal/taskFunction";
+import { ElTable, UploadRequestOptions } from "element-plus";
+
+import TaskModal from '@/components/TaskModal/index.vue'
+import DeteleBusiness from './component/deteleTables.vue'
 
 // 定义类型
 interface fixedDataInterface {
@@ -121,9 +202,9 @@ interface personnelInterface {
 }
 
 interface customerCriteriaFormType { // 线索筛选条件类型
-  clueName: string,
-  clueSourceId: string | number,
-  phone: string,
+  customName: string,
+  customSourceId: string | number,
+  telPhone: string,
   email: string,
   customerIndustryId: string | number,
   customerLevelId: string | number,
@@ -135,13 +216,12 @@ interface customerCriteriaFormType { // 线索筛选条件类型
 }
 
 // 定义变量
-const route = useRoute()
 const router = useRouter()
 const globalPopup = inject<GlobalPopup>('globalPopup')
 const customerCriteriaForm = reactive<customerCriteriaFormType>({ // 筛选条件form
-  clueName: '',
-  clueSourceId: '',
-  phone: '',
+  customName: '',
+  customSourceId: '',
+  telPhone: '',
   email: '',
   customerIndustryId: '',
   customerLevelId: '',
@@ -151,53 +231,214 @@ const customerCriteriaForm = reactive<customerCriteriaFormType>({ // 筛选条
   pageIndex: 1,
   pageFrom: 10
 })
+const customerTable = ref([]) // 线索table数据
+const customerTotalTable = ref(0) // 线索 table 数据总数
 const allLoading = reactive({
-  clueTableLading: false,
+  customerTableLading: false,
+  editCustomerSaveLoading: false,
+  customerTemplateRefLoading: false,
+  transferLoading: false,
+  exoprtLoading: false,
+  importLoading: false
+})
+const allVisible = reactive({
+  editCustomerVisible: false,
+  taskModalVisible: false,
+  deteleCustomerVisible: false,
+  batchTransferVisible: false,
+  importVisible: false
+})
+const allText = reactive({
+  editCustomerText: '新建客户',
+  transferText: '批量转移',
+  importText: '客户导入模板.xlsx',
+  exportText: '客户表导出.xlsx'
 })
 const fixedData = reactive({
-  CustomSources: [] as fixedDataInterface[],
+  ClueSources: [] as fixedDataInterface[],
   CustomIndustry: [] as fixedDataInterface[],
   CustomLevel: [] as fixedDataInterface[],
   Personnel: [] as personnelInterface[]
 })
-const clueTable = ref([{ clueName: '客户名称', clueSourceId: '客户来源', id: 123456789 }]) // 线索table数据
-const clueTotalTable = ref(0) // 线索 table 数据总数
+
+const customerTemplate = ref({
+  list: [],
+  config: {}
+})
+const customerTemplateRef = ref<typeof GenerateForm>() // 自定义表单dom
+const customerTemplateValue = ref({})
+const customerTemplateRefKey = ref(1)
+const taskModalForm = ref({})
+const taskLoading = ref<saveLoadingType>('1')
+const batchTableData = ref<any>([])
+const customerTableRef = ref<InstanceType<typeof ElTable>>()
+const transferPersonnel = ref('')
+
 
 // 定义方法
-function searchTable() {
-  getClueTable()
+async function importBusiness(param: UploadRequestOptions) {
+  allLoading.importLoading = true
+  const formData = new FormData();
+  formData.append('multipartFile', param.file)
+  const res = await uploadFile('接口名称', formData).finally(() => {
+    allLoading.importLoading = false
+  })
+  if (res.code == 'ok') {
+    globalPopup?.showSuccess('导入成功' || '')
+    getCustomerTable()
+    return
+  }
+  globalPopup?.showError(res.msg || '')
 }
 
-function resetTable() {
-  let newResetForm = resetFromValue(customerCriteriaForm, { startTime: getFirstDayOfMonth(new Date()), endTime: formatDate(new Date()), pageIndex: 1, pageFrom: 10 })
-  Object.assign(customerCriteriaForm, newResetForm)
-  getClueTable()
+function exportCustomerTableList() {
+  allLoading.exoprtLoading = true
+  let valueForm = getFromValue(customerCriteriaForm)
+  post('接口名称', { ...valueForm }).then((res) => {
+    downloadFile(res.data, allText.exportText)
+  }).finally(() => {
+    allLoading.exoprtLoading = false
+  })
+}
+
+function transferBusiness() {
+  const ids = batchTableData.value.map((item: any) => item.id).join(',')
+  allLoading.transferLoading = true
+  post(URL_CLAIM, { ids, inchargerId: transferPersonnel.value }).then(() => {
+    transferPersonnel.value = ''
+    globalPopup?.showSuccess('转移成功')
+    closeVisible('batchTransferVisible')
+    getCustomerTable()
+  }).finally(() => {
+    allLoading.transferLoading = false
+  })
+}
+
+function batchDeteleItem() {
+  const value = batchTableData.value.map((item: any) => item.id).join(',')
+  const label = batchTableData.value.map((item: any) => item.customName).join(',')
+  customerDeteleItem(value, label, true)
 }
 
-function deleteRow(_row: any) {
-  console.log('点击了删除')
+function customerDeteleItem(value: string | number, label: string, batch: boolean = false) {
+  confirmAction(`确定${batch ? '批量' : ''}删除【${label}】客户吗?`).then(() => {
+    post(URL_DETELER, { ids: value }).then(res => {
+      if (res.code != 'ok') {
+        globalPopup?.showError(res.msg)
+        return
+      }
+      globalPopup?.showSuccess('删除成功')
+      changeBatch(false)
+      getCustomerTable()
+    }).catch((err) => {
+      globalPopup?.showError(err.message)
+    })
+  })
 }
 
-function batchTransfer() {
-  console.log('点击了批量转移')
+function submitForm(submitData: any, isClose: boolean) {
+  taskLoading.value = '2'
+  createTask(submitData, isClose).then((res) => {
+    const { saveLoading, isClose } = res
+    taskLoading.value = saveLoading
+    allVisible.taskModalVisible = isClose
+    globalPopup?.showSuccess('新增成功')
+  }).catch((err) => {
+    const { saveLoading, isClose, message } = err
+    taskLoading.value = saveLoading
+    allVisible.taskModalVisible = isClose
+    globalPopup?.showError(message)
+  })
 }
 
-function batchDelete() {
-  console.log('批量删除')
+function newTask(item: any) {
+  const { id } = item
+  taskModalForm.value = { ...createTaskFromType(0), customId: id, }
+  showVisible('taskModalVisible')
+}
+
+function editCustomerSave(flag: boolean) {
+  customerTemplateRef.value?.getData().then((res: any) => {
+    allLoading.editCustomerSaveLoading = true
+    post(URL_EDITSAVE, { ...res }).then((_res) => {
+      allVisible.editCustomerVisible = flag
+      globalPopup?.showSuccess('保存成功')
+      if (flag) {
+        customerTemplateRef.value?.reset()
+      }
+      getCustomerTable()
+    }).finally(() => {
+      allLoading.editCustomerSaveLoading = false
+    })
+  }).catch((_err: any) => {
+    console.log(_err)
+    globalPopup?.showError('请填写完整')
+  })
+}
+
+function editCustomer(row: any) { // row 有数据代表编辑
+  if (row) {
+    const { id, companyPhone, customName, inchargerId, createTime, customSourceId, customerIndustryId, customerLevelId, email } = row
+    const formVal = {
+      id, customName, inchargerId, customerIndustryId, customerLevelId, email, customSourceId,
+      createTime: formatDate(new Date(createTime)),
+      telPhone: companyPhone,
+    }
+    allLoading.customerTemplateRefLoading = true
+    setTimeout(() => {
+      customerTemplateRefKey.value++
+      allLoading.customerTemplateRefLoading = false
+    }, 1000);
+    customerTemplateValue.value = formVal
+    allText.editCustomerText = '编辑客户'
+  } else { // 没有数据代表新建
+    allText.editCustomerText = '新增客户'
+  }
+  showVisible('editCustomerVisible')
+}
+
+function changeBatch(flag: boolean = true) {
+  if (flag) {
+    batchTableData.value = customerTableRef.value && customerTableRef.value.getSelectionRows()
+  } else {
+    batchTableData.value = []
+    customerTableRef.value && customerTableRef.value.clearSelection()
+  }
 }
 
 function toCustomerTableDetail(_row: any) {
-  console.log('点击跳转详情')
-  router.push({path: `${MOD}/detail`, query: {id: _row.id}})
+  router.push({ path: `${MOD}/detail`, query: { id: _row.id } })
+}
+
+function searchTable() {
+  getCustomerTable()
 }
 
-function getClueTable() {
+function resetTable() {
+  let newResetForm = resetFromValue(customerCriteriaForm, { startTime: getFirstDayOfMonth(new Date()), endTime: formatDate(new Date()), pageIndex: 1, pageFrom: 10 })
+  Object.assign(customerCriteriaForm, newResetForm)
+  getCustomerTable()
+}
+
+function getCustomerTable() {
   let valueForm = getFromValue(customerCriteriaForm)
-  console.log(valueForm, '<=== valueForm')
+  allLoading.customerTableLading = true
+  post(URL_TABLELIST, { ...valueForm }).then((res) => {
+    const { data, total } = res.data
+    customerTable.value = (data || []).map((item: any) => {
+      return {
+        ...item,
+        newCreateTime: formatDate(new Date(item.createTime)),
+      }
+    })
+    customerTotalTable.value = total
+  }).finally(() => {
+    allLoading.customerTableLading = false
+  })
 }
 
 async function getSystemField() {
-  const systemField = getAllListByCode(['客户来源', '客户行业', '客户级别'])
+  const systemField = getAllListByCode(['线索来源', '客户行业', '客户级别'])
   for (let i in systemField) {
     const { data } = await get(`${GETSYSFILED}?code=${systemField[i]}`)
     for (let key of Object.keys(fixedData)) {
@@ -214,11 +455,33 @@ async function getSystemField() {
       id, name, phone, jobNumber
     }
   })
+
+  const res = await get(URL_TEMPLALE)
+  customerTemplate.value = JSON.parse(res.data[0].config)
+}
+
+function showVisible(type: keyof typeof allVisible) {
+  allVisible[type] = true
+}
+
+function closeVisible(type: keyof typeof allVisible) {
+  allVisible[type] = false
+}
+
+function handleSizeChange(val: number) {
+  customerCriteriaForm.pageIndex = 1
+  customerCriteriaForm.pageFrom = val
+  getCustomerTable()
+}
+
+function handleCurrentChange(val: number) {
+  customerCriteriaForm.pageIndex = val
+  getCustomerTable()
 }
 
 onMounted(() => {
   getSystemField()
-  getClueTable()
+  getCustomerTable()
 })
 </script>
 

+ 29 - 22
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/api.ts

@@ -1,13 +1,19 @@
 export const MOD = '/order'
+export const IMPOERMOD = 'Order'
 
 export const GETSYSFILED = "/sys-dict/getListByCode";
 export const GETPERSONNEL = "/user/getSimpleActiveUserList";
 export const GETGENERATEFOEM = `/sys-form/getListByCode${MOD}`
-export const GETALLPRODUCT = `/sys-form/getListByCode/product`
-export const GETTABLELIST =  `${MOD}/list`
+export const GETALLPRODUCT = `/sys-form/getListByCode/Order`
+export const GETTABLELIST = `${MOD}/list`
 
-export const actionButtons: any[] = [
-    { text: '新建订单' },
+export function useBtn<T extends ()=>string>(fun: T) {
+    fun && fun();
+}
+
+export type ActionButton<Fun=(f:()=>void)=>void> = { text: string, event?:Fun  };
+export const actionButtons: ActionButton[] = [
+    { text: '新建订单', event: useBtn },
     { text: '批量转移' },
     { text: '批量删除' },
     { text: '回收站' },
@@ -16,21 +22,22 @@ export const actionButtons: any[] = [
 ]
 
 export const tableColumns: TableColumn[] = [
-    { prop: 'name', label: '订单编号', event: 'toDetali', width: '150' },
-    { prop: 'mobile', label: '订单名称', width: '150' },
-    { prop: 'email', label: '客户名称', width: '200' },
-    { prop: 'wechat', label: '商机名称', width: '200' },
-    { prop: 'position', label: '订单金额', width: '100' },
-    { prop: 'company', label: '已回款', width: '100' },
-    { prop: 'companya', label: '未回款', width: '100' },
-    { prop: 'companyb', label: '回放状态', width: '100' },
-    { prop: 'companyc', label: '订单类型', width: '200' },
-    { prop: 'companyd', label: '下单时间', width: '200' },
-    { prop: 'companye', label: '订单开始时间', width: '200' },
-    { prop: 'companyf', label: '订单结束时间', width: '200' },
-    { prop: 'companyg', label: '客户签的人', width: '200' },
-    { prop: 'companyh', label: '公司签的人', width: '200' },
-    { prop: 'companyj', label: '负责人', width: '200' },
-    { prop: 'companyk', label: '创建人', width: '200' },
-    { prop: 'companyl', label: '创建时间', width: '200' },
-]
+    { prop: 'orderCode', label: '订单编号', event: 'toDetali', width: '150' },
+    { prop: 'orderName', label: '订单名称', width: '150' },
+    { prop: 'customName', label: '客户名称', width: '200' },
+    { prop: 'businessOpportunityName', label: '商机名称', width: '200' },
+    { prop: 'price', label: '订单金额', width: '100' },
+    { prop: 'receivedPayment', label: '已回款', width: '100' },
+    { prop: 'unReceivedPayment', label: '未回款', width: '100' },
+    { prop: 'status', label: '回放状态', width: '100' },
+    { prop: 'typeName', label: '订单类型', width: '200' },
+    { prop: 'placeTime', label: '下单时间', width: '200' },
+    { prop: 'orderStartDate', label: '订单开始时间', width: '200' },
+    { prop: 'orderEndDate', label: '订单结束时间', width: '200' },
+    { prop: 'customSigner', label: '客户签的人', width: '200' },
+    { prop: 'companySigner', label: '公司签的人', width: '200' },
+    { prop: 'inchargerName', label: '负责人', width: '200' },
+    { prop: 'creatorName', label: '创建人', width: '200' },
+    { prop: 'createTime', label: '创建时间', width: '200' },
+]
+

+ 34 - 25
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/order/index.vue

@@ -36,17 +36,17 @@
       <div class="bg-white w-full h-full p-3 shadow-md rounded-md flex flex-col">
         <div class="flex justify-end pb-3">
           <!-- 操作按钮 -->
-          <el-button v-for="(button, index) in actionButtons" :key="index" type="primary">{{ button.text }}</el-button>
+          <el-button v-for="(button, index) in actionButtons" :key="index" type="primary" @click="button.event && button.event(aabbcc)">{{ button.text }}</el-button>
         </div>
         <div class="flex-1 w-full overflow-hidden">
           <!-- 表格 -->
-          <el-table ref="clueTableRef" :data="formTable" border v-loading="allLoading.formTableLading"
+          <el-table ref="otherTableRef" :data="formTable" border v-loading="allLoading.formTableLading"
             style="width: 100%;height: 100%;">
             <el-table-column v-for="(column, index) in tableColumns" :key="index" :prop="column.prop"
               :label="column.label" :width="column.width">
               <template #default="scope">
                 <template v-if="column.event === 'toDetali'">
-                  <el-button link type="primary" size="large" @click="toDetali(scope.row)">{{ scope.row.name
+                  <el-button link type="primary" size="large" @click="toDetali(scope.row)">{{ scope.row[column.prop]
                   }}</el-button>
                 </template>
               </template>
@@ -71,11 +71,12 @@
 </template>
 
 <script lang="ts" setup>
-import { ref, reactive, onMounted, inject } from "vue";
+import { ref, reactive, onMounted, inject, defineExpose } from "vue";
 import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, getLastDayOfMonth, formatDate } from '@/utils/tools'
 import { post, get } from "@/utils/request";
-import { actionButtons, tableColumns, GETSYSFILED, GETPERSONNEL, GETGENERATEFOEM, MOD, GETTABLELIST, GETALLPRODUCT } from "./api";
+import { actionButtons, tableColumns, GETSYSFILED, GETPERSONNEL, GETGENERATEFOEM, MOD, GETTABLELIST, GETALLPRODUCT, useBtn } from "./api";
 import { useRouter, useRoute } from "vue-router";
+import { URL_FETALL } from "../customer/api";
 
 const router = useRouter()
 
@@ -93,7 +94,7 @@ const selectData = reactive({ // 下拉数据
   Personnel: [] as personnelInterface[],
   Customer: [] as any[], // 客户名称
   OrderType: [] as any[], // 订单类型
-  RemittanceStatus: [] as any[], // 回款状态
+  RemittanceStatus: [{ id: 0, name: '已回款' }, { id: 1, name: '未回款' }, { id: 2, name: '已完全回款' }] as any[], // 回款状态
   AllProduct: [] as any[] // 所有产品
 })
 const filterItems = ref<FilterItem[]>([]) // 渲染筛选条件
@@ -102,22 +103,18 @@ const formTablePaging = reactive({ // 分页条件
   pageSize: 10,
   total: 0,
 })
-const generateFormData = ref([]) // 自定义表单数据
-
-const formTable = ref([
-  { name: '订单名称', mobile: '13311112222', email: '123@qq.com', responsible: '张三', createId: '张三' }
-]) // 表格数据
+const formTable = ref([]) // 表格数据
 const allLoading = reactive({ // 按钮加载 Loading
   formTableLading: false,
 })
-const dialogVisible = reactive({
-  
+const allVisible = reactive({
+
 })
 
 function toDetali(row: any) {
   router.push({
     path: `${MOD}/detail`,
-    query: { id: row.name }
+    query: { id: row.id }
   })
 }
 
@@ -141,14 +138,14 @@ function resetFilterForm() {
 }
 
 function getAllProduct() {
-  post(GETALLPRODUCT, { pageIndex: -1, pageSize: -1 }).then((res) => {
+  get(GETALLPRODUCT, { pageIndex: -1, pageSize: -1 }).then((res) => {
     const { record } = res.data
     selectData.AllProduct = record
   })
 }
 
 async function getSystemField() {
-  const systemField = getAllListByCode([])
+  const systemField = getAllListByCode(['订单类型'])
   for (let i in systemField) {
     const { data } = await get(`${GETSYSFILED}?code=${systemField[i]}`)
     for (let key of Object.keys(selectData)) {
@@ -158,14 +155,17 @@ async function getSystemField() {
     }
   }
 
-  const { data } = await post(GETPERSONNEL, {})
-  selectData.Personnel = data.map((item: any) => {
+  const { data: personnelData } = await post(GETPERSONNEL, {})
+  selectData.Personnel = personnelData.map((item: any) => {
     const { id, name, phone, jobNumber } = item
-    return {
-      id, name, phone, jobNumber
-    }
+    return { id, name, phone, jobNumber }
   })
 
+  const { data: customerData } = await post(URL_FETALL, {})
+  selectData.Customer = (customerData || []).map((item: any) => {
+    const { id, customName } = item
+    return { id, name: customName }
+  })
   // const res = await get(GETGENERATEFOEM)
   // generateFormData.value = JSON.parse(res.data[0].config)
 
@@ -177,14 +177,23 @@ function setFilterItems() {
     { label: '订单编号', key: 'orderCode', type: 'input' },
     { label: '订单名称', key: 'orderName', type: 'input' },
     { label: '客户名称', key: 'customId', type: 'select', options: selectData.Customer },
-    { label: '商机名称', key: 'email', type: 'input' },
-    { label: '订单类型', key: 'type', type: 'select', options: selectData.OrderType },
-    { label: '回款状态', key: 'remittanceStatus', type: 'select', options: selectData.RemittanceStatus },
-    { label: '负责人', key: 'responsibleId', type: 'select', options: selectData.Personnel },
+    { label: '商机名称', key: 'businessOpportunityId', type: 'input' },
+    { label: '订单类型', key: 'ordertype', type: 'select', options: selectData.OrderType },
+    { label: '回款状态', key: 'receivedStatus', type: 'select', options: selectData.RemittanceStatus },
+    { label: '负责人', key: 'inchargerId', type: 'select', options: selectData.Personnel },
     { label: '下单时间', key: '', type: 'date' },
   ]
 }
 
+function aabbcc() {
+  console.log('我被调用了')
+}
+
+// 向外暴露方法
+defineExpose({
+  aabbcc
+})
+
 onMounted(() => {
   getSystemField()
   getAllProduct()

+ 3 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/index.vue

@@ -276,7 +276,9 @@ async function importProducts(param: UploadRequestOptions) {
   allLoading.importLoading = true
   const formData = new FormData();
   formData.append('multipartFile', param.file)
-  const res = await uploadFile(UPLOADFILE, formData)
+  const res = await uploadFile(UPLOADFILE, formData).finally(() => {
+    allLoading.importLoading = false
+  })
   allLoading.importLoading = false
   if (res.code == 'ok') {
     globalPopup?.showSuccess('导入成功' || '')

+ 3 - 2
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/system/customForm/index.vue

@@ -31,7 +31,7 @@
           <el-button type="primary" @click="updateJson()" v-loading="AllLoading.createFormLoading">
             保存
           </el-button>
-          <div @click="setData()">设置数据</div>
+          <!-- <div @click="setData()">设置数据</div> -->
         </div>
       </template>
     </el-dialog>
@@ -178,7 +178,8 @@ function setData() {
   // let data = JSON.parse(localStorage.getItem('threadDataJson') || '')
   // let data = JSON.parse(localStorage.getItem('business') || '')
   // let data = JSON.parse(localStorage.getItem('tasks') || '')
-  let data = JSON.parse(localStorage.getItem('product') || '')
+  // let data = JSON.parse(localStorage.getItem('product') || '')
+  let data = JSON.parse(localStorage.getItem('kehu') || '')
   data.list.forEach((element: any) => {
     element.allDisable = true
   });

+ 1 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/api.ts

@@ -1,5 +1,6 @@
 import { EpPropMergeType } from "element-plus/es/utils";
 export const MOD = "/tasks";
+export const IMPORTMOD = "Task"
 
 type StatusType = {
   label: "全部" | "未开始" | "进行中" | "已完成" | "已超时";

+ 4 - 4
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/index.vue

@@ -121,18 +121,18 @@
             <el-table-column fixed="right" label="操作" header-align="center" align="center" width="150">
 
               <template #default="scope">
-                <el-button link type="primary" size="small" @click.prevent="editRow(scope.row)">
+                <el-button link type="primary" @click.prevent="editRow(scope.row)">
                   编辑
                 </el-button>
-                <el-button link type="primary" size="small" v-if="scope.row.status == '2'"
+                <el-button link type="primary" v-if="scope.row.status == '2'"
                   @click.prevent="restart(scope.row)">
                   重启
                 </el-button>
-                <el-button link type="primary" size="small" v-else @click.prevent="finishRow(scope.row)">
+                <el-button link type="primary" v-else @click.prevent="finishRow(scope.row)">
                   完成
                 </el-button>
 
-                <el-button link type="danger" size="small" @click.prevent="deleteRow(scope.row)">
+                <el-button link type="danger" @click.prevent="deleteRow(scope.row)">
                   删除
                 </el-button>
               </template>

+ 4 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/constant.ts

@@ -1,8 +1,10 @@
 export const MOD = '/thread'
+export const IMPORMOD = 'Thread'
 export const prefix = '/clue'
 export const GETSYSFILED = '/sys-dict/getListByCode'
 export const GETPERSONNEL = '/user/getSimpleActiveUserList'
 export const GETTEMPLATE = `/sys-form/getListByCode${MOD}`
+export const GETTEMPLATETWO = `/sys-form/getListByCode/business`
 export const GETTABLE = `${prefix}/listClue`
 export const GETDETAIL = `${prefix}/getDetail`
 export const UNDATECLAIM = `${prefix}/claim`
@@ -14,4 +16,5 @@ export const REFIENAME = `${prefix}/reFileName`
 export const UPLOADFILE = `${prefix}/uploadFile`
 export const DEYELWCLUE = `${prefix}/listDeleterClue`
 export const DETERDETELE = `${prefix}/deleterDelete`
-export const ROLLBACK = `${prefix}/rollback`
+export const ROLLBACK = `${prefix}/rollback`
+export const URL_IMPORTTHREAD = `${prefix}/importData`

+ 86 - 22
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/information.vue

@@ -3,7 +3,7 @@
         <div class="flex justify-between">
             <div class="title">基本信息</div>
             <div>
-                <el-button type="primary">转为商机</el-button>
+                <el-button type="primary" @click="showVisible('newBusinessisible')">转为商机</el-button>
                 <el-button type="primary" @click="claimClues()" v-if="!information.inchargerName">认领</el-button>
                 <el-button type="primary" @click="showVisible('clueDialogVisible')" v-else>转移</el-button>
                 <el-button type="primary" @click="editClue(information)">编辑</el-button>
@@ -72,12 +72,12 @@
                     <h4 :id="titleId">{{ allText.editClueText }}</h4>
                     <div>
                         <el-button type="primary" @click="saveEditClue()"
-                            v-loading="allLoading.saveBtnLoading">保存</el-button>
+                            :loading="allLoading.saveBtnLoading">保存</el-button>
                         <el-button @click="dialogVisible.editClueDialogVisible = false">取消</el-button>
                     </div>
                 </div>
             </template>
-            <div class="h-[60vh] overflow-y-auto scroll-bar pt-3" v-loading="allLoading.generateFormLading">
+            <div class="h-[60vh] overflow-y-auto scroll-bar pt-3" :loading="allLoading.generateFormLading">
                 <div class="ml-4 mr-4">
                     <GenerateForm ref="generateForm" :data="clueTemplate" :value="editForm" :key="generateFormKey" />
                 </div>
@@ -89,7 +89,7 @@
                 <div class="flex justify-between items-center border-b pb-3 dialog-header">
                     <h4 :id="titleId">{{ allText.clueText }}</h4>
                     <div>
-                        <el-button type="primary" v-loading="allLoading.clueLoading" @click="transferClues()">转移</el-button>
+                        <el-button type="primary" :loading="allLoading.clueLoading" @click="transferClues()">转移</el-button>
                         <el-button @click="dialogVisible.clueDialogVisible = false">取消</el-button>
                     </div>
                 </div>
@@ -104,16 +104,39 @@
                 <div class="pl-3 text-[#e94a4a]">转移后,将看不到此线索</div>
             </div>
         </el-dialog>
+
+        <!-- 转成商机 -->
+        <el-dialog v-model="dialogVisible.newBusinessisible" 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">{{ allText.businessisText }}</h4>
+                    <div>
+                        <el-button type="primary" @click="transferBusiness()"
+                            :loading="allLoading.businessSaveLading">转为商机</el-button>
+                        <el-button @click="closeVisible('newBusinessisible')">取消</el-button>
+                    </div>
+                </div>
+            </template>
+            <div class="h-[60vh] overflow-y-auto scroll-bar pt-3">
+                <GenerateForm ref="generateFormDataRef" :data="generateFormData" :value="generateFormVal" />
+                <div>相关产品</div>
+                <RelatedProducts ref="relatedProductsRef" :productTableList="productTableList" />
+            </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 { formatDate, confirmAction } from '@/utils/tools'
-import { GETTEMPLATE, UNDATEFORM, GETPERSONNEL, UNDATECLAIM } from '../../constant'
+import { formatDate, confirmAction, backPath } from '@/utils/tools'
+import { GETTEMPLATE, UNDATEFORM, GETPERSONNEL, UNDATECLAIM, GETTEMPLATETWO } from '../../constant'
 import { get, post } from '@/utils/request'
 import { useStore } from '@/store/index'
 import { ElMessageBox } from 'element-plus';
+import RelatedProducts from '@/components/relatedProducts/relatedProducts.vue'
+import { all } from 'axios';
+import { formatDateTime } from '@/utils/times';
+import { UPDATEINSET } from '@/pages/business/api';
 
 interface personnelInterface {
     id: string | number,
@@ -134,25 +157,59 @@ const generateFormKey = ref(1)
 const allLoading = reactive({
     generateFormLading: false,
     saveBtnLoading: false,
-    clueLoading: false
+    clueLoading: false,
+    businessSaveLading: false,
 })
 const dialogVisible = reactive({
     editClueDialogVisible: false,
-    clueDialogVisible: false
+    clueDialogVisible: false,
+    newBusinessisible: false
 })
 const allText = reactive({
     editClueText: '新建线索',
-    clueText: '认领线索'
+    clueText: '认领线索',
+    businessisText: '转为商机'
 })
 const generateForm: any = ref(null) // 模板
 const clueTemplate = ref({
     list: [],
     config: {}
 }) // 线索模板
+const generateFormData = ref({ // 商机模板
+    config: {},
+    list: []
+})
+const generateFormDataRef = ref<typeof GenerateForm>() // 商机模板dom
+const relatedProductsRef = ref<typeof RelatedProducts>()
+const productTableList = ref([])
+const generateFormVal = ref<any>({})
 const editForm = ref({}) // 编辑表单
 const transferValue = ref('') // 转移/认领 id
 const transferOptions = ref<personnelInterface[]>([]) // 转移人员列表
 
+// 转为商机
+function transferBusiness() {
+    generateFormDataRef.value?.getData().then((res: any) => {
+        let productTableListData = relatedProductsRef?.value?.returnData()
+        let newForm = {
+            ...res,
+            expectedTransactionDate: res.expectedTransactionDate ? formatDateTime(new Date(res.expectedTransactionDate)) : '',
+            businessItemProductList: productTableListData ? JSON.stringify(productTableListData) : []
+        }
+        allLoading.businessSaveLading = true
+        post(UPDATEINSET, { ...newForm }).then((_res) => {
+            dialogVisible.newBusinessisible = false
+            globalPopup?.showSuccess('操作成功')
+            backPath()
+        }).finally(() => {
+            allLoading.businessSaveLading = false
+        })
+    }).catch((_err: any) => {
+        console.log(_err)
+        globalPopup?.showError('请填写完整')
+    })
+}
+
 // 转移/认领 线索
 function transferClues() {
     const ids = information.value?.id
@@ -171,18 +228,9 @@ function transferClues() {
 }
 
 function claimClues() {
-    ElMessageBox.confirm(
-        `确定认领【${information.value.clueName}】线索吗?`,
-        'Warning',
-        {
-            confirmButtonText: '确定',
-            cancelButtonText: '取消',
-            type: 'warning',
-        }
-    )
-        .then(() => {
-            transferClues()
-        })
+    confirmAction(`确定认领【${information.value.clueName}】线索吗?`).then(() => {
+        transferClues()
+    })
 }
 
 // 保存
@@ -229,8 +277,21 @@ function receiveAssignment(item: any) {
 function showVisible(filed: keyof typeof dialogVisible) {
     if (filed == 'clueDialogVisible') {
         transferValue.value = ''
+        allText.clueText = '转移线索'
+    } else {
+        allText.clueText = '认领线索'
+    }
+
+    if (filed == 'newBusinessisible') {
+        generateFormVal.value.name = information.value.clueName
     }
-    dialogVisible[filed] = true
+    setTimeout(() => {
+        dialogVisible[filed] = true
+    }, 100)
+}
+
+function closeVisible(filed: keyof typeof dialogVisible) {
+    dialogVisible[filed] = false
 }
 
 async function getSystemField() {
@@ -252,6 +313,9 @@ onMounted(async () => {
     receiveAssignment(props)
     const res = await get(GETTEMPLATE)
     clueTemplate.value = JSON.parse(res.data[0].config)
+    const data = await get(GETTEMPLATETWO)
+    let newData = JSON.parse(data.data[0].config)
+    generateFormData.value = newData
     getSystemField()
 });
 </script>

+ 85 - 51
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/relatedTasks.vue

@@ -3,73 +3,107 @@
         <div class="flex justify-between">
             <div class="title">相关任务</div>
             <div>
-                <el-button type="primary">新建任务</el-button>
+                <el-button type="primary" @click="newTask()">新建任务</el-button>
             </div>
         </div>
         <div class="flex-1 overflow-auto pt-3">
             <el-table :data="relatedTaskstable" border style="width: 100%;height: 100%;">
                 <el-table-column prop="taskName" label="任务名称">
                     <template #default="scope">
-                        <el-button link type="primary" size="large">{{
+                        <el-button link type="primary" size="large" @click="toTask()">{{
                             scope.row.taskName
                         }}</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="priorityStr" label="优先级" width="130" />
+                <el-table-column prop="statusStr" label="状态" width="130" />
+                <el-table-column prop="executorNamesStr" label="执行人" width="130" />
+                <el-table-column prop="startTimes" label="开始时间" width="130" />
+                <el-table-column prop="endTimes" label="截至时间" width="130" />
             </el-table>
         </div>
     </div>
+
+    <!-- 新建任务 -->
+    <TaskModal :visible="allVisible.taskModalVisible" :edit-form="taskModalForm" :save-loading="taskLoading"
+        @close="closeTaskModal" @submit="submitForm" :title="'新建任务'" :disabled-list="['taskType', 'clueId']" />
 </template>
 <script lang="ts" setup>
-import { ref, reactive, onMounted, onUnmounted, defineExpose, inject } from 'vue'
+import { PRIORITY } from '@/components/TaskModal/api';
+import { STATUS } from '@/pages/tasks/api';
+import { formatDate } from '@/utils/times';
+import { createTaskFromType } from '@/utils/tools';
+import { ref, reactive, onMounted, watchEffect, inject } from 'vue'
+import { useRouter } from "vue-router";
+import TaskModal from '@/components/TaskModal/index.vue'
+import { createTask } from '@/components/TaskModal/taskFunction';
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const emits = defineEmits(['refreshData']);
+
+const props = defineProps<{
+    data: any,
+    information: any
+}>()
+
+const relatedTaskstable = ref([])
+const information = ref<any>({})
+const router = useRouter()
+const taskLoading = ref<saveLoadingType>('1')
+const taskModalForm = ref({}) // 任务弹窗表单
+const allVisible = reactive({
+    taskModalVisible: false
+})
+
+function submitForm(submitData: any, isClose: boolean) { // 任务提交
+    taskLoading.value = '2'
+    createTask(submitData, isClose).then((res) => {
+        const { saveLoading, isClose } = res
+        taskLoading.value = saveLoading
+        allVisible.taskModalVisible = isClose
+        globalPopup?.showSuccess('新增成功')
+        emits('refreshData')
+    }).catch((err) => {
+        const { saveLoading, isClose, message } = err
+        taskLoading.value = saveLoading
+        allVisible.taskModalVisible = isClose
+        globalPopup?.showError(message)
+    })
+}
+
+function newTask() {
+    const { id } = information.value
+    taskModalForm.value = { ...createTaskFromType(3), clueId: id, }
+    allVisible.taskModalVisible = true
+}
+
+function toTask() {
+    router.push({
+        path: `/tasks`
+    })
+}
+
+function closeTaskModal() {
+    allVisible.taskModalVisible = false
+}
+
+// 接收参数赋值
+function receiveAssignment(item: any) {
+    information.value = item.information
+    const dataVal = item.data
+    for (let i in dataVal) {
+        dataVal[i].executorNamesStr = (dataVal[i].executorNames || []).join(','),
+            dataVal[i].startTimes = dataVal[i].startDate ? formatDate(new Date(dataVal[i].startDate)) : '',
+            dataVal[i].endTimes = dataVal[i].endDate ? formatDate(new Date(dataVal[i].endDate)) : '',
+            dataVal[i].priorityStr = PRIORITY.find(item => item.value == dataVal[i].priority)?.label || '',
+            dataVal[i].statusStr = STATUS.find(item => item.value == dataVal[i].status)?.label || ''
+    }
+    relatedTaskstable.value = dataVal
+}
+
+watchEffect(() => {
+    receiveAssignment(props)
+});
 
-const relatedTaskstable = ref([{
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}, {
-    taskName: '任务名称20240316-tempalsbls',
-    priority: '中',
-    status: '进行中',
-    executor: '张三',
-    startTime: '2024-04-01',
-    endTime: '2024-04-01',
-}])
 // 生命周期钩子
 onMounted(() => {
 });

+ 2 - 2
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/index.vue

@@ -10,7 +10,7 @@
     </div>
     <div class="layout pl-3 pr-3 pb-3">
       <div class="bg-white w-2/3 shadow-md rounded-md">
-        <RelatedTasks :data="relatedTasks" :information="information" @refreshData="refreshData"></RelatedTasks>
+        <DetailCompinents :data="relatedTasks" :information="information" :formTaskType="3" :disabledList="['taskType', 'clueId']" :filed="'clueId'" @refreshData="refreshData"></DetailCompinents>
       </div>
       <div class="bg-white w-1/3 ml-3 shadow-md rounded-md">
         <OperationRecord :data="operationRecord" :information="information" @refreshData="refreshData"></OperationRecord>
@@ -22,7 +22,7 @@
 <script lang="ts" setup>
 import Information from './components/information.vue'
 import Attachment from './components/attachment.vue'
-import RelatedTasks from './components/relatedTasks.vue';
+import DetailCompinents from '@/components/detailcompinents/relatedTasks.vue'
 import OperationRecord from './components/operationRecord.vue';
 import { ref, reactive, onMounted, inject } from "vue";
 import { post, get } from "@/utils/request";

+ 63 - 10
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/index.vue

@@ -56,7 +56,7 @@
           <el-button type="primary" @click="batchTransfer()">批量转移</el-button>
           <el-button type="primary" @click="batchDeletes()">批量删除</el-button>
           <el-button type="primary" @click="showDeteleClue(true)">回收站</el-button>
-          <el-button type="primary">导入</el-button>
+          <el-button type="primary" @click="dialogVisible.importVisible = true">导入</el-button>
           <el-button type="primary">导出</el-button>
         </div>
         <div class="flex-1 w-full overflow-hidden">
@@ -83,8 +83,7 @@
                 <el-button link type="primary" size="large" @click="editClue(scope.row)">编辑</el-button>
                 <!-- <el-button link type="primary" size="large"
                   @click="dialogVisible.taskModalVisible = true">新建任务</el-button> -->
-                <el-button link type="primary" size="large"
-                  @click="newTask(scope.row)">新建任务</el-button>
+                <el-button link type="primary" size="large" @click="newTask(scope.row)">新建任务</el-button>
                 <el-button link type="danger" size="large" @click.prevent="deleteRow(scope.row)">删除</el-button>
               </template>
             </el-table-column>
@@ -140,21 +139,45 @@
 
     <DeteleTables :visibles="dialogVisible.deteleClueDialogVisible" @showDeteleClue="showDeteleClue" />
 
-    <TaskModal :visible="dialogVisible.taskModalVisible" :edit-form="taskModalForm" :save-loading="'1'"
+    <TaskModal :visible="dialogVisible.taskModalVisible" :edit-form="taskModalForm" :save-loading="taskLoading"
       @close="closeTaskModal" @submit="submitForm" :title="'新建任务'" :disabled-list="['taskType', 'clueId']" />
+
+    <!-- 导入线索 -->
+    <el-dialog v-model="dialogVisible.importVisible" width="680" :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 class="flex">
+            <el-upload class="upload-demo mr-3" :limit="1" :show-file-list="false" accept=".xlsx"
+              :http-request="importProducts">
+              <el-button type="primary" :loading="allLoading.importLoading">导入</el-button>
+            </el-upload>
+            <el-button @click="dialogVisible.importVisible = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="p-8">
+        <div class="ml-4 mr-4">
+          <div class="flex items-center">1、点击下载 <el-link type="primary"
+              @click="downloadTemplate(IMPORMOD, '线索导入模板.xlsx')">线索导入模板.xlsx</el-link></div>
+          <div class="mt-4">2、填写excel文件、线索名称、线索来源必填</div>
+        </div>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref, reactive, onMounted, inject } from "vue";
-import { GETSYSFILED, MOD, GETPERSONNEL, GETTABLE, GETTEMPLATE, GETDETAIL, UNDATECLAIM, UNDATEFORM, DELTEROW } from './constant'
-import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, getLastDayOfMonth, formatDate, createTaskFromType, confirmAction } from '@/utils/tools'
-import { FormInstance, FormRules, ElMessageBox, ElTable } from 'element-plus'
-import { post, get } from "@/utils/request";
+import { GETSYSFILED, MOD, IMPORMOD, GETPERSONNEL, GETTABLE, GETTEMPLATE, GETDETAIL, UNDATECLAIM, UNDATEFORM, DELTEROW, URL_IMPORTTHREAD } from './constant'
+import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, getLastDayOfMonth, formatDate, createTaskFromType, confirmAction, downloadTemplate } from '@/utils/tools'
+import { FormInstance, FormRules, ElMessageBox, ElTable, UploadRequestOptions } from 'element-plus'
+import { post, get, uploadFile } from "@/utils/request";
 import { useRouter, useRoute } from "vue-router";
 import { GenerateForm } from '@zmjs/form-design';
 import TaskModal from '@/components/TaskModal/index.vue'
 import DeteleTables from "./deteleTables.vue";
+import { createTask } from "@/components/TaskModal/taskFunction";
 
 // 定义类型
 interface fixedDataInterface {
@@ -190,16 +213,19 @@ const filterCriteriaForm = reactive<filterCriteriaFormType>({ // 筛选条件for
   pageFrom: 10
 })
 const generateFormKey = ref(1)
+const taskLoading = ref<saveLoadingType>('1')
 const allLoading = reactive({
   clueTableLading: false,
   generateFormLading: false,
   clueLoading: false,
+  importLoading: false,
 })
 const dialogVisible = reactive({
   editClueDialogVisible: false,
   taskModalVisible: false,
   clueDialogVisible: false,
-  deteleClueDialogVisible: false
+  deteleClueDialogVisible: false,
+  importVisible: false
 })
 const allText = reactive({
   editClueText: '新建线索',
@@ -272,7 +298,18 @@ function closeTaskModal() {
 }
 
 function submitForm(submitData: any, isClose: boolean) {
-  console.log(submitData, isClose)
+  taskLoading.value = '2'
+  createTask(submitData, isClose).then((res) => {
+    const { saveLoading, isClose } = res
+    taskLoading.value = saveLoading
+    dialogVisible.taskModalVisible = isClose
+    globalPopup?.showSuccess('新增成功')
+  }).catch((err) => {
+    const { saveLoading, isClose, message } = err
+    taskLoading.value = saveLoading
+    dialogVisible.taskModalVisible = isClose
+    globalPopup?.showError(message)
+  })
 }
 
 function editClue(item: any) {
@@ -327,6 +364,22 @@ function deleteRow(row: any) {
   })
 }
 
+async function importProducts(param: UploadRequestOptions) {
+  allLoading.importLoading = true
+  const formData = new FormData();
+  formData.append('multipartFile', param.file)
+  const res = await uploadFile(URL_IMPORTTHREAD, formData).finally(() => {
+    allLoading.importLoading = false
+  })
+  allLoading.importLoading = false
+  if (res.code == 'ok') {
+    globalPopup?.showSuccess('导入成功' || '')
+    getClueTable()
+    return
+  }
+  globalPopup?.showError(res.msg || '')
+}
+
 function batchTransfer() {
   const data = clueTableRef.value && clueTableRef.value.getSelectionRows()
   if (!data.length) {

+ 11 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/type.d.ts

@@ -21,6 +21,10 @@ type ListByCodeType = (
   | "订单类型"
 )[];
 
+type templateKey = { // 自定义模板键值
+  [key: string]: any;
+}
+
 type saveLoadingType = "1" | "2" | "3" | "4"; //1是没有保存, 2是正在保存, 3是保存成功, 4是保存失败
 
 type TASK_VALUE_TYPE = 0 | 1 | 2 | 3; //0是客户, 1是商机, 2是销售订单 ,3是线索
@@ -28,4 +32,10 @@ type REPEAT_VALUE_TYPE = 0 | 1 | 2 | 3 | 4; //0是每天, 1是每周, 2是每月
 
 type componentType = "success" | "info" | "warning" | "error"
 
-type TaskResponse = { saveLoading: saveLoadingType, isClose: boolean, message?: string }
+type TaskResponse = { saveLoading: saveLoadingType, isClose: boolean, message?: string }
+
+type optionType = { value: number | string, label: string | number }
+
+type sexTYpe = { value: number | string, label: string }
+
+type buttonType = { label?: string, value?: string, type?: string, icon?: string, disabled?: boolean, loading?: boolean, text?: string, event?: void }

+ 22 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/tools.ts

@@ -215,3 +215,25 @@ export function downloadTemplate(mod: any, fileName: string) {
     downloadFile(res.data, fileName)
   });
 }
+
+/**
+ * 取出自定义模板需要的 key
+ * @param list 自定义模板数据(list)
+ * @returns 
+ */
+export function getTemplateKey(list: Array<any>) {
+  const parsedList = list;
+  const models: Array<string> = [];
+  parsedList.forEach((item: any) => {
+    if (item.type === 'grid') {
+      item.columns.forEach((column: any) => {
+        column.list.forEach((subItem: any) => {
+          models.push(subItem.model);
+        });
+      });
+    } else {
+      models.push(item.model);
+    }
+  });
+  return models;
+}

+ 16 - 16
fhKeeper/formulahousekeeper/customerBuler-crm/vite.config.ts

@@ -1,40 +1,40 @@
-import { defineConfig } from "vite";
-import vue from "@vitejs/plugin-vue";
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
 
-import { resolve } from "path";
+import { resolve } from 'path';
 
-// const target = "http://192.168.2.8:10080";
+const target = 'http://192.168.2.8:10010';
 // const target = "http://127.0.0.1:10010";
 // const target = "http://192.168.2.178:10010";
-const target = "http://47.101.180.183:10010";
+// const target = 'http://47.101.180.183:10010';
 
 export default defineConfig({
   plugins: [vue()],
   server: {
-    host: "0.0.0.0",
+    host: '0.0.0.0',
     port: 19123,
     open: true,
     proxy: {
-      "/api": {
+      '/api': {
         // 这里的'/api'表示需要转发到的接口路径前缀
         target, // 将请求转发到的目标地址
         changeOrigin: true, // 支持跨域
-        rewrite: (path) => path.replace(/^\/api/, ""), // 去除请求路径中的'/api'前缀
-      },
-    },
+        rewrite: (path) => path.replace(/^\/api/, '') // 去除请求路径中的'/api'前缀
+      }
+    }
   },
   resolve: {
     alias: {
-      "@": resolve(__dirname, "./src"),
+      '@': resolve(__dirname, './src')
     },
-    extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
+    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
   },
   css: {
     preprocessorOptions: {
       scss: {
         additionalData: '@import "@/styles/global.scss";'
-      },
-    },
+      }
+    }
   },
 
   build: {
@@ -47,8 +47,8 @@ export default defineConfig({
         manualChunks: {
           vue: ['vue', 'vue-router', 'vuex'],
           echarts: ['echarts']
-        },
-      },
+        }
+      }
     }
   }
 });

+ 5 - 0
fhKeeper/formulahousekeeper/management-crm/pom.xml

@@ -14,6 +14,11 @@
     <version>3.4.0</version>
 
     <dependencies>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.9.8</version>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>

+ 20 - 2
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/BusinessOpportunityController.java

@@ -82,7 +82,25 @@ public class BusinessOpportunityController {
 
         }
         return msg;
-    } @RequestMapping("rollBack")
+    }
+
+    @RequestMapping("deleterDelete")
+    public Object deleterDelete(BusinessOpportunity bo) {
+        HttpRespMsg msg = new HttpRespMsg();
+        String ids1 = bo.getIds();
+        List<Integer> ids = new ArrayList<>();
+        if (!ids1.isEmpty()) {
+            for (String id : ids1.split(",")) {
+                ids.add(Integer.parseInt(id));
+            }
+            bOservice.deleterDelete(ids);
+            msg.setMsg("操作成功");
+        } else {
+            msg.setError("请选择线索");
+        }
+        return msg;
+    }
+    @RequestMapping("rollBack")
     public Object rollBack(BusinessOpportunity bo) {
         HttpRespMsg msg = new HttpRespMsg();
         String ids1 = bo.getIds();
@@ -200,7 +218,7 @@ public class BusinessOpportunityController {
 
     @RequestMapping("saveStage")
     public HttpRespMsg insertStage(List<Stage> stages, HttpServletRequest request) {
-        return stageService.changeStage(stages);
+        return stageService.changeStage(stages,request);
     }
     @RequestMapping("getStage")
     public HttpRespMsg Stage() {

+ 12 - 6
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/ClueController.java

@@ -73,12 +73,6 @@ public class ClueController {
         return msg;
     }
 
-    // 导入
-    @RequestMapping("exportData")
-    public Object exportData(Clue clue, HttpServletRequest request) throws Exception {
-        return clueService.exportData(clue, request);
-    }
-
     // 上传
     @RequestMapping("uploadFile")
     public Object uploadFile(Clue clue, HttpServletRequest request, MultipartFile file) throws Exception {
@@ -167,6 +161,18 @@ public class ClueController {
         return httpRespMsg;
     }
 
+    @RequestMapping("/importData")
+    public HttpRespMsg importData(MultipartFile multipartFile){
+        return clueService.importData(multipartFile);
+    }
+
+
+    @RequestMapping("/exportData")
+    public HttpRespMsg exportData(Clue clue) throws Exception {
+        return clueService.exportData(clue);
+    }
+
+
     @RequestMapping("/insertAndUpdate")
     public Object inserANdUpdate(Clue clue, HttpServletRequest request) {
         HttpRespMsg msg = new HttpRespMsg();

+ 20 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/CustomController.java

@@ -6,6 +6,7 @@ import com.management.platform.entity.Clue;
 import com.management.platform.entity.Custom;
 import com.management.platform.entity.UploadFile;
 import com.management.platform.entity.User;
+import com.management.platform.mapper.ClueMapper;
 import com.management.platform.mapper.CustomMapper;
 import com.management.platform.mapper.UserMapper;
 import com.management.platform.service.CustomService;
@@ -40,6 +41,8 @@ public class CustomController {
     @Autowired
     private CustomMapper customMapper;
     @Autowired
+    private ClueMapper clueMapper;
+    @Autowired
     private UserMapper userMapper;
 
     @RequestMapping("list")
@@ -170,6 +173,23 @@ public class CustomController {
         return customService.deleteFile(file, request);
     }
 
+    //删除
+    @RequestMapping("deleterDelete")
+    public Object deleterDelete(Custom custom , HttpServletRequest request) throws Exception {
+        HttpRespMsg msg = new HttpRespMsg();
+        String ids1 = custom.getIds();
+        List<Integer> ids = new ArrayList<>();
+        if (!ids1.isEmpty()) {
+            for (String id : ids1.split(",")) {
+                ids.add(Integer.parseInt(id));
+            }
+            customService.deleterDelete(ids);
+            msg.setMsg("操作成功");
+        } else {
+            msg.setError("请选择线索");
+        }
+        return msg;    }
+
     //重命名
     @RequestMapping("reFileName")
     public Object reFileName(UploadFile uploadFile, HttpServletRequest request) throws Exception {

+ 9 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/SalesOrderController.java

@@ -360,6 +360,15 @@ public class SalesOrderController {
         unReceivedPayment=unReceivedPayment.subtract(price);
         salesOrder.setReceivedPayment(receivedPayment);
         salesOrder.setUnReceivedPayment(unReceivedPayment);
+        if(money>0){
+            //回款金额大于0的情况下才能算一次回款
+            if(unReceivedPayment.doubleValue()<=0){
+                //未回款金额小于等于0 说明完全回款修改状态
+                salesOrder.setReceivedStatus(2);
+            }else {
+                salesOrder.setReceivedStatus(1);
+            }
+        }
         salesOrderPayment.setUnReceivedPayment(unReceivedPayment);
         salesOrderService.updateById(salesOrder);
         salesOrderPaymentService.save(salesOrderPayment);

+ 4 - 2
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/BusinessOpportunity.java

@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.extension.activerecord.Model;
 import com.baomidou.mybatisplus.annotation.TableId;
 
 import java.math.BigDecimal;
-import java.time.LocalDateTime;
 import com.baomidou.mybatisplus.annotation.TableField;
 import java.io.Serializable;
 import java.util.Date;
@@ -174,8 +173,11 @@ public class BusinessOpportunity extends Model<BusinessOpportunity> {
      */
     @TableField("plate5")
     private String plate5;
+//    @JsonDeserialize(using = BusinessItemProductListDeserializer.class)
     @TableField(exist = false)
-    private List<BusinessItemProduct> businessItemProductList;
+    private String businessItemProductList;
+    @TableField(exist = false)
+    private List<BusinessItemProduct> businessItemProducts;
     @TableField(exist = false)
     private List<ActionLog> actionLogList;
     @TableField(exist = false)

+ 1 - 1
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/Custom.java

@@ -75,7 +75,7 @@ public class Custom extends Model<Custom> {
     @TableField("customer_industry_id")
     private Integer customerIndustryId;
     @TableField(exist = false)
-    private Integer customerIndustryValue;
+    private String customerIndustryValue;
 
     /**
      * 客户级别id

+ 9 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/SalesOrder.java

@@ -115,6 +115,13 @@ public class SalesOrder extends Model<SalesOrder> {
     @TableField("un_received_payment")
     private BigDecimal unReceivedPayment;
 
+    /**
+     * 回款状态 0-未回款 1-已回款 2-已完全回款
+     */
+    @TableField("received_status")
+    private Integer receivedStatus;
+
+
     /**
      * 客户签约人
      */
@@ -208,6 +215,8 @@ public class SalesOrder extends Model<SalesOrder> {
     @TableField(exist = false)
     private String creatorName;
 
+    @TableField(exist = false)
+    private String businessOpportunityName;
 
     @Override
     protected Serializable pkVal() {

+ 1 - 1
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/BusinessOpportunityMapper.java

@@ -33,5 +33,5 @@ public interface BusinessOpportunityMapper extends BaseMapper<BusinessOpportunit
 
     Map<String, Object> getDataSummary(Integer companyId, String startDate, String endDate, String userId,@Param("list") List<String> targetUserIds);
 
-    Map<String, Object> getDataStage(Integer companyId, String startDate, String endDate, String userId,@Param("list") List<String> targetUserIds);
+    List<Map<String, Object>> getDataStage(Integer companyId, String startDate, String endDate, String userId,@Param("list") List<String> targetUserIds);
 }

+ 3 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/ClueMapper.java

@@ -2,9 +2,11 @@ package com.management.platform.mapper;
 
 import com.management.platform.entity.Clue;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.management.platform.util.HttpRespMsg;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.List;
 import java.util.Map;
 
@@ -38,4 +40,5 @@ public interface ClueMapper extends BaseMapper<Clue> {
     Clue selectById2Info(@Param("id") Integer id);
 
     Map<String, Object> getDataSummary(Integer companyId, String startDate, String endDate, String userId,@Param("list") List<String> targetUserIds);
+
 }

+ 3 - 1
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/BusinessOpportunityService.java

@@ -57,5 +57,7 @@ public interface BusinessOpportunityService extends IService<BusinessOpportunity
 
     Map<String, Object> getDataSummary(Integer companyId, String startDate, String endDate, String userId,List<String> targetUserIds);
 
-    Map<String, Object> getDataStage(Integer companyId, String startDate, String endDate, String userId,List<String> targetUserIds);
+    List<Map<String, Object>> getDataStage(Integer companyId, String startDate, String endDate, String userId,List<String> targetUserIds);
+
+    void deleterDelete(List<Integer> ids);
 }

+ 4 - 1
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/ClueService.java

@@ -43,7 +43,6 @@ public interface ClueService extends IService<Clue> {
     int getTotal1(Clue clue, User user);
     int getTotal2(Clue clue, User user);
 
-    HttpRespMsg exportData(Clue clue, HttpServletRequest request) throws Exception;
 
     Object uploadFile(Clue clue, HttpServletRequest request, MultipartFile file);
 
@@ -52,4 +51,8 @@ public interface ClueService extends IService<Clue> {
     Object deleteFile(UploadFile file, HttpServletRequest request);
 
     Object downFile(UploadFile file, HttpServletRequest request, HttpServletResponse response);
+
+    HttpRespMsg importData(MultipartFile multipartFile);
+
+    HttpRespMsg exportData(Clue clue);
 }

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

@@ -52,4 +52,6 @@ public interface CustomService extends IService<Custom> {
     HttpRespMsg getAllCustom(HttpServletRequest request);
 
     Map<String, Object> getDataSummary(Integer companyId,String startDate, String endDate, String userId,@Param("list") List<String> targetUserIds);
+
+    void deleterDelete(List<Integer> ids);
 }

+ 2 - 1
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/StageService.java

@@ -4,6 +4,7 @@ import com.management.platform.entity.Stage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.management.platform.util.HttpRespMsg;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.List;
 
 /**
@@ -16,7 +17,7 @@ import java.util.List;
  */
 public interface StageService extends IService<Stage> {
 
-    HttpRespMsg changeStage(List<Stage> stages);
+    HttpRespMsg changeStage(List<Stage> stages, HttpServletRequest request);
 
     HttpRespMsg getStage();
 }

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

@@ -3,9 +3,11 @@ package com.management.platform.service.impl;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.TypeReference;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.management.platform.entity.*;
 import com.management.platform.mapper.*;
 import com.management.platform.service.BusinessOpportunityService;
@@ -77,7 +79,7 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
         businessOpportunity.setUploadFilePList(uploadFileMapper.selectByInfoList("business",bo.getId()));
         businessOpportunity.setTaskList(taskMapper.selectList(new QueryWrapper<Task>().eq("business_opportunity_id",bo.getId())));
         List<BusinessItemProduct> businessItemProducts = biMapper.selectListToBoId(bo.getId());
-        businessOpportunity.setBusinessItemProductList(businessItemProducts);
+        businessOpportunity.setBusinessItemProducts(businessItemProducts);
         if (businessItemProducts.size() > 0){
             BigDecimal finalPrice = new BigDecimal(0);
             BigDecimal discountedPrice = new BigDecimal(0);
@@ -123,11 +125,13 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
         setNull(bo);
         bo.setCreateTime(new Date());
         bOMapper.insert(bo);
+        List<BusinessItemProduct> businessItemProducts = JSONArray.parseArray(bo.getBusinessItemProductList(), BusinessItemProduct.class);
+        System.out.println(businessItemProducts);
         ActionLog actionLog = new ActionLog();
         actionLog.setUserId(bo.getUserId());
         actionLog.setItemId(bo.getId());
         actionLog.setCode("business");
-        actionLog.setName("创建了线索");
+        actionLog.setName("创建了商机");
         actionLogMapper.insert(actionLog);
     }
 
@@ -168,13 +172,13 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
         for (BusinessOpportunity clue1 : clues) {
             if (clue1.getInchargerId() == null) {
                 //认领
-                clueLog.setName("认领了线索");
+                clueLog.setName("认领了商机");
                 clueLog.setUserId(user.getId());
                 bo.setInchargerId(user.getId());
                 actionLogMapper.insert(clueLog);
             } else {
                 //转移
-                clueLog.setName("转移了线索");
+                clueLog.setName("转移了商机");
                 bo.setInchargerId(bo.getInchargerId());
                 actionLogMapper.insert(clueLog);
             }
@@ -216,9 +220,8 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
         HttpRespMsg msg = new HttpRespMsg();
         msg.setMsg("操作成功");
         biMapper.delete(new QueryWrapper<BusinessItemProduct>().eq("business_id",bo.getId()));
-        List<BusinessItemProduct> businessItemProductList = bo.getBusinessItemProductList();
-//        biMapper.saveBatch()
-        for (BusinessItemProduct businessItemProduct : businessItemProductList) {
+        List<BusinessItemProduct> businessItemProducts = JSONArray.parseArray(bo.getBusinessItemProductList(), BusinessItemProduct.class);
+        for (BusinessItemProduct businessItemProduct : businessItemProducts) {
             biMapper.insert(businessItemProduct);
         }
         ActionLog al = new ActionLog();
@@ -367,10 +370,15 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
     }
 
     @Override
-    public Map<String, Object> getDataStage(Integer companyId, String startDate, String endDate, String userId, List<String> targetUserIds) {
+    public List<Map<String, Object>> getDataStage(Integer companyId, String startDate, String endDate, String userId, List<String> targetUserIds) {
         return bOMapper.getDataStage(companyId,startDate,endDate,userId,targetUserIds);
     }
 
+    @Override
+    public void deleterDelete(List<Integer> ids) {
+        bOMapper.deleteBatchIds(ids);
+    }
+
     private BusinessOpportunity setNull(BusinessOpportunity bo) {
         if (bo.getPlate1() == "") {
             bo.setPlate1(null);

+ 215 - 42
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/ClueServiceImpl.java

@@ -14,14 +14,21 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.management.platform.service.SysFunctionService;
 import com.management.platform.service.UserService;
 import com.management.platform.service.WxCorpInfoService;
+import com.management.platform.util.ExcelUtil;
 import com.management.platform.util.FileUtil;
 import com.management.platform.util.HttpRespMsg;
 import com.management.platform.util.MessageUtils;
 import org.apache.poi.ss.formula.functions.T;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.xssf.usermodel.XSSFCell;
+import org.apache.poi.xssf.usermodel.XSSFRow;
+import org.apache.poi.xssf.usermodel.XSSFSheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
@@ -29,8 +36,10 @@ import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.beans.Transient;
-import java.io.File;
-import java.io.IOException;
+import java.io.*;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
 import java.net.URLEncoder;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
@@ -51,6 +60,8 @@ public class ClueServiceImpl extends ServiceImpl<ClueMapper, Clue> implements Cl
     private SysFunctionService sysFunctionService;
     @Autowired
     private ClueMapper clueMapper;
+    @Resource
+    private HttpServletRequest request;
     @Autowired
     private TaskMapper taskMapper;
     @Autowired
@@ -71,7 +82,8 @@ public class ClueServiceImpl extends ServiceImpl<ClueMapper, Clue> implements Cl
     private ActionLogMapper actionLogMapper;
     @Resource
     private ExcelExportServiceImpl excelExportService;
-
+    @Resource
+    private SysDictMapper sysDictMapper;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -255,28 +267,7 @@ public class ClueServiceImpl extends ServiceImpl<ClueMapper, Clue> implements Cl
     }
 
 
-    @Override
-    public HttpRespMsg exportData(Clue clue, HttpServletRequest request) throws Exception {
-        User user = userMapper.selectById(request.getHeader("token"));
-        SysForm sysForm = sysFormMapper.selectOne(new LambdaQueryWrapper<SysForm>().eq(SysForm::getCompanyId, user.getCompanyId()).eq(SysForm::getCode, "Clue").eq(SysForm::getIsCurrent, 1));
-        WxCorpInfo wxCorpInfo = wxCorpInfoService.getOne(new LambdaQueryWrapper<WxCorpInfo>().eq(WxCorpInfo::getCompanyId, user.getCompanyId()));
-        String config = sysForm.getConfig();
-        JSONObject configOb = JSON.parseObject(config);
-        JSONArray configObJSONArray = configOb.getJSONArray("list");
-        List<List<String>> dataList = new ArrayList<>();
-        List<String> titleList = new ArrayList<>();
-        for (int i = 0; i < configObJSONArray.size(); i++) {
-            JSONObject item = configObJSONArray.getJSONObject(i);
-            titleList.add(item.getString("label"));
-        }
-        dataList.add(titleList);//设置表头
-
-
-        clue.setCompanyId(user.getCompanyId());
 
-        String fileName = "线索表导出_" + System.currentTimeMillis();
-        return excelExportService.exportGeneralExcelByTitleAndList(wxCorpInfo, fileName, dataList, path);
-    }
 
     @Value(value = "${upload.file}")
     private String filePath;
@@ -371,30 +362,212 @@ public class ClueServiceImpl extends ServiceImpl<ClueMapper, Clue> implements Cl
     @Override
     public Object downFile(UploadFile file, HttpServletRequest request, HttpServletResponse response) {
         HttpRespMsg msg = new HttpRespMsg();
-//        try {
-//            ServletOutputStream os = response.getOutputStream();
-            UploadFile uploadFile = uploadFileMapper.selectById(file.getId());
+        UploadFile uploadFile = uploadFileMapper.selectById(file.getId());
         String path1 = uploadFile.getPath();
         path1 = path1.substring(2);
         msg.setData(path1);
-//            if (uploadFile == null ){
-//                msg.setError("文件未找到");
-//                return msg;
+        return msg;
+    }
+
+    @Override
+    public HttpRespMsg importData(MultipartFile multipartFile) {
+        HttpRespMsg msg=new HttpRespMsg();
+        String fileName = multipartFile.getOriginalFilename();
+        File file = new File(fileName == null ? "file" : fileName);
+        User user = userMapper.selectById(request.getHeader("token"));
+        Integer companyId = user.getCompanyId();
+        WxCorpInfo wxCorpInfo = wxCorpInfoService.getOne(new LambdaQueryWrapper<WxCorpInfo>().eq(WxCorpInfo::getCompanyId, companyId));
+//        List<Clue> clueList = clueMapper.selectList(new LambdaQueryWrapper<Clue>().eq(Clue::getCompanyId, companyId));
+        List<User> userList = userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getCompanyId, companyId));
+        List<SysDict> sysDictOfClueSources = sysDictMapper.selectList(new LambdaQueryWrapper<SysDict>().eq(SysDict::getCompanyId, companyId).eq(SysDict::getCode, "ClueSources"));
+        List<SysDict> sysDictOfCustomLevel = sysDictMapper.selectList(new LambdaQueryWrapper<SysDict>().eq(SysDict::getCompanyId, companyId).eq(SysDict::getCode, "CustomLevel"));
+        List<SysDict> sysDictOfCustomIndustry = sysDictMapper.selectList(new LambdaQueryWrapper<SysDict>().eq(SysDict::getCompanyId, companyId).eq(SysDict::getCode, "CustomIndustry"));
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+        try {
+            inputStream = multipartFile.getInputStream();
+            outputStream = new FileOutputStream(file);
+            byte[] buffer = new byte[4096];
+            int temp = 0;
+            while ((temp = inputStream.read(buffer, 0, 4096)) != -1) {
+                outputStream.write(buffer, 0, temp);
+            }
+            inputStream.close();
+            outputStream.close();
+            //解析表格
+            XSSFWorkbook workbook = new XSSFWorkbook(file);
+            //我们只需要第一个sheet
+            XSSFSheet sheet = workbook.getSheetAt(0);
+            //由于第一行需要指明列对应的标题
+            int rowNum = sheet.getLastRowNum();
+            //获取当前表单模板 校验规则
+            SysForm sysForm = sysFormMapper.selectOne(new LambdaQueryWrapper<SysForm>().eq(SysForm::getCode, "Thread").eq(SysForm::getCompanyId, companyId).eq(SysForm::getIsCurrent, 1));
+            if(sysForm==null){
+                msg.setError("当前模块未配置自定义模板,需先完成配置");
+                return msg;
+            }
+            String config = sysForm.getConfig();
+            JSONObject configOb = JSON.parseObject(config);
+            JSONArray configObJSONArray = configOb.getJSONArray("list");
+//            List<Clue> importClueList=new ArrayList<>();
+            List<String> userNameList=new ArrayList<>();
+            HttpRespMsg respMsg=new HttpRespMsg();
+            for (int rowIndex = 0; rowIndex <= rowNum; rowIndex++) {
+                XSSFRow row = sheet.getRow(rowIndex);
+                if (row == null) {
+                    continue;
+                }
+                //跳过空行
+                if (ExcelUtil.isRowEmpty(row)) {
+                    continue;
+                }
+                //获取到当前行的列数据
+                int cellNum = row.getLastCellNum();
+                for (int i = 0; i < cellNum; i++) {
+                    JSONObject item = configObJSONArray.getJSONObject(i);
+                    String modelName = item.getString("model");
+                    XSSFCell cell = row.getCell(i);
+                    if(cell!=null){
+                        switch (item.getString("type")){
+                            case "time":cell.setCellType(CellType.NUMERIC);
+                                break;
+                            default:cell.setCellType(CellType.STRING);
+                        }
+                    }
+//                    if(modelName.equals("inchargerId")){
+//                        System.out.println("=====");
+//                        System.out.println(modelName);
+//                        System.out.println(cell.toString());
+//                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+//                            userNameList.add(cell.getStringCellValue());
+//                        }
+//                    }
+
+                }
+            }
+//            System.out.println("参与搜索的人员列表"+userNameList + userNameList.size());
+//            if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1&&userNameList.size()>0){
+//                respMsg = wxCorpInfoService.getBatchSearchUserInfo(wxCorpInfo, userNameList,null);
+//                if(respMsg.code.equals("0")){
+//                    msg.setError("姓名为["+String.valueOf(respMsg.data)+"]的人员存在重复,请使用工号!");
+//                    return msg;
+//                }
 //            }
-//
-//            File uploadFile1 = new File(uploadFile.getPath());
-//            response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(uploadFile.getName(), "UTF-8"));
-//            response.setContentType("application/octet-stream");
-//            // 读取文件的字节流
-//            os.write(FileUtil.readFileByBytes(uploadFile1));
-//            os.flush();
-//
-//        } catch (IOException e) {
-//            msg.setError(MessageUtils.message("file.error"));
-//            e.printStackTrace();
-//        }
+            List<User> targetUserList= (List<User>) respMsg.data;
+            //直接忽略空行 从row1开始
+            for (int rowIndex = 1; rowIndex <= rowNum; rowIndex++) {
+                XSSFRow row = sheet.getRow(rowIndex);
+                if (row == null) {
+                    continue;
+                }
+                //跳过空行
+                if (ExcelUtil.isRowEmpty(row)) {
+                    continue;
+                }
+                //获取到当前行的列数据
+                int cellNum = row.getLastCellNum();
+                Clue clue =new Clue();
+                clue.setCompanyId(companyId);
+                clue.setCreateTime(new Date());
+                clue.setCreateId(user.getId());
+                for (int i = 0; i < cellNum; i++) {
+                    JSONObject item = configObJSONArray.getJSONObject(i);
+                    String modelName = item.getString("model");
+                    String className = modelName.substring(0, 1).toUpperCase() + modelName.substring(1);
+                    String getter="get"+className;
+                    String setter="set"+className;
+                    XSSFCell cell = row.getCell(i);
+                    if(cell!=null){
+                        switch (item.getString("type")){
+                            case "time":cell.setCellType(CellType.NUMERIC);
+                                break;
+                            default:cell.setCellType(CellType.STRING);
+                        }
+                    }
+                    //校验当前列是否为必填
+                    JSONObject options = item.getJSONObject("options");
+                    JSONObject rules = options.getJSONObject("rules");
+                    Boolean required = rules.getBoolean("required");
+                    if(required){
+                        if(StringUtils.isEmpty(cell.getStringCellValue())){
+                            msg.setError(item.getString("label")+"值不能为空值");
+                            return msg;
+                        }
+                    }
+                   if(modelName.equals("inchargerId")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            String userName = cell.getStringCellValue();
+                            Optional<User> first;
+                            if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
+                                Optional<User> optional = targetUserList.stream().filter(tl -> tl.getName().equals(userName)).findFirst();
+                                first= userList.stream().filter(u ->(u.getJobNumber()!=null&&u.getJobNumber().equals(userName))||(optional.isPresent()&&u.getCorpwxUserid()!=null&&u.getCorpwxUserid().equals(optional.get().getCorpwxUserid()))).findFirst();
+                            }else {
+                                first= userList.stream().filter(u -> u.getName().equals(userName)||(u.getJobNumber()!=null&&u.getJobNumber().equals(userName))).findFirst();
+                            }
+                            if (first.isPresent()) {
+                                clue.setInchargerId(first.get().getId());
+                            } else {
+                                msg.setError("负责人["+userName+"]在系统中不存在");
+                                return msg;
+                            }
+                        }
+                    }else if(modelName.equals("customLevel")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            Optional<SysDict> first = sysDictOfCustomLevel.stream().filter(s -> s.getName().equals(cell.getStringCellValue())).findFirst();
+                            if(first.isPresent()){
+                                clue.setCustomerLevelId(first.get().getId());
+                            }else {
+                                throw new Exception("客户级别["+cell.getStringCellValue()+"不存在,请在系统字典中增加");
+                            }
+                        }
+                    }else if(modelName.equals("clueSources")){
+                       if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                           Optional<SysDict> first = sysDictOfClueSources.stream().filter(s -> s.getName().equals(cell.getStringCellValue())).findFirst();
+                           if(first.isPresent()){
+                               Integer id = first.get().getId();
+                               clue.setClueSourceId(id);
+                           }else {
+                               throw new Exception("线索来源["+cell.getStringCellValue()+"不存在,请在系统字典中增加");
+                           }
+                       }
+                   }else if(modelName.equals("customIndustry")){
+                       if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                           Optional<SysDict> first = sysDictOfCustomIndustry.stream().filter(s -> s.getName().equals(cell.getStringCellValue())).findFirst();
+                           if(first.isPresent()){
+                               clue.setCustomerIndustryId(first.get().getId());
+                           }else {
+                               throw new Exception("线索来源["+cell.getStringCellValue()+"不存在,请在系统字典中增加");
+                           }
+                       }
+                   }else {
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            Class<Clue> clueClass = Clue.class;
+                            Method method = clueClass.getMethod(setter,String.class);
+                            method.invoke(clue,cell.getStringCellValue());
+                        }
+                    }
+
+                }
+                clueMapper.insert(clue);
+            }
+        } catch (IOException | NoSuchMethodException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        } catch (InvocationTargetException e) {
+            e.printStackTrace();
+        } catch (Exception e) {
+            e.printStackTrace();
+            msg.setError("验证失败");
+            return msg;
+        }
         return msg;
     }
 
+    @Override
+    public HttpRespMsg exportData(Clue clue) {
+        return null;
+    }
+
 
 }

+ 10 - 4
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/CustomServiceImpl.java

@@ -95,11 +95,12 @@ public class CustomServiceImpl extends ServiceImpl<CustomMapper, Custom> impleme
                 return msg;
             }
             if (customMapper.selectCount(new QueryWrapper<Custom>().eq("tel_phone", custom.getTelPhone())) > 0) {
-                msg.setError("电话号码重复了");
+//                msg.setError("电话号码重复了");
                 return msg;
             }
             custom.setCreateTime(new Date());
             custom.setCreatorId(user.getId());
+            custom.setIsDelete(0);
             ActionLog actionLog = new ActionLog();
             actionLog.setCode("custom");
             actionLog.setCreatTime(new Date());
@@ -243,12 +244,12 @@ public class CustomServiceImpl extends ServiceImpl<CustomMapper, Custom> impleme
         boolean isAll = sysFunctionService.hasPriviledge(user.getRoleId(), "查看全部客户");
         boolean isNotAll = sysFunctionService.hasPriviledge(user.getRoleId(), "查看负责部门客户");
         List<Custom> list = new ArrayList<>();
-        int i = 0;
-        if (!isAll) {
+        int i = 1;
+        if (isAll) {
             //查看全部线索
             list = customMapper.getList(custom);
             i = customMapper.getTotal(custom);
-        } else if (!isNotAll) {
+        } else if (isNotAll) {
             //查看负责部门线索 找出所处部门下所有的负责人
             list = customMapper.getList1(custom);
             i = customMapper.getTotal1(custom);
@@ -390,6 +391,11 @@ public class CustomServiceImpl extends ServiceImpl<CustomMapper, Custom> impleme
         return customMapper.getDataSummary(companyId,startDate,endDate,userId,targetUserIds);
     }
 
+    @Override
+    public void deleterDelete(List<Integer> ids) {
+        customMapper.deleteBatchIds(ids);
+    }
+
 
     private Custom setNull(Custom clue) {
         if (clue.getPlate1() == "") {

+ 86 - 38
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/ProductServiceImpl.java

@@ -216,10 +216,28 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
             String config = sysForm.getConfig();
             JSONObject configOb = JSON.parseObject(config);
             JSONArray configObJSONArray = configOb.getJSONArray("list");
+            //可能存在栅格布局的情况需要特殊处理
+            List<String> modelNameList=new ArrayList<>();
+            for (int i = 0; i < configObJSONArray.size(); i++) {
+                JSONObject jsonObject = configObJSONArray.getJSONObject(i);
+                if(jsonObject.getString("type").equals("grid")){
+                    JSONArray columns = jsonObject.getJSONArray("columns");
+                    for (int i1 = 0; i1 < columns.size(); i1++) {
+                        JSONObject columnsJSONObject = columns.getJSONObject(i1);
+                        JSONArray list = columnsJSONObject.getJSONArray("list");
+                        for (int i2 = 0; i2 < list.size(); i2++) {
+                            JSONObject object = list.getJSONObject(i2);
+                            modelNameList.add(object.getString("model"));
+                        }
+                    }
+                }else {
+                    modelNameList.add(jsonObject.getString("model"));
+                }
+            }
             List<Product>  importProductList=new ArrayList<>();
             List<String> userNameList=new ArrayList<>();
             HttpRespMsg respMsg=new HttpRespMsg();
-            for (int rowIndex = 0; rowIndex <= rowNum; rowIndex++) {
+            for (int rowIndex = 1; rowIndex <= rowNum; rowIndex++) {
                 XSSFRow row = sheet.getRow(rowIndex);
                 if (row == null) {
                     continue;
@@ -231,16 +249,15 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
                 //获取到当前行的列数据
                 int cellNum = row.getLastCellNum();
                 for (int i = 0; i < cellNum; i++) {
-                    JSONObject item = configObJSONArray.getJSONObject(i);
-                    String modelName = item.getString("model");
+                    String modelName = modelNameList.get(i);
                     XSSFCell cell = row.getCell(i);
-                    if(cell!=null){
-                        switch (item.getString("type")){
-                            case "time":cell.setCellType(CellType.NUMERIC);
-                                break;
-                            default:cell.setCellType(CellType.STRING);
-                        }
-                    }
+//                    if(cell!=null){
+//                        switch (item.getString("type")){
+//                            case "time":cell.setCellType(CellType.NUMERIC);
+//                                break;
+//                            default:cell.setCellType(CellType.STRING);
+//                        }
+//                    }
                     if(modelName.equals("inchargerId")){
                         if(!StringUtils.isEmpty(cell.getStringCellValue())){
                             userNameList.add(cell.getStringCellValue());
@@ -280,14 +297,13 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
                     String getter="get"+className;
                     String setter="set"+className;
                     XSSFCell cell = row.getCell(i);
-                    if(cell!=null){
-                        switch (item.getString("type")){
-                            case "time":cell.setCellType(CellType.NUMERIC);
-                                break;
-                            default:cell.setCellType(CellType.STRING);
-                        }
-                    }
-//                    Class<?> productClass = Class.forName("com.management.platform.entity.Product");
+//                    if(cell!=null){
+//                        switch (item.getString("type")){
+//                            case "time":cell.setCellType(CellType.NUMERIC);
+//                                break;
+//                            default:cell.setCellType(CellType.STRING);
+//                        }
+//                    }
                     //校验当前列是否为必填
                     JSONObject options = item.getJSONObject("options");
                     JSONObject rules = options.getJSONObject("rules");
@@ -320,7 +336,7 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
                             if (first.isPresent()) {
                                 product.setInchargerId(first.get().getId());
                             } else {
-                                msg.setError("["+userName+"]在系统中不存在");
+                                msg.setError("负责人["+userName+"]在系统中不存在");
                                 return msg;
                             }
                         }
@@ -413,27 +429,59 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
             List<String> item=new ArrayList<>();
             for (int i = 0; i < configObJSONArray.size(); i++) {
                 JSONObject target = configObJSONArray.getJSONObject(i);
-                String model = target.getString("model");
-                String targetName = model.substring(0, 1).toUpperCase() + model.substring(1);
-                Class<? extends Product> aClass = product.getClass();
-                String value = String.valueOf(aClass.getMethod("get" + targetName).invoke(product)==null?"":aClass.getMethod("get" + targetName).invoke(product));
-                if(model.equals("inchargerId")){
-                    if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
-                        value = "$userName"+String.valueOf(aClass.getMethod("getInchargerName").invoke(product))+"$";
-                    }else {
-                        value = String.valueOf(aClass.getMethod("getInchargerName").invoke(product));
+                if(target.getString("type").equals("grid")){
+                    JSONArray columns = target.getJSONArray("columns");
+                    for (int i1 = 0; i1 < columns.size(); i1++) {
+                        JSONObject columnsJSONObject = columns.getJSONObject(i1);
+                        JSONArray list = columnsJSONObject.getJSONArray("list");
+                        for (int i2 = 0; i2 < list.size(); i2++) {
+                            JSONObject object = list.getJSONObject(i2);
+                            String model = object.getString("model");
+                            String targetName = model.substring(0, 1).toUpperCase() + model.substring(1);
+                            Class<? extends Product> aClass = product.getClass();
+                            String value = String.valueOf(aClass.getMethod("get" + targetName).invoke(product)==null?"":aClass.getMethod("get" + targetName).invoke(product));
+                            if(model.equals("inchargerId")){
+                                if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
+                                    value = "$userName"+String.valueOf(aClass.getMethod("getInchargerName").invoke(product))+"$";
+                                }else {
+                                    value = String.valueOf(aClass.getMethod("getInchargerName").invoke(product));
+                                }
+                            }
+                            if(model.equals("unit")){
+                                value = String.valueOf(aClass.getMethod("getUnitName").invoke(product));
+                            }
+                            if(model.equals("type")){
+                                value = String.valueOf(aClass.getMethod("getTypeName").invoke(product));
+                            }
+                            if(model.equals("status")){
+                                value = Integer.valueOf(String.valueOf(aClass.getMethod("getStatus").invoke(product)))==0?"下架":"上架";
+                            }
+                            item.add(value);
+                        }
                     }
+                }else {
+                    String model = target.getString("model");
+                    String targetName = model.substring(0, 1).toUpperCase() + model.substring(1);
+                    Class<? extends Product> aClass = product.getClass();
+                    String value = String.valueOf(aClass.getMethod("get" + targetName).invoke(product)==null?"":aClass.getMethod("get" + targetName).invoke(product));
+                    if(model.equals("inchargerId")){
+                        if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
+                            value = "$userName"+String.valueOf(aClass.getMethod("getInchargerName").invoke(product))+"$";
+                        }else {
+                            value = String.valueOf(aClass.getMethod("getInchargerName").invoke(product));
+                        }
+                    }
+                    if(model.equals("unit")){
+                        value = String.valueOf(aClass.getMethod("getUnitName").invoke(product));
+                    }
+                    if(model.equals("type")){
+                        value = String.valueOf(aClass.getMethod("getTypeName").invoke(product));
+                    }
+                    if(model.equals("status")){
+                        value = Integer.valueOf(String.valueOf(aClass.getMethod("getStatus").invoke(product)))==0?"下架":"上架";
+                    }
+                    item.add(value);
                 }
-                if(model.equals("unit")){
-                    value = String.valueOf(aClass.getMethod("getUnitName").invoke(product));
-                }
-                if(model.equals("type")){
-                    value = String.valueOf(aClass.getMethod("getTypeName").invoke(product));
-                }
-                if(model.equals("status")){
-                    value = Integer.valueOf(String.valueOf(aClass.getMethod("getStatus").invoke(product)))==0?"下架":"上架";
-                }
-                item.add(value);
             }
             dataList.add(item);
         }

+ 213 - 34
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/SalesOrderServiceImpl.java

@@ -34,6 +34,7 @@ import java.math.RoundingMode;
 import java.text.NumberFormat;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.Year;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.TemporalAdjusters;
@@ -97,6 +98,7 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
         List<SysDict> sysDictOfOrderType = sysDictMapper.selectList(new LambdaQueryWrapper<SysDict>().eq(SysDict::getCompanyId, user.getCompanyId()).eq(SysDict::getCode, "OrderType"));
         LambdaQueryWrapper<SalesOrder> orderLambdaQueryWrapper = new LambdaQueryWrapper<>();
         List<Custom> customList = customService.list(new LambdaQueryWrapper<Custom>().eq(Custom::getCompanyId, user.getCompanyId()));
+        List<BusinessOpportunity> businessOpportunityList = businessOpportunityService.list(new LambdaQueryWrapper<BusinessOpportunity>().eq(BusinessOpportunity::getCompanyId, user.getCompanyId()));
         orderLambdaQueryWrapper.eq(SalesOrder::getCompanyId,user.getCompanyId());
         if(isDelete!=null){
             orderLambdaQueryWrapper.eq(SalesOrder::getIsDelete,isDelete);
@@ -193,6 +195,10 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
             if(custom.isPresent()){
                 r.setCustomName(custom.get().getCustomName());
             }
+            Optional<BusinessOpportunity> businessOpportunity = businessOpportunityList.stream().filter(b -> r.getBusinessOpportunityId() != null && b.getId().equals(r.getBusinessOpportunityId())).findFirst();
+            if(businessOpportunity.isPresent()){
+                r.setBusinessOpportunityName(businessOpportunity.get().getName());
+            }
         });
         Map map=new HashMap();
         map.put("record",records);
@@ -208,11 +214,12 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
         File file = new File(fileName == null ? "file" : fileName);
         User user = userMapper.selectById(request.getHeader("token"));
         Integer companyId = user.getCompanyId();
+        DateTimeFormatter df=DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        DateTimeFormatter df1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
         WxCorpInfo wxCorpInfo = wxCorpInfoService.getOne(new LambdaQueryWrapper<WxCorpInfo>().eq(WxCorpInfo::getCompanyId, companyId));
         List<SalesOrder> orderList = salesOrderMapper.selectList(new LambdaQueryWrapper<SalesOrder>().eq(SalesOrder::getCompanyId, companyId));
         List<User> userList = userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getCompanyId, companyId));
-        List<SysDict> sysDictOfProductType = sysDictMapper.selectList(new LambdaQueryWrapper<SysDict>().eq(SysDict::getCompanyId, companyId).eq(SysDict::getCode, "ProductType"));
-        List<SysDict> sysDictOfProductUnit = sysDictMapper.selectList(new LambdaQueryWrapper<SysDict>().eq(SysDict::getCompanyId, companyId).eq(SysDict::getCode, "ProductUnit"));
+        List<SysDict> sysDictOfOrderType = sysDictMapper.selectList(new LambdaQueryWrapper<SysDict>().eq(SysDict::getCompanyId, companyId).eq(SysDict::getCode, "OrderType"));
         InputStream inputStream = null;
         OutputStream outputStream = null;
         try {
@@ -240,10 +247,28 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
             String config = sysForm.getConfig();
             JSONObject configOb = JSON.parseObject(config);
             JSONArray configObJSONArray = configOb.getJSONArray("list");
+            //可能存在栅格布局的情况需要特殊处理
+            List<String> modelNameList=new ArrayList<>();
+            for (int i = 0; i < configObJSONArray.size(); i++) {
+                JSONObject jsonObject = configObJSONArray.getJSONObject(i);
+                if(jsonObject.getString("type").equals("grid")){
+                    JSONArray columns = jsonObject.getJSONArray("columns");
+                    for (int i1 = 0; i1 < columns.size(); i1++) {
+                        JSONObject columnsJSONObject = columns.getJSONObject(i1);
+                        JSONArray list = columnsJSONObject.getJSONArray("list");
+                        for (int i2 = 0; i2 < list.size(); i2++) {
+                            JSONObject object = list.getJSONObject(i2);
+                            modelNameList.add(object.getString("model"));
+                        }
+                    }
+                }else {
+                    modelNameList.add(jsonObject.getString("model"));
+                }
+            }
             List<SalesOrder>  importOrderList=new ArrayList<>();
             List<String> userNameList=new ArrayList<>();
             HttpRespMsg respMsg=new HttpRespMsg();
-            for (int rowIndex = 0; rowIndex <= rowNum; rowIndex++) {
+            for (int rowIndex = 1; rowIndex <= rowNum; rowIndex++) {
                 HSSFRow row = sheet.getRow(rowIndex);
                 if (row == null) {
                     continue;
@@ -255,17 +280,26 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                 //获取到当前行的列数据
                 int cellNum = row.getLastCellNum();
                 for (int i = 0; i < cellNum; i++) {
-                    JSONObject item = configObJSONArray.getJSONObject(i);
-                    String modelName = item.getString("model");
+                    String modelName = modelNameList.get(i);
                     HSSFCell cell = row.getCell(i);
-                    if(cell!=null){
-                        switch (item.getString("type")){
-                            case "time":cell.setCellType(CellType.NUMERIC);
-                                break;
-                            default:cell.setCellType(CellType.STRING);
+//                    if(cell!=null){
+//                        switch (item.getString("type")){
+//                            case "time":cell.setCellType(CellType.NUMERIC);
+//                                break;
+//                            default:cell.setCellType(CellType.STRING);
+//                        }
+//                    }
+                    if(modelName.equals("inchargerId")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            userNameList.add(cell.getStringCellValue());
                         }
                     }
-                    if(modelName.equals("inchargerId")){
+                    if(modelName.equals("customSigner")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            userNameList.add(cell.getStringCellValue());
+                        }
+                    }
+                    if(modelName.equals("companySigner")){
                         if(!StringUtils.isEmpty(cell.getStringCellValue())){
                             userNameList.add(cell.getStringCellValue());
                         }
@@ -282,7 +316,7 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                 }
             }
             List<User> targetUserList= (List<User>) respMsg.data;
-            for (int rowIndex = 0; rowIndex <= rowNum; rowIndex++) {
+            for (int rowIndex = 1; rowIndex <= rowNum; rowIndex++) {
                 HSSFRow row = sheet.getRow(rowIndex);
                 if (row == null) {
                     continue;
@@ -310,9 +344,6 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                             default:cell.setCellType(CellType.STRING);
                         }
                     }
-//                    Class<?> productClass = Class.forName("com.management.platform.entity.Product");
-                    Class<Product> productClass = Product.class;
-                    Method method = productClass.getMethod(setter, String.class);
                     //校验当前列是否为必填
                     JSONObject options = item.getJSONObject("options");
                     JSONObject rules = options.getJSONObject("rules");
@@ -325,14 +356,70 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                     }
                     if(modelName.equals("orderCode")){
                         if(!StringUtils.isEmpty(cell.getStringCellValue())){
-                            //系统中同公司已存在的产品编码 更新
+                            //系统中同公司已存在的订单编码 更新
                             Optional<SalesOrder> first = orderList.stream().filter(p ->p.getOrderCode()!=null&& p.getOrderCode().equals(cell.getStringCellValue())).findFirst();
                             if(first.isPresent()){
                                 order.setId(first.get().getId());
                             }
+                            order.setOrderCode(cell.getStringCellValue());
                         }
-                    }
-                    if(modelName.equals("inchargerId")){
+                    }else if(modelName.equals("customId")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            //系统中同公司已存在的订单编码 更新
+                            Custom one = customService.getOne(new LambdaQueryWrapper<Custom>().eq(Custom::getCompanyId, companyId).eq(Custom::getCustomName, cell.getStringCellValue()));
+                            if(one!=null){
+                                order.setCustomId(one.getId());
+                            }else {
+                                msg.setError("客户["+cell.getStringCellValue()+"]在系统中不存在");
+                                return msg;
+                            }
+                        }
+                    } else if(modelName.equals("businessOpportunityId")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            //系统中同公司已存在的订单编码 更新
+                            BusinessOpportunity one = businessOpportunityService.getOne(new LambdaQueryWrapper<BusinessOpportunity>().eq(BusinessOpportunity::getCompanyId, companyId).eq(BusinessOpportunity::getName, cell.getStringCellValue()));
+                            if(one!=null){
+                                order.setBusinessOpportunityId(one.getId());
+                            }else {
+                                msg.setError("商机["+cell.getStringCellValue()+"]在系统中不存在");
+                                return msg;
+                            }
+                        }
+                    } else if(modelName.equals("price")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            order.setPrice(new BigDecimal(cell.getStringCellValue()));
+                        }
+                    } else if(modelName.equals("receivedPayment")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            order.setReceivedPayment(new BigDecimal(cell.getStringCellValue()));
+                        }
+                    } else if(modelName.equals("unReceivedPayment")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            order.setUnReceivedPayment(new BigDecimal(cell.getStringCellValue()));
+                        }
+                    }else if(modelName.equals("type")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            Optional<SysDict> first = sysDictOfOrderType.stream().filter(s -> s.getName().equals(cell.getStringCellValue())).findFirst();
+                            if(first.isPresent()){
+                                order.setType(first.get().getId());
+                            }else {
+                                msg.setError("订单类型["+cell.getStringCellValue()+"]不存在,请在系统字典中增加");
+                                return msg;
+                            }
+                        }
+                    }else if(modelName.equals("placeTime")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            order.setPlaceTime(LocalDateTime.parse(cell.getStringCellValue(),df1));
+                        }
+                    }else if(modelName.equals("orderStartDate")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            order.setOrderStartDate(LocalDate.parse(cell.getStringCellValue(),df));
+                        }
+                    }else if(modelName.equals("orderEndDate")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            order.setOrderEndDate(LocalDate.parse(cell.getStringCellValue(),df));
+                        }
+                    }else if(modelName.equals("inchargerId")){
                         if(!StringUtils.isEmpty(cell.getStringCellValue())){
                             String userName = cell.getStringCellValue();
                             Optional<User> first;
@@ -345,12 +432,50 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                             if (first.isPresent()) {
                                 order.setInchargerId(first.get().getId());
                             } else {
-                                throw new Exception("["+userName+"]在系统中不存在");
+                                msg.setError("负责人["+userName+"]在系统中不存在");
+                                return msg;
                             }
                         }
-                    }
-                    if(!StringUtils.isEmpty(cell.getStringCellValue())){
-                        method.invoke(order,cell.getStringCellValue());
+                    }else if(modelName.equals("customSigner")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            String userName = cell.getStringCellValue();
+                            Optional<User> first;
+                            if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
+                                Optional<User> optional = targetUserList.stream().filter(tl -> tl.getName().equals(userName)).findFirst();
+                                first= userList.stream().filter(u ->(u.getJobNumber()!=null&&u.getJobNumber().equals(userName))||(optional.isPresent()&&u.getCorpwxUserid()!=null&&u.getCorpwxUserid().equals(optional.get().getCorpwxUserid()))).findFirst();
+                            }else {
+                                first= userList.stream().filter(u -> u.getName().equals(userName)||(u.getJobNumber()!=null&&u.getJobNumber().equals(userName))).findFirst();
+                            }
+                            if (first.isPresent()) {
+                                order.setCustomSigner(first.get().getId());
+                            } else {
+                                msg.setError("客户签约人["+userName+"]在系统中不存在");
+                                return msg;
+                            }
+                        }
+                    }else if(modelName.equals("companySigner")){
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            String userName = cell.getStringCellValue();
+                            Optional<User> first;
+                            if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
+                                Optional<User> optional = targetUserList.stream().filter(tl -> tl.getName().equals(userName)).findFirst();
+                                first= userList.stream().filter(u ->(u.getJobNumber()!=null&&u.getJobNumber().equals(userName))||(optional.isPresent()&&u.getCorpwxUserid()!=null&&u.getCorpwxUserid().equals(optional.get().getCorpwxUserid()))).findFirst();
+                            }else {
+                                first= userList.stream().filter(u -> u.getName().equals(userName)||(u.getJobNumber()!=null&&u.getJobNumber().equals(userName))).findFirst();
+                            }
+                            if (first.isPresent()) {
+                                order.setCompanySigner(first.get().getId());
+                            } else {
+                                msg.setError("公司签约人["+userName+"]在系统中不存在");
+                                return msg;
+                            }
+                        }
+                    }else{
+                        if(!StringUtils.isEmpty(cell.getStringCellValue())){
+                            Class<SalesOrder> orderClass = SalesOrder.class;
+                            Method method = orderClass.getMethod(setter, String.class);
+                            method.invoke(order,cell.getStringCellValue());
+                        }
                     }
                 }
                 importOrderList.add(order);
@@ -385,7 +510,19 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
         List<String> titleList=new ArrayList<>();
         for (int i = 0; i < configObJSONArray.size(); i++) {
             JSONObject item = configObJSONArray.getJSONObject(i);
-            titleList.add(item.getString("label"));
+            if(item.getString("type").equals("grid")){
+                JSONArray columns = item.getJSONArray("columns");
+                for (int i1 = 0; i1 < columns.size(); i1++) {
+                    JSONObject columnsJSONObject = columns.getJSONObject(i1);
+                    JSONArray list = columnsJSONObject.getJSONArray("list");
+                    for (int i2 = 0; i2 < list.size(); i2++) {
+                        JSONObject object = list.getJSONObject(i2);
+                        titleList.add(object.getString("label"));
+                    }
+                }
+            }else {
+                titleList.add(item.getString("label"));
+            }
         }
         dataList.add(titleList);
         HttpRespMsg respMsg = getList( userId,null, orderName,orderCode,null,null,null,null,null,productCode, null,null,0);
@@ -395,18 +532,41 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
             List<String> item=new ArrayList<>();
             for (int i = 0; i < configObJSONArray.size(); i++) {
                 JSONObject target = configObJSONArray.getJSONObject(i);
-                String model = target.getString("model");
-                String targetName = model.substring(0, 1).toUpperCase() + model.substring(1);
-                Class<? extends Product> aClass = product.getClass();
-                String value = String.valueOf(aClass.getMethod("get" + targetName).invoke(product)==null?"":aClass.getMethod("get" + targetName).invoke(product));
-                if(model.equals("inchargerId")){
-                    if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
-                        value = "$userName"+String.valueOf(aClass.getMethod("getInchargerName").invoke(product))+"$";
-                    }else {
-                        value = String.valueOf(aClass.getMethod("getInchargerName").invoke(product));
+                if(target.getString("type").equals("grid")){
+                    JSONArray columns = target.getJSONArray("columns");
+                    for (int i1 = 0; i1 < columns.size(); i1++) {
+                        JSONObject columnsJSONObject = columns.getJSONObject(i1);
+                        JSONArray list = columnsJSONObject.getJSONArray("list");
+                        for (int i2 = 0; i2 < list.size(); i2++) {
+                            JSONObject object = list.getJSONObject(i2);
+                            String model = object.getString("model");
+                            String targetName = model.substring(0, 1).toUpperCase() + model.substring(1);
+                            Class<? extends Product> aClass = product.getClass();
+                            String value = String.valueOf(aClass.getMethod("get" + targetName).invoke(product)==null?"":aClass.getMethod("get" + targetName).invoke(product));
+                            if(model.equals("inchargerId")){
+                                if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
+                                    value = "$userName"+String.valueOf(aClass.getMethod("getInchargerName").invoke(product))+"$";
+                                }else {
+                                    value = String.valueOf(aClass.getMethod("getInchargerName").invoke(product));
+                                }
+                            }
+                            item.add(value);
+                        }
+                    }
+                }else {
+                    String model = target.getString("model");
+                    String targetName = model.substring(0, 1).toUpperCase() + model.substring(1);
+                    Class<? extends Product> aClass = product.getClass();
+                    String value = String.valueOf(aClass.getMethod("get" + targetName).invoke(product)==null?"":aClass.getMethod("get" + targetName).invoke(product));
+                    if(model.equals("inchargerId")){
+                        if(wxCorpInfo!=null&&wxCorpInfo.getSaasSyncContact()==1){
+                            value = "$userName"+String.valueOf(aClass.getMethod("getInchargerName").invoke(product))+"$";
+                        }else {
+                            value = String.valueOf(aClass.getMethod("getInchargerName").invoke(product));
+                        }
                     }
+                    item.add(value);
                 }
-                item.add(value);
             }
             dataList.add(item);
         }
@@ -512,6 +672,7 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                     deptIds.add(-1);
                     List<User> userList = userMapper.selectList(new LambdaQueryWrapper<User>().in(User::getDepartmentId, deptIds));
                     List<String> userIds = userList.stream().map(User::getId).distinct().collect(Collectors.toList());
+                    userIds.add("-1");
                     customLambdaQueryWrapper.in(Custom::getCreatorId,userIds);
                     contactsLambdaQueryWrapper.in(Contacts::getCreatorId,userIds);
                     businessOpportunityLambdaQueryWrapper.in(BusinessOpportunity::getCreatorId,userIds);
@@ -529,6 +690,7 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                     Integer targetDeptId = user.getDepartmentId();
                     List<User> users = userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getDepartmentId, targetDeptId));
                     List<String> targetUserIds = users.stream().map(User::getId).distinct().collect(Collectors.toList());
+                    targetUserIds.add("-1");
                     customLambdaQueryWrapper.in(Custom::getCreatorId,targetUserIds);
                     contactsLambdaQueryWrapper.in(Contacts::getCreatorId,targetUserIds);
                     businessOpportunityLambdaQueryWrapper.in(BusinessOpportunity::getCreatorId,targetUserIds);
@@ -547,6 +709,7 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                     List<Integer> branchDepartment = getBranchDepartment(targetDeptId1, allDeptList);
                     List<User> users1 = userMapper.selectList(new LambdaQueryWrapper<User>().in(User::getDepartmentId, branchDepartment));
                     List<String> targetUserIds1 = users1.stream().map(User::getId).distinct().collect(Collectors.toList());
+                    targetUserIds1.add("-1");
                     customLambdaQueryWrapper.in(Custom::getCreatorId,targetUserIds1);
                     contactsLambdaQueryWrapper.in(Contacts::getCreatorId,targetUserIds1);
                     businessOpportunityLambdaQueryWrapper.in(BusinessOpportunity::getCreatorId,targetUserIds1);
@@ -566,13 +729,21 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
         int customCount = customService.count(customLambdaQueryWrapper);
         int contactsCount = contactsService.count(contactsLambdaQueryWrapper);
         int businessOpportunityCount = businessOpportunityService.count(businessOpportunityLambdaQueryWrapper);
+        List<BusinessOpportunity> businessOpportunityList = businessOpportunityService.list(businessOpportunityLambdaQueryWrapper);
+        double businessOpportunityPrice = businessOpportunityList.stream().filter(b->!StringUtils.isEmpty(b.getAmountOfMoney())).mapToDouble(b ->Double.valueOf(b.getAmountOfMoney())).sum();
         Integer salesOrderCount = salesOrderMapper.selectCount(salesOrderLambdaQueryWrapper);
+        List<SalesOrder> salesOrders = salesOrderMapper.selectList(salesOrderLambdaQueryWrapper);
+        double salesOrdersPrice = salesOrders.stream().mapToDouble(s -> s.getPrice().doubleValue()).sum();
         Integer clueCount = clueMapper.selectCount(clueLambdaQueryWrapper);
 
         int customCount1 = customService.count(customLambdaQueryWrapper1);
         int contactsCount1 = contactsService.count(contactsLambdaQueryWrapper1);
         int businessOpportunityCount1 = businessOpportunityService.count(businessOpportunityLambdaQueryWrapper1);
+        List<BusinessOpportunity> businessOpportunityList1 = businessOpportunityService.list(businessOpportunityLambdaQueryWrapper1);
+        double businessOpportunityPrice1 = businessOpportunityList1.stream().filter(b->!StringUtils.isEmpty(b.getAmountOfMoney())).mapToDouble(b -> Double.valueOf(b.getAmountOfMoney())).sum();
         Integer salesOrderCount1 = salesOrderMapper.selectCount(salesOrderLambdaQueryWrapper1);
+        List<SalesOrder> salesOrders1 = salesOrderMapper.selectList(salesOrderLambdaQueryWrapper1);
+        double salesOrdersPrice1 = salesOrders1.stream().mapToDouble(s -> s.getPrice().doubleValue()).sum();
         Integer clueCount1 = clueMapper.selectCount(clueLambdaQueryWrapper1);
         Map<String,Object> customMap=new HashMap<>();
         customMap.put("customCount",customCount);
@@ -586,10 +757,18 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
         businessOpportunityMap.put("businessOpportunityCount",businessOpportunityCount);
         businessOpportunityMap.put("businessOpportunityPromote",getPromote(businessOpportunityCount,businessOpportunityCount1));
         resultMap.put("businessOpportunity",businessOpportunityMap);
+        Map<String,Object> businessOpportunityPriceMap=new HashMap<>();
+        businessOpportunityPriceMap.put("businessOpportunityPrice",businessOpportunityPrice);
+        businessOpportunityPriceMap.put("businessOpportunityPromote",getPromote((int)businessOpportunityPrice,(int)businessOpportunityPrice1));
+        resultMap.put("businessOpportunityPrice",businessOpportunityPriceMap);
         Map<String,Object> salesOrderMap=new HashMap<>();
         salesOrderMap.put("salesOrderCount",salesOrderCount);
         salesOrderMap.put("salesOrderPromote",getPromote(salesOrderCount,salesOrderCount1));
         resultMap.put("salesOrder",salesOrderMap);
+        Map<String,Object> salesOrderPriceMap=new HashMap<>();
+        salesOrderPriceMap.put("salesOrdersPrice",salesOrdersPrice);
+        salesOrderPriceMap.put("salesOrderPricePromote",getPromote((int)salesOrdersPrice,(int)salesOrdersPrice1));
+        resultMap.put("salesOrdersPrice",salesOrderPriceMap);
         Map<String,Object> clueMap=new HashMap<>();
         clueMap.put("clueCount",clueCount);
         clueMap.put("cluePromote",getPromote(clueCount,clueCount1));
@@ -747,7 +926,7 @@ public class SalesOrderServiceImpl extends ServiceImpl<SalesOrderMapper, SalesOr
                     targetUserIds=targetUserIds2;
             }
         }
-        Map<String,Object> dataMap=businessOpportunityService.getDataStage(companyId,startDate,endDate,userId,targetUserIds);
+        List<Map<String,Object>> dataMap=businessOpportunityService.getDataStage(companyId,startDate,endDate,userId,targetUserIds);
         Map<String,Object> resultMap=new HashMap<>();
         resultMap.put("dataMap",dataMap);
         msg.setData(resultMap);

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

@@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.List;
 
 /**
@@ -27,7 +28,7 @@ public class StageServiceImpl extends ServiceImpl<StageMapper, Stage> implements
     private StageMapper stageMapper;
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public HttpRespMsg changeStage(List<Stage> stages) {
+    public HttpRespMsg changeStage(List<Stage> stages, HttpServletRequest request) {
         HttpRespMsg msg = new HttpRespMsg();
         int i = 0;
         for (Stage stage : stages) {

+ 15 - 2
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/SysFormServiceImpl.java

@@ -72,13 +72,26 @@ public class SysFormServiceImpl extends ServiceImpl<SysFormMapper, SysForm> impl
         JSONArray configObJSONArray = configOb.getJSONArray("list");
         for (int i = 0; i < configObJSONArray.size(); i++) {
             JSONObject item = configObJSONArray.getJSONObject(i);
-            heads.add(item.getString("label"));
+            if(item.getString("type").equals("grid")){
+                JSONArray columns = item.getJSONArray("columns");
+                for (int i1 = 0; i1 < columns.size(); i1++) {
+                    JSONObject columnsJSONObject = columns.getJSONObject(i1);
+                    JSONArray list = columnsJSONObject.getJSONArray("list");
+                    for (int i2 = 0; i2 < list.size(); i2++) {
+                        JSONObject object = list.getJSONObject(i2);
+                        heads.add(object.getString("label"));
+                    }
+                }
+            }else {
+                heads.add(item.getString("label"));
+            }
+
         }
         List<List<String>> allList = new ArrayList<>();
         allList.add(heads);
         String title;
         switch (code){
-            case "Clue":title = company.getCompanyName()+"_线索导入模板";
+            case "Thread":title = company.getCompanyName()+"_线索导入模板";
             break;
             case "Custom":title = company.getCompanyName()+"_客户导入模板";
                 break;

+ 31 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/util/BusinessItemProductListDeserializer.java

@@ -0,0 +1,31 @@
+package com.management.platform.util;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.management.platform.entity.BusinessItemProduct;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BusinessItemProductListDeserializer extends JsonDeserializer<List<BusinessItemProduct>> {
+
+    @Override
+    public List<BusinessItemProduct> deserialize(JsonParser p, DeserializationContext ctxt)
+            throws IOException, JsonProcessingException {
+        ObjectMapper mapper = (ObjectMapper) p.getCodec();
+        JsonNode node = mapper.readTree(p);
+        List<BusinessItemProduct> products = new ArrayList<>();
+        if (node.isArray()) {
+            for (JsonNode jsonNode : node) {
+                BusinessItemProduct product = mapper.treeToValue(jsonNode, BusinessItemProduct.class);
+                products.add(product);
+            }
+        }
+        return products;
+    }
+}

+ 15 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/CustomMapper.xml

@@ -48,10 +48,15 @@
         email,
         company_phone,
         create_time,
+        customer_level_id,
         (select name from sys_dict where customer_level_id = id and code = 'CustomLevel') customerLevelValue,
+        customer_industry_id,
         (select name from sys_dict where customer_industry_id = id and code = 'CustomIndustry') customerIndustryValue,
+        custom_source_id,
         (select name from sys_dict where custom_source_id = id and code = 'CustomSources') customSourceValue,
+        incharger_id,
         (select `name` from `user` where id = incharger_id) inchargerName,
+        creator_id,
         (select `name` from `user` where id = creator_id) creatorName
         from custom
         where company_id = #{companyId}
@@ -118,10 +123,15 @@
         email,
         company_phone,
         create_time,
+        customer_level_id,
         (select name from sys_dict where customer_level_id = id and code = 'CustomLevel') customerLevelValue,
+        customer_industry_id,
         (select name from sys_dict where customer_industry_id = id and code = 'CustomIndustry') customerIndustryValue,
+        custom_source_id,
         (select name from sys_dict where custom_source_id = id and code = 'CustomSources') customSourceValue,
+        incharger_id,
         (select `name` from `user` where id = incharger_id) inchargerName,
+        creator_id,
         (select `name` from `user` where id = creator_id) creatorName
         from custom
         where company_id = #{companyId}
@@ -194,10 +204,15 @@
         email,
         company_phone,
         create_time,
+        customer_level_id,
         (select name from sys_dict where customer_level_id = id and code = 'CustomLevel') customerLevelValue,
+        customer_industry_id,
         (select name from sys_dict where customer_industry_id = id and code = 'CustomIndustry') customerIndustryValue,
+        custom_source_id,
         (select name from sys_dict where custom_source_id = id and code = 'CustomSources') customSourceValue,
+        incharger_id,
         (select `name` from `user` where id = incharger_id) inchargerName,
+        creator_id,
         (select `name` from `user` where id = creator_id) creatorName
         from custom
         where company_id = #{companyId}

+ 2 - 1
fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/SalesOrderMapper.xml

@@ -18,6 +18,7 @@
         <result column="order_end_date" property="orderEndDate" />
         <result column="received_payment" property="receivedPayment" />
         <result column="un_received_payment" property="unReceivedPayment" />
+        <result column="received_status" property="receivedStatus" />
         <result column="custom_signer" property="customSigner" />
         <result column="company_signer" property="companySigner" />
         <result column="incharger_id" property="inchargerId" />
@@ -35,7 +36,7 @@
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id, company_id, order_name, order_code, custom_id, business_opportunity_id, price, contacts_id, type, place_time, order_start_date, order_end_date, received_payment, un_received_payment, custom_signer, company_signer, incharger_id, remark, create_time, creator_id, is_delete, status, plate1, plate2, plate3, plate4, plate5
+        id, company_id, order_name, order_code, custom_id, business_opportunity_id, price, contacts_id, type, place_time, order_start_date, order_end_date, received_payment, un_received_payment, received_status, custom_signer, company_signer, incharger_id, remark, create_time, creator_id, is_delete, status, plate1, plate2, plate3, plate4, plate5
     </sql>
 
 </mapper>

+ 1 - 0
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ProjectController.java

@@ -1193,6 +1193,7 @@ public class ProjectController {
 
     //todo 同步项目相关数据
     @RequestMapping("/synchronizationProject")
+    @LimitRequest(count = 1,time = 1000)
     public HttpRespMsg synchronizationProject(@RequestBody String dataJson){
         return projectService.synchronizationProject(dataJson);
     }

+ 1 - 1
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/ReportController.java

@@ -2546,7 +2546,7 @@ public class ReportController {
         return reportService.pushProjectReportToSap(pushDate,reportId);
     }
 
-    //todo:推送工时管家工时考勤数据到SAP
+    //todo:取消推送工时管家工时考勤数据到SAP
     @RequestMapping("/cancelHasPushForSap")
     public HttpRespMsg cancelHasPushForSap(String uuids){
         return reportService.cancelHasPushForSap(uuids);

+ 153 - 26
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/UserWithBeisenController.java

@@ -448,7 +448,8 @@ public class UserWithBeisenController {
         DateTimeFormatter df=DateTimeFormatter.ofPattern("yyyy-MM-dd");
         DateTimeFormatter df1=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
         DateTimeFormatter df2=DateTimeFormatter.ofPattern("HH:mm");
-        DateTimeFormatter df3=DateTimeFormatter.ofPattern("HH:mm:ss");
+        DateTimeFormatter df3=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+        DateTimeFormatter df4=DateTimeFormatter.ofPattern("HH:mm:ss");
         User user = userMapper.selectById(request.getHeader("token"));
         Integer companyId = user.getCompanyId();
         TimeType timeType = timeTypeMapper.selectById(companyId);
@@ -481,44 +482,170 @@ public class UserWithBeisenController {
             boolean workDay = WorkDayCalculateUtils.isWorkDay(LocalDate.parse(createDate,df));
             //todo:针对景昱 工作日默认以8小时工作制度加上加班时长 非工作日以加班时长为准
             Double workTime;
-            Duration between = Duration.between(LocalTime.parse(firstCard, df3), LocalTime.parse(lastCard, df3));
-            if(between.toHours()>8){
-                workTime=8.0;
-            }else if(between.toHours()<0) {
-                workTime=0.0;
+            Duration between = Duration.between(LocalTime.parse(firstCard, df4), LocalTime.parse(lastCard, df4));
+            LocalTime min = LocalTime.parse(firstCard, df4);
+            LocalTime max = LocalTime.parse(lastCard, df4);
+            if(timeType.getCompanyId()==5978){
+                if(workDay){
+                    workTime=8.0;
+                }else {
+                    workTime=0.0;
+                }
             }else {
-                BigDecimal decimal = new BigDecimal(between.toMinutes());
-                decimal=decimal.divide(new BigDecimal(60),1,RoundingMode.HALF_UP);
-                workTime=decimal.doubleValue();
+                if(between.toHours()<0){
+                    workTime=0.0;
+                }else {
+                    BigDecimal decimal = new BigDecimal(between.toMinutes());
+                    decimal=decimal.divide(new BigDecimal(60),1,RoundingMode.HALF_UP);
+                    workTime=decimal.doubleValue();
+                }
             }
             Stream<JSONObject> overTimeStream = allOverTimeList.stream().map(elment -> (JSONObject) elment);
             Stream<JSONObject> vacationStream = vacationList.stream().map(elment -> (JSONObject) elment);
             Optional<UserWithBeisen> beisen = userWithBeisenList.stream().filter(u -> u.getJobNumber() != null && u.getJobNumber().equals(first.get().getJobNumber())).findFirst();
             if(beisen.isPresent()){
+                //todo:之前的逻辑
+//                        List<JSONObject> overTimeList = overTimeStream.filter(a -> a.getString("StaffId").equals(beisen.get().getUserId())
+//                                && (a.getIntValue("ApproveStatus") == 2||a.getIntValue("ApproveStatus") == 1)
+//                                &&LocalDateTime.parse(a.getString("StartDate"),df1).toLocalDate().isEqual(localDate)).collect(Collectors.toList());
                 List<JSONObject> overTimeList = overTimeStream.filter(a -> a.getString("StaffId").equals(beisen.get().getUserId())
                         && (a.getIntValue("ApproveStatus") == 2||a.getIntValue("ApproveStatus") == 1)).collect(Collectors.toList());
-                if(overTimeList.size()>0){
-                    double actualOverTimeDuration = overTimeList.stream().mapToDouble(i -> i.getDouble("OverTimeDuration")).sum();
-                    if(workDay){
-                        workTime= workTime+actualOverTimeDuration;
+                //加班数据可能存在结束日期是当前日期的情况的情况
+                BigDecimal overTimeBigDecimal = new BigDecimal(0);
+                LocalDate localDate = LocalDate.parse(createDate, df);
+                for (JSONObject o : overTimeList) {
+                    //存在开始日期为当前日期的数据以及结束日期为当天日期的数据 分开计算
+                    if(LocalDateTime.parse(o.getString("StartDate"),df1).toLocalDate().isEqual(localDate)){
+                        //存在开始日期为当天的数据
+                        if(LocalDateTime.parse(o.getString("StartDate"),df1).toLocalDate().isEqual(LocalDateTime.parse(o.getString("StopDate"),df1).toLocalDate())){
+                            //开始日期和结束日期是相同的情况 说明是加班区间只存在于当天的情况
+                            if(max.isAfter(LocalTime.parse("19:00:00",df4))){
+                                //判断打卡时间是不是大于19:00 大于才算加班 通过打卡计算加班时长 与加班单作比较 取小
+                                Duration timeDurantion = Duration.between(LocalTime.parse("19:00:00", df4), max);
+                                //如果结束时间大于13:00
+                                if(max.isAfter(LocalTime.parse("13:00:00",df4))){
+                                    timeDurantion.minusHours(1);
+                                }
+                                //如果结束时间大于18:00
+                                if(max.isAfter(LocalTime.parse("18:00:00",df4))){
+                                    timeDurantion.minusMinutes(30);
+                                }
+                                long l = timeDurantion.toMinutes() / 60;
+                                if(l<o.getDouble("OverTimeDuration")){
+                                    overTimeBigDecimal=overTimeBigDecimal.add(new BigDecimal(l));
+                                }else {
+                                    overTimeBigDecimal=overTimeBigDecimal.add(new BigDecimal(o.getDouble("OverTimeDuration")));
+                                }
+                            }
+                        }else {
+                            //开始日期和结束日期是不相同的情况 说明是加班区间存在加班到第二天的情况
+                            LocalDateTime start = LocalDateTime.parse(o.getString("StartDate"), df1);
+                            LocalDateTime stop = start.toLocalDate().atTime(LocalTime.MAX);
+                            Duration duration = Duration.between(start, stop);
+                            if(stop.toLocalTime().isAfter(LocalTime.parse("13:00:00",df4))){
+                                duration.minusHours(1);
+                            }
+                            //如果结束时间大于18:00
+                            if(stop.toLocalTime().isAfter(LocalTime.parse("18:00:00",df4))){
+                                duration.minusMinutes(30);
+                            }
+                            long l = duration.toMinutes() / 60;
+                            if(max.isAfter(LocalTime.parse("19:00:00",df4))){
+                                //判断打卡时间是不是大于19:00 通过打卡计算加班时长 与加班单作比较 取小
+                                Duration timeDurantion = Duration.between(LocalTime.parse("19:00:00", df4), max);
+                                long l1 = timeDurantion.toMinutes() / 60;
+                                if(l1<l){
+                                    overTimeBigDecimal=overTimeBigDecimal.add(new BigDecimal(l1));
+                                }else {
+                                    overTimeBigDecimal=overTimeBigDecimal.add(new BigDecimal(l));
+                                }
+                            }
+                        }
+                    }else if(LocalDateTime.parse(o.getString("StopDate"),df1).toLocalDate().isEqual(localDate)){
+                        //存在结束日期为当天的数据
+                        if(LocalDateTime.parse(o.getString("StartDate"),df1).toLocalDate().isEqual(LocalDateTime.parse(o.getString("StopDate"),df1).toLocalDate())){
+                            //开始日期和结束日期是相同的情况 说明是加班区间只存在于当天的情况
+                            if(max.isAfter(LocalTime.parse("19:00:00",df4))){
+                                //判断打卡时间是不是大于19:00 通过打卡计算加班时长 与加班单作比较 取小
+                                Duration timeDurantion = Duration.between(LocalTime.parse("19:00:00", df4), max);
+                                //如果结束时间大于13:00
+                                if(max.isAfter(LocalTime.parse("13:00:00",df4))){
+                                    timeDurantion.minusHours(1);
+                                }
+                                //如果结束时间大于18:00
+                                if(max.isAfter(LocalTime.parse("18:00:00",df4))){
+                                    timeDurantion.minusMinutes(30);
+                                }
+                                long l = timeDurantion.toMinutes() / 60;
+                                if(l<o.getDouble("OverTimeDuration")){
+                                    overTimeBigDecimal=overTimeBigDecimal.add(new BigDecimal(l));
+                                }else {
+                                    overTimeBigDecimal=overTimeBigDecimal.add(new BigDecimal(o.getDouble("OverTimeDuration")));
+                                }
+                            }
+                        }else {
+                            //开始日期和结束日期是不相同的情况 说明是加班区间存在加班到第二天的情况
+                            LocalDateTime stop = LocalDateTime.parse(o.getString("StopDate"), df1);
+                            LocalDateTime start = stop.toLocalDate().atTime(LocalTime.MIN);
+                            Duration duration = Duration.between(start, stop);
+                            //如果结束时间大于13:00
+                            if(stop.toLocalTime().isAfter(LocalTime.parse("13:00:00",df4))){
+                                duration.minusHours(1);
+                            }
+                            //如果结束时间大于18:00
+                            if(stop.toLocalTime().isAfter(LocalTime.parse("18:00:00",df4))){
+                                duration.minusMinutes(30);
+                            }
+                            long l = duration.toMinutes() / 60;
+                            if(max.isAfter(LocalTime.parse("19:00:00",df4))){
+                                //判断打卡时间是不是大于19:00 通过打卡计算加班时长 与加班单作比较 取小
+                                Duration timeDurantion = Duration.between(LocalTime.parse("19:00:00", df4), max);
+                                long l1 = timeDurantion.toMinutes() / 60;
+                                if(l1<l){
+                                    overTimeBigDecimal=overTimeBigDecimal.add(new BigDecimal(l1));
+                                }else {
+                                    overTimeBigDecimal=overTimeBigDecimal.add(new BigDecimal(l));
+                                }
+                            }
+                        }
+                    }
+                }
+                if(workDay){
+                    //工作日计算按照前面的逻辑计算加班时长计算
+                    workTime= workTime+overTimeBigDecimal.doubleValue();
+                }else {
+                    //非工作日加班 根据实际打卡时长 比较 加班单时长
+                    if((between.toMinutes()/60)>overTimeBigDecimal.doubleValue()){
+                        workTime= workTime+overTimeBigDecimal.doubleValue();
                     }else {
-                        workTime= actualOverTimeDuration;
+                        workTime= workTime+(between.toMinutes()/60);
                     }
                 }
-                //计算休假
-                List<JSONObject> targetVacation = vacationStream.filter(a -> a.getString("StaffId").equals(beisen.get().getUserId())
-                        && (a.getString("ApproveStatus").equals("通过") || a.getString("ApproveStatus").equals("审批中"))).collect(Collectors.toList());
-                if(targetVacation.size()>0){
-                    double vacationDuration = targetVacation.stream().mapToDouble(i -> i.getDouble("VacationDuration")).sum();
+                //处理修改
+                List<JSONObject> lastVacationList = vacationStream.filter(a ->{
+                    LocalDate vacationStartDate = LocalDateTime.parse(a.getString("VacationStartDateTime"), df3).toLocalDate();
+                    LocalDate vacationStopDate = LocalDateTime.parse(a.getString("VacationStopDateTime"), df3).toLocalDate();
+                    boolean b=false;
+                    if((localDate.isAfter(vacationStartDate)||localDate.isEqual(vacationStartDate))
+                            &&(localDate.isBefore(vacationStopDate)||localDate.isEqual(vacationStopDate))){
+                        b=true;
+                    }
+                    if(a.getString("StaffId").equals(beisen.get().getUserId())
+                            && b
+                            &&  (a.getString("ApproveStatus").equals("通过") || a.getString("ApproveStatus").equals("审批中"))){
+                        return true;
+                    }
+                    return false;
+                }).collect(Collectors.toList());
+                if(lastVacationList.size()>0){
+                    double vacationDuration = lastVacationList.stream().mapToDouble(i -> i.getDouble("VacationDuration")).sum();
                     BigDecimal decimal = new BigDecimal(vacationDuration);
                     decimal=decimal.divide(new BigDecimal(60),1,RoundingMode.HALF_UP);
-                    //考勤打卡区间大于8小时 但是存在休假数据
-                    if(between.toHours()>8){
-                        if(decimal.doubleValue()>8){
-                            workTime= workTime-8;
-                        }else {
-                            workTime= workTime-decimal.doubleValue();
-                        }
+//                            //可能存在休假多天 只减去一天
+                    if(decimal.doubleValue()>=8){
+                        workTime= workTime-8;
+                    }else {
+                        workTime= workTime-decimal.doubleValue();
                     }
                 }
             }

+ 16 - 9
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ProjectServiceImpl.java

@@ -245,6 +245,8 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
     private CompanyDingdingMapper companyDingdingMapper;
     @Resource
     private CompanyDingdingService companyDingdingService;
+    @Resource
+    private TaskExecutorService taskExecutorService;
 
     @Value(value = "${upload.path}")
     private String path;
@@ -13794,18 +13796,21 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
             resultList=projectMapper.projectExpendProcessList(startDate,endDate,projectId,categoryId,userId,companyId,null,start,size);
             total=projectMapper.projectExpendProcessListCount(startDate,endDate,projectId,categoryId,userId,companyId,null);
         }
+        List<Integer> projectList = resultList.stream().map(i -> (Integer) i.get("projectId")).distinct().collect(Collectors.toList());
+        projectList.add(-1);
+        List<TaskExecutor> taskExecutorList = taskExecutorService.list(new LambdaQueryWrapper<TaskExecutor>().in(TaskExecutor::getProjectId, projectList));
+        List<String> executorIds = taskExecutorList.stream().map(TaskExecutor::getExecutorId).distinct().collect(Collectors.toList());
+        executorIds.add("-1");
+        List<Report> reportList = reportMapper.selectList(new LambdaQueryWrapper<Report>().eq(Report::getCompanyId,companyId).eq(Report::getState,1).in(Report::getProjectId, projectList).in(Report::getCreatorId, executorIds).between(Report::getCreateDate, startDate, endDate));
         for (Map<String, Object> map : resultList) {
-            if(StringUtils.isEmpty(String.valueOf(map.get("executorString")))){
-                continue;
-            }
-            String executorString = String.valueOf(map.get("executorString"));
-            List<String> list = ListUtil.convertLongIdsArrayToList(executorString);
             List<Map<String,Object>> itemList=new ArrayList<>();
-            for (String str : list) {
-                String[] split = str.split("\\|");
+            List<TaskExecutor> executors = taskExecutorList.stream().filter(t -> t.getProjectId().equals((Integer) map.get("projectId"))).collect(Collectors.toList());
+            for (TaskExecutor taskExecutor : executors) {
+                List<Report> reports = reportList.stream().filter(r -> r.getCreatorId().equals(taskExecutor.getExecutorId())&&r.getProjectId().equals((Integer)map.get("projectId"))).collect(Collectors.toList());
+                double sum = reports.stream().mapToDouble(Report::getWorkingTime).sum();
                 Map<String,Object> item=new HashMap<>();
-                item.put("userName",split[0]);
-                item.put("progress",split[1]);
+                item.put("userName",taskExecutor.getExecutorName());
+                item.put("progress",sum);
                 itemList.add(item);
             }
             Map<String, List<Map<String, Object>>> listMapGroup = itemList.stream().collect(Collectors.groupingBy(i -> String.valueOf(i.get("userName"))));
@@ -13823,8 +13828,10 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
         }
         //处理项目下的负责组长
         List<Integer> needProjectIds = resultList.stream().map(m -> Integer.valueOf(String.valueOf(m.get("projectId")))).collect(Collectors.toList());
+        needProjectIds.add(-1);
         List<Participation> participationList = participationMapper.selectList(new LambdaQueryWrapper<Participation>().in(Participation::getProjectId, needProjectIds));
         List<String> needUserIds = participationList.stream().map(m -> String.valueOf(m.getUserId())).collect(Collectors.toList());
+        needUserIds.add("-1");
         List<User> userList = userMapper.selectList(new LambdaQueryWrapper<User>().in(User::getId, needUserIds));
         resultList.forEach(r->{
             List<Participation> targetParticipationList = participationList.stream().filter(p -> p.getProjectId().equals(Integer.valueOf(String.valueOf(r.get("projectId"))))).collect(Collectors.toList());

+ 2 - 2
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/service/impl/ReportServiceImpl.java

@@ -5582,11 +5582,11 @@ public class ReportServiceImpl extends ServiceImpl<ReportMapper, Report> impleme
                 if(needCorpWxTranslate){
                     item.add("$userName="+(map.get("corpwxUserId")==null?"":map.get("corpwxUserId"))+"$");
                     item.add(departmentService.exportWxDepartment(dept,departments));
-                    item.add(manager.isPresent()?manager.get().getName():"");
+                    item.add(manager.isPresent()?("$userName="+manager.get().getName()+"$"):"");
                 }else if(dingding!=null&&dingding.getContactNeedTranslate()==1){
                     item.add("$userName="+(map.get("name")==null?"":map.get("name"))+"$");
                     item.add(departmentService.exportDdDepartment(dept,departments));
-                    item.add(manager.isPresent()?manager.get().getName():"");
+                    item.add(manager.isPresent()?("$userName="+manager.get().getName()+"$"):"");
                 }else  {
                     item.add((String) map.get("name"));
                     item.add(departmentService.getSupDepartment(dept,departments));

+ 3 - 4
fhKeeper/formulahousekeeper/management-platform/src/main/resources/mapper/ProjectMapper.xml

@@ -1918,15 +1918,14 @@
     <select id="projectExpendProcessList" resultType="java.util.Map">
         select p.id AS projectId,p.project_name as projectName,DATE_FORMAT(p.`plan_start_date`,'%Y-%m-%d') AS planStartDate,DATE_FORMAT(p.`plan_end_date`,'%Y-%m-%d') AS planEndDate,pc.name as categoryName,p.project_code as projectCode,IFNULL(SUM(te.plan_hours),0) as planHour,
         IFNULL((select SUM(working_time) from report where project_id=p.id and create_date BETWEEN #{startDate} AND #{endDate} and state=1),0) as realHour, IFNULL((select SUM(cost) from report where project_id=p.id and create_date BETWEEN #{startDate} AND #{endDate} and state=1),0) as realCost,
-        IFNULL(IFNULL(SUM(te.plan_hours),0)-IFNULL((select SUM(working_time) from report where project_id=p.id and create_date BETWEEN #{startDate} AND #{endDate} and state=1),0),0) as residueHour,
-        GROUP_CONCAT(CONCAT_WS('|',te.executor_name,IFNULL((select SUM(working_time) from report where creator_id=te.executor_id and project_id=p.id and create_date BETWEEN #{startDate} AND #{endDate} and state=1),0))) as executorString
+        IFNULL(IFNULL(SUM(te.plan_hours),0)-IFNULL((select SUM(working_time) from report where project_id=p.id and create_date BETWEEN #{startDate} AND #{endDate} and state=1),0),0) as residueHour
         from task_executor te
         left join  user u on u.id=te.executor_id
         left join department d on u.department_id=d.department_id
         left join task t on t.id=te.task_id
         left join project p on p.id=t.project_id
         left join project_category pc on pc.id=p.category
-        where u.company_id=#{companyId} and te.project_id is not null  and pc.name in ('报价项目','售后报价项目','售后工程项目','工程项目')
+        where u.company_id=#{companyId} and te.project_id is not null  and pc.name not in ('报价项目','售后报价项目','研发项目')
         <if test="projectId!=null">
             and p.id=#{projectId}
         </if>
@@ -1957,7 +1956,7 @@
         left join task t on t.id=te.task_id
         left join project p on p.id=t.project_id
         left join project_category pc on pc.id=p.category
-        where u.company_id=#{companyId} and te.project_id is not null  and pc.name in ('报价项目','售后报价项目','售后工程项目','工程项目')
+        where u.company_id=#{companyId} and te.project_id is not null  and pc.name not in ('报价项目','售后报价项目','研发项目')
         <if test="projectId!=null">
             and p.id=#{projectId}
         </if>

+ 6 - 4
fhKeeper/formulahousekeeper/timesheet/src/components/vueMultipleDept.vue

@@ -4,8 +4,9 @@
             <div @click.stop="showVisible(true)">
                 <div class="bionicClassText" v-if="selectedDept.length <= 0">全部部门</div>
                 <div v-else>
-                    <el-tag @click.stop="" type="info" size="small" closable @close="deteleItem(0)">{{ selectedDeptLabel[0]
-                    }}</el-tag>
+                    <el-tag @click.stop="" type="info" size="small" closable @close="deteleItem(0)">
+                        <TranslationOpenDataText type='departmentName' :openid='selectedDeptLabel[0]'></TranslationOpenDataText>
+                    </el-tag>
                     <el-tag @click.stop="" type="info" size="small" v-if="selectedDeptLabel.length > 1">+ {{
                         selectedDeptLabel.length - 1 }}</el-tag>
                 </div>
@@ -26,13 +27,14 @@
                         :filter-node-method="treeDatafilterNode" default-expand-all @check-change="handleCheckChange"
                         ref="treeDataComtent">
                         <span class="custom-tree-node" slot-scope="{ node, data }">
-                            <span v-if="node.data.children">
+                            <!-- <span v-if="node.data.children">
                                 <TranslationOpenDataText type='departmentName' :openid='node.label'>
                                 </TranslationOpenDataText>
                             </span>
                             <span v-else>
                                 <TranslationOpenDataText type='userName' :openid='node.label'></TranslationOpenDataText>
-                            </span>
+                            </span> -->
+                            <TranslationOpenDataText type='departmentName' :openid='node.label'></TranslationOpenDataText>
                         </span>
                     </el-tree>
                 </div>

+ 2 - 1
fhKeeper/formulahousekeeper/timesheet/src/views/corpreport/list.vue

@@ -3628,6 +3628,7 @@ export default {
         this.getConsumptionSchedule()
       }
       if(this.ins == 25) {
+        this.page=1
         this.getConsumptionScheduleTwo()
       }
       if(this.ins == 26) {
@@ -3962,7 +3963,7 @@ export default {
       let dataList = data || []
       dataList.push({id: 0, name: '未分类'})
       this.projectSortList = dataList
-      this.projectSortListTwo = dataList.filter((item) => item.name == '报价项目' || item.name == '售后工程项目' || item.name == '售后报价项目'||item.name == '工程项目')
+      this.projectSortListTwo = dataList.filter((item) => item.name != '报价项目' &&  item.name != '售后报价项目' && item.name != '研发项目' && item.name != '未分类')
       this.projectSortId = dataList[0].id
       this.projectSortName = dataList[0].name
     },