Explorar o código

提交客户daima

Lijy hai 5 meses
pai
achega
a8e712779d

+ 22 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/package-lock.json

@@ -19,6 +19,7 @@
         "pinia": "^2.1.7",
         "pinia-plugin-persistedstate": "^3.2.1",
         "vue": "^3.4.19",
+        "vue-draggable-plus": "^0.6.0",
         "vue-router": "^4.3.0",
         "vuex": "^4.1.0"
       },
@@ -868,6 +869,11 @@
         "undici-types": "~5.26.4"
       }
     },
+    "node_modules/@types/sortablejs": {
+      "version": "1.15.8",
+      "resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.8.tgz",
+      "integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg=="
+    },
     "node_modules/@types/web-bluetooth": {
       "version": "0.0.16",
       "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
@@ -2950,6 +2956,22 @@
         }
       }
     },
+    "node_modules/vue-draggable-plus": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmmirror.com/vue-draggable-plus/-/vue-draggable-plus-0.6.0.tgz",
+      "integrity": "sha512-G5TSfHrt9tX9EjdG49InoFJbt2NYk0h3kgjgKxkFWr3ulIUays0oFObr5KZ8qzD4+QnhtALiRwIqY6qul4egqw==",
+      "dependencies": {
+        "@types/sortablejs": "^1.15.8"
+      },
+      "peerDependencies": {
+        "@types/sortablejs": "^1.15.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/vue-router": {
       "version": "4.3.2",
       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz",

+ 1 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/package.json

@@ -22,6 +22,7 @@
     "pinia": "^2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",
     "vue": "^3.4.19",
+    "vue-draggable-plus": "^0.6.0",
     "vue-router": "^4.3.0",
     "vuex": "^4.1.0"
   },

+ 2 - 2
fhKeeper/formulahousekeeper/customerBuler-crm/src/components/TaskModal/taskFunction.ts

@@ -9,8 +9,8 @@ export async function createTask(submitData: any, isClose: boolean) : Promise<Ta
         const { executorId, startDate, endDate, repeatEndDate } = submitData;
         let params = {
             ...submitData,
-            startDate: startDate && dayjs(startDate).format('YYYY-MM-DD 00:00:00'),
-            endDate: endDate && dayjs(endDate).format('YYYY-MM-DD 23:59:59'),
+            startDate: startDate && dayjs(startDate).format('YYYY-MM-DD'),
+            endDate: endDate && dayjs(endDate).format('YYYY-MM-DD'),
             repeatEndDate: repeatEndDate && dayjs(repeatEndDate).format('YYYY-MM-DD 23:59:59')
         }
         if (executorId) {

+ 79 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/components/svgIcon/index.vue

@@ -0,0 +1,79 @@
+<template>
+  <div v-if="isColorIcon || isSvgIcon">
+    <svg :style="setIconSVGStyle" aria-hidden="true" class="icon">
+      <use :xlink:href="'#icon-' + name"></use>
+    </svg>
+  </div>
+  <i v-else :class="getIconName" :style="setIconSvgStyle"/>
+</template>
+
+<script lang="ts" name="svgIcon" setup>
+import 'http://at.alicdn.com/t/c/font_4766628_7ekfe85jxt9.js' // 引入阿里图标库
+import {computed} from 'vue';
+
+// 定义父组件传过来的值
+const props = defineProps({
+  name: { // svg 图标组件名字
+    type: String,
+  },
+  size: { // svg 大小
+    type: Number,
+    default: () => 14,
+  },
+  color: { // svg 颜色
+    type: String,
+  },
+  colorIcon: { //彩色
+    type: Boolean,
+    default: false,
+  },
+  isSvg: { // 是否是阿里图标库
+    type: Boolean,
+    default: true,
+  }
+});
+
+const linesString = ['https', 'http', '/src', '/assets', 'data:image', import.meta.env.VITE_PUBLIC_PATH];
+
+const getIconName = computed(() => {
+  return 'iconfont icon-' + props?.name;
+});
+// 用于判断 element plus 自带 svg 图标的显示、隐藏
+const isShowIconSvg = computed(() => {
+  return props?.name?.startsWith('ele-');
+});
+// 用于判断在线链接、本地引入等图标显示、隐藏
+const isShowIconImg = computed(() => {
+  return linesString.find((str) => props.name?.startsWith(str));
+});
+// 设置图标样式
+const setIconSvgStyle = computed(() => {
+  return `font-size: ${props.size}px;color: ${props.color};`;
+});
+// 设置图片样式
+const setIconImgOutStyle = computed(() => {
+  return `width: ${props.size}px;height: ${props.size}px;display: inline-block;overflow: hidden;`;
+});
+// 设置图片样式
+// https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0
+const setIconSvgInsStyle = computed(() => {
+  const filterStyle: string[] = [];
+  const compatibles: string[] = ['-webkit', '-ms', '-o', '-moz'];
+  compatibles.forEach((j) => filterStyle.push(`${j}-filter: drop-shadow(${props.color} 30px 0);`));
+  return `width: ${props.size}px;height: ${props.size}px;position: relative;left: -${props.size}px;${filterStyle.join('')}`;
+});
+
+const setIconSVGStyle = computed(() => {
+  return `width: ${props.size}px;height: ${props.size}px;display: inline-block;overflow: hidden;`;
+});
+//是否是彩色图标
+const isColorIcon = computed(() => {
+  
+  return props.colorIcon;
+});
+// 是否是阿里图标库
+const isSvgIcon = computed(() => {
+  
+  return props.isSvg;
+});
+</script>

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

@@ -24,6 +24,14 @@ export const URL_SAVECONTACT = `/business-opportunity/saveContactsId`
 export const URL_STAGEIDNEXT = `/business-opportunity/saveStageId`
 export const URL_SAVEREASON = `/business-opportunity/saveReason`
 export const URL_EXPORTBUSINESS = `/business-opportunity/exportData`
+export const PANEL_MOBILE_DATA = `/business-opportunity/changeOrder`
+
+// 看板视图
+export const OBTAIN_KANBAN_VIEW_DATA = `/business-opportunity/getAllByStage`
+
+// 看板类型
+export const TABLE_VIEW = 'table'
+export const KANBAN_VIEW = 'view'
 
 
 export const stageStatus = [

+ 229 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/component/kanbanView.vue

@@ -0,0 +1,229 @@
+<script lang="ts" setup>
+import { ref, reactive, onMounted, inject } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { VueDraggable } from 'vue-draggable-plus';
+import { MOD, OBTAIN_KANBAN_VIEW_DATA, PANEL_MOBILE_DATA, URL_STAGEIDNEXT } from '../api'
+import { Loading, MoreFilled } from '@element-plus/icons-vue'
+import { post, get, uploadFile } from "@/utils/request";
+
+import SvgIcon from "@/components/svgIcon/index.vue";
+import { formatDate, formatDateTime } from "@/utils/times";
+
+const emit = defineEmits()
+const router = useRouter()
+const viewList = ref<viewListInterface[]>([])
+const excessiveData = ref<any>({})
+const selectionStage = ref<number>()
+const switchingStagesVisable = ref(false)
+const allLoading = reactive({
+  kanbanViewLoading: false,
+  switchingStagesLoading: false
+})
+
+onMounted(() => {
+  getKanbanViewData();
+})
+
+function promotionStage() {
+  const item = viewList.value.find((item: any) => item.id == selectionStage.value)
+  const { id: stageId, label: stageValue } = item as any
+  allLoading.switchingStagesLoading = true
+  post(URL_STAGEIDNEXT, { id: excessiveData.value.id, stageId, stageValue }).then(() => {
+    switchingStagesVisable.value = false
+    getKanbanViewData()
+  }).finally(() => {
+    allLoading.switchingStagesLoading = false
+  })
+}
+
+function switchingStages(row: any) {
+  excessiveData.value = row
+  switchingStagesVisable.value = true
+}
+
+function onChange(e: any) {
+  const data = {
+    id: e.data.id,
+    oldIndex: e.oldIndex,
+    newIndex: e.newIndex,
+    oldStagesId: e.from.id,
+    newStagesId: e.to.id
+  }
+  setDataLoading(viewList.value, data.newStagesId, data.id, true)
+  post(PANEL_MOBILE_DATA, { ...data }).then(() => {
+    getKanbanViewData()
+  }).finally(() => {
+    setDataLoading(viewList.value, data.newStagesId, data.id, false)
+  })
+}
+
+function setDataLoading(list: any, targetLevel_1Id: number | string, targetLevel_2Id: number | string, flag: boolean) {
+  for (let i = 0; i < list.length; i++) {
+    if (list[i].id == targetLevel_1Id) {
+      for (let j = 0; j < list[i].list.length; j++) {
+        if (list[i].list[j].id == targetLevel_2Id) {
+          list[i].list[j].loadData = flag;
+        }
+      }
+    }
+  }
+}
+
+function selectData(_value: boolean, _row: any) {
+  let data = [...viewList.value].flatMap(item =>
+    item.list.filter(subItem => subItem.multipleChoice)
+  );
+  let newData =  JSON.parse(JSON.stringify(data))
+  newData.forEach((item: any) => {
+    delete item.multipleChoice
+    delete item.loadData
+  });
+
+  emit('kanbanViewClick', 'multipleChoice', newData)
+}
+
+function deteleItem(row: any) {
+  emit('kanbanViewClick', 'delete', row)
+}
+
+function editItem(row: any) {
+  emit('kanbanViewClick', 'edit', row)
+}
+
+function addTaskItem(row: any) {
+  emit('kanbanViewClick', 'addTask', row)
+}
+
+function toDetailPath(row: any) {
+  router.push({
+    path: `${MOD}/detail`,
+    query: { id: row.id }
+  })
+}
+
+function searchDashboardView(row: businessOpportunityFormType) {
+  getKanbanViewData(row || {})
+}
+
+function getKanbanViewData(formVal: any = {}) { // 获取看板视图数据
+  allLoading.kanbanViewLoading = true
+  post(OBTAIN_KANBAN_VIEW_DATA, { ...formVal }).then(res => {
+    res.data.forEach((item: any) => {
+      item.list = setArrList(item.list)
+    })
+    emit('kanbanViewClick', 'multipleChoice', [])
+    viewList.value = res.data || []
+  }).finally(() => {
+    allLoading.kanbanViewLoading = false
+  })
+}
+
+function setArrList(value: any) {
+  const val = Array.isArray(value) ? value : []
+  return val.map((item: any) => {
+    return {
+      ...item,
+      expectedTransactionDate: item.expectedTransactionDate ? formatDate(new Date(item.expectedTransactionDate)) : '',
+      multipleChoice: false,
+      loadData: false
+    }
+  })
+}
+
+defineExpose({
+  searchDashboardView,
+});
+
+</script>
+
+<template>
+  <div class="w-full h-full overflow-auto flex pb-3 scroll-bar" v-loading="allLoading.kanbanViewLoading">
+    <template v-if="viewList.length > 0">
+      <div class="w-auto h-full" v-for="(item, index) in viewList" :key="index">
+        <div class="h-full flex flex-col mx-3 w-72">
+          <div class="w-full flex justify-between px-3 py-3">
+            <div class="w-9/12">
+              <div class="w-auto px-4 py-1 text-white rounded-2xl" :style="{ backgroundColor: item.color }">
+                {{ item.label }}
+              </div>
+            </div>
+            <div class="w-2/12 text-right">{{ item.length }}</div>
+          </div>
+          <VueDraggable v-model="item.list" class="flex-1 overflow-y-auto overflow-x-hidden scroll-bar" :animation="150"
+            group="people" @update="onChange" @add="onChange" :id="item.id">
+            <div
+              class="break-words border border-inherit rounded h-72 mx-3 my-3 p-3 shadow-md hover:shadow-xl duration-300 ease-in-out cursor-pointer"
+              v-for="(subItem, subIndex) in item.list" :key="subIndex" v-loading="subItem.loadData">
+              <div class="w-full text-left text-lg flex flex-row items-center" :style="{ color: item.color }">
+                <el-checkbox v-model="subItem.multipleChoice" class="pr-2" size="large"
+                  @change="(val: any) => selectData(val, subItem)" />
+                <div class="flex-1 truncate" @click.stop="toDetailPath(subItem)" v-ellipsis-tooltip>
+                  {{ subItem.name }}
+                </div>
+                <el-dropdown placement="bottom-start">
+                  <el-link :icon="MoreFilled" :underline="false"></el-link>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item @click="addTaskItem(subItem)">新建任务</el-dropdown-item>
+                      <el-dropdown-item @click="switchingStages(subItem)">切换阶段</el-dropdown-item>
+                      <el-dropdown-item @click="editItem(subItem)">编辑</el-dropdown-item>
+                      <el-dropdown-item @click="deteleItem(subItem)">删除</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+              </div>
+              <div class="flex items-center mt-4">
+                <SvgIcon name="kehu" :size="20" class="mr-2" />
+                {{ subItem.customerName }}
+              </div>
+              <div class="flex items-center mt-4">
+                <SvgIcon name="lianxiren" :size="20" color="#606266" class="mr-2" />
+                {{ subItem.contactsName }}
+              </div>
+              <div class="flex items-center mt-4">
+                <SvgIcon name="fuzeren" :size="20" color="#606266" class="mr-2" />
+                {{ subItem.inchargerName }}
+              </div>
+              <div class="flex items-center mt-4">
+                ¥ {{ subItem.amountOfMoney || 0 }}
+              </div>
+              <div class="flex items-center mt-4">
+                {{ subItem.expectedTransactionDate }}
+              </div>
+            </div>
+          </VueDraggable>
+        </div>
+      </div>
+    </template>
+    <template v-if="viewList.length == 0">
+      <div class="w-full h-full flex items-center justify-center">
+        <el-empty description="暂无数据" />
+      </div>
+    </template>
+
+    <!-- 弹窗 -->
+    <el-dialog width="700px" v-model="switchingStagesVisable" append-to-body :show-close="false">
+      <template #header="{ close, titleId, titleClass }">
+        <div class="flex justify-between items-center border-b pb-3 dialog-header">
+          <h4 :id="titleId">切换阶段</h4>
+          <div>
+            <el-button type="primary" @click="promotionStage()" :loading="allLoading.switchingStagesLoading">保存</el-button>
+            <el-button @click="switchingStagesVisable = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="h-[80px] flex flex-col pt-5">
+        <div class="flex flex-row w-full items-center">
+          <div class="w-[100px] mr-2 text-right">切换阶段:</div>
+          <div class="flex-1">
+            <el-select v-model="selectionStage" placeholder="请选择" style="width: 240px">
+              <el-option v-for="item in viewList" :key="item.id" :label="item.label" :value="item.id" />
+            </el-select>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped></style>

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

@@ -21,6 +21,11 @@
                             </template>
                         </el-table-column>
                         <el-table-column :prop="'name'" :label="'阶段名称'"></el-table-column>
+                        <el-table-column :prop="'plan'" :label="'颜色'" width="100">
+                            <template #default="scope">
+                                <div class="w-full h-[20px]" :style="`background: ${scope.row.color}`"></div>
+                            </template>
+                        </el-table-column>
                         <el-table-column :prop="'plan'" :label="'进度'" width="100">
                             <template #default="scope">
                                 {{ scope.row.plan }} %
@@ -28,12 +33,14 @@
                         </el-table-column>
                         <el-table-column label="操作" fixed="right" width="200">
                             <template #default="scope">
-                                <el-button link type="primary" size="large" @click="addStage(scope.row)" :disabled="scope.row.isFinish == 1">编辑</el-button>
-                                <el-button link type="danger" size="large" @click="deteStage(+scope.$index, scope.row)" :disabled="scope.row.isFinish == 1">删除</el-button>
-                                <el-button link type="primary" size="large" @click="moveStage(+scope.$index, 'up')"
-                                    v-if="scope.$index != 0">上移</el-button>
-                                <el-button link type="primary" size="large" @click="moveStage(+scope.$index, 'down')"
-                                    v-if="scope.$index < stageTableList.length - 1">下移</el-button>
+                                <template v-if="scope.row.isFinish != 1">
+                                    <el-button link type="primary" size="large" @click="addStage(scope.row)" :disabled="scope.row.isFinish == 1">编辑</el-button>
+                                    <el-button link type="danger" size="large" @click="deteStage(+scope.$index, scope.row)" :disabled="scope.row.isFinish == 1">删除</el-button>
+                                    <el-button link type="primary" size="large" @click="moveStage(+scope.$index, 'up')"
+                                        v-if="scope.$index != 0">上移</el-button>
+                                    <el-button link type="primary" size="large" @click="moveStage(+scope.$index, 'down')"
+                                        v-if="scope.$index < stageTableList.length - 4">下移</el-button>
+                                </template>
                             </template>
                         </el-table-column>
                     </el-table>
@@ -59,12 +66,20 @@
                             <el-input v-model="stageForm.name" placeholder="请输入阶段名称" clearable></el-input>
                         </div>
                     </div>
-                    <div class="flex flex-row w-full items-center pt-3">
-                        <div class="w-[100px] mr-2 text-right">进度:</div>
-                        <div class="flex-1">
-                            <el-input-number v-model="stageForm.plan" controls-position="right" :min="0"
-                                :max="100"></el-input-number>
-                            <span class="inline-block ml-2">%</span>
+                    <div class="flex justify-between w-full items-center pt-3">
+                        <div class="flex items-center">
+                            <div class="w-[100px] mr-2 text-right">颜色:</div>
+                            <div class="flex-1">
+                                <el-color-picker v-model="stageForm.color" size="small" />
+                            </div>
+                        </div>
+                        <div class="flex items-center">
+                            <div class="w-[100px] mr-2 text-right">进度:</div>
+                            <div class="flex-1">
+                                <el-input-number v-model="stageForm.plan" controls-position="right" :min="0"
+                                    :max="100"></el-input-number>
+                                <span class="inline-block ml-2">%</span>
+                            </div>
                         </div>
                     </div>
                 </div>
@@ -76,18 +91,18 @@
 import { post } from '@/utils/request';
 import { ref, reactive, onMounted, watch, inject } from 'vue'
 import { BUSIESS_GETSATE, BUSIESS_SAVESAIE, URL_DETELESTAGE } from '../api';
-import { List } from 'echarts';
 
 type moveStageType = 'up' | 'down';
 type stageFormType = {
     name: string,
     plan: number,
     seq: number,
+    color: string,
     id?: number,
     isFinish?: number
 }
 
-const emits = defineEmits(['closeVisible']);
+const emits = defineEmits(['closeVisible', 'change']);
 const globalPopup = inject<GlobalPopup>('globalPopup')
 const stageVisible = ref(false)
 const stageTableList = ref<stageFormType[]>([])
@@ -102,6 +117,7 @@ const allVisible = reactive({
 const stageForm = reactive<stageFormType>({
     name: '',
     plan: 0,
+    color: '#075985',
     seq: 0,
 })
 
@@ -128,6 +144,7 @@ function saveState() {
     allLoading.saveLoading = true
     post(BUSIESS_SAVESAIE, { stages: JSON.stringify(data) }).then(() => {
         globalPopup?.showSuccess('保存成功')
+        emits('change')
         cancel()
     }).finally(() => {
         allLoading.saveLoading = false
@@ -147,7 +164,8 @@ function editState(flag: boolean) {
     if (listIndex != -1) {
         stageTableList.value.splice(listIndex, 1, newStage)
     } else {
-        stageTableList.value.push(newStage)
+        stageTableList.value.splice(stageTableList.value.length - 3, 0, newStage)
+        // stageTableList.value.push(newStage)
     }
 
     if (flag) {
@@ -166,6 +184,7 @@ function addStage(item: any) {
             name: row.name,
             plan: row.plan,
             seq: row.seq,
+            color: row.color,
             ...(row.id && { id: row.id }),
             ...(row.companyId && { companyId: row.companyId }),
             ...(row.isFinish && { isFinish: row.isFinish })
@@ -182,7 +201,8 @@ function resetStage() {
     let formVal = {
         name: '',
         plan: 0,
-        seq: +maxnum.seq + 1
+        seq: +maxnum.seq + 1,
+        color: '#075985'
     }
     delete stageForm.id
     delete stageForm.isFinish

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

@@ -11,7 +11,7 @@
           <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
         </el-select>
       </div>
-      <div class="flex-1 flex h-full justify-end overflow-auto scroll-bar-hide cursor-pointer" @wheel="handleScroll">
+      <div class="scroll-bar-hide h-full cursor-pointer" style="flex: 1;display: flex;overflow-x: auto;" @wheel="handleScroll">
         <div
           :class="`${index === 0 ? 'startStep' : 'nextStep'} ${(currentStage >= index || businessInfo.stageValue == '赢单') ? 'selected' : (currentStage >= index || businessInfo.stageValue == '输单') ? 'backOrange' : 'backGray'} relative rounded-md flex items-center pl-6 pr-6`"
           v-for="(item, index) in stageList" :key="index">

+ 122 - 60
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/business/index.vue

@@ -28,7 +28,8 @@
               <!-- <el-select v-model="businessOpportunityForm.inchargerId" placeholder="请选择" clearable>
                 <el-option v-for="item in fixedData.Personnel" :key="item.id" :label="item.name" :value="item.id" />
               </el-select> -->
-              <personnel-search v-model="businessOpportunityForm.inchargerId" :size="''" placeholder="请选择"></personnel-search>
+              <personnel-search v-model="businessOpportunityForm.inchargerId" :size="''"
+                placeholder="请选择"></personnel-search>
             </el-form-item>
             <el-form-item label="创建时间">
               <el-date-picker v-model="businessOpportunityForm.startTime" type="date" placeholder="请选择"
@@ -42,61 +43,80 @@
         </div>
         <div class="w-full flex p-3 shadow-[0_-3px_5px_0px_rgba(0,0,0,0.2)]">
           <El-button class="w-full" @click="resetForm()">重置</El-Button>
-          <El-button type="primary" class="w-full" @click="getBusinessTableList()">搜索</El-Button>
+          <El-button type="primary" class="w-full" @click="searchForBusinessOpportunities()">搜索</El-Button>
         </div>
       </div>
     </div>
     <div class="flex-1 p-5 overflow-auto">
       <div class="bg-white w-full h-full p-3 shadow-md rounded-md flex flex-col">
-        <div class="flex justify-end pb-3">
-          <el-button v-permission="['businessAddAnEdit']" type="primary"
-            @click="editNewBusiness(false)">新建商机</el-button>
-          <el-button type="primary" @click="showVisible('batchTransferVisible')"
-            :disabled="batchTableData.length <= 0">批量转移</el-button>
-          <el-button type="primary" v-permission="['businessDelete']" @click="batchDeteleItem()"
-            :disabled="batchTableData.length <= 0">批量删除</el-button>
-          <el-button type="primary" @click="showVisible('stageSetVisible')">阶段设置</el-button>
-          <el-button type="primary" v-permission="['businessRecycle']"
-            @click="showVisible('deteleBusinessVisible')">回收站</el-button>
-          <el-button v-permission="['businessImport']" type="primary"
-            @click="showVisible('importVisible')">导入</el-button>
-          <el-button v-permission="['businessExport']" type="primary" @click="exportBusinessTableList()"
-            :loading="allLoading.exoprtLoading">导出</el-button>
+        <div class="flex justify-between pb-3">
+          <div>
+            <el-radio-group v-model="layoutSingleChoice" @change="viewsSwitching">
+              <el-radio-button label="看板视图" :value="KANBAN_VIEW" />
+              <el-radio-button label="表格视图" :value="TABLE_VIEW" />
+            </el-radio-group>
+          </div>
+          <div class="justify-end flex">
+            <el-button v-permission="['businessAddAnEdit']" type="primary"
+              @click="editNewBusiness(false)">新建商机</el-button>
+            <el-button type="primary" @click="showVisible('batchTransferVisible')"
+              :disabled="batchTableData.length <= 0">批量转移</el-button>
+            <el-button type="primary" v-permission="['businessDelete']" @click="batchDeteleItem()"
+              :disabled="batchTableData.length <= 0">批量删除</el-button>
+            <el-button type="primary" @click="showVisible('stageSetVisible')">阶段设置</el-button>
+            <el-button type="primary" v-permission="['businessRecycle']"
+              @click="showVisible('deteleBusinessVisible')">回收站</el-button>
+            <el-button v-permission="['businessImport']" type="primary"
+              @click="showVisible('importVisible')">导入</el-button>
+            <el-button v-permission="['businessExport']" type="primary" @click="exportBusinessTableList()"
+              :loading="allLoading.exoprtLoading">导出</el-button>
+          </div>
         </div>
-        <div class="flex-1 w-full overflow-hidden">
-          <el-table ref="businessTableRef" :data="businessTable" border v-loading="allLoading.businessTableLading"
-            :show-overflow-tooltip="tableShowOverflowTooltip" @selection-change="changeBatch"
-            style="width: 100%;height: 100%;">
-            <el-table-column type="selection" width="55" />
-            <el-table-column v-for="(item, index) in tableColumn" :prop="item.prop" :label="item.label" :key="index"
-              :width="item.width">
-              <template #default="scope">
-                <div class="table-text-textnowrap" v-if="item.eventName"
-                  @click="dealWithTableColumn(scope.row, item.eventName)">{{ scope.row[item.prop] }}</div>
-                <template v-else-if="['inchargerName', 'creatorName'].includes(item.prop)">
-                  <TextTranslation translationTypes="userName" :translationValue="scope.row[item.prop]"></TextTranslation>
+        <!-- 表格视图 -->
+        <template v-if="layoutSingleChoice == TABLE_VIEW">
+          <div class="flex-1 w-full overflow-hidden">
+            <el-table ref="businessTableRef" :data="businessTable" border v-loading="allLoading.businessTableLading"
+              :show-overflow-tooltip="tableShowOverflowTooltip" @selection-change="changeBatch"
+              style="width: 100%;height: 100%;">
+              <el-table-column type="selection" width="55" />
+              <el-table-column v-for="(item, index) in tableColumn" :prop="item.prop" :label="item.label" :key="index"
+                :width="item.width">
+                <template #default="scope">
+                  <div class="table-text-textnowrap" v-if="item.eventName"
+                    @click="dealWithTableColumn(scope.row, item.eventName)">{{ scope.row[item.prop] }}</div>
+                  <template v-else-if="['inchargerName', 'creatorName'].includes(item.prop)">
+                    <TextTranslation translationTypes="userName" :translationValue="scope.row[item.prop]">
+                    </TextTranslation>
+                  </template>
+                  <template v-else>{{ scope.row[item.prop] }}</template>
                 </template>
-                <template v-else>{{ scope.row[item.prop] }}</template>
-              </template>
-            </el-table-column>
-            <el-table-column label="操作" fixed="right" width="200"
-              v-permission="['businessAddAnEdit', 'tasksAdd', 'businessDelete']">
-              <template #default="scope">
-                <el-button link type="primary" size="large" @click="editNewBusiness(scope.row)"
-                  v-permission="['businessAddAnEdit']">编辑</el-button>
-                <el-button link type="primary" size="large" @click="newTask(scope.row)"
-                  v-permission="['tasksAdd']">新建任务</el-button>
-                <el-button link type="danger" size="large" @click="businessDeteleItem(scope.row.id, scope.row.name)"
-                  v-permission="['businessDelete']">删除</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="businessOpportunityForm.pageFrom"
-            @size-change="handleSizeChange" @current-change="handleCurrentChange" :total="businessTotalTable"
-            :hide-on-single-page="true" />
-        </div>
+              </el-table-column>
+              <el-table-column label="操作" fixed="right" width="200"
+                v-permission="['businessAddAnEdit', 'tasksAdd', 'businessDelete']">
+                <template #default="scope">
+                  <el-button link type="primary" size="large" @click="editNewBusiness(scope.row)"
+                    v-permission="['businessAddAnEdit']">编辑</el-button>
+                  <el-button link type="primary" size="large" @click="newTask(scope.row)"
+                    v-permission="['tasksAdd']">新建任务</el-button>
+                  <el-button link type="danger" size="large" @click="businessDeteleItem(scope.row.id, scope.row.name)"
+                    v-permission="['businessDelete']">删除</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="businessOpportunityForm.pageFrom"
+              @size-change="handleSizeChange" @current-change="handleCurrentChange" :total="businessTotalTable"
+              :hide-on-single-page="true" />
+          </div>
+        </template>
+
+        <!-- 看板视图 --> 
+        <template v-else-if="layoutSingleChoice == KANBAN_VIEW">
+          <div class="flex-1 w-full h-full overflow-hidden">
+            <kanbanView ref="kanbanViewRef" @kanbanViewClick="kanbanViewClick" />
+          </div>
+        </template>
       </div>
     </div>
     <!-- 弹窗 -->
@@ -179,7 +199,7 @@
     <DeteleBusiness :visibles="allVisible.deteleBusinessVisible" @closeVisible="closeVisible" />
 
     <!-- 阶段设置 -->
-    <StageSetting :visibles="allVisible.stageSetVisible" @closeVisible="closeVisible" />
+    <StageSetting :visibles="allVisible.stageSetVisible" @closeVisible="closeVisible" @change="searchForBusinessOpportunities" />
   </div>
 </template>
 
@@ -187,7 +207,7 @@
 import { ref, reactive, onMounted, inject } from "vue";
 import type { ElTable, FormInstance, FormRules, UploadRequestOptions } from 'element-plus'
 import { useRouter, useRoute } from "vue-router";
-import { GETSYSFILED, MOD, GETPERSONNEL, GETGENERATEFOEM, GETBUSINESSLIST, UPDATEINSET, BUSINESSDETELE, BATCHTRANSFER, MODURL, tableColumn, BUSIESS_GETSATE, URL_IMPOERBUSINESS, BUSIESS_INFO, URL_EXPORTBUSINESS } from './api'
+import { GETSYSFILED, MOD, GETPERSONNEL, GETGENERATEFOEM, GETBUSINESSLIST, UPDATEINSET, BUSINESSDETELE, BATCHTRANSFER, MODURL, tableColumn, BUSIESS_GETSATE, URL_IMPOERBUSINESS, BUSIESS_INFO, URL_EXPORTBUSINESS, TABLE_VIEW, KANBAN_VIEW } from './api'
 import { GETTABLELIST } from '@/pages/product/api'
 import { post, get, uploadFile } from "@/utils/request";
 import { getAllListByCode, getFromValue, resetFromValue, getFirstDayOfMonth, createTaskFromType, formatDate, confirmAction, downloadTemplate, downloadFile, judgmentaAmounteEqual } from '@/utils/tools'
@@ -201,6 +221,7 @@ import DeteleBusiness from './component/deteleTables.vue'
 import StageSetting from './component/stageSetting.vue'
 import { GETTABLELISTPRODUCT } from "../order/api";
 import personnelSearch from '@/components/translationComponent/personnelSearch/personnelSearch.vue';
+import kanbanView from "./component/kanbanView.vue";
 
 const route = useRoute()
 const router = useRouter()
@@ -241,7 +262,7 @@ const allText = reactive({
 
 const taskModalForm = ref({}) // 任务弹窗表单
 const taskLoading = ref<saveLoadingType>("1");
-const batchTableData = ref([]) // 批量数据
+const batchTableData = ref<any>([]) // 批量数据
 const transferPersonnel = ref('') // 转移人
 
 const businessOpportunityForm = reactive<businessOpportunityFormType>({
@@ -263,6 +284,8 @@ const fixedData = reactive({
 })
 const productTableList = ref([])
 const productTableListValue = ref([])
+const layoutSingleChoice = ref(KANBAN_VIEW)
+const kanbanViewRef = ref<any>(null)
 
 
 function editBusiness(visibles: boolean) {
@@ -286,7 +309,7 @@ function editBusiness(visibles: boolean) {
       post(UPDATEINSET, { ...businessTemplateValue.value, ...newForm }).then((_res) => {
         allVisible.newBusinessisible = visibles
         globalPopup?.showSuccess('保存成功')
-        getBusinessTableList()
+        searchForBusinessOpportunities()
       }).finally(() => {
         allLoading.businessSaveLading = false
       })
@@ -348,7 +371,7 @@ function transferBusiness() {
     transferPersonnel.value = ''
     globalPopup?.showSuccess('转移成功')
     closeVisible('batchTransferVisible')
-    getBusinessTableList()
+    searchForBusinessOpportunities()
   }).finally(() => {
     allLoading.transferLoading = false
   })
@@ -369,7 +392,7 @@ function businessDeteleItem(value: string | number, label: string, batch: boolea
       }
       globalPopup?.showSuccess('删除成功')
       changeBatch(false)
-      getBusinessTableList()
+      searchForBusinessOpportunities()
     }).catch((err) => {
       globalPopup?.showError(err.msg)
     })
@@ -385,7 +408,7 @@ async function importBusiness(param: UploadRequestOptions) {
   })
   if (res.code == 'ok') {
     globalPopup?.showSuccess('导入成功' || '')
-    getBusinessTableList()
+    searchForBusinessOpportunities()
     return
   }
   globalPopup?.showError(res.msg || '')
@@ -401,6 +424,34 @@ function exportBusinessTableList() {
   })
 }
 
+function viewsSwitching() {
+  batchTableData.value = []
+  resetForm()
+}
+
+function kanbanViewClick(type: 'multipleChoice' | 'delete' | 'edit' | 'addTask', data: any) {
+  if(type == 'multipleChoice') {
+    multipleChoiceBoxSwitching(Array.isArray(data) ? data : [])
+  }
+
+  if(type == 'delete') {
+    const { id, name = '' } = data
+    businessDeteleItem(id, name)
+  }
+
+  if(type == 'edit') {
+    editNewBusiness(data)
+  }
+
+  if(type == 'addTask') {
+    newTask(data)
+  }
+}
+
+function multipleChoiceBoxSwitching(list: any[] = []) {
+  batchTableData.value =  Array.isArray(list) ? list : []
+}
+
 function changeBatch(flag: boolean = true) {
   if (flag) {
     batchTableData.value = businessTableRef.value && businessTableRef.value.getSelectionRows()
@@ -431,12 +482,12 @@ function editBusinessData(item: any) {
 function handleSizeChange(val: number) {
   businessOpportunityForm.pageIndex = 1
   businessOpportunityForm.pageFrom = val
-  getBusinessTableList()
+  searchForBusinessOpportunities()
 }
 
 function handleCurrentChange(val: number) {
   businessOpportunityForm.pageIndex = val
-  getBusinessTableList()
+  searchForBusinessOpportunities()
 }
 
 function showVisible(type: keyof typeof allVisible) { // 显示弹窗
@@ -451,6 +502,17 @@ function handleClose(done: () => void) {
   done()
 }
 
+function searchForBusinessOpportunities() {
+  if(layoutSingleChoice.value == TABLE_VIEW) {
+    getBusinessTableList()
+  }
+
+  if(layoutSingleChoice.value == KANBAN_VIEW) {
+    const formValue = getFromValue(businessOpportunityForm)
+    kanbanViewRef.value.searchDashboardView({...formValue})
+  }
+}
+
 function getBusinessTableList() {
   const formValue = getFromValue(businessOpportunityForm)
   allLoading.businessTableLading = true
@@ -477,7 +539,7 @@ function resetForm() {
   }
   let newBusinessOpportunityForm = resetFromValue(businessOpportunityForm, { ...reset })
   Object.assign(businessOpportunityForm, newBusinessOpportunityForm)
-  getBusinessTableList()
+  searchForBusinessOpportunities()
 }
 
 async function getSystemField() {
@@ -560,7 +622,7 @@ function getProductTableList() {
 onMounted(() => {
   getSystemField()
   getProductTableList()
-  getBusinessTableList()
+  searchForBusinessOpportunities()
 })
 </script>
 

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

@@ -38,4 +38,12 @@ interface businessTableColumnInterface {
 interface productInterface {
   value: string | number;
   label: string;
+}
+
+interface viewListInterface {
+  label: string;
+  list: any[];
+  length?: number;
+  color?: string;
+  id: string | number;
 }

+ 2 - 4
fhKeeper/formulahousekeeper/customerBuler-crm/src/styles/global.scss

@@ -23,7 +23,7 @@ $modena: #6f4afe;
 }
 
 .scroll-bar::-webkit-scrollbar-thumb {
-  background: linear-gradient(to bottom right, #c1c1c1 0%, #c1c1c1 100%);
+  background: linear-gradient(to bottom right, #DDDEE0 0%, #c1c1c1 100%);
   border-radius: 5px;
 }
 
@@ -33,9 +33,7 @@ $modena: #6f4afe;
 }
 
 .scroll-bar::-webkit-scrollbar-button {
-  background-color: #c1c1c1;
-  border-radius: 2px;
-  height: 4px;
+  height: 0px;
 }
 
 .scroll-bar::-webkit-scrollbar-button:hover {

+ 3 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/vite.config.ts

@@ -17,6 +17,9 @@ export default defineConfig({
       }
     }
   })],
+  optimizeDeps: {
+    include: ['vue-draggable-plus']
+  },
   server: {
     host: '0.0.0.0',
     port: 19123,