Selaa lähdekoodia

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

seyason 1 vuosi sitten
vanhempi
commit
a15a1122f1
43 muutettua tiedostoa jossa 3743 lisäystä ja 2516 poistoa
  1. 4 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/components/TaskModal/api.ts
  2. 17 11
      fhKeeper/formulahousekeeper/customerBuler-crm/src/components/TaskModal/index.vue
  3. 18 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/components/TaskModal/type.d.ts
  4. 8 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/api.ts
  5. 76 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/attachment.vue
  6. 73 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/information.vue
  7. 58 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/operationRecord.vue
  8. 93 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/products.vue
  9. 81 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/relatedBusiness.vue
  10. 92 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/relatedProducts.vue
  11. 152 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/detail/index.vue
  12. 264 2
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/index.vue
  13. 11 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/type.d.ts
  14. 2 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/system/customForm/index.vue
  15. 3 2
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/index.vue
  16. 9 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/constant.ts
  17. 118 45
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/attachment.vue
  18. 208 17
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/information.vue
  19. 18 35
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/operationRecord.vue
  20. 40 5
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/index.vue
  21. 166 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/deteleTables.vue
  22. 222 28
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/index.vue
  23. 2 2
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/type.d.ts
  24. 3 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/type.d.ts
  25. 31 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/times.ts
  26. 55 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/utils/tools.ts
  27. 1 1
      fhKeeper/formulahousekeeper/customerBuler-crm/vite.config.ts
  28. 34 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/BusinessOpportunityController.java
  29. 18 10
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/ProductController.java
  30. 7 3
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/BusinessOpportunity.java
  31. 3 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/Product.java
  32. 4 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/mapper/BusinessOpportunityMapper.java
  33. 4 0
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/BusinessOpportunityService.java
  34. 21 2
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/BusinessOpportunityServiceImpl.java
  35. 5 1
      fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/service/impl/ProductServiceImpl.java
  36. 2 2
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/BusinessItemProductMapper.xml
  37. 12 2
      fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/BusinessOpportunityMapper.xml
  38. 70 6
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/HolidaySettingController.java
  39. 13 1
      fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/HolidaySetting.java
  40. 2 1
      fhKeeper/formulahousekeeper/management-platform/src/main/resources/mapper/HolidaySettingMapper.xml
  41. 1400 2334
      fhKeeper/formulahousekeeper/ops-platform/octopus.log
  42. 3 1
      fhKeeper/formulahousekeeper/timesheet/src/views/projectApproval/projectApproval.vue
  43. 320 1
      fhKeeper/formulahousekeeper/timesheet/src/views/settings/timetype.vue

+ 4 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/components/TaskModal/api.ts

@@ -35,7 +35,10 @@ export const TASK_TYPE = [
   { label: "销售订单", value: 2, show: true },
   { label: "线索", value: 3, show: false },
 ];
-export const TASK_TYPE_FIELD = [
+export const TASK_TYPE_FIELD: {
+  type: string;
+  field: "customerId" | "businessId" | "orderId" | "clueId";
+}[] = [
   {
     type: "0",
     field: "customerId",

+ 17 - 11
fhKeeper/formulahousekeeper/customerBuler-crm/src/components/TaskModal/index.vue

@@ -2,7 +2,7 @@
   <el-dialog v-model="props.visible" width="800px" :show-close="false" :close-on-click-modal="false" top="10vh">
     <template #header="{ titleId, titleClass }">
       <div class="flex justify-between items-center border-b pb-3">
-        <h4 :id="titleId" :class="titleClass">{{ editForm ? '编辑任务' : '新增任务' }}</h4>
+        <h4 :id="titleId" :class="titleClass">{{ title || "新建任务" }}</h4>
         <div>
           <el-button v-if="!editForm" type="primary" :loading="['2'].includes(props.saveLoading)"
             @click="submitForm(formRef, true)">保存并新建</el-button>
@@ -16,33 +16,37 @@
       <el-form ref="formRef" :model="form" label-width="7em" :rules="rules" class="flex flex-wrap form">
         <el-form-item label="任务名称:" prop="taskName" required>
           <el-input v-model="form.taskName" type="textarea" placeholder="请输入任务名称" clearable maxlength="100"
-            show-word-limit />
+            show-word-limit :disabled="disabledList && disabledList.includes('taskName')" />
         </el-form-item>
         <el-form-item prop="priority" label="优先级:" required>
-          <el-select v-model="form.priority" placeholder="请选择" clearable>
+          <el-select v-model="form.priority" placeholder="请选择" clearable
+            :disabled="disabledList && disabledList.includes('priority')">
             <el-option v-for="item in PRIORITY " :key="item.value" :value="item.value" :label="item.label" />
           </el-select>
         </el-form-item>
         <el-form-item :label="form.taskType">
           <template #label>
-            <el-select v-model="form.taskType" class="border resetSelect" style="width: 100px" @change="changeTaskType">
+            <el-select v-model="form.taskType" class="border resetSelect" style="width: 100px" @change="changeTaskType"
+              :disabled="disabledList && disabledList.includes('taskType')">
               <el-option v-for="item in TASK_TYPE" :key="item.value" :value="item.value" :label="item.label" />
             </el-select>
           </template>
           <template v-for="item in TASK_TYPE_FIELD">
             <el-select v-model="form[item.field]" v-if="form.taskType == item.type" placeholder="请选择" clearable
-              filterable>
+              filterable :disabled="disabledList && disabledList.includes(item.field)">
               <el-option v-for="item in taskTypeValueData" :key="item.value" :value="item.value" :label="item.label" />
             </el-select>
           </template>
         </el-form-item>
         <el-form-item label="联系人:" v-if="TASK_TYPE.find(v => v.value === (form.taskType || '1'))?.show">
-          <el-select v-model="form.contactsId" placeholder="请选择" clearable filterable>
+          <el-select v-model="form.contactsId" placeholder="请选择" clearable filterable
+            :disabled="disabledList && disabledList.includes('contactsId')">
             <el-option v-for="item in contactValueData" :key="item.value" :value="item.value" :label="item.label" />
           </el-select>
         </el-form-item>
         <el-form-item label="执行人:">
-          <el-select v-model="form.executorId" placeholder="请选择" clearable multiple filterable>
+          <el-select v-model="form.executorId" placeholder="请选择" clearable multiple filterable
+            :disabled="disabledList && disabledList.includes('executorId')">
             <el-option v-for="item in executorValueData" :key="item.value" :value="item.value" :label="item.label" />
           </el-select>
         </el-form-item>
@@ -55,7 +59,7 @@
               <el-option v-for="item in REPEAT_TYPE" :key="item.value" :value="item.value" :label="item.label" />
             </el-select>
           </el-form-item>
-          <template v-if="[0,1,2,3].includes(form.repeatType)">
+          <template v-if="[0, 1, 2, 3].includes(form.repeatType)">
             <el-form-item label="每:" v-if="form.repeatType == 3">
               <el-input-number v-model="form.repeatDesignSameday" controls-position="right" :min="1" />天
             </el-form-item>
@@ -98,10 +102,12 @@
           </template>
         </template>
         <el-form-item label="开始时间:" class="w50">
-          <el-date-picker v-model="form.startDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
+          <el-date-picker v-model="form.startDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD"
+            :disabled="disabledList && disabledList.includes('startDate')" />
         </el-form-item>
         <el-form-item label="截止时间:" class="w50">
-          <el-date-picker v-model="form.endDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" />
+          <el-date-picker v-model="form.endDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD"
+            :disabled="disabledList && disabledList.includes('endDate')" />
         </el-form-item>
       </el-form>
       <GenerateForm ref="generateFormRef" :data="generateFormData" :value="form" />
@@ -232,7 +238,7 @@ function changeTaskType(value: TASK_VALUE_TYPE) {
     case 3:
       taskTypeValueData.value = [];
       setTimeout(() => {
-        taskTypeValueData.value = [{ label: "线索1", value: 1 }, { label: "线索2", value: 2}]
+        taskTypeValueData.value = [{ label: "线索1", value: 1 }, { label: "线索2", value: 2 }]
       }, 500)
       break;
     default:

+ 18 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/components/TaskModal/type.d.ts

@@ -11,6 +11,24 @@ export interface Props {
    * form表单的值
    */
   editForm: any;
+  /**
+   *  弹窗标题
+   * @default '新建任务'
+   */
+  title?: string;
+  disabledList?: (
+    | "taskName"
+    | "priority"
+    | "taskType"
+    | "customerId"
+    | "businessId"
+    | "orderId"
+    | "clueId"
+    | "contactsId"
+    | "executorId"
+    | "startDate"
+    | "endDate"
+  )[];
 }
 
 export interface Emits {

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

@@ -0,0 +1,8 @@
+export const MOD = '/product'
+export const prefix = '/product'
+export const GETSYSFILED = '/sys-dict/getListByCode'
+export const GETPERSONNEL = '/user/getSimpleActiveUserList'
+export const GETTEMPLATE = `/sys-form/getListByCode${MOD}`
+export const GETTABLELIST = `${prefix}/list`
+export const ADDPRODUCT = `${prefix}/addOrUpdate`
+export const ALLDETELE = `${prefix}/delete`

+ 76 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/attachment.vue

@@ -0,0 +1,76 @@
+<template>
+    <div class="attachment pl-4 pr-4 pt-3 pb-3 h-full flex flex-col">
+        <div class="flex justify-between">
+            <div class="title">附件</div>
+            <div>
+                <el-button type="primary">上传</el-button>
+            </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 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>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </div>
+    </div>
+</template>
+<script lang="ts" setup>
+import { ref, reactive, onMounted, onUnmounted, defineExpose, inject } from 'vue'
+
+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(() => {
+});
+</script>
+<style scoped lang="scss">
+.attachment {
+    .title {
+        font-size: 18px;
+        color: #000
+    }
+}
+</style>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 73 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/information.vue


+ 58 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/operationRecord.vue

@@ -0,0 +1,58 @@
+<template>
+    <div class="operationRecord pl-4 pr-4 pt-3 pb-3 h-full flex flex-col">
+        <div class="flex justify-between">
+            <div class="title">操作记录</div>
+        </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>
+        </div>
+    </div>
+</template>
+<script lang="ts" setup>
+import { ref, reactive, onMounted, onUnmounted, defineExpose, inject } from 'vue'
+
+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(() => {
+});
+</script>
+<style scoped lang="scss">
+.operationRecord {
+    .title {
+        font-size: 18px;
+        color: #000
+    }
+}
+</style>

+ 93 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/products.vue

@@ -0,0 +1,93 @@
+<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">编辑产品</el-button>
+            </div>
+        </div>
+        <div class="flex-1 overflow-auto pt-3">
+            <el-table :data="relatedTaskstable" border style="width: 100%;height: 300px;">
+                <el-table-column label="序号" width="80">
+                    <template #default="scope">
+                        {{ scope.$index + 1 }}
+                    </template>
+                </el-table-column>
+                <el-table-column prop="taskName" label="产品名称">
+                    <template #default="scope">
+                        <el-button link type="primary" size="large">{{
+                            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="endTime" label="售价" width="130" />
+                <el-table-column prop="endTime" label="数量" width="130" />
+                <el-table-column prop="endTime" label="折扣(%)" width="130" />
+                <el-table-column prop="endTime" label="合计" width="130" />
+            </el-table>
+        </div>
+    </div>
+</template>
+<script lang="ts" setup>
+import { ref, reactive, onMounted, onUnmounted, defineExpose, inject } from 'vue'
+
+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(() => {
+});
+</script>
+<style scoped lang="scss">
+.relatedTasks {
+    .title {
+        font-size: 18px;
+        color: #000
+    }
+}
+</style>

+ 81 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/relatedBusiness.vue

@@ -0,0 +1,81 @@
+<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>
+        <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">{{
+                            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>
+        </div>
+    </div>
+</template>
+<script lang="ts" setup>
+import { ref, reactive, onMounted, onUnmounted, defineExpose, inject } from 'vue'
+
+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(() => {
+});
+</script>
+<style scoped lang="scss">
+.relatedTasks {
+    .title {
+        font-size: 18px;
+        color: #000
+    }
+}
+</style>

+ 92 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/component/relatedProducts.vue

@@ -0,0 +1,92 @@
+<template>
+    <div>
+        <el-table ref="productTableRef" :data="productTable" border :row-class-name="tableRowClassName"
+            @row-click="tableRowItem" style="width: 100%;height: 200px">
+            <el-table-column label="序号" width="80">
+                <template #default="scope">
+                    <span>{{ scope.$index + 1 }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column prop="productName" label="产品名称" width="180">
+                <template #default="scope">
+                    <el-select v-model="productTable[scope.$index].productName" placeholder="请选择"
+                        v-if="productTableIndex == scope.$index" clearable @clear="clearTableItem(scope.$index)"
+                        @change="selectChange(scope.$index, productTable[scope.$index].productName)">
+                        <el-option v-for="item in productArrar" :key="item.id" :label="item.productName" :value="item.id" />
+                    </el-select>
+                    <span v-else>{{ productTable[scope.$index].productName }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column prop="productType" label="产品类型" width="180"></el-table-column>
+            <el-table-column prop="unit" label="单位" width="80"></el-table-column>
+            <el-table-column prop="price" label="标准价格" width="120"></el-table-column>
+            <el-table-column prop="stock" label="库存" width="80"></el-table-column>
+            <el-table-column prop="sellingPrice" label="售价" width="180">
+                <template #default="scope">
+                    <el-input-number v-model="productTable[scope.$index].sellingPrice" class="mx-4" :min="0" :max="100000000" controls-position="right" v-if="productTableIndex == scope.$index" />
+                    <span v-else>{{ productTable[scope.$index].sellingPrice }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column prop="quantity" label="数量" width="180">
+                <template #default="scope">
+                    <el-input-number v-model="productTable[scope.$index].quantity" class="mx-4" :min="0" :max="100000000" controls-position="right" v-if="productTableIndex == scope.$index" />
+                    <span v-else>{{ productTable[scope.$index].quantity }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column prop="discount" label="折扣(%)" width="180">
+                <template #default="scope">
+                    <el-input-number v-model="productTable[scope.$index].discount" class="mx-4" :min="0" :max="100" controls-position="right" v-if="productTableIndex == scope.$index" />
+                    <span v-else>{{ productTable[scope.$index].discount }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column prop="total" 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.stop="addTableItem(scope.$index)">添加</el-button>
+                    <el-button link type="danger" size="large" v-if="productTable.length > 1"
+                        @click.stop="deteleTableItem(scope.$index)">删除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+    </div>
+</template>
+  
+<script lang="ts" setup>
+import { ref, reactive, onMounted, inject } from "vue";
+
+const productTable: any = ref([{}])
+const productTableIndex = ref(0) // 可以编辑索引
+const productArrar = ref([
+    { id: 1, productName: '产品1', productType: '类别1', unit: '台', price: '1122', stock: '100', sellingPrice: 0, quantity: 0, discount: 0, total: '' },
+    { id: 2, productName: '产品2', productType: '类别2', unit: '台', price: '2211', stock: '300', sellingPrice: 0, quantity: 0, discount: 0, total: '' },
+])
+
+function selectChange(index: number, val: number | string) {
+    let newObj = productArrar.value.find((item: any) => item.id == val)
+    console.log(newObj)
+    productTable.value.splice(index, 1, newObj)
+}
+
+function tableRowItem(row: any) {
+    productTableIndex.value = row.index
+}
+
+function tableRowClassName({ row, rowIndex, }: { row: any, rowIndex: number }) {
+    row.index = rowIndex
+    return ''
+}
+
+function addTableItem(index: number) {
+    productTable.value.splice(index + 1, 0, {})
+}
+
+function clearTableItem(index: number) {
+    productTable.value.splice(index, 0, {})
+}
+
+function deteleTableItem(index: number) {
+    productTable.value.splice(index, 1)
+}
+</script>
+  
+<style lang="scss" scoped></style>

+ 152 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/detail/index.vue

@@ -0,0 +1,152 @@
+<template>
+  <div class="h-full flex p-3 flex-col businessDetail">
+    <div class="w-full bg-white p-2 mb-2 shadow-md rounded-md flex items-center">
+      <div class="icon mr-4">
+        <el-link :underline="false" @click="backPath()">
+          <el-icon class="el-icon--right"><icon-view /></el-icon> 返回产品列表
+        </el-link>
+      </div>
+      <div class="mr-8">
+        <el-select v-model="value" placeholder="请选择" style="width: 150px">
+          <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="w-full h-auto flex justify-between">
+        <div class="bg-white shadow-md rounded-md" style="width: 46%;">
+          <Information />
+        </div>
+        <div class="bg-white ml-2 shadow-md rounded-md flex-1">
+          <Attachment />
+        </div>
+      </div>
+
+      <div class="w-full h-auto flex justify-between mt-2">
+        <div class="bg-white shadow-md rounded-md" style="width: 65%;">
+          <RelatedBusiness />
+        </div>
+        <div class="bg-white ml-2 shadow-md rounded-md flex-1">
+          <OperationRecord />
+        </div>
+      </div>
+
+      <div class="w-full h-auto flex justify-between mt-2">
+        <div class="bg-white shadow-md rounded-md w-full">
+          <Products />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+  
+<script lang="ts" setup>
+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 Information from '../component/information.vue'
+import Attachment from '../component/attachment.vue'
+import RelatedBusiness from '../component/relatedBusiness.vue';
+import OperationRecord from '../component/operationRecord.vue';
+import Products from '../component/products.vue';
+
+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;
+  }
+}
+</script>
+  
+<style lang="scss" scoped>
+.businessDetail {
+  .icon {
+    .el-link {
+      color: #0052CC;
+    }
+  }
+
+  .text {
+    .el-link {
+      color: #fff;
+      font-size: 14px;
+    }
+  }
+
+  .backDarkBlue {
+    background-color: #0052CC;
+    color: #fff;
+  }
+
+  .backGray {
+    background-color: #F4F5F7;
+    color: #000;
+  }
+
+  .startStep {
+    clip-path: polygon(0% 0%,
+        90% 0%,
+        100% 50%,
+        90% 100%,
+        0% 100%);
+  }
+
+  .nextStep {
+    clip-path: polygon(0% 0%,
+        90% 0%,
+        100% 50%,
+        90% 100%,
+        0% 100%,
+        10% 50%);
+  }
+
+  .endStep {
+
+    clip-path: polygon(0% 0%,
+        100% 0%,
+        100% 100%,
+        0% 100%,
+        10% 50%);
+  }
+
+  .itemPing {
+    padding-top: 4px;
+    padding-bottom: 4px;
+  }
+
+  .selectClas >>> .el-select__wrapper {
+    background-color: none !important;
+    box-shadow: none !important;
+  }
+}
+</style>

+ 264 - 2
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/index.vue

@@ -1,11 +1,273 @@
 <template>
-  <div>
-    product
+  <div class="h-full flex">
+    <div class="p-5 w-80 pr-0">
+      <div class="bg-white w-full h-full shadow-md rounded-md flex flex-col">
+        <div class="flex-1 p-3 overflow-y-auto">
+          <el-form :model="filterProductForm" label-width="70px" style="max-width: 600px">
+            <el-form-item label="产品编号">
+              <el-input v-model="filterProductForm.productCode" clearable placeholder="请输入"></el-input>
+            </el-form-item>
+            <el-form-item label="产品名称">
+              <el-input v-model="filterProductForm.productName" clearable placeholder="请输入"></el-input>
+            </el-form-item>
+            <el-form-item label="产品类别">
+              <el-select v-model="filterProductForm.type" placeholder="请选择" clearable>
+                <el-option v-for="item in fixedData.ProductType" :key="item.id" :label="item.name" :value="item.id" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="负责人">
+              <el-select v-model="filterProductForm.userId" placeholder="请选择">
+                <el-option v-for="item in fixedData.Personnel" :key="item.id" :label="item.name" :value="item.id" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="创建时间">
+              <el-date-picker v-model="filterProductForm.startTime" type="date" placeholder="请选择" :clearable="false"
+                format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
+            </el-form-item>
+            <el-form-item label="">
+              <el-date-picker v-model="filterProductForm.endTime" type="date" placeholder="请选择" :clearable="false"
+                format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
+            </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="resetFilter()">重置</El-Button>
+          <El-button type="primary" class="w-full" @click="getProductTableList()">搜索</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 type="primary" @click="editProduct(true)">新建产品</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>
+        </div>
+        <div class="flex-1 w-full overflow-hidden">
+          <el-table ref="productTableRef" :data="productTableList" border v-loading="allLoading.productTableLading"
+            style="width: 100%;height: 100%;">
+            <el-table-column type="selection" width="55" />
+            <el-table-column prop="productCode" label="产品编号" width="180"></el-table-column>
+            <el-table-column prop="productName" label="产品名称" width="180">
+              <template #default="scope">
+                <el-button link type="primary" size="large" @click="toProductDetail(scope.row)">{{
+                  scope.row.productName
+                }}</el-button>
+              </template>
+            </el-table-column>
+            <el-table-column prop="type" label="产品类别" width="180"></el-table-column>
+            <el-table-column prop="unit" label="单位" width="180"></el-table-column>
+            <el-table-column prop="price" label="标准价格" width="180"></el-table-column>
+            <el-table-column prop="inventory" label="库存" width="180"></el-table-column>
+            <el-table-column prop="status" label="状态" width="180"></el-table-column>
+            <el-table-column prop="inchargerId" label="负责人" width="190"></el-table-column>
+            <el-table-column prop="creatorId" label="创建人" width="180"></el-table-column>
+            <el-table-column prop="createTime" 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="danger" size="large" @click.stop="deteleRow([scope.row])">删除</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="filterProductForm.pageSize"
+            @size-change="handleSizeChange" @current-change="handleCurrentChange" :total="productTableTotal" :hide-on-single-page="true" />
+        </div>
+      </div>
+    </div>
+
+    <!-- 弹窗 -->
+    <el-dialog v-model="dialogVisible.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">{{ allText.editClueText }}</h4>
+          <div>
+            <el-button type="primary" @click="saveProductRow(true)" v-loading="allLoading.saveLoading">保存并新建</el-button>
+            <el-button type="primary" @click="saveProductRow(false)" v-loading="allLoading.saveLoading">保存</el-button>
+            <el-button @click="dialogVisible.editProductVisible = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="h-[60vh] overflow-y-auto scroll-bar pt-3" v-loading="allLoading.generateFormLading">
+        <div class="ml-4 mr-4">
+          <GenerateForm ref="generateForm" :data="productTemplate" :value="genereditForm" :key="generateFormKey" />
+        </div>
+      </div>
+    </el-dialog>
+
   </div>
 </template>
 
 <script lang="ts" setup>
+import { ref, reactive, onMounted, inject } from "vue";
+import { GETSYSFILED, MOD, GETPERSONNEL, GETTEMPLATE, GETTABLELIST, ADDPRODUCT, ALLDETELE } from './api'
+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 { useRouter, useRoute } from "vue-router";
+import { GenerateForm } from '@zmjs/form-design';
+
+const router = useRouter()
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const filterProductForm = reactive<filterProductFormType>({ // 筛选条件form
+  userId: '',
+  productName: '',
+  productCode: '',
+  startTime: getFirstDayOfMonth(new Date()),
+  endTime: formatDate(new Date()),
+  pageIndex: 1,
+  pageSize: 10,
+  id: '',
+  type: '',
+})
+const allLoading = reactive({
+  productTableLading: false,
+  generateFormLading: false,
+  saveLoading: false,
+})
+const dialogVisible = reactive({
+  editProductVisible: false,
+  taskModalVisible: false,
+  clueDialogVisible: false,
+  deteleClueDialogVisible: false
+})
+const allText = reactive({
+  editClueText: '新建产品',
+})
+const fixedData = reactive({
+  ProductType: [] as fixedDataInterface[],
+  Personnel: [] as personnelInterface[]
+})
+const productTableList = ref([]) // 产品table数据
+const productTableTotal = ref(0) // 产品 table 数据总数
+const productTableRef = ref<InstanceType<typeof ElTable>>() // 产品table dom
+const generateForm: any = ref(null) // 模板
+const productTemplate = ref({
+  list: [],
+  config: {}
+}) // 产品模板
+const genereditForm = ref({}) // 编辑表单
+const generateFormKey = ref(1)
+
+// 方法定义
+function deteleRow(items: any[]) {
+  let ids = items.map(item => item.id).join(',')
+  let str = items.map(item => item.productName).join(',')
+  confirmAction(`确定${items.length > 1 ? '批量删除' : '删除'}【${str}】线索吗?`).then(() => {
+    post(ALLDETELE, { ids: ids }).then(res => {
+      if (res.code != 'ok') {
+        globalPopup?.showError(res.msg)
+        return
+      }
+      globalPopup?.showSuccess('删除成功')
+      getProductTableList()
+    })
+  })
+}
+
+async function saveProductRow(flag: boolean) {
+  const data = await generateForm.value.getData()
+  let newData = { ...genereditForm.value, ...data }
+  allLoading.saveLoading = true
+  post(ADDPRODUCT, { ...newData }).then((res) => {
+    console.log(res)
+    if(res.code != 'ok') {
+      globalPopup?.showError(res.msg)
+      return
+    }
+    globalPopup?.showSuccess('保存成功')
+    if (!flag) {
+      genereditForm.value = {}
+      generateForm.value && generateForm.value.reset()
+      generateFormKey.value++
+    }
+    dialogVisible.editProductVisible = flag
+  }).catch(() => {
+    dialogVisible.editProductVisible = flag
+  }).finally(() => {
+    allLoading.saveLoading = false
+  })
+}
+
+function editProduct(_flag: boolean) {
+  dialogVisible.editProductVisible = true
+  allLoading.generateFormLading = true
+  setTimeout(() => {
+    generateForm.value && generateForm.value.reset()
+    generateFormKey.value++
+    allLoading.generateFormLading = false
+  }, 1000)
+}
+
+function getProductTableList() {
+  allLoading.productTableLading = true
+  let valueForm = getFromValue(filterProductForm)
+  post(GETTABLELIST, { ...valueForm }).then((res) => {
+    if(res.code == 'ok') {
+      const { record, total } = res.data
+      productTableList.value = record
+      productTableTotal.value = total
+    }
+  }).finally(() => {
+    allLoading.productTableLading = false
+  })
+}
+
+function handleSizeChange(val: number) {
+  filterProductForm.pageIndex = 1
+  filterProductForm.pageSize = val
+  getProductTableList()
+}
+
+function handleCurrentChange(val: number) {
+  filterProductForm.pageIndex = val
+  getProductTableList()
+}
+
+function resetFilter() {
+  let newResetForm = resetFromValue(filterProductForm, { startTime: getFirstDayOfMonth(new Date()), endTime: formatDate(new Date()), pageIndex: 1, pageFrom: 10 })
+  Object.assign(filterProductForm, newResetForm)
+  getProductTableList()
+}
+
+function toProductDetail(row: any) {
+  router.push({ 
+    path: `${MOD}/detail`, 
+    query: { id: row.id } 
+  })
+}
+
+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 { data } = await post(GETPERSONNEL, {})
+  fixedData.Personnel = data.map((item: any) => {
+    const { id, name, phone, jobNumber } = item
+    return {
+      id, name, phone, jobNumber
+    }
+  })
+
+  const res = await get(GETTEMPLATE)
+  productTemplate.value = JSON.parse(res.data[0].config)
+}
 
+onMounted(() => {
+  getSystemField()
+  getProductTableList()
+})
 </script>
 
 <style lang="scss" scoped></style>

+ 11 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/product/type.d.ts

@@ -0,0 +1,11 @@
+interface filterProductFormType {
+    userId: string | number,
+    productName: string,
+    productCode: string,
+    pageIndex: number,
+    pageSize: number,
+    type: string | number,
+    id: string | number,
+    startTime: string,
+    endTime: string
+}

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

@@ -177,7 +177,8 @@ function handleClose(done: any) {
 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('tasks') || '')
+  let data = JSON.parse(localStorage.getItem('product') || '')
   data.list.forEach((element: any) => {
     element.allDisable = true
   });

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

@@ -117,8 +117,8 @@
         </div>
       </div>
     </div>
-    <TaskModal :visible="taskModalVisible" :save-loading="taskLoading" :edit-form="taskForm" @close="closeTaskModal"
-      @submit="submitForm" />
+    <TaskModal :visible="taskModalVisible" :title="taskForm?'编辑任务':'新建任务'" :save-loading="taskLoading"
+      :edit-form="taskForm" @close="closeTaskModal" @submit="submitForm" :disabled-list="[]"/>
     <ImportModal :visible="importVisible" :save-loading="importLoading" @close="closeImportModal"
       @submit="importExcel" />
     <ExportModal :visible="exportVisible" :save-loading="exportLoading" @close="closeExportModal"
@@ -263,6 +263,7 @@ const tableData = ref<any[]>([
   }
 ])
 function search() {
+  return
   loading.value = true;
   post(PAGE_LIST, getFromValue(searchForm.value)).then(({ data }) => {
     loading.value = false;

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

@@ -2,8 +2,16 @@ export const MOD = '/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 GETTABLE = `${prefix}/listClue`
 export const GETDETAIL = `${prefix}/getDetail`
 export const UNDATECLAIM = `${prefix}/claim`
 export const UNDATEFORM = `${prefix}/insertAndUpdate`
-export const DELTEROW = `${prefix}/delete`
+export const DELTEROW = `${prefix}/delete`
+export const DETELEFILE = `${prefix}/deleteFile`
+export const DOWNFILE = `${prefix}/downFile`
+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`

+ 118 - 45
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/attachment.vue

@@ -3,67 +3,140 @@
         <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: 100%;">
-                <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="200" />
+                <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 { ref, watch, onMounted, inject, defineEmits, watchEffect } from 'vue'
+import { uploadFile, post } from '@/utils/request'
+import { formatDate, downloadFile, confirmAction } from '@/utils/tools'
+import { DETELEFILE, DOWNFILE, REFIENAME, UPLOADFILE } from '../../constant';
+
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const emits = defineEmits(['refreshData']);
+const attachmenttable = ref([])
+const information: any = ref({})
+const renameDialogVisible = ref(false)
+const renameVal = ref('')
+const props = defineProps<{
+    data: any,
+    information: any
+}>()
+
+// 下载文件
+function fileDownload(item: any) {
+    const id = item.id
+    post(DOWNFILE, { id }).then((res) => {
+        downloadFile(res, item.name)
+    }).catch((err) => {
+        downloadFile(err, item.name)
+    })
+}
+
+// 删除文件
+function deteleFile(item: any) {
+    const id = item.id
+    confirmAction(`确定删除【${item.name}】文件吗?`).then(() => {
+        post(DETELEFILE, { id }).then((_res) => {
+            globalPopup?.showSuccess('删除成功')
+            emits('refreshData')
+        })
+    })
+}
+
+// 保存重命名
+function saveEditClue() {
+    if (!renameVal.value) {
+        globalPopup?.showWarning('请输入文件名称')
+        return
+    }
+    const clueId = information.value.id
+    post(REFIENAME, { name: renameVal.value, id: clueId }).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 clueId = information.value.id
+    const formData = new FormData();
+    formData.append('file', param.file)
+    formData.append('id', clueId)
+    const res = await uploadFile(UPLOADFILE, formData)
+    if (res.code == 'ok') {
+        globalPopup?.showSuccess(res.msg || '')
+        emits('refreshData');
+        return
+    }
+    globalPopup?.showError(res.msg || '')
+    return res
+}
+
+// 接收参数赋值
+function receiveAssignment(item: any) {
+    item.data.forEach((item: any) => {
+        item.createTime = formatDate(new Date(item.createTime))
+    });
+    attachmenttable.value = item.data
+    information.value = item.information
+}
+
+watchEffect(() => {
+    receiveAssignment(props)
+});
 
-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(() => {
+    receiveAssignment(props)
 });
 </script>
 <style scoped lang="scss">

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 208 - 17
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/information.vue


+ 18 - 35
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/detail/components/operationRecord.vue

@@ -5,48 +5,31 @@
         </div>
         <div class="flex-1 overflow-auto pt-5">
             <el-table :data="operationRecordtable" border style="width: 100%;height: 100%;">
-                <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 { formatDate } from '@/utils/tools';
+import { ref, onMounted, watchEffect } from 'vue'
 
-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(() => {
+const props = defineProps<{
+    data: any
+}>()
+
+const operationRecordtable = ref([])
+
+watchEffect(() => {
+    props.data.forEach((item: any) => {
+        item.creatTime = formatDate(new Date(item.creatTime))
+    })
+    operationRecordtable.value = props.data
 });
+// 生命周期钩子
+onMounted(() => {});
 </script>
 <style scoped lang="scss">
 .operationRecord {

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

@@ -2,35 +2,70 @@
   <div class="h-full threadDetail">
     <div class="layout p-3">
       <div class="bg-white w-1/2 shadow-md rounded-md">
-        <Information></Information>
+        <Information :data="information" @refreshData="refreshData"></Information>
       </div>
       <div class="bg-white w-1/2 ml-3 shadow-md rounded-md">
-        <Attachment></Attachment>
+        <Attachment :data="attachment" :information="information" @refreshData="refreshData"></Attachment>
       </div>
     </div>
     <div class="layout pl-3 pr-3 pb-3">
       <div class="bg-white w-2/3 shadow-md rounded-md">
-        <RelatedTasks></RelatedTasks>
+        <RelatedTasks :data="relatedTasks" :information="information" @refreshData="refreshData"></RelatedTasks>
       </div>
       <div class="bg-white w-1/3 ml-3 shadow-md rounded-md">
-        <OperationRecord></OperationRecord>
+        <OperationRecord :data="operationRecord" :information="information" @refreshData="refreshData"></OperationRecord>
       </div>
     </div>
   </div>
 </template>
   
 <script lang="ts" setup>
-
 import Information from './components/information.vue'
 import Attachment from './components/attachment.vue'
 import RelatedTasks from './components/relatedTasks.vue';
 import OperationRecord from './components/operationRecord.vue';
+import { ref, reactive, onMounted, inject } from "vue";
+import { post, get } from "@/utils/request";
+import { useRouter, useRoute } from "vue-router";
+import { GETDETAIL } from "../constant"
+
+const route = useRoute()
+
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const addressParameters: any = ref(0) // 地址上的参数
+const information = ref({}) // 基本信息
+const attachment = ref([]) // 附件
+const relatedTasks = ref([]) // 相关任务
+const operationRecord = ref([]) // 操作记录
+
+function getDetail() {
+  const id = addressParameters.value
+  post(GETDETAIL, { id }).then((res) => {
+    const { clueLogList, files, taskList } = res.data
+    information.value = res.data
+    attachment.value = files || []
+    relatedTasks.value = taskList || []
+    operationRecord.value = clueLogList || []
+  })
+}
+
+function refreshData() {
+  getDetail()
+}
+
+onMounted(() => {
+  const { id } = route.query
+  addressParameters.value = id
+  getDetail()
+
+})
 </script>
   
 <style lang="scss" scoped>
 .threadDetail {
   display: flex;
   flex-direction: column;
+
   .layout {
     width: 100%;
     height: 50%;

+ 166 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/deteleTables.vue

@@ -0,0 +1,166 @@
+<template>
+    <el-dialog v-model="deteleClueDialogVisible" 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="batchRestore()" v-loading="allLoading.batchRecoveryLoading">批量恢复</el-button>
+                    <el-button type="primary" @click="batchDeletes()"
+                        v-loading="allLoading.batchDeteleLoading">批量删除</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="clueTableRef" :data="deteleClueTable" border v-loading="allLoading.tableLoading"
+                    style="width: 100%;height: 100%;">
+                    <el-table-column type="selection" width="55" />
+                    <el-table-column prop="clueName" label="线索名称" width="180"></el-table-column>
+                    <el-table-column prop="clueSourceValue" 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="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="createName" label="创建人" width="180"></el-table-column>
+                    <el-table-column prop="createTime" 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="restoreItemRow([scope.row])">恢复</el-button>
+                            <el-button link type="danger" size="large" @click="deteItemRow([scope.row])">删除</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="clueTotalTable"
+                    :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 { DETERDETELE, DEYELWCLUE, ROLLBACK } from './constant';
+import { ElTable } from 'element-plus';
+import { confirmAction } from '@/utils/tools';
+
+const emits = defineEmits(['showDeteleClue']);
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const deteleClueTable = ref([])
+const deteleClueDialogVisible = ref(false)
+const clueTotalTable = ref(0)
+const allLoading = reactive({
+    batchRecoveryLoading: false,
+    batchDeteleLoading: false,
+    tableLoading: false
+})
+
+const tableForm = reactive({
+    pageIndex: 1,
+    pageFrom: 10
+})
+
+const clueTableRef = ref<InstanceType<typeof ElTable>>() // 线索table dom
+
+const props = defineProps<{
+    visibles: boolean
+}>()
+
+watch(() => props.visibles, (newVal) => {
+    deteleClueDialogVisible.value = newVal
+    if (newVal) {
+        getTableList()
+    }
+})
+
+function batchRestore() {
+    const data = clueTableRef.value && clueTableRef.value.getSelectionRows()
+    if (!data.length) {
+        globalPopup?.showWarning('请选择需要恢复的数据')
+        return
+    }
+    restoreItemRow(data)
+}
+
+function batchDeletes() {
+    const data = clueTableRef.value && clueTableRef.value.getSelectionRows()
+    if (!data.length) {
+        globalPopup?.showWarning('请选择需要删除的数据')
+        return
+    }
+    deteItemRow(data)
+}
+
+function deteItemRow(items: any[]) {
+    const ids = items.map((item: any) => item.id).join(',')
+    const str = items.map((item: any) => item.clueName).join(',')
+    confirmAction(`确定${items.length > 1 ? '批量删除这些' : '删除'}【${str}】线索吗?`, '', 'warning').then(() => {
+    post(DETERDETELE, { ids }).then(res => {
+      if (res.code != 'ok') {
+        globalPopup?.showError(res.msg)
+        return
+      }
+      globalPopup?.showSuccess('删除成功')
+      getTableList()
+    })
+  })
+}
+
+function restoreItemRow(items: any[]) {
+    const ids = items.map((item: any) => item.id).join(',')
+    const str = items.map((item: any) => item.clueName).join(',')
+    confirmAction(`确定${items.length > 1 ? '批量恢复这些' : '恢复'}【${str}】线索吗?`, '', 'warning').then(() => {
+    post(ROLLBACK, { ids }).then(res => {
+      if (res.code != 'ok') {
+        globalPopup?.showError(res.msg)
+        return
+      }
+      globalPopup?.showSuccess('恢复成功')
+      getTableList()
+    })
+  })
+}
+
+function getTableList() {
+    allLoading.tableLoading = true
+    post(DEYELWCLUE, { ...tableForm }).then((res) => {
+        if (res.code == 'ok') {
+            const { data, total } = res.data
+            deteleClueTable.value = data
+            clueTotalTable.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('showDeteleClue', false)
+}
+
+function beForeCancel(done: () => void) {
+    emits('showDeteleClue', false)
+    done()
+}
+
+onMounted(() => {
+
+})
+
+</script>
+<style lang="scss" scoped></style>

+ 222 - 28
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/thread/index.vue

@@ -8,7 +8,7 @@
               <el-input v-model="filterCriteriaForm.clueName" clearable placeholder="请输入"></el-input>
             </el-form-item>
             <el-form-item label="线索来源">
-              <el-select v-model="filterCriteriaForm.clueSourceId" placeholder="请选择">
+              <el-select v-model="filterCriteriaForm.clueSourceId" placeholder="请选择" clearable>
                 <el-option v-for="item in fixedData.ClueSources" :key="item.id" :label="item.name" :value="item.id" />
               </el-select>
             </el-form-item>
@@ -52,10 +52,10 @@
     <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="editClue(false)">新建线索</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" @click="batchDeletes()">批量删除</el-button>
+          <el-button type="primary" @click="showDeteleClue(true)">回收站</el-button>
           <el-button type="primary">导入</el-button>
           <el-button type="primary">导出</el-button>
         </div>
@@ -65,44 +65,94 @@
             <el-table-column type="selection" width="55" />
             <el-table-column prop="clueName" label="线索名称" width="180">
               <template #default="scope">
-                <el-button link type="primary" size="large" @click.prevent="toClueTableDetail(scope.row)">{{ scope.row.clueName
+                <el-button link type="primary" size="large" @click.prevent="toClueTableDetail(scope.row)">{{
+                  scope.row.clueName
                 }}</el-button>
               </template>
             </el-table-column>
-            <el-table-column prop="clueSourceId" label="线索来源" width="180"></el-table-column>
+            <el-table-column prop="clueSourceValue" 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="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="createName" label="创建人" width="180"></el-table-column>
             <el-table-column prop="createTime" 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="editClue(scope.row)">编辑</el-button>
+                <el-button link type="primary" size="large"
+                  @click="dialogVisible.taskModalVisible = true">新建任务</el-button>
+                <el-button link type="danger" size="large" @click.prevent="deleteRow(scope.row)">删除</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" :page-size="filterCriteriaForm.pageFrom"
+            @size-change="handleSizeChange" @current-change="handleCurrentChange" :total="clueTotalTable"
+            :hide-on-single-page="true" />
         </div>
       </div>
     </div>
 
     <!-- 弹窗 -->
-    
+    <el-dialog v-model="dialogVisible.editClueDialogVisible" 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.editClueText }}</h4>
+          <div>
+            <el-button type="primary" @click="saveEditClue(true)">保存并新建</el-button>
+            <el-button type="primary" @click="saveEditClue(false)">保存</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="ml-4 mr-4">
+          <GenerateForm ref="generateForm" :data="clueTemplate" :value="editForm" :key="generateFormKey" />
+        </div>
+      </div>
+    </el-dialog>
+
+    <el-dialog v-model="dialogVisible.clueDialogVisible" 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.clueText }}</h4>
+          <div>
+            <el-button type="primary" v-loading="allLoading.clueLoading" @click="transferClues()">转移</el-button>
+            <el-button @click="dialogVisible.clueDialogVisible = 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="transferForm.transferValue" 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>
+
+    <DeteleTables :visibles="dialogVisible.deteleClueDialogVisible" @showDeteleClue="showDeteleClue" />
+
+    <TaskModal :visible="dialogVisible.taskModalVisible" :edit-form="createTaskFromType(3)" :save-loading="'1'"
+      @close="closeTaskModal" @submit="submitForm" />
   </div>
 </template>
 
 <script lang="ts" setup>
 import { ref, reactive, onMounted, inject } from "vue";
-import { GETSYSFILED, MOD, GETPERSONNEL, GETTABLE, GETDETAIL, UNDATECLAIM, UNDATEFORM, DELTEROW } from './constant'
-import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, getLastDayOfMonth, formatDate } from '@/utils/tools'
+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 { useRouter, useRoute } from "vue-router";
+import { GenerateForm } from '@zmjs/form-design';
+import TaskModal from '@/components/TaskModal/index.vue'
+import DeteleTables from "./deteleTables.vue";
 
 // 定义类型
 interface fixedDataInterface {
@@ -137,8 +187,21 @@ const filterCriteriaForm = reactive<filterCriteriaFormType>({ // 筛选条件for
   pageIndex: 1,
   pageFrom: 10
 })
+const generateFormKey = ref(1)
 const allLoading = reactive({
   clueTableLading: false,
+  generateFormLading: false,
+  clueLoading: false,
+})
+const dialogVisible = reactive({
+  editClueDialogVisible: false,
+  taskModalVisible: false,
+  clueDialogVisible: false,
+  deteleClueDialogVisible: false
+})
+const allText = reactive({
+  editClueText: '新建线索',
+  clueText: '转移线索'
 })
 const fixedData = reactive({
   ClueSources: [] as fixedDataInterface[],
@@ -146,10 +209,45 @@ const fixedData = reactive({
   CustomLevel: [] as fixedDataInterface[],
   Personnel: [] as personnelInterface[]
 })
-const clueTable = ref([{ clueName: '线索名称', clueSourceId: '线索来源', id: 123456789 }]) // 线索table数据
+const clueTable = ref([]) // 线索table数据
 const clueTotalTable = ref(0) // 线索 table 数据总数
+const clueTableRef = ref<InstanceType<typeof ElTable>>() // 线索table dom
+const generateForm: any = ref(null) // 模板
+const clueTemplate = ref({
+  list: [],
+  config: {}
+}) // 线索模板
+const editForm = ref({}) // 编辑表单
+
+// 批量变量
+const transferForm = reactive({
+  transferValue: '',
+  batchTransferIds: '',
+})
+
 
 // 定义方法
+function showDeteleClue(flag: boolean) {
+  dialogVisible.deteleClueDialogVisible = flag
+}
+
+function transferClues() {
+  const { transferValue, batchTransferIds } = transferForm
+  const ids = batchTransferIds
+  const inchargerId = transferValue
+  if (!inchargerId) {
+    globalPopup?.showWarning('请选择转移的人员')
+    return
+  }
+  post(UNDATECLAIM, { ids, inchargerId }).then((res) => {
+    if (res.code == 'ok') {
+      globalPopup?.showSuccess('批量转移成功')
+      dialogVisible.clueDialogVisible = false
+      getClueTable()
+    }
+  })
+}
+
 function searchTable() {
   getClueTable()
 }
@@ -160,23 +258,102 @@ function resetTable() {
   getClueTable()
 }
 
-function deleteRow(_row: any) {
-  console.log('点击了删除')
+function closeTaskModal() {
+  dialogVisible.taskModalVisible = false
+}
+
+function submitForm(submitData: any, isClose: boolean) {
+  console.log(submitData, isClose)
+}
+
+function editClue(item: any) {
+  dialogVisible.editClueDialogVisible = true
+  allLoading.generateFormLading = true
+  setTimeout(() => {
+    generateForm.value && generateForm.value.reset()
+    generateFormKey.value++
+    allLoading.generateFormLading = false
+  }, 1000);
+  if (!item) {
+    allText.editClueText = '新建线索'
+    editForm.value = {}
+    return
+  }
+  editForm.value = JSON.parse(JSON.stringify(item))
+  allText.editClueText = '编辑线索'
+}
+
+async function saveEditClue(flag: boolean) {
+  const data = await generateForm.value.getData()
+  let newData = { ...editForm.value, ...data }
+  delete newData.createTime
+  post(UNDATEFORM, { ...newData }).then((res) => {
+    if (res.code != 'ok') {
+      globalPopup?.showError(res.msg)
+      return
+    }
+    globalPopup?.showSuccess('保存成功')
+    if (!flag) {
+      editForm.value = {}
+      generateForm.value && generateForm.value.reset()
+      generateFormKey.value++
+    }
+    dialogVisible.editClueDialogVisible = flag
+    getClueTable()
+  }).catch((_err) => {
+    console.log(_err)
+  })
+}
+
+function deleteRow(row: any) {
+  confirmAction(`确定删除【${row.clueName}】线索吗?`).then(() => {
+    post(DELTEROW, { ids: row.id }).then(res => {
+      if (res.code != 'ok') {
+        globalPopup?.showError(res.msg)
+        return
+      }
+      globalPopup?.showSuccess('删除成功')
+      getClueTable()
+    })
+  })
 }
 
 function batchTransfer() {
-  console.log('点击了批量转移')
+  const data = clueTableRef.value && clueTableRef.value.getSelectionRows()
+  if (!data.length) {
+    globalPopup?.showWarning('请选择数据')
+    return
+  }
+  transferForm.batchTransferIds = data.map((item: any) => item.id).join(',')
+  dialogVisible.clueDialogVisible = true
 }
 
-function batchDelete() {
-  console.log('批量删除')
+function batchDeletes() {
+  const data = clueTableRef.value && clueTableRef.value.getSelectionRows()
+  if (!data.length) {
+    globalPopup?.showWarning('请选择数据')
+    return
+  }
+  const str = data.map((item: any) => item.clueName).join(',')
+  const ids = data.map((item: any) => item.id).join(',')
+
+  confirmAction(`确定批量删除这些【${str}】线索吗?`, '', 'warning').then(() => {
+    post(DELTEROW, { ids }).then(res => {
+      if (res.code != 'ok') {
+        globalPopup?.showError(res.msg)
+        return
+      }
+      globalPopup?.showSuccess('删除成功')
+      getClueTable()
+    })
+  })
 }
 
 function toClueTableDetail(row: any) {
   console.log('点击跳转详情')
-  router.push({ 
-    path: `${MOD}/detail`, 
-    query: { id: row.id } 
+  router.push({
+    path: `${MOD}/detail`,
+    query: { id: row.id }
   })
 }
 
@@ -184,15 +361,29 @@ function getClueTable() {
   allLoading.clueTableLading = true
   let valueForm = getFromValue(filterCriteriaForm)
   post(GETTABLE, { ...valueForm }).then((res: any) => {
-    const { total, data } = res
-    clueTable.value = data
-    clueTotalTable.value = total
+    let { data } = res
+    for (let i in data.data) {
+      data.data[i].createTime = formatDate(new Date(data.data[i].createTime))
+    }
+    clueTable.value = data.data
+    clueTotalTable.value = data.total
     allLoading.clueTableLading = false
   }).catch((_error) => {
     allLoading.clueTableLading = false
   })
 }
 
+function handleSizeChange(val: number) {
+  filterCriteriaForm.pageIndex = 1
+  filterCriteriaForm.pageFrom = val
+  getClueTable()
+}
+
+function handleCurrentChange(val: number) {
+  filterCriteriaForm.pageIndex = val
+  getClueTable()
+}
+
 async function getSystemField() {
   const systemField = getAllListByCode(['线索来源', '客户行业', '客户级别'])
   for (let i in systemField) {
@@ -211,6 +402,9 @@ async function getSystemField() {
       id, name, phone, jobNumber
     }
   })
+
+  const res = await get(GETTEMPLATE)
+  clueTemplate.value = JSON.parse(res.data[0].config)
 }
 
 onMounted(() => {

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

@@ -8,6 +8,6 @@ interface filterCriteriaFormType { // 线索筛选条件类型
   inchargerId: string | number,
   startTime: string | number,
   endTime: string | number,
-  pageIndex: string | number,
-  pageFrom: string | number
+  pageIndex: number,
+  pageFrom: number
 }

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

@@ -24,4 +24,6 @@ type ListByCodeType = (
 type saveLoadingType = "1" | "2" | "3" | "4"; //1是没有保存, 2是正在保存, 3是保存成功, 4是保存失败
 
 type TASK_VALUE_TYPE = 0 | 1 | 2 | 3; //0是客户, 1是商机, 2是销售订单 ,3是线索
-type REPEAT_VALUE_TYPE = 0 | 1 | 2 | 3 | 4; //0是每天, 1是每周, 2是每月, 3是自定义周期, 4是自定义日期
+type REPEAT_VALUE_TYPE = 0 | 1 | 2 | 3 | 4; //0是每天, 1是每周, 2是每月, 3是自定义周期, 4是自定义日期
+
+type componentType = "success" | "info" | "warning" | "error"

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

@@ -0,0 +1,31 @@
+/**
+ * 获取当月第一天
+ * @param date 日期 new Date()
+ * @returns
+ */
+export function getFirstDayOfMonth(date: Date) {
+  const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
+  return formatDate(firstDay);
+}
+
+/**
+ * 获取当月最后一天
+ * @param date 日期 new Date()
+ * @returns
+ */
+export function getLastDayOfMonth(date: Date) {
+  const nextMonth = new Date(date.getFullYear(), date.getMonth() + 1, 0);
+  return formatDate(nextMonth);
+}
+
+/**
+ * 将 Date 对象格式化为 "YYYY-MM-DD" 的形式
+ * @param date 日期 new Date()
+ * @returns
+ */
+export function formatDate(date: Date) {
+  const year = date.getFullYear();
+  const month = (1 + date.getMonth()).toString().padStart(2, "0");
+  const day = date.getDate().toString().padStart(2, "0");
+  return `${year}-${month}-${day}`;
+}

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

@@ -1,3 +1,5 @@
+import { defalutModalForm } from '@/components/TaskModal/api'
+import { ElMessageBox } from 'element-plus';
 /**
  * 判断值是否为空
  * @param value 值
@@ -139,6 +141,59 @@ export function formatDate(date: Date) {
   return `${year}-${month}-${day}`;  
 }  
 
+/**
+ * 获取创建任务的 form
+ * @param taskType 任务类型
+ * @returns form
+ */
+export function createTaskFromType(taskType: TASK_VALUE_TYPE) {
+  return {
+    ...defalutModalForm,
+    taskType
+  }
+}
+
+/**
+ * 下载文件
+ * @param dataFile 接口返回的数据
+ * @param fileName 文件名称
+ */
+export function downloadFile(dataFile: any, fileName: string) {
+  const data = dataFile;
+  const blob = new Blob([data]);
+  const url = window.URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = fileName;
+  document.body.appendChild(a);
+  a.click();
+  window.URL.revokeObjectURL(url);
+  document.body.removeChild(a);
+};
+
+/**
+ * 消息弹窗框
+ * @param message 消息弹窗框提示
+ * @param title 消息弹窗框标题
+ * @param type type 类型
+ * @param options 消息弹窗框其他配置
+ * @returns promise
+ */
+export function confirmAction(message: string, title = '', type: componentType = 'warning', options = {}) {  
+  return new Promise<void>((resolve, reject) => {  
+    ElMessageBox.confirm(message, title, {  
+      ...{  
+        confirmButtonText: '确定',  
+        cancelButtonText: '取消',  
+        type: type,  
+      },  
+      ...options,  
+    })  
+      .then(() => resolve())  
+      .catch(() => reject());  
+  });  
+}
+
 /**
  * 返回上一级
  */

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

@@ -4,7 +4,7 @@ import vue from "@vitejs/plugin-vue";
 import { resolve } from "path";
 
 // const target = "http://192.168.2.8:10010";
-const target = "http://192.168.2.118:10010";
+const target = "http://192.168.2.93:10010";
 // const target = "http://47.101.180.183:10010";
 
 export default defineConfig({

+ 34 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/BusinessOpportunityController.java

@@ -52,6 +52,40 @@ public class BusinessOpportunityController {
         return bOservice.exportData(bo,request);
     }
 
+    // 批量放入回收站
+    @RequestMapping("delete")
+    public Object delete(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.isDelete(ids);
+            msg.setMsg("操作成功");
+        } else {
+            msg.setError("请选择线索");
+
+        }
+        return msg;
+    } @RequestMapping("rollBack")
+    public Object rollBack(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.isRollBack(ids);
+            msg.setMsg("操作成功");
+        } else {
+            msg.setError("请选择线索");
+
+        }
+        return msg;
+    }
     @RequestMapping("insertAndUpdate")
     public HttpRespMsg insertAndUpdate( BusinessOpportunity bo, HttpServletRequest request) {
         User user = userMapper.selectById(request.getHeader("Token"));

+ 18 - 10
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/controller/ProductController.java

@@ -12,6 +12,7 @@ import com.management.platform.util.MessageUtils;
 import org.apache.poi.hssf.usermodel.*;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.ss.util.CellRangeAddressList;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.RequestMapping;
 
 import org.springframework.web.bind.annotation.RestController;
@@ -22,6 +23,7 @@ import javax.servlet.http.HttpServletRequest;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -54,8 +56,10 @@ public class ProductController {
     @RequestMapping("/addOrUpdate")
     public HttpRespMsg addOrUpdate(Product product){
         HttpRespMsg msg=new HttpRespMsg();
-        Integer companyId = userMapper.selectById(request.getHeader("token")).getCompanyId();
+        User user = userMapper.selectById(request.getHeader("token"));
+        Integer companyId = user.getCompanyId();
         product.setCompanyId(companyId);
+        product.setCreatorId(user.getId());
         int count;
         if(product.getId()==null){
             count = productService.count(new LambdaQueryWrapper<Product>().eq(Product::getCompanyId, companyId).eq(Product::getProductCode, product.getProductCode()));
@@ -75,16 +79,20 @@ public class ProductController {
 
 
     @RequestMapping("/delete")
-    public HttpRespMsg delete(Integer id){
+    public HttpRespMsg delete(String ids){
         HttpRespMsg msg=new HttpRespMsg();
-        int count = taskService.count(new LambdaQueryWrapper<Task>().eq(Task::getProductId, id));
-        if(count>0){
-            msg.setError("当前产品已绑定到相关任务,删除失败");
-            return msg;
-        }
-        if(!productService.removeById(id)){
-            msg.setError("验证失败");
-            return msg;
+        if(!StringUtils.isEmpty(ids)){
+            String[] split = ids.split(",");
+            List<String> idList = Arrays.asList(split);
+            int count = taskService.count(new LambdaQueryWrapper<Task>().in(Task::getProductId, idList));
+            if(count>0){
+                msg.setError("当前产品已绑定到相关任务,删除失败");
+                return msg;
+            }
+            if(!productService.removeByIds(idList)){
+                msg.setError("验证失败");
+                return msg;
+            }
         }
         return msg;
     }

+ 7 - 3
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/BusinessOpportunity.java

@@ -11,9 +11,11 @@ import java.io.Serializable;
 import java.util.Date;
 import java.util.List;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
 
 /**
  * <p>
@@ -51,7 +53,7 @@ public class BusinessOpportunity extends Model<BusinessOpportunity> {
     @TableField("contacts_id")
     private Integer contactsId;
     @TableField(exist = false)
-    private Integer contactsName;
+    private String contactsName;
 
     /**
      * 客户id
@@ -76,10 +78,12 @@ public class BusinessOpportunity extends Model<BusinessOpportunity> {
      * 预计成交日期
      */
     @TableField("expected_transaction_date")
-    private LocalDateTime expectedTransactionDate;
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date expectedTransactionDate;
 
     /**
-     * 商机阶段 0-验证客户 1-需求分析 2-方案报价 3-赢单
+     * 商机阶段
      */
     @TableField("stage_id")
     private Integer stageId;

+ 3 - 0
fhKeeper/formulahousekeeper/management-crm/src/main/java/com/management/platform/entity/Product.java

@@ -136,6 +136,9 @@ public class Product extends Model<Product> {
     @TableField(exist = false)
     private String inchargerName;
 
+    @TableField(exist = false)
+    private String creatorName;
+
     @TableField(exist = false)
     private String typeName;
 

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

@@ -25,4 +25,8 @@ public interface BusinessOpportunityMapper extends BaseMapper<BusinessOpportunit
     int getTotal(BusinessOpportunity bo);
     int getTotal1(@Param("bo") BusinessOpportunity bo,@Param("userId") String userId);
     int getTotal2(@Param("bo") BusinessOpportunity bo,@Param("userId") String userId);
+
+    BusinessOpportunity selectByIdToInfo(Integer id);
+
+    void updateBatchById(List<Integer> ids);
 }

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

@@ -48,4 +48,8 @@ public interface BusinessOpportunityService extends IService<BusinessOpportunity
     Object deleteFile(UploadFile file, HttpServletRequest request);
 
     Object reFileName(UploadFile uploadFile, HttpServletRequest request);
+
+    void isDelete(List<Integer> ids);
+
+    void isRollBack(List<Integer> ids);
 }

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

@@ -75,7 +75,7 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
 
     @Override
     public BusinessOpportunity getInfo(BusinessOpportunity bo, User user) {
-        BusinessOpportunity businessOpportunity = bOMapper.selectById(bo.getId());
+        BusinessOpportunity businessOpportunity = bOMapper.selectByIdToInfo(bo.getId());
         businessOpportunity.setActionLogList(actionLogMapper.selectList(new QueryWrapper<ActionLog>().eq("item_id", bo.getId()).eq("code", "business")));
         businessOpportunity.setUploadFilePList(uploadFileMapper.selectByInfoList("business",bo.getId()));
         businessOpportunity.setTaskList(taskMapper.selectList(new QueryWrapper<Task>().eq("business_opportunity_id",bo.getId())));
@@ -93,7 +93,7 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
                 // 折后价格
                 discountedPrice = discountedPrice.add(price.multiply(divide));
             }
-            BigDecimal divide = discountedPrice.divide(finalPrice);
+            BigDecimal divide = discountedPrice.divide(finalPrice,2,BigDecimal.ROUND_UP);
             // 整单折扣率
             businessOpportunity.setFinalPrice(divide.multiply(new BigDecimal(100)));
         }
@@ -340,6 +340,25 @@ public class BusinessOpportunityServiceImpl extends ServiceImpl<BusinessOpportun
         return uploadFileMapper.update(null, new UpdateWrapper<UploadFile>().eq("id", uploadFile.getId()).set("name", uploadFile.getName()));
     }
 
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void isDelete(List<Integer> ids) {
+        UpdateWrapper<BusinessOpportunity> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.in("id", ids);
+        BusinessOpportunity bo = new BusinessOpportunity();
+        bo.setIsDelete(1);
+        bOMapper.update(bo, updateWrapper);
+    }
+
+    @Override
+    public void isRollBack(List<Integer> ids) {
+        UpdateWrapper<BusinessOpportunity> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.in("id", ids);
+        BusinessOpportunity bo = new BusinessOpportunity();
+        bo.setIsDelete(0);
+        bOMapper.update(bo, updateWrapper);
+    }
+
     private BusinessOpportunity setNull(BusinessOpportunity bo) {
         if (bo.getPlate1() == "") {
             bo.setPlate1(null);

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

@@ -131,10 +131,14 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
         IPage<Product> productIPage = productMapper.selectPage(new Page<>(pageIndex, pageSize), queryWrapper);
         List<Product> records = productIPage.getRecords();
         records.forEach(r->{
-            Optional<User> user = userList.stream().filter(u -> u.getId().equals(r.getCreatorId())).findFirst();
+            Optional<User> user = userList.stream().filter(u -> u.getId().equals(r.getInchargerId())).findFirst();
             if(user.isPresent()){
                 r.setInchargerName(user.get().getName());
             }
+            Optional<User> creator = userList.stream().filter(u -> u.getId().equals(r.getCreatorId())).findFirst();
+            if(creator.isPresent()){
+                r.setCreatorName(creator.get().getName());
+            }
             Optional<SysDict> unit = sysDictOfProductUnit.stream().filter(u -> u.getId().equals(r.getUnit())).findFirst();
             if(unit.isPresent()){
                 r.setUnitName(unit.get().getName());

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

@@ -25,11 +25,11 @@
                p.price price,
                (select `name` from sys_dict where id = p.unit and code = 'ProductUnit') unit,
                (select `name` from sys_dict where id = p.type and code = 'ProductType') productType,
-               p.inventory inventory,
+               p.inventory inventory
         from business_item_product i
                  left join product p on i.product_id = p.id
 --         left join sys_dict d on
-        where business_id = #{id}
+        where i.business_id = #{id}
     </select>
 
 </mapper>

+ 12 - 2
fhKeeper/formulahousekeeper/management-crm/src/main/resources/mapper/BusinessOpportunityMapper.xml

@@ -28,7 +28,7 @@
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id, company_id, name, contacts_id, customer_id, product_id, amount_of_money, expected_transaction_date, stage, create_time, edit_time, creator_id, incharger_id, remark, is_delete, plate1, plate2, plate3, plate4, plate5
+        id, company_id, name, contacts_id, customer_id, product_id, amount_of_money, expected_transaction_date, stage_id, create_time, edit_time, creator_id, incharger_id, remark, is_delete, plate1, plate2, plate3, plate4, plate5
     </sql>
     <select id="selectAllList" resultType="com.management.platform.entity.BusinessOpportunity">
         select
@@ -230,7 +230,17 @@
             and id in (select business_id from business_product where product_id = #{productId})
         </if>
     </select>
-
+    <select id="selectByIdToInfo" resultType="com.management.platform.entity.BusinessOpportunity">
+        SELECT *,
+               (select `name` from stage where id = stage_id)          stageValue,
+               (select custom_name from custom where id = customer_id) customerName,
+               (select `name` from contacts where id = contacts_id)    contactsName,
+               (select `name` from `user` where id = contacts_id)      inchargerName,
+               (select `name` from `user` where id = creator_id)       creatorName,
+               (SELECT sum(total) from business_product WHERE business_id = id)
+        from business_opportunity
+        where id = #{id}
+    </select>
 
 
 </mapper>

+ 70 - 6
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/controller/HolidaySettingController.java

@@ -1,7 +1,11 @@
 package com.management.platform.controller;
 
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.management.platform.entity.Department;
 import com.management.platform.entity.HolidaySetting;
+import com.management.platform.entity.User;
+import com.management.platform.mapper.DepartmentMapper;
 import com.management.platform.mapper.UserMapper;
 import com.management.platform.service.HolidaySettingService;
 import com.management.platform.util.HttpRespMsg;
@@ -15,8 +19,11 @@ import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
 
 /**
  * <p>
@@ -36,22 +43,79 @@ public class HolidaySettingController {
     private UserMapper userMapper;
     @Resource
     private HttpServletRequest request;
+    @Resource
+    private DepartmentMapper departmentMapper;
 
     @RequestMapping("/holidaySetting")
-    public HttpRespMsg holidaySetting(String date,String userIds,String deptIds){
+    public HttpRespMsg holidaySetting(HolidaySetting holidaySetting){
         HttpRespMsg msg=new HttpRespMsg();
-        DateTimeFormatter df=DateTimeFormatter.ofPattern("yyyy-MM-dd");
         Integer companyId = userMapper.selectById(request.getHeader("token")).getCompanyId();
-        HolidaySetting holidaySetting=new HolidaySetting();
+        int count ;
+        if(holidaySetting.getId()==null){
+            count = holidaySettingService.count(new LambdaQueryWrapper<HolidaySetting>().eq(HolidaySetting::getCompanyId, companyId).eq(HolidaySetting::getHolidayDate, holidaySetting.getHolidayDate()));
+        }else {
+            count = holidaySettingService.count(new LambdaQueryWrapper<HolidaySetting>().eq(HolidaySetting::getCompanyId, companyId).ne(HolidaySetting::getId,holidaySetting.getId()).eq(HolidaySetting::getHolidayDate, holidaySetting.getHolidayDate()));
+        }
+        if(count>0){
+            msg.setError("当前日期已设置为特殊日期,请重新选择");
+            return msg;
+        }
         holidaySetting.setCompanyId(companyId);
-        holidaySetting.setHolidayDate(LocalDate.parse(date,df));
-        holidaySetting.setTargetUsers(userIds);
-        holidaySetting.setTargetDepts(deptIds);
         if(!holidaySettingService.saveOrUpdate(holidaySetting)){
             msg.setError("验证失败");
         }
         return msg;
     }
 
+    @RequestMapping("/list")
+    public HttpRespMsg list(){
+        HttpRespMsg msg=new HttpRespMsg();
+        Integer companyId = userMapper.selectById(request.getHeader("token")).getCompanyId();
+        List<Department> departmentList = departmentMapper.selectList(new LambdaQueryWrapper<Department>().eq(Department::getCompanyId, companyId));
+        List<User> userList = userMapper.selectList(new LambdaQueryWrapper<User>().eq(User::getCompanyId, companyId));
+        List<HolidaySetting> holidaySettings = holidaySettingService.list(new LambdaQueryWrapper<HolidaySetting>().eq(HolidaySetting::getCompanyId, companyId));
+        holidaySettings.forEach(h->{
+            if(!StringUtils.isEmpty(h.getTargetUsers())){
+                String[] userSplit = h.getTargetUsers().split(",");
+                List<String> userNames=new ArrayList<>();
+                for (String userId : userSplit) {
+                    Optional<User> first = userList.stream().filter(u -> u.getId().equals(userId)).findFirst();
+                    if(first.isPresent()){
+                        userNames.add(first.get().getName());
+                    }
+                }
+                if(userNames.size()>0){
+                    String collect = userNames.stream().collect(Collectors.joining(","));
+                    h.setTargetUsersNames(collect);
+                }
+            }
+            if(!StringUtils.isEmpty(h.getTargetDepts())){
+                String[] deptSplit = h.getTargetDepts().split(",");
+                List<String> deptNames=new ArrayList<>();
+                for (String deptId : deptSplit) {
+                    Optional<Department> first = departmentList.stream().filter(d -> d.getDepartmentId().equals(Integer.valueOf(deptId))).findFirst();
+                    if(first.isPresent()){
+                        deptNames.add(first.get().getDepartmentName());
+                    }
+                }
+                if(deptNames.size()>0){
+                    String collect = deptNames.stream().collect(Collectors.joining(","));
+                    h.setTargetDeptsNames(collect);
+                }
+            }
+        });
+        msg.setData(holidaySettings);
+        return msg;
+    }
+
+    @RequestMapping("/delete")
+    public HttpRespMsg delete(Integer id){
+        HttpRespMsg msg=new HttpRespMsg();
+        if(!holidaySettingService.removeById(id)){
+            msg.setError("验证失败");
+        }
+        return msg;
+    }
+
 }
 

+ 13 - 1
fhKeeper/formulahousekeeper/management-platform/src/main/java/com/management/platform/entity/HolidaySetting.java

@@ -19,7 +19,7 @@ import org.springframework.format.annotation.DateTimeFormat;
  * </p>
  *
  * @author Seyason
- * @since 2024-04-09
+ * @since 2024-04-10
  */
 @Data
 @EqualsAndHashCode(callSuper = false)
@@ -42,12 +42,24 @@ public class HolidaySetting extends Model<HolidaySetting> {
     @JsonFormat(pattern = "yyyy-MM-dd")
     private LocalDate holidayDate;
 
+    /**
+     * 0-全公司 1-部分成员 有效范围
+     */
+    @TableField("range_type")
+    private Integer rangeType;
+
     @TableField("target_users")
     private String targetUsers;
 
     @TableField("target_depts")
     private String targetDepts;
 
+    @TableField(exist = false)
+    private String targetUsersNames;
+
+    @TableField(exist = false)
+    private String targetDeptsNames;
+
 
     @Override
     protected Serializable pkVal() {

+ 2 - 1
fhKeeper/formulahousekeeper/management-platform/src/main/resources/mapper/HolidaySettingMapper.xml

@@ -7,13 +7,14 @@
         <id column="id" property="id" />
         <result column="company_id" property="companyId" />
         <result column="holiday_date" property="holidayDate" />
+        <result column="range_type" property="rangeType" />
         <result column="target_users" property="targetUsers" />
         <result column="target_depts" property="targetDepts" />
     </resultMap>
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id, company_id, holiday_date, target_users, target_depts
+        id, company_id, holiday_date, range_type, target_users, target_depts
     </sql>
 
 </mapper>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1400 - 2334
fhKeeper/formulahousekeeper/ops-platform/octopus.log


+ 3 - 1
fhKeeper/formulahousekeeper/timesheet/src/views/projectApproval/projectApproval.vue

@@ -463,7 +463,9 @@ return {
     addLoading:false,
     importingData:false,
     showImportResult:false,
-    importResultMsg:null
+    importResultMsg:null,
+    filterText:'',
+    wxFilterText:''
 }
 },
 computed: {},

+ 320 - 1
fhKeeper/formulahousekeeper/timesheet/src/views/settings/timetype.vue

@@ -432,6 +432,13 @@
             <span v-if="timeType.needDeptAudit == 1" style="margin-left:10px;color:#999;">{{ $t('eachdepartment') }}</span>
         </div>
 
+        <!-- 特殊节假日设置 -->
+        <div class="yanjiu">
+            <p style="margin: 0 68px 0 10px;color:#666;">{{'特殊节假日设置'}}</p>
+            <el-button size="small" type="primary" @click="holidaySeeting()">设置</el-button>
+            <span style="margin-left:10px;color:#999;">{{'设置部门或人员的特殊节假日'}}</span>
+        </div>
+
         <!-- 是否开启主项目管理 -->
         <!-- <div class="yanjiu">
             <p style="margin-left:10px;color:#666;">是否开启主项目管理</p>
@@ -442,6 +449,92 @@
         <div style="width:80px;margin:0 auto;padding:20px;">
             <el-button  type="primary" @click="submitInsert" :loading="addLoading">{{ $t('save') }}</el-button>
         </div>
+        <!-- 特殊节假日设置列表 -->
+        <el-dialog :title="'特殊节假日列表'" show-header="false" :top="'10vh'" v-if="holidaySeetingDialog" :visible.sync="holidaySeetingDialog" :close-on-click-modal="false" customClass="customWidth" width="1000px">
+            <el-table :data="holidaySeetingDatas" highlight-current-row  height="400" style="width: 100%;">
+            <el-table-column prop="holidayDate" width="120" :label="'日期'">
+            </el-table-column>
+            <el-table-column prop="name" :label="'有效范围'" width="120">
+                <template slot-scope="scope">
+                    {{scope.row.rangeType==0?"全公司":"部分成员"}}
+                </template>
+            </el-table-column>
+            <el-table-column prop="name" :label="'有效部门'" width="300">
+                <template slot-scope="scope">
+                    <div v-if="user.userNameNeedTranslate != '1'">{{ scope.row.targetDeptsNames }}</div>
+                    <div v-else>
+                        <span v-for="(v,i) in (scope.row.newDeptName || [])">
+                            <ww-open-data type='departmentName' :openid='v'></ww-open-data>
+                            <span v-if="i < (scope.row.newDeptName || []).length - 1">,</span>
+                        </span>
+                    </div>
+                </template>
+            </el-table-column>
+            <el-table-column prop="name" :label="'有效人员'" width="300">
+                <template slot-scope="scope">
+                    <div v-if="user.userNameNeedTranslate != '1'">{{ scope.row.targetUsersNames }}</div>
+                    <div v-else>
+                        <span v-for="(v,i) in (scope.row.newDeptName || [])">
+                            <ww-open-data type='userName' :openid='v'></ww-open-data>
+                            <span v-if="i < (scope.row.newUserName || []).length - 1">,</span>
+                        </span>
+                    </div>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('operation')" width="150" fixed="right">
+                <template slot-scope="scope" >
+                    <el-button size="small" type="primary" @click="addNewHolidaySeeting(scope.row)">{{ $t('bian-ji') }}</el-button>
+                    <el-button size="small" type="danger" @click="deleteHolidaySeeting(scope.row)">{{ $t('btn.delete') }}</el-button>
+                </template>
+            </el-table-column>
+            </el-table>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="holidaySeetingDialog = false" >{{ $t('Shutdown') }}</el-button>
+                <el-button type="primary" @click="addNewHolidaySeeting()" >{{ $t('addData') }}</el-button>
+            </div>
+        </el-dialog>
+        <!-- 新增节假日设置 -->
+        <el-dialog :title="'特殊节假日设置'" show-header="false" v-if="addHolidaySeeting" :visible.sync="addHolidaySeeting" :top="'10vh'" :close-on-click-modal="false" customClass="customWidth" width="600px">
+            <el-form ref="form" :model="holidaySeetingForm" label-width="80px">
+                <el-form-item label="日期">
+                    <el-date-picker v-model="holidaySeetingForm.holidayDate"
+                    :editable="false"
+                    format="yyyy-MM-dd" 
+                    value-format="yyyy-MM-dd"
+                    :clearable="false" type="date" 
+                    :placeholder="$t('optiondate')"></el-date-picker>
+                </el-form-item>
+                <el-form-item label="有效范围">
+                    <el-radio v-model="holidaySeetingForm.rangeType" label="0">全公司</el-radio>
+                    <el-radio v-model="holidaySeetingForm.rangeType" label="1">部分成员</el-radio>
+                </el-form-item>
+                <div style="height: 36vh" v-if="holidaySeetingForm.rangeType!=1"></div>
+                <div v-if="holidaySeetingForm.rangeType==1">
+                    <div class="specialHolidaysClass">
+                        <div class="titles">有效部门 <el-link type="primary" :underline="false" @click="showSpecialHolidays(1)">添加</el-link></div>
+                        <div class="contents">
+                            <el-tag style="margin:10px 0 0 10px" v-for="(item, index) in specialHolidaysDept.labels" :key="item.id" closable @close="specialHolidaysDelete('specialHolidaysDept', index)">
+                                <span v-if="user.userNameNeedTranslate != '1'">{{item}}</span>
+                                <span v-if="user.userNameNeedTranslate == '1'"><ww-open-data type='departmentName' :openid='item'></ww-open-data></span>
+                            </el-tag>
+                        </div>
+                    </div>
+                    <div class="specialHolidaysClass">
+                        <div class="titles">有效人员 <el-link type="primary" :underline="false" @click="showSpecialHolidays(2)">添加</el-link></div>
+                        <div class="contents">
+                            <el-tag style="margin:10px 0 0 10px" v-for="(item, index) in specialHolidaysUser.labels" :key="item.id" closable @close="specialHolidaysDelete('specialHolidaysUser', index)">
+                                <span v-if="user.userNameNeedTranslate != '1'">{{item}}</span>
+                                <span v-if="user.userNameNeedTranslate == '1'"><ww-open-data type='userName' :openid='item'></ww-open-data></span>
+                            </el-tag>
+                        </div>
+                    </div>
+                </div>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="addHolidaySeeting = false" >{{ $t('Shutdown') }}</el-button>
+                <el-button type="primary" @click="submitInsertHolidaySeeting()" >{{'保存'}}</el-button>
+            </div>
+        </el-dialog>
         <!-- 维护数据 -->
         <el-dialog :title="$t('maintenanceDatalist')" show-header="false" v-if="subProjectVisible" :visible.sync="subProjectVisible" :close-on-click-modal="false" customClass="customWidth" width="500px">
             <el-table :data="subProjectList" highlight-current-row  height="400" style="width: 100%;">
@@ -594,6 +687,45 @@
                 <el-button type="primary" @click="setWhiteList()">{{ $t('btn.determine') }}</el-button>
             </div>
         </el-dialog>
+
+        <!-- 特殊节假日设置 -->
+        <el-dialog :title="specialHolidays == 1 ? '请选择部门' : '请选择人员'" :top="'10vh'"  v-if="specialHolidaysDialog" :visible.sync="specialHolidaysDialog" :close-on-click-modal="false" customClass="customWidth" width="600px">
+            <div v-if="user.userNameNeedTranslate == '1'">
+                <el-input placeholder="请输入名字搜索" v-model.trim="wxFilterText" clearable @clear="clearEchartDepartment()" class="input-with-select">
+                    <el-button slot="append" icon="el-icon-search" @click="echartDepartment()"></el-button>
+                </el-input>
+            </div>
+            <div class="tree" style="height:400px">
+                <el-scrollbar style="height:100%">
+                    <el-input v-if="user.userNameNeedTranslate != '1'"
+                    :placeholder="$t('keywordfiltering')"
+                    v-model="filterText">
+                    </el-input>
+                <el-tree :data="whiteListAll" :key="searchPersonnelFlgnum" :default-expand-all="searchPersonnelFlg" show-checkbox :props="defaultProps" node-key="id"
+                    ref="specialHolidaysTree"
+                    highlight-current  :filter-node-method="filterNode">
+                        <span class="custom-tree-node" slot-scope="{ node, data }">
+                            <span v-if="user.userNameNeedTranslate == '1'">
+                                <span v-if="node.data.children">
+                                    <ww-open-data type='departmentName' :openid='node.label'></ww-open-data>
+                                </span>
+                                <span v-else>
+                                    <ww-open-data type='userName' :openid='node.label'></ww-open-data>
+                                </span>
+                            </span>
+                            <span v-if="user.userNameNeedTranslate != '1'">
+                                {{ node.label }}
+                            </span>
+                        </span>
+                    </el-tree>
+                </el-scrollbar>
+            </div>
+            <!-- <div>已选中&nbsp;{{chosenMembCount}}&nbsp;人</div>         :default-checked-keys="alreadyPartArray"  @check-change="onTreeItemChange" -->
+            <div slot="footer" class="dialog-footer">
+                <el-button @click="specialHolidaysDialog = false">{{ $t('btn.cancel') }}</el-button>
+                <el-button type="primary" @click="specialHolidaysChange()">{{ $t('btn.determine') }}</el-button>
+            </div>
+        </el-dialog>
     </section>
 </template>
 <script>
@@ -728,6 +860,24 @@
                 searchPersonnelFlg: false,
                 searchPersonnelFlgnum: 1,
                 options: [],
+                holidaySeetingDialog:false,
+                addHolidaySeeting:false,
+                holidaySeetingForm:{
+                    rangeType:"0",
+                    holidayDate: ''
+                },
+                holidaySeetingDatas:[],
+
+                specialHolidays: 0, // 1 部门, 2 人员
+                specialHolidaysDialog: false,
+                specialHolidaysDept: {
+                    values: [],
+                    labels: []
+                },
+                specialHolidaysUser: {
+                    values: [],
+                    labels: []
+                }
             };
         },
         watch: {
@@ -758,6 +908,71 @@
             }, 1000)
         },
          methods: {
+            setspecialHolidaysNodes() {
+                let key = []
+                if(this.specialHolidays == 1) {
+                    key = this.specialHolidaysDept.values || []
+                }
+                if(this.specialHolidays == 2) {
+                    key = this.specialHolidaysUser.values || []
+                }
+                console.log(key, '<=== 设置数据')
+                this.$refs.specialHolidaysTree.setCheckedKeys(key)
+            },
+            specialHolidaysDelete(flied, index) {
+                this[flied].labels.splice(index, 1)
+                this[flied].values.splice(index, 1)
+            },
+            specialHolidaysChange() {
+                let selectList = this.$refs.specialHolidaysTree.getCheckedNodes();
+                if(this.specialHolidays == 1) {
+                    let values = selectList.map(item => item.id)
+                    let labels = selectList.map(item => item.label)
+                    this.specialHolidaysDept = { values, labels }
+                }
+                if(this.specialHolidays == 2) {
+                    let values = selectList.filter(item => !item.userList).map(item => item.id)
+                    let labels = selectList.filter(item => !item.userList).map(item => item.label)
+                    this.specialHolidaysUser = { values, labels }
+                }
+                this.specialHolidaysDialog = false
+            },
+            showSpecialHolidays(key) {
+                this.specialHolidays = key
+                this.filterText = ''
+                this.getWhiteListAll()
+                this.specialHolidaysDialog = true
+            },
+            submitInsertHolidaySeeting(){
+                let newForm = {
+                    ...this.holidaySeetingForm,
+                    targetUsers: this.specialHolidaysUser.values.join(','),
+                    targetDepts: this.specialHolidaysDept.values.join(','),
+                }
+                this.http.post('/holiday-setting/holidaySetting',{...newForm},res => {
+                    if(res.code == 'ok'){
+                        this.$message({
+                            message: "保存成功",
+                            type: 'success'
+                        })
+                        this.specialHolidaysDialog = false
+                        this.addHolidaySeeting = false
+                        this.getHolidaySeetingList()
+                    }else {
+                        this.$message({
+                            message: res.msg,
+                            type: 'error'
+                        })
+                    }
+                },err => {
+                    this.$message({
+                        message: err,
+                        type: 'error'
+                    })
+                    this.specialHolidaysDialog = false
+                    this.addHolidaySeeting = false
+                })
+            },
             onHideTask() {
                 if (this.timeType.hideTask) {
                     //任务必填属性变为false
@@ -768,7 +983,54 @@
                 if (this.timeType.notAllowedOnNonWorkday) {
                     this.timeType.includeWeekends = false;
                 }
+            },
+            addNewHolidaySeeting(item){
+                if(item) {
+                    this.holidaySeetingForm.holidayDate = item.holidayDate
+                    this.holidaySeetingForm.id = item.id
+                    this.holidaySeetingForm.rangeType = item.rangeType+""
+                    let newDeptForm = {
+                        values: item.newDeptId,
+                        labels: item.newDeptName
+                    }
+                    let newUserForm = {
+                        values: item.newUserId,
+                        labels: item.newUserName
+                    }
+                    this.specialHolidaysDept = newDeptForm
+                    this.specialHolidaysUser = newUserForm
+                }
+                this.addHolidaySeeting = true;
+            },
+            holidaySeeting(){
+                this.holidaySeetingDialog=true,
+                this.getHolidaySeetingList()
             },  
+            // 获取节假日设置列表
+            getHolidaySeetingList(){
+                this.http.post('/holiday-setting/list',{
+                },res => {
+                    if(res.code == 'ok'){
+                        res.data.forEach((item) => {
+                            item.newDeptName = item.targetDeptsNames ? item.targetDeptsNames.split(',') : []
+                            item.newUserName = item.targetUsersNames ? item.targetUsersNames.split(',') : []
+                            item.newDeptId = item.targetDepts ? item.targetDepts.split(',') : []
+                            item.newUserId = item.targetUsers ? item.targetUsers.split(',') : []
+                        })
+                        this.holidaySeetingDatas = res.data
+                    }else {
+                        this.$message({
+                            message: res.msg,
+                            type: 'error'
+                        })
+                    }
+                },err => {
+                    this.$message({
+                        message: err,
+                        type: 'error'
+                    })
+                })
+            },
             getDept(){
                 this.http.post('/department/list',{
                 },res => {
@@ -1044,6 +1306,36 @@
                 }
                 this.addSubProject = true;
             },
+            deleteHolidaySeeting(item) {
+                this.$confirm('确定删除这条数据吗?','删除', {
+                    confirmButtonText: this.$t('btn.determine'),
+                    cancelButtonText: this.$t('btn.cancel'),
+                    type: "warning"
+                })
+                .then(() => {
+                    this.listLoading = true;
+                    this.http.post('/holiday-setting/delete',{ 
+                        id: item.id 
+                    },
+                    res => {
+                        if (res.code == "ok") {
+                            this.$message({
+                                message: this.$t('message.successfullyDeleted'),
+                                type: "success"
+                            });
+                            this.getHolidaySeetingList();
+                        }
+                    },
+                    error => {
+                        this.$message({
+                            message: error,
+                            type: "error"
+                        });
+                        }
+                    );
+                })
+                .catch(() => {});
+            },
             deleteSubPro(subProject) {
                 this.$confirm(this.$t('takethisone') + subProject.name + this.$t('shu-ju-ma'),this.$t('btn.delete'), {
                     confirmButtonText: this.$t('btn.determine'),
@@ -1141,8 +1433,14 @@
                 },res => {
                     if(res.code == 'ok'){
                         let list = res.data
-                        this.haveUsersList(list)
+                        if(this.specialHolidays != 1) {
+                            this.haveUsersList(list)
+                        }
                         this.whiteListAll = JSON.parse(JSON.stringify(list))
+
+                        if(this.specialHolidays != 0) {
+                            this.setspecialHolidaysNodes()
+                        }
                     }else {
                         this.$message({
                             message: res.msg,
@@ -1182,6 +1480,7 @@
             //     // this.chosenMembCount = list.length;
             // },
             addWhite(){
+                this.specialHolidays = 0
                 this.whiteListDialog = true
                 this.filterText = ''
                 this.getWhiteListAll()
@@ -1605,4 +1904,24 @@
 .panelFixation {
     height: 300px;
 }
+
+.specialHolidaysClass {
+    display: flex;
+    width: 100%;
+    padding: 0 10px;
+    flex-wrap: wrap;
+    .titles {
+        width: 100%;
+        display: flex;
+        padding-right: 20px;
+        justify-content: space-between;
+        align-items: center;
+    }
+    .contents {
+        width: 100%;
+        height: 15.5vh;
+        overflow-y: auto;
+        overflow-x: hidden;
+    }
+}
 </style>