瀏覽代碼

提交代码

Lijy 3 月之前
父節點
當前提交
c768d0d710
共有 19 個文件被更改,包括 2649 次插入385 次删除
  1. 6 0
      fhKeeper/formulahousekeeper/customerBuler-crm/package.json
  2. 45 8
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/flowChart.vue
  3. 90 18
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/index.vue
  4. 6 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/selectDeptUser.vue
  5. 6 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/api.ts
  6. 143 14
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/cusReportForm/index.vue
  7. 421 4
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/dragEdit/index.vue
  8. 184 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/dragEdit/previewTable.vue
  9. 131 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/dragEdit/rangeFilter.vue
  10. 167 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/function.ts
  11. 1 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/index.vue
  12. 1152 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/list.ts
  13. 25 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/reportView/index.vue
  14. 22 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/type.d.ts
  15. 1 1
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/contacts/index.vue
  16. 4 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/api.ts
  17. 127 104
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/index.vue
  18. 55 0
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/taskCalendar.vue
  19. 63 234
      fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/test/index.vue

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

@@ -12,6 +12,12 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
+    "@fullcalendar/core": "^6.1.15",
+    "@fullcalendar/daygrid": "^6.1.15",
+    "@fullcalendar/interaction": "^6.1.15",
+    "@fullcalendar/list": "^6.1.15",
+    "@fullcalendar/timegrid": "^6.1.15",
+    "@fullcalendar/vue3": "^6.1.15",
     "@vue-flow/background": "^1.3.2",
     "@vue-flow/core": "^1.42.2",
     "@zmjs/form-design": "file:../plugIn/form-design-master/update",

+ 45 - 8
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/flowChart.vue

@@ -1,6 +1,6 @@
 <script lang="ts" setup>
 import { ref, onMounted } from 'vue'
-import { VueFlow, Handle, Position, type Node, Edge } from '@vue-flow/core'
+import { VueFlow, Handle, Position, useVueFlow, type Node, Edge } from '@vue-flow/core'
 import { Background } from '@vue-flow/background'
 import { post, get } from "@/utils/request";
 import { GET_STRUCT_BY_TABLE_NAME, GET_ALL_BUS_TABLE, GET_RELATE_TABLE_BY_FROM_COLUMN, GET_RELATE_BUS_TABLE_BY_FROM_TABLE } from "../api"
@@ -8,6 +8,8 @@ import { GET_STRUCT_BY_TABLE_NAME, GET_ALL_BUS_TABLE, GET_RELATE_TABLE_BY_FROM_C
 import '@vue-flow/core/dist/style.css';
 import '@vue-flow/core/dist/theme-default.css';
 
+const { fitView } = useVueFlow();
+
 interface selectedNode {
   id: string;
   data: any
@@ -15,7 +17,7 @@ interface selectedNode {
 const FixedConfiguration = {
   type: 'custom',
   style: {
-    width: '120px', padding: '5px 20px', fontSize: '14px',
+    width: '200px', padding: '5px 20px', fontSize: '14px',
     border: '1px solid #dcdfe6', borderRadius: '8px'
   },
   sourcePosition: Position.Right,
@@ -26,6 +28,7 @@ const fixedLabel = {
   selected: false, // 是否选中
 }
 const connectionType = 'step'
+const vueFlowRef = ref<InstanceType<typeof VueFlow> | null>(null);
 const nodes = ref<Node[]>([])
 const edges = ref<Edge[]>([])
 const clickedNodes = ref<selectedNode[]>([]) // 记录点击的节点
@@ -75,7 +78,12 @@ async function onNodeClick(event: any) {
     // console.log(eventData.id, '<==== id')
     // if(eventData.id) {
     const addData = await post(GET_RELATE_BUS_TABLE_BY_FROM_TABLE, { tableName: eventData.tblName })
-    const fieldList = addData.data || []
+    const fieldList = (addData.data || []).map((item: any) => {
+      return {
+        ...item,
+        newFromColBusName: eventData.label
+      }
+    })
     AddData(fieldList, x, y, nodeId)
     // }
     
@@ -166,8 +174,8 @@ function AddData(list: any[], x: number, y: number, nodeId: string) {
 
     newNodes.push({
       id: newNodeId,
-      data: { label: item.fromColBusName, tblName: item.toTbl, ...item, ...fixedLabel, },
-      position: { x: x + 240, y: newY },
+      data: { label: item.fromColBusName, newLabel: `${item.newFromColBusName}-${item.fromColBusName}`, tblName: item.toTbl, ...item, ...fixedLabel, },
+      position: { x: x + 300, y: newY },
       ...FixedConfiguration,
     })
 
@@ -236,6 +244,23 @@ function initData(row: any) {
   selectNodes.value = []
 }
 
+// 获取数据
+function exportGetData() {
+  return {
+    nodes: nodes.value || [],
+    edges: edges.value || [],
+    clickedNodes: clickedNodes.value || [],
+    selectNodes: selectNodes.value || [],
+  }
+}
+// 回显数据
+function displayBackData(row: any) {
+  nodes.value = row.nodes
+  edges.value = row.edges
+  clickedNodes.value = row.clickedNodes
+  selectNodes.value = row.selectNodes
+}
+
 onMounted(() => {
   getAllBusTable()
 })
@@ -243,6 +268,8 @@ onMounted(() => {
 // 向外暴露方法
 defineExpose({
   initData,
+  exportGetData,
+  displayBackData
 });
 </script>
 
@@ -254,14 +281,14 @@ defineExpose({
         <el-option v-for="item in businessTableList" :key="item.value" :label="item.label" :value="item.value" />
       </el-select>
     </div> -->
-    <VueFlow :nodes="nodes" :edges="edges" @node-click="onNodeClick" fit-view-on-init :edge-updater-layer="true">
+    <VueFlow ref="vueFlowRef" :nodes="nodes" :edges="edges" @node-click="onNodeClick" fit-view-on-init :edge-updater-layer="true" :nodeDraggable="() => false">
       <template #node-custom="{ data, id }">
-        <div class="flex flex-row">
+        <div class="flex flex-row" @mousedown.stop.prevent>
           <Handle class="handle left-handle" type="target" :position="Position.Left" :id="`${id}`" />
           <div class="flex items-center">
             <el-checkbox v-model="data.selected" size="large" @change="processSelectedNodes(id)" @click.stop
               class="pr-2 tops"></el-checkbox>
-            <span :class="{ 'text-white': data.selected }">{{ data.label }}</span>
+            <span :class="{ 'text-white': data.selected }">{{ data.newLabel || data.label }}</span>
           </div>
           <Handle class="handle right-handle" type="source" :position="Position.Right" :id="`${id}`" />
         </div>
@@ -285,4 +312,14 @@ defineExpose({
   z-index: 9999 !important;
   /* 提高连线层级 */
 }
+
+::deep(.vue-flow__node-custom) {
+  pointer-events: none !important;
+}
+
+::deep(.vue-flow__node-custom .el-checkbox),
+::deep(.vue-flow__node-custom .el-checkbox *),
+::deep(.vue-flow__node-custom span) {
+  pointer-events: auto !important;
+}
 </style>

+ 90 - 18
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/index.vue

@@ -1,24 +1,26 @@
 <script lang="ts" setup>
 import { ref, reactive, onMounted, inject, nextTick } from "vue";
 import type { ComponentSize, FormInstance, FormRules } from 'element-plus'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import { post, get } from "@/utils/request";
 import { GET_ALL_STORES_TREE, GET_ALL_BUS_TABLE } from "../api"
 
 import flowChart from './flowChart.vue'
 import selectDeptUser from "./selectDeptUser.vue";
+import { number } from "echarts";
 
 const router = useRouter()
+const route = useRoute()
 
 const globalPopup = inject<GlobalPopup>('globalPopup')
-const formVal = ref({
+const formVal = ref<addEditReportFormVal>({
   reportFormName: '',
   privilege: false,
-  storeType: '',
+  parentStoreId: '',
   description: '',
   deptAccessList: [],
   userAccessList: [],
-  BusinessObject: []
+  businessObject: []
 })
 
 const allVisable = reactive({
@@ -32,8 +34,8 @@ const selectDeptUserRef = ref<InstanceType<typeof selectDeptUser> | null>(null);
 const flowChartRef = ref<InstanceType<typeof flowChart> | null>()
 const visibleRangeData = ref<any>([])
 
-function openReport(val: boolean) {
-  if(val) {
+function openReport(val: string | number | boolean) {
+  if (val) {
     visibleRangeData.value = []
   }
 }
@@ -68,7 +70,11 @@ function businessObjectSelectChange(val: any) {
 }
 
 function showTreeDeptUserVis() {
+  const list = (visibleRangeData.value || []).map((item: any) => item.id)
   allVisable.treeSelectUserVisable = true
+  setTimeout(() => {
+    selectDeptUserRef.value?.setTreeData(list)
+  }, 200)
 }
 
 function getTreeClassification() {
@@ -90,56 +96,122 @@ function getAllBusTable() {
 }
 
 function dragAndDropEditing() {
-  router.push('/biReport/dragEdit')
+  const mapData = flowChartRef.value?.exportGetData()
+  const { reportFormName = '', privilege } = formVal.value
+  if(!reportFormName) {
+    globalPopup?.showWarning('请输入名称')
+    return
+  }
+  if (!visibleRangeData.value.length && !privilege) {
+    globalPopup?.showWarning('请选择可见范围')
+    return
+  }
+  if (!mapData?.selectNodes.length) {
+    globalPopup?.showWarning('请先选择业务对象')
+    return
+  }
+
+  sessionStorage.setItem('reportJson', JSON.stringify({
+    addFormVal: {
+      ...formVal.value,
+      visibleRangeData: visibleRangeData.value
+    },
+    mindMapJSON: mapData
+  }))
+  router.push({
+    path: '/biReport/dragEdit',
+  })
+}
+
+// 回显数据
+function initializedData() {
+  const reportJson = JSON.parse(sessionStorage.getItem('reportJson') || '{}')
+  if(!Object.keys(reportJson).length) {
+    return
+  }
+  console.log('reportJson', reportJson)
+  const { addFormVal = {}, mindMapJSON = {} } = reportJson
+  visibleRangeData.value = addFormVal.visibleRangeData || []
+  formVal.value = addFormVal || {}
+  setTimeout(() => {
+    flowChartRef.value?.displayBackData(mindMapJSON)
+  }, 500)
 }
 
 onMounted(() => {
+  if(route?.query?.allParentStoreId) {
+    formVal.value.parentStoreId = Number(route.query.allParentStoreId)
+  }
   getTreeClassification()
   getAllBusTable()
+  initializedData()
 })
 </script>
 
 <template>
   <div class="w-full h-full flex flex-col bg-white rounded-md">
-    <div class="p-5 text-[18px] border-b-2">新建报表</div>
+    <div class="p-5 text-[18px] border-b-2">
+      {{ formVal.id ? '编辑报表' : '新建报表' }}
+    </div>
     <div class="flex-1 py-5 px-16 flex-col flex h-[90%]">
       <div class="flex-1 h-full overflow-auto mb-8 scroll-bar">
-        <el-form style="max-width: 600px" :model="formVal" label-width="auto">
-          <el-form-item label="名称">
+        <el-form style="max-width: 600px" :model="formVal" label-width="80px">
+          <el-form-item class="relative">
+            <template #label>
+              <div class="relative">
+                名称
+                <div class="absolute left-[-10px] top-[2px] text-[red]">*</div>
+              </div>
+            </template>
             <el-input v-model="formVal.reportFormName" placeholder="请输入" />
           </el-form-item>
           <el-form-item label="分类">
-            <el-tree-select v-model="formVal.storeType" :data="treeSelectData" check-strictly
-              :render-after-expand="false" show-checkbox style="width: 100%" :props="{
+            <el-tree-select v-model="formVal.parentStoreId" :data="treeSelectData" check-strictly
+            :render-after-expand="false" style="width: 100%" :props="{
                 label: 'storeName', value: 'id', children: 'childStoreList'
-              }" />
+              }" clearable />
           </el-form-item>
           <el-form-item label="描述">
             <el-input v-model="formVal.description" :rows="2" type="textarea" placeholder="请输入" />
           </el-form-item>
           <el-form-item label="可见范围">
+            <template #label>
+              <div class="relative">
+                可见范围
+                <div class="absolute left-[-10px] top-[2px] text-[red]">*</div>
+              </div>
+            </template>
             <div class="flex items-center w-full">
-              <el-input placeholder="+选择可见部门和人员" readonly :disabled="formVal.privilege" @click="showTreeDeptUserVis()" />
+              <el-input placeholder="+选择可见部门和人员" readonly :disabled="formVal.privilege"
+                @click="showTreeDeptUserVis()" />
               <el-checkbox v-model="formVal.privilege" label="公开" size="large" class="ml-4" @change="openReport" />
             </div>
           </el-form-item>
           <el-form-item label=" " v-if="visibleRangeData.length">
             <div class="flex flex-wrap w-full">
               <template v-for="(item, index) in visibleRangeData">
-                <el-tag :type="`${item.isUser ? 'warning' : 'primary'}`" closable class="mr-2 mb-2"  @close="visibilityClose(index)">
-                  <TextTranslation :translationTypes="`${item.isUser ? 'userName' : 'departmentName'}`" :translationValue="item.label"></TextTranslation>
+                <el-tag :type="`${item.isUser ? 'warning' : 'primary'}`" closable class="mr-2 mb-2"
+                  @close="visibilityClose(index)">
+                  <TextTranslation :translationTypes="`${item.isUser ? 'userName' : 'departmentName'}`"
+                    :translationValue="item.label"></TextTranslation>
                 </el-tag>
               </template>
             </div>
           </el-form-item>
           <el-form-item label="业务对象">
-            <el-select v-model="formVal.BusinessObject" placeholder="请选择" @change="businessObjectSelectChange">
+            <template #label>
+              <div class="relative">
+                业务对象
+                <div class="absolute left-[-10px] top-[2px] text-[red]">*</div>
+              </div>
+            </template>
+            <el-select v-model="formVal.businessObject" placeholder="请选择" @change="businessObjectSelectChange">
               <el-option v-for="item in businessTableList" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
           </el-form-item>
         </el-form>
 
-        <div class="h-[500px] border mr-6" v-if="formVal.BusinessObject">
+        <div class="h-[500px] border mr-6" v-if="formVal.businessObject">
           <flowChart ref="flowChartRef"></flowChart>
         </div>
       </div>

+ 6 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/addEdit/selectDeptUser.vue

@@ -47,9 +47,15 @@ function getSelectData() {
   return treeRef.value!.getCheckedNodes()
 }
 
+// 设置数据
+function setTreeData(list: (number | string)[]) {
+  treeRef.value!.setCheckedKeys(list)
+}
+
 // 向外暴露方法
 defineExpose({
   getSelectData,
+  setTreeData
 });
 </script>
 

+ 6 - 1
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/api.ts

@@ -6,4 +6,9 @@ export const GET_ALL_STORES_TREE = `/tableColumn/getAllStoresTree` // 获取所
 export const GET_ALL_BUS_TABLE = `/tableColumn/getAllBusTable` // 获取所有业务表
 export const GET_STRUCT_BY_TABLE_NAME = `/tableColumn/getStructByTableName` // 查询表字段
 export const GET_RELATE_TABLE_BY_FROM_COLUMN = `/tableColumn/getRelateTableByFromColumn` // 通过来源字段获取关联表
-export const GET_RELATE_BUS_TABLE_BY_FROM_TABLE = `/tableColumn/getRelateBusTableByFromTable` // 根据来源表名获取关联表
+export const GET_RELATE_BUS_TABLE_BY_FROM_TABLE = `/tableColumn/getRelateBusTableByFromTable` // 根据来源表名获取关联表
+export const GET_RES_BY_FORM_JSON = `/tableColumn/getResByFormJson` // 根据表单json获取预览结果
+export const ADD_OR_UPDATE_REPORT_FORM = `/tableColumn/addOrUpdateReportForm` // 新增或修改报表数据
+export const MOVE_FORM_STORE = `/tableColumn/moveFormStore` // 移动报表至文件夹
+export const GET_FORM_JSON_BY_FORM_ID = `/tableColumn/getFormJsonByFormId` // 根据表单id获取表单json
+export const EXPORT_CUS_REPORT_FORM = `/tableColumn/exportCusReportForm` // 导出报表

+ 143 - 14
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/cusReportForm/index.vue

@@ -6,7 +6,8 @@ import type { FormInstance } from 'element-plus'
 import { ElMessage, ElMessageBox } from "element-plus";
 import { ArrowRight } from '@element-plus/icons-vue'
 import { post } from "@/utils/request";
-import { GET_FORM_STORE_PAGE, ADD_OR_UPDATE_FORM_STORE, DELETE_REPORT_FORM, DELETE_FORM_STORE } from "../api"
+import { GET_FORM_STORE_PAGE, ADD_OR_UPDATE_FORM_STORE, DELETE_REPORT_FORM, DELETE_FORM_STORE, GET_ALL_STORES_TREE, MOVE_FORM_STORE, GET_FORM_JSON_BY_FORM_ID, EXPORT_CUS_REPORT_FORM } from "../api"
+import { downloadFile } from "@/utils/tools";
 
 const router = useRouter()
 
@@ -21,17 +22,71 @@ const classificationForm = ref({
   description: "",
   parentStoreId: 0,
 });
+const moveForm = ref({
+  targetStoreId: '',
+  formId: ''
+})
 const paging = ref({
   pageIndex: 1,
-  pageSize: 10,
+  pageSize: 300,
 });
 const pagingTotal = ref(0);
 const allVisible = reactive({
   addEditingCategoryVisable: false,
+  moveVisable: false
 });
 const allLoading = reactive({
   addEditingCategoryLoading: false,
+  moveLoading: false
 });
+const treeSelectData = ref([])
+
+function previewReport(row: any) {
+  if(!row.relateFormId) {
+    return
+  }
+  post(GET_FORM_JSON_BY_FORM_ID, { formId: row.relateFormId }).then(res => {
+    const json = JSON.parse(res.data)
+    sessionStorage.setItem('reportViewJson', JSON.stringify({ 
+      SQL: row.formSql,
+      rangeFilterData: json.rangeFilterData,
+      tableData: json.tableData
+    }))
+    router.push('/biReport/reportView')
+  })
+}
+
+function edit(row: any) {
+  if(!row.relateFormId) {
+    return
+  }
+  post(GET_FORM_JSON_BY_FORM_ID, { formId: row.relateFormId }).then(res => {
+    const json = JSON.parse(res.data)
+    json.addEditData.addFormVal.id = row.relateFormId
+    sessionStorage.setItem('reportJson', JSON.stringify(json.addEditData))
+    sessionStorage.setItem('editReportJson', res.data)
+    newReport(true)
+  })
+}
+
+function moveSubmit() {
+  allLoading.moveLoading = true
+  post(MOVE_FORM_STORE, { ...moveForm.value }).then(_res => {
+    ElMessage.success('操作成功')
+    allVisible.moveVisable = false
+    getTableList()
+  }).finally(() => {
+    allLoading.moveLoading = false
+  })
+}
+
+function move(row: any) {
+  moveForm.value =  {
+    targetStoreId: '',
+    formId: row.relateFormId
+  }
+  allVisible.moveVisable = true
+}
 
 function goToTheNextLevel(row: any) {
   allParentStoreId.value = row.id
@@ -125,6 +180,9 @@ function addEditingCategory(row: any = {}) {
       parentStoreId: 0
     }
   }
+  setTimeout(() => {
+    classificationFormRef.value?.resetFields()
+  }, 100)
   allVisible.addEditingCategoryVisable = true;
 }
 
@@ -139,18 +197,46 @@ function handleCurrentChange(val: number) {
   paging.value.pageIndex = val;
 }
 
+function exportReport(row: any) {
+  const { storeName } = row
+  post(EXPORT_CUS_REPORT_FORM, { formId: row.relateFormId }).then((res) => {
+    downloadFile(res.data, `${storeName}.xlsx`)
+  })
+}
+
 function getTableList() {
   post(GET_FORM_STORE_PAGE, { ...paging.value, parentStoreId: allParentStoreId.value }).then(res => {
     tableList.value = res.data.records || []
   })
 }
 
-function newReport() {
-  router.push('/biReport/addEdit')
+function newReport(flag = false) {
+  let path: any = {
+    path: '/biReport/addEdit'
+  }
+  if(!flag) {
+    sessionStorage.removeItem('reportViewJson')
+    sessionStorage.removeItem('reportJson')
+    sessionStorage.removeItem('editReportJson')
+    if(allParentStoreId.value) {
+      path.query = {
+        allParentStoreId: allParentStoreId.value
+      }
+    }
+  }
+
+  router.push(path)
+}
+
+function getTreeClassification() {
+  post(GET_ALL_STORES_TREE, {}).then((res) => {
+    treeSelectData.value = res.data || []
+  })
 }
 
 onMounted(() => {
   getTableList()
+  getTreeClassification()
 })
 </script>
 
@@ -171,7 +257,7 @@ onMounted(() => {
     </div>
     <div class="flex-1 px-5">
       <el-table :data="tableList" border style="width: 100%; height: 100%">
-        <el-table-column prop="storeName" label="名称">
+        <el-table-column prop="storeName" label="名称" width="200px">
           <template #default="scope">
             <div class="flex items-center">
               <template v-if="scope.row.storeType == 1">
@@ -180,7 +266,7 @@ onMounted(() => {
               </template>
               <template v-else>
                 <el-icon size="18" color="#409eff" class="mr-2"><Document /></el-icon>
-                <el-link type="primary" :underline="false">{{ scope.row.storeName }}</el-link>
+                <el-link type="primary" :underline="false" @click="previewReport(scope.row)">{{ scope.row.storeName }}</el-link>
               </template>
             </div>
           </template>
@@ -190,13 +276,37 @@ onMounted(() => {
             <TextTranslation translationTypes="userName" :translationValue="scope.row.createName"></TextTranslation>
           </template>
         </el-table-column>
-        <el-table-column prop="storeName" label="权限" />
-        <el-table-column prop="storeName" label="可见人" />
-        <el-table-column prop="storeName" label="可见部门" />
+        <el-table-column prop="storeName" label="权限">
+          <template #default="scope">
+            {{ scope.row.privilege == 1 ? '公开' : '隐私' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="storeName" label="可见人员">
+          <template #default="scope">
+            <template v-if="scope.row?.userAccessList && scope.row?.userAccessList.length">
+              <template v-for="(item, index) in scope.row.userAccessList">
+                <TextTranslation translationTypes="userName" :translationValue="item.finalName" />
+                <template v-if="index < (scope.row.userAccessList || []).length - 1">,</template>
+              </template>
+            </template>
+            <template v-else>--</template>
+          </template>
+        </el-table-column>
+        <el-table-column prop="storeName" label="可见部门">
+          <template #default="scope">
+            <template v-if="scope.row?.deptAccessList && scope.row?.deptAccessList.length">
+              <template v-for="(item, index) in scope.row.deptAccessList">
+                <TextTranslation translationTypes="departmentName" :translationValue="item.finalName" />
+                <template v-if="index < (scope.row.deptAccessList || []).length - 1">,</template>
+              </template>
+            </template>
+            <template v-else>--</template>
+          </template>
+        </el-table-column>
         <el-table-column prop="createTime" label="创建时间" width="180" />
         <el-table-column prop="updateName" label="修改人">
           <template #default="scope">
-            <TextTranslation translationTypes="userName" :translationValue="scope.row.createName"></TextTranslation>
+            <TextTranslation translationTypes="userName" :translationValue="scope.row.updateName"></TextTranslation>
           </template>
         </el-table-column>
         <el-table-column prop="updateTime" label="修改时间" width="180" />
@@ -210,16 +320,16 @@ onMounted(() => {
               <template #dropdown>
                 <el-dropdown-menu>
                   <el-dropdown-item v-if="scope.row.storeType == 2">
-                    <el-text class="mx-1" type="primary">编辑</el-text>
+                    <el-text class="mx-1" type="primary" @click="edit(scope.row)">编辑</el-text>
                   </el-dropdown-item>
-                  <el-dropdown-item>
+                  <el-dropdown-item v-if="scope.row.storeType == 1">
                     <el-text class="mx-1" type="primary" @click="rename(scope.row)">重命名</el-text>
                   </el-dropdown-item>
                   <el-dropdown-item v-if="scope.row.storeType == 2">
-                    <el-text class="mx-1" type="primary">导出</el-text>
+                    <el-text class="mx-1" type="primary" @click="exportReport(scope.row)">导出</el-text>
                   </el-dropdown-item>
                   <el-dropdown-item v-if="scope.row.storeType == 2">
-                    <el-text class="mx-1" type="primary">移动</el-text>
+                    <el-text class="mx-1" type="primary" @click="move(scope.row)">移动</el-text>
                   </el-dropdown-item>
                   <el-dropdown-item>
                     <el-text class="mx-1" type="danger" @click="deleteTableItem(scope.row)">删除</el-text>
@@ -262,6 +372,25 @@ onMounted(() => {
         </el-form>
       </div>
     </el-dialog>
+
+    <!-- 移动弹窗 -->
+    <el-dialog v-model="allVisible.moveVisable" 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">移动报表</h4>
+          <div>
+            <el-button type="primary" v-loading="allLoading.moveLoading" :disabled="!moveForm.targetStoreId" @click="moveSubmit()">确定</el-button>
+            <el-button @click="allVisible.moveVisable = false">取消</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="scroll-bar m-6">
+        <el-tree-select v-model="moveForm.targetStoreId" :data="treeSelectData" check-strictly
+            :render-after-expand="false" style="width: 100%" :props="{
+                label: 'storeName', value: 'id', children: 'childStoreList'
+              }" />
+      </div>
+    </el-dialog>
   </div>
 </template>
 

+ 421 - 4
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/dragEdit/index.vue

@@ -1,14 +1,431 @@
 <script lang="ts" setup>
-import { ref, onMounted, watch } from "vue";
+import { ref, onMounted, reactive, watch, inject } from "vue";
+import { useRouter, useRoute } from 'vue-router'
+import { post, get } from "@/utils/request";
+import { GET_STRUCT_BY_TABLE_NAME, ADD_OR_UPDATE_REPORT_FORM } from "../api"
+import { generateSql, rangeGetSql } from "../function"
+import { ElMessage, ElMessageBox } from 'element-plus'
 
-onMounted(() => {})
+import { VueDraggable } from 'vue-draggable-plus'
+
+import rangeFilter from "./rangeFilter.vue";
+import previewTable from "./previewTable.vue";
+import { confirmAction } from "@/utils/tools";
+
+interface collapseItemList {
+  busObject: any,
+  columnList: any[],
+}
+
+interface CollapseItem {
+  data: {
+    label: string;
+  };
+  list: collapseItemList;
+}
+
+const globalPopup = inject<GlobalPopup>('globalPopup')
+const router = useRouter()
+const route = useRoute()
+const reportName = ref('')
+const addEditData = JSON.parse(sessionStorage.getItem('reportJson') || '{}')
+const activeNames = ref([])
+const collapseList = ref<CollapseItem[]>([])
+
+// 拖拽表格参数
+const tableData = ref<any>([]);
+const tableColumns = ref<any>([{}, {}, {}]);
+
+// 全部Loading的Visable
+const allLoading = reactive({
+  collapseListLoading: false,
+  saveLoading: false
+})
+const allVisable = reactive({
+  previewTableVisable: false
+})
+
+// ref
+const rangeFilterRef = ref<InstanceType<typeof rangeFilter> | null>(null);
+const previewTableRef = ref<InstanceType<typeof previewTable> | null>(null);
+
+// 保存预览的判断以及生成
+function preMethod(): any {
+  const rangeFilterData = rangeFilterRef.value?.getRangeData()
+  if(!tableData.value.length) {
+    ElMessage.warning('请选择列')
+    return
+  }
+  if(rangeFilterData.length) {
+    for(let i in rangeFilterData) {
+      if(!rangeFilterData[i].twoDisable && !rangeFilterData[i].filterValueTwo) {
+        ElMessage.warning('请选择第二个条件')
+        return
+      }
+    }
+  }
+
+  const tabeName = tableData.value.map((item: any) => item.tableName)
+  const tableNames = addEditData.mindMapJSON.selectNodes[0].data.tblName
+  if(!tabeName.includes(tableNames)) {
+    ElMessage.warning('请选择主表数据')
+    return
+  }
+
+  const list = rangeFilterData.filter((item: any) => item.filterValueThree)
+  const SQL = generateSql(tableData.value, rangeFilterData, addEditData)
+  const rangeSQL = rangeGetSql(rangeFilterData, addEditData)
+  const finalSql = `${SQL} ${list.length ? rangeSQL : 'where 1=1'}`
+
+  return {
+    finalSql,
+    formTransConditionJson: '',
+  }
+}
+
+// 保存页面
+function saveReport() {
+  const rangeFilterData = rangeFilterRef.value?.getRangeData()
+  
+  const { finalSql } = preMethod()
+
+  // 需要转存的 JSON
+  const formJson = {
+    addEditData,
+    tableData: tableData.value,
+    rangeFilterData: rangeFilterData,
+  }
+
+  const formFieldHead = tableData.value.map((item: any) => {
+    return {
+      tableName: item.tableName,
+      columnName: item.columnName,
+      columnVal: item.columnComment
+    }
+  })
+
+  const { reportFormName, description, userAccessList = [], visibleRangeData = [], parentStoreId, privilege, id } = addEditData.addFormVal
+
+  const formVal: any = {
+    reportFormName,
+    description,
+    parentStoreId: parentStoreId ? parentStoreId : 0,
+    privilege: privilege ? 1 : 2,
+    executeSql: finalSql,
+    userIds: visibleRangeData.filter((item: any) => item.isUser).map((item: any) => item.id).join(','),
+    departmentIds: visibleRangeData.filter((item: any) => !item.isUser).map((item: any) => item.id).join(','),
+    formJson: JSON.stringify(formJson),
+    formFieldHead: JSON.stringify(formFieldHead),
+  }
+
+  if(id) {
+    formVal.id = id
+  }
+  
+  allLoading.saveLoading = true
+  post(ADD_OR_UPDATE_REPORT_FORM, { ...formVal }).then(_res => {
+    globalPopup?.showSuccess('保存成功')
+    sessionStorage.removeItem('reportJson');
+    sessionStorage.removeItem('editReportJson');
+    router.go(-2)
+  }).finally(() => {
+    allLoading.saveLoading = false
+  })
+}
+
+// 预览页面
+function preview() {
+  const rangeFilterData = rangeFilterRef.value?.getRangeData()
+
+  const { finalSql } = preMethod()
+
+  allVisable.previewTableVisable = true
+  setTimeout(() => {
+    previewTableRef.value?.initializedData(finalSql, rangeFilterData, tableData.value)
+  }, 100)
+}
+
+// 关闭页面
+function closePage() {
+  ElMessageBox.confirm('要保存当前修改吗', '', {
+    distinguishCancelAndClose: true,
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(() => {
+    saveReport()
+  }).catch((action) => {
+    if(action === 'cancel') {
+      sessionStorage.removeItem('reportJson');
+      sessionStorage.removeItem('editReportJson');
+      router.go(-2)
+    }
+  })
+}
+
+// 分组
+function grouping(index: number) {
+  const val = tableData.value[index]?.singleColumnOperation?.grouping
+  if(!tableData.value[index].singleColumnOperation) {
+    tableData.value[index].singleColumnOperation = {}
+    tableData.value[index].singleColumnOperation.grouping = val ? false : true
+  } else {
+    tableData.value[index].singleColumnOperation.grouping = val ? false : true
+  }
+}
+
+// 统计
+function statistics(index: number) {
+  const val = tableData.value[index]?.singleColumnOperation?.statistics
+  if(!tableData.value[index].singleColumnOperation) {
+    tableData.value[index].singleColumnOperation = {}
+    tableData.value[index].singleColumnOperation.statistics = val ? false : true
+  } else {
+    tableData.value[index].singleColumnOperation.statistics = val ? false : true
+  }
+}
+
+// 修改名称
+function changeName(index: number) {
+  ElMessageBox.prompt('修改名称', '', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    inputPattern: /^.+$/,
+    inputErrorMessage: '请输入',
+  }).then(({ value }) => {
+    tableData.value[index].columnComment = value
+  })
+}
+
+// 删除列
+function deleteColumn(index: number) {
+  tableData.value.splice(index, 1);
+}
+
+// 添加列
+function addTable(row: any) {
+  const { tableName = '', columnName = '', dictCode = '', columnComment = '' } = row.data
+  const index = row.newIndex;
+  console.log(tableData.value, '<====== tableData.value')
+  if (tableData.value.filter((item: any) => `${item.tableName}.${item.columnName}.${item.dictCode ? item.dictCode : ''}` === `${tableName}.${columnName}.${dictCode}`).length >= 2) {
+    ElMessage.warning('该列已存在')
+    tableData.value.splice(index, 1);
+    return
+  }
+
+  if(tableData.value.filter((item: any) => item.columnComment === columnComment).length >= 2) {
+    ElMessage.warning(`【${columnComment}】列名称已存在、请先修改列名称`)
+    tableData.value.splice(index, 1);
+    return
+  }
+}
+
+function listOnClone(clonedItem: any) {
+  clonedItem.prop = `col_${Date.now()}`; // 赋予唯一 key
+}
+
+async function getCollapseListData() {
+  const { mindMapJSON = {} } = addEditData
+  const selectNodes = mindMapJSON?.selectNodes
+  allLoading.collapseListLoading = true
+  for (let i in selectNodes) {
+    console.log(selectNodes, '<===== selectNodes')
+    const { dictCode = '', tblName = '' } = selectNodes[i].data
+    const res = await post(GET_STRUCT_BY_TABLE_NAME, { tableName: tblName })
+
+    res.data?.columnList.forEach((item: any) => {
+      item.dictCode = dictCode
+    });
+
+    selectNodes[i].list = res.data
+  }
+  collapseList.value = selectNodes
+  allLoading.collapseListLoading = false
+}
+
+function goBack() {
+  router.go(-1)
+}
+
+function displayBackData() {
+  const json = JSON.parse(sessionStorage.getItem('editReportJson') || '{}')
+  const { rangeFilterData = [], tableData: tableDataList = [] } = json
+  if(rangeFilterData.length) {
+    setTimeout(() => {
+      rangeFilterRef.value?.setRangeData(rangeFilterData, false)
+    }, 300)
+  }
+  if(tableDataList.length) {
+    setTimeout(() => {
+      tableData.value = tableDataList
+    }, 300)
+  }
+}
+
+onMounted(() => {
+  console.log(addEditData)
+  const { reportFormName } = addEditData.addFormVal
+  reportName.value = reportFormName
+  getCollapseListData()
+  displayBackData()
+  setTimeout(() => {
+    rangeFilterRef.value?.setIsItAnEditor()
+  }, 100)
+})
 </script>
 
 <template>
-  <div>
-    111222333
+  <div class="dragEdit flex flex-row h-full bg-white rounded-md w-full">
+    <div class="w-[15%] h-full overflow-auto scroll-bar p-4" v-loading="allLoading.collapseListLoading">
+      <el-collapse v-model="activeNames">
+        <el-collapse-item :title="item.data.newLabel || item.data.label" :name="index" v-for="(item, index) in (collapseList as any)" :key="index">
+          <VueDraggable v-model="item.list.columnList" :animation="150"
+            :group="{ name: 'people', pull: 'clone', put: false }" :sort="false"
+            class="flex flex-col gap-2 p-4 w-300px bg-gray-500/5 rounded" @clone="listOnClone">
+            <div v-for="listItem in item.list.columnList" class="cursor-all-scroll px-3 py-2 bg-white">
+              {{ listItem.columnComment }}
+            </div>
+          </VueDraggable>
+        </el-collapse-item>
+      </el-collapse>
+    </div>
+    <div class="w-[85%] h-full">
+      <div class="h-full w-full flex flex-col p-3 box-border">
+        <div class="dragEdit-header">
+          <div class="flex justify-between">
+            <div><el-button @click="goBack()">返回上一级</el-button></div>
+            <div class="text-[20px] font-bold">{{ reportName }}</div>
+            <div>
+              <el-button @click="closePage()">关闭</el-button>
+              <el-button @click="preview()">预览</el-button>
+              <el-button type="primary" @click="saveReport()" :loading="allLoading.saveLoading">保存</el-button>
+            </div>
+          </div>
+          <div class="range mt-3 mb-3">
+            <div class="flex items-center pb-3 font-bold text-[16px]">
+              数据范围
+            </div>
+            <rangeFilter ref="rangeFilterRef" />
+          </div>
+        </div>
+        <!-- 中间 -->
+        <div class="flex justify-between items-center mb-3">
+          <div class="flex items-center">
+            <div class="font-bold text-[16px]">报表示例</div>
+          </div>
+          <div>点击顶部预览按钮可查看全部数据</div>
+        </div>
+        <!-- 表格 -->
+        <div class="flex-1 relative border-2">
+          <div class="table-container scroll-bar">
+            <VueDraggable v-model="tableData" group="people" target=".sort-target" @add="addTable">
+              <div class="absolute top-0 left-0 text-center text-[#999] w-full text-[20px] p-3" v-if="!tableData.length">
+                <div class="border-2 border-dashed py-2">这里是表格头部区域,请将左侧字段拖入此区域生成报表</div>
+              </div>
+              <table class="table table-striped">
+                <thead class="thead-dark">
+                  <tr class="sort-target">
+                    <th class="cursor-move" v-for="(header, headerIndex) in tableData" :key="header.value">
+                      <div class="w-full flex justify-between items-center">
+                        <div>{{ header.columnComment }}</div>
+                        <el-dropdown>
+                          <span class="el-dropdown-link">
+                            <el-icon color="#075985">
+                              <InfoFilled />
+                            </el-icon>
+                          </span>
+                          <template #dropdown>
+                            <el-dropdown-menu>
+                              <el-dropdown-item v-if="['varchar', 'text'].includes(header.dataType)">
+                                <el-text :underline="false" type="primary" @click="grouping(headerIndex)">
+                                  {{ header?.singleColumnOperation?.grouping ? '取消分组' : '分组' }}
+                                </el-text>
+                              </el-dropdown-item>
+                              <el-dropdown-item v-if="['decimal'].includes(header.dataType)">
+                                <el-text :underline="false" type="primary" @click="statistics(headerIndex)">
+                                  {{ header?.singleColumnOperation?.statistics ? '取消统计' : '统计' }}
+                                </el-text>
+                              </el-dropdown-item>
+                              <el-dropdown-item>
+                                <el-text :underline="false" type="primary" @click="changeName(headerIndex)">
+                                  修改名称
+                                </el-text>
+                              </el-dropdown-item>
+                              <el-dropdown-item>
+                                <el-text :underline="false" type="danger"
+                                  @click="deleteColumn(headerIndex)">删除列</el-text>
+                              </el-dropdown-item>
+                            </el-dropdown-menu>
+                          </template>
+                        </el-dropdown>
+                      </div>
+                    </th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="item in tableColumns" :key="item.name">
+                    <td v-for="header in tableData" :key="header.value">
+                      示例数据
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </VueDraggable>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 全屏预览 -->
+    <el-dialog v-model="allVisable.previewTableVisable" fullscreen top="40vh" width="70%" draggable>
+      <previewTable ref="previewTableRef"></previewTable>
+    </el-dialog>
   </div>
 </template>
 
 <style lang="scss" scoped>
+.dragEdit {
+  .table {
+    display: table;
+    width: 100%;
+    font-size: 15px;
+    border-collapse: collapse;
+    margin: 0 0;
+    overflow-x: auto;
+  }
+
+  tr {
+    background-color: #fff;
+    border-top: 1px solid #e2e2e3;
+    transition: background-color .5s;
+  }
+
+  th {
+    text-align: left;
+    font-size: 14px;
+    font-weight: 600;
+    color: rgba(60, 60, 67, .78);
+    background-color: #f6f6f7;
+    border: 1px solid #e2e2e3;
+    padding: 8px 16px;
+    min-width: 240px;
+  }
+
+  td {
+    padding: 8px 16px;
+    font-size: 14px;
+    min-width: 200px;
+  }
+
+  .cursor-move {
+    cursor: move;
+  }
+
+  .table-container {
+    overflow-x: auto;
+    white-space: nowrap;
+    width: 100%;
+    height: 100%;
+  }
+}
 </style>

+ 184 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/dragEdit/previewTable.vue

@@ -0,0 +1,184 @@
+<script setup lang="ts">
+import { ref, provide, onMounted } from 'vue'
+import { post, get } from "@/utils/request";
+import { GET_RES_BY_FORM_JSON } from '../api';
+import type { TableColumnCtx, SummaryMethod  } from 'element-plus'
+
+import rangeFilter from "./rangeFilter.vue";
+import { rangeGetSql } from '../function';
+import { formatDate } from '@/utils/times';
+
+const rangeFilterRef = ref<InstanceType<typeof rangeFilter> | null>(null);
+
+const SQLstr = ref<string>('')
+const driginalDataTableList = ref<any[]>([])
+const addEditData = JSON.parse(sessionStorage.getItem('reportJson') || '{}')
+const tableHeader = ref<any[]>([])
+const tableData = ref<any[]>([])
+const tableLoading = ref(false)
+const dataRangeData = ref<any[]>([])
+const dataRangeDataVisable = ref(false)
+const listThatNeedsToBeGrouped = ref<number[]>([]) // 分组索引
+const listStatisticsList = ref<number[]>([]) // 统计索引
+const tableKey = ref<number>(1)
+
+// 统计
+const getSummaries: SummaryMethod<any> = (params) => {
+  const { columns, data } = params
+  const sums: (string)[] = []
+
+  columns.forEach((column, index) => {
+    if (index === 0) {
+      sums[index] = '合计'
+      return
+    }
+    const values = data.map(item => Number(item[column.property as string]))
+    if (!values.every(value => isNaN(value)) && listStatisticsList.value.includes(index)) {
+      const total = values.reduce((prev, curr) => {
+        const value = Number(curr)
+        if (!isNaN(value)) {
+          return prev + curr
+        } else {
+          return prev
+        }
+      }, 0)
+      sums[index] = total.toFixed(2) // 保证返回 string,解决类型报错
+    } else {
+      sums[index] = ''
+    }
+  })
+
+  return sums
+}
+
+// 合并分组
+function mergeColumns({ row, rowIndex, columnIndex }: { row: any, rowIndex: number, columnIndex: number }) {
+  // 只处理需要合并的列
+  if (!listThatNeedsToBeGrouped.value.includes(columnIndex)) {
+    return { rowspan: 1, colspan: 1 };
+  }
+
+  const field = tableHeader.value[columnIndex]?.label;
+  if (!field) return { rowspan: 1, colspan: 1 };
+
+  // 当前行之前的内容已经合并过,直接跳过
+  if (rowIndex > 0 && tableData.value[rowIndex - 1][field] === row[field]) {
+    return { rowspan: 0, colspan: 0 }; // 隐藏
+  }
+
+  // 计算当前单元格应该合并多少行
+  let rowspan = 1;
+  for (let i = rowIndex + 1; i < tableData.value.length; i++) {
+    if (tableData.value[i][field] === row[field]) {
+      rowspan++;
+    } else {
+      break;
+    }
+  }
+
+  return { rowspan, colspan: 1 };
+}
+
+function determinationOfDataRange() {
+  const rangeFilterData = rangeFilterRef.value?.getRangeData()
+  const rangeSQL = rangeGetSql(rangeFilterData, addEditData)
+  const sqlList = SQLstr.value.split('where')[0]
+  SQLstr.value = `${sqlList}${rangeSQL}`
+  dataRangeDataVisable.value = false
+  getData()
+}
+
+function setDataRange() {
+  dataRangeDataVisable.value = true
+  setTimeout(() => {
+    rangeFilterRef.value?.setRangeData(dataRangeData.value)
+  }, 100)
+}
+
+function getData() {
+  tableLoading.value = true
+  post(GET_RES_BY_FORM_JSON, { formJson: SQLstr.value }).then(res => {
+    tableData.value = res.data
+    setTimeout(() => {
+      tableKey.value++
+    }, 100)
+  }).finally(() => {
+    tableLoading.value = false
+  })
+} 
+
+async function initializedData(SQL: string, rangeFilterList: any[], tableData: any[]) {
+  await filterGroupStatistics(tableData)
+  
+  const heading = tableData.map(item => ({
+    filed: `${item.tableName}_${item.columnName}`,
+    label: item.columnComment,
+    type: item.dataType,
+  }))
+  
+  driginalDataTableList.value = tableData
+  tableHeader.value = heading
+  dataRangeData.value = rangeFilterList
+  SQLstr.value = SQL
+
+  console.log(tableHeader.value, '<==== tableHeader.value')
+  getData()
+}
+
+// 过滤出需要分组和统计的数组
+function filterGroupStatistics(tableData: any[]) {
+  return new Promise<void>((resolve) => {
+    const numberList = tableData.map((item: any, index: number) => item?.singleColumnOperation?.grouping ? index : 9999)
+    const statisticsList = tableData.map((item: any, index: number) => item?.singleColumnOperation?.statistics ? index : 9999)
+    listThatNeedsToBeGrouped.value = numberList.filter((item: any) => item != 9999)
+    listStatisticsList.value = statisticsList.filter((item: any) => item != 9999)
+    resolve()
+  })
+}
+
+defineExpose({
+  initializedData
+});
+
+onMounted(() => { })
+</script>
+
+<template>
+  <div class="h-full flex flex-col">
+    <div class="flex items-center mb-4 h-[5%]">
+      <el-button type="primary" @click="setDataRange()">设置数据范围</el-button>
+    </div>
+    <div class="flex-1 h-[92%]">
+      <el-table :data="tableData" border style="width: 100%;height: 100%" v-loading="tableLoading" :span-method="mergeColumns" :summary-method="getSummaries" :show-summary="listStatisticsList.length > 0 ? true : false" :key="tableKey">
+        <el-table-column :prop="item.label" :label="item.label" min-width="240" v-for="(item, index) in tableHeader">
+          <template #default="scope">
+            <template v-if="item.type == 'timestamp' || item.type == 'datetime' || item.type == 'timestamp' || item.type == 'date'">
+              {{ scope.row[item.label] ? formatDate(new Date(scope.row[item.label])) : '' }}
+            </template>
+            <template v-else>
+              {{ scope.row[item.label] }}
+            </template>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 数据范围弹框 -->
+    <el-dialog v-model="dataRangeDataVisable" width="680" :show-close="false" top="10vh">
+      <template #header="{ close, titleId, titleClass }">
+        <div class="flex justify-between items-center border-b pb-3 dialog-header">
+          <h4 :id="titleId">数据范围</h4>
+          <div class="flex">
+            <el-button @click="dataRangeDataVisable = false">取消</el-button>
+            <el-button type="primary" @click="determinationOfDataRange()">确定</el-button>
+          </div>
+        </div>
+      </template>
+      <div class="p-8">
+        <rangeFilter ref="rangeFilterRef" />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss"></style>

+ 131 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/dragEdit/rangeFilter.vue

@@ -0,0 +1,131 @@
+<script setup lang="ts">
+import { ref, provide, onMounted } from 'vue'
+import { VueDraggable } from 'vue-draggable-plus'
+
+import { timeSelectionBox, amountSelect, dropdownBoxFieldIntegration, textSelectLlist } from '../list'
+
+const filterList = ref<any>([])
+const isItAnEditor = ref(false)
+
+function functionssss(row: any) {
+  let data = row.data
+  const index = row.newIndex
+  const typeName = dropdownBoxFieldIntegration.map((item: any) => `${item.indicate}.${item.fieldName}`)
+  // 初始化数据
+  data.list = []
+  data.filterValueThree = ''
+  data.filterValueTwo = ''
+
+  // 第三个下拉框
+  if(typeName.includes(`${data.tableName}.${data.columnName}`)) {
+    data.valueList = typeName.includes(`${data.tableName}.${data.columnName}`) ? dropdownBoxFieldIntegration.find((item: any) => `${item.indicate}.${item.fieldName}` == `${data.tableName}.${data.columnName}`)?.list : []
+    data.valueFlag = typeName.includes(`${data.tableName}.${data.columnName}`) ? true : false
+    data.twoDisable = true
+  } else if(['double', 'decimal', 'tinyint', 'int'].includes(data.dataType)) {
+    data.list = amountSelect
+  } else if(['datetime', 'timetamp', 'timestamp'].includes(data.dataType)) {
+    data.list = timeSelectionBox
+  }
+  
+  if(['varchar', 'text'].includes(data.dataType)) {
+    data.filterValueTwo = '包含'
+    data.list = textSelectLlist
+    // data.twoDisable = true
+  }
+  
+  filterList.value[index] = data
+  console.log(filterList.value)
+}
+
+function getRangeData() {
+  return filterList.value
+}
+
+function setRangeData(list: any[], flag: boolean = true) {
+  isItAnEditor.value = flag
+  filterList.value = list
+}
+
+function setIsItAnEditor() {
+  filterList.value = []
+  isItAnEditor.value = false
+}
+
+// 向外暴露方法
+defineExpose({
+  getRangeData,
+  setRangeData,
+  setIsItAnEditor
+});
+
+onMounted(() => {
+})
+</script>
+
+<template>
+  <VueDraggable v-model="filterList" target=".dragAndDropArea" group="people" @add="functionssss" :disabled="isItAnEditor">
+    <div class="w-full h-[40vh] px-6 overflow-auto dragAndDropArea border-2 relative">
+      <div class="flex items-center mt-4 relative" v-for="(item, index) in filterList">
+        <!-- 元素渲染 -->
+        <div class="textInput"> {{ item.columnComment }} </div>
+        <div class="w-[240px] mr-[20px]">
+          <!-- 第二个条件 -->
+          <el-select v-model="item.filterValueTwo" placeholder="请选择" style="width: 100%" :disabled="item.twoDisable">
+            <el-option v-for="(twoItem, twoIndex) in (item.list || [])" :key="twoIndex" :label="twoItem.label"
+              :value="twoItem.value" />
+          </el-select>
+        </div>
+        <div class="w-[240px] mr-[20px]">
+          <!-- 第三个条件 -->
+          <template v-if="['datetime', 'timetamp', 'timestamp'].includes(item.dataType) && item.filterValueTwo != '自定义' && !item.valueFlag"> <!-- 日期 -->
+            <el-date-picker v-model="item.filterValueThree" type="date" placeholder="请选择" :clearable="true"
+              format="YYYY-MM-DD" value-format="YYYY-MM-DD"  style="width: 100%;"/>
+          </template>
+          <template v-if="['datetime', 'timetamp', 'timestamp'].includes(item.dataType) && item.filterValueTwo == '自定义' && !item.valueFlag"> <!-- 时间段 -->
+            <el-date-picker v-model="item.filterValueThree" type="daterange"
+              start-placeholder="开始时间" end-placeholder="结束时间" format="YYYY-MM-DD" value-format="YYYY-MM-DD" style="width: 100%;" />
+          </template>
+          <template v-if="['varchar', 'text'].includes(item.dataType) && !item.valueFlag"> <!-- 文本框 -->
+            <el-input v-model="item.filterValueThree" style="width: 100%" placeholder="请输入" clearable />
+          </template>
+          <template v-if="item.valueFlag"> <!-- 下拉选择 -->
+            <el-select v-model="item.filterValueThree" placeholder="请选择" style="width: 100%">
+              <el-option v-for="(treeItem, treeIndex) in item.valueList" :key="treeIndex" :label="treeItem.label"
+                :value="treeItem.value" />
+            </el-select>
+          </template>
+          <template v-if="['decimal', 'int'].includes(item.dataType) && !item.valueFlag">
+            <el-input-number v-model="item.filterValueThree" :min="0" controls-position="right" style="width: 100%" />
+          </template>
+        </div>
+        <!-- 操作图标 -->
+        <div v-if="!isItAnEditor" class="flex items-center">
+          <div class="cursor-pointer" @click="filterList.splice(index, 1)"><el-icon color="#f56c6c" size="20"><Delete /></el-icon></div>
+        </div>
+      </div>
+
+      <div class="absolute text-[#999] top-1/2 left-1/2 transform -translate-x-1/2 text-[20px]"
+        v-if="!filterList.length && !isItAnEditor">
+        请拖入字段生成范围条件
+      </div>
+
+      <div class="absolute text-[#999] top-1/2 left-1/2 transform -translate-x-1/2 text-[20px]"
+        v-if="!filterList.length && isItAnEditor">
+        暂无数据
+      </div>
+    </div>
+  </VueDraggable>
+
+</template>
+
+<style lang="scss">
+.textInput {
+  width: 200px;
+  height: 32px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  margin-right: 20px;
+  text-align: center;
+  line-height: 32px;
+}
+</style>

+ 167 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/function.ts

@@ -0,0 +1,167 @@
+/**
+ * 生成拖拽表和关联表的 SQL
+ * @param tableList 拖拽表数据
+ * @param rangeFilterList 数据范围
+ * @param addEditData 添加表单数据
+ * @returns string
+ */
+export function generateSql(tableList: any[] = [], rangeFilterList: any[] = [], addEditData: addEditDataType) {
+  // 添加表明前缀
+  const alphabet = 'abcdefghijklmnopqrstuvwxyz';
+  addEditData.mindMapJSON.selectNodes.forEach((obj, index) => {
+    obj.data.seqPrefix = alphabet[index % 26]; // 通过取余处理
+  });
+
+  // 用到的那些表
+  const list = filterTableData(tableList, rangeFilterList, addEditData)
+  const tableSqlList = generateDragAndDropTableSql(list, tableList)
+  const relationshipSqlList = associationRelationshipArray(addEditData.mindMapJSON.selectNodes, list)
+
+  // 生成拖拽表 SQL
+  const tableSqlStr = `select ${tableSqlList.map(item => `${item.seqPrefix}.${item.columnName} as '${item.columnComment}'`).join(', ')}`
+
+  // 生成关联关系 SQL
+  const relationshipTableSqlStr = relationshipSqlList.map(item => item.paragraphSql).join(' ')
+
+  console.log(tableSqlStr, '<======= 表格SQL')
+  console.log(relationshipTableSqlStr, '<======= 关联关系SQL')
+
+  return `${tableSqlStr} ${relationshipTableSqlStr}`
+}
+
+/**
+ * 生成数据范围 SQL
+ * @param rangeFilterList 数据范围数据
+ * @param addEditData 新建表单页面数据
+ * @returns string
+ */
+export function rangeGetSql(rangeFilterList: any[] = [], addEditData: addEditDataType) {
+  const alphabet = 'abcdefghijklmnopqrstuvwxyz';
+  addEditData.mindMapJSON.selectNodes.forEach((obj, index) => {
+    obj.data.seqPrefix = alphabet[index % 26]; // 通过取余处理
+  });
+  const nodes = addEditData.mindMapJSON.selectNodes.map(item => item.data)
+  const list = rangeFilterList.filter(item => item.filterValueThree)
+  const newList = list.map((item) => {
+    let sql = ''
+    const seqPrefix = nodes.find(node => node.tblName == item.tableName).seqPrefix
+    // 自定义日期筛选条件
+    if(['datetime', 'timetamp', 'timestamp'].includes(item.dataType) && item.filterValueTwo == '自定义' && !item.valueFlag) {
+      const templateItem = (item.list || []).find((em: any) => em.value == item.filterValueTwo)
+      sql = templateItem.sql.replace(/表别名/g, seqPrefix)
+      .replace(/字段名称/g, item.columnName)
+      .replace(/值1/g, item.filterValueThree[0])
+      .replace(/值2/g, item.filterValueThree[1]);
+    }
+
+    // 普通日期筛选条件
+    if(['datetime', 'timetamp', 'timestamp'].includes(item.dataType) && item.filterValueTwo != '自定义' && !item.valueFlag) {
+      const templateItem = (item.list || []).find((em: any) => em.value == item.filterValueTwo)
+      sql = templateItem.sql.replace(/表别名/g, seqPrefix)
+      .replace(/字段名称/g, item.columnName)
+      .replace(/值/g, item.filterValueThree)
+    }
+
+    // 文本框
+    if(['varchar', 'text'].includes(item.dataType) && !item.valueFlag) {
+      const templateItem = (item.list || []).find((em: any) => em.value == item.filterValueTwo)
+      sql = `${seqPrefix}.${item.columnName} ${templateItem.sql} concat('%','${item.filterValueThree}','%')`
+    }
+
+    // 数字输入框
+    if(['decimal', 'int'].includes(item.dataType) && !item.valueFlag) {
+      const templateItem = (item.list || []).find((em: any) => em.value == item.filterValueTwo)
+      sql = templateItem.sql.replace(/表别名/g, seqPrefix)
+      .replace(/字段名称/g, item.columnName)
+      .replace(/值/g, item.filterValueThree)
+    }
+
+    // 下拉框
+    if(item.valueFlag) {
+      sql = `${seqPrefix}.${item.columnName} = '${item.filterValueThree}`
+    }
+
+    return {
+      ...item,
+      strSQL: sql
+    }
+  })
+
+  return `where ${newList.map((item: any) => item.strSQL).join(' and ')}`
+}
+
+/**
+ * 生成关联关系 SQL 的数组
+ * @param nodes 节点数据
+ * @param list 用到的那些表
+ * @returns Array
+ */
+function associationRelationshipArray(nodes: any[] = [], _list: any[]) {
+  const nodeList = nodes.map(item => item.data)
+  const mainTable = nodeList.filter(item => item.seqPrefix == 'a').map(item => {
+    return {
+      ...item,
+      paragraphSql: `from ${item.tblName} ${item.seqPrefix}`
+    }
+  })
+  const newList = nodeList.filter(item => item.seqPrefix != 'a')
+
+  const sqlList = newList.map(item => {
+    const strSeqPrefix = nodeList.find(node => node.tblName == item.fromTbl).seqPrefix
+    let strSql = `left join ${item.toTbl} ${item.seqPrefix} on ${strSeqPrefix}.${item.fromCol} = ${item.seqPrefix}.${item.toCol}`
+    if(item.toTbl == 'sys_dict') {
+      strSql = strSql + ` and ${item.seqPrefix}.code = '${item.dictCode}'`
+    }
+    item.paragraphSql = strSql
+    return item
+  })
+
+  return [...mainTable, ...sqlList]
+}
+
+/**
+ * 生成拖拽表 SQL 的数组
+ * @param list 用到的那些表
+ * @param tableList 拖拽表数据
+ * @returns Array
+ */
+function generateDragAndDropTableSql(list: any[] = [], tableList: any[] = []) {
+  const result = tableList.filter(item1 => {
+    return list.some(item2 => item2.tblName === item1.tableName);
+  }).map(item1 => {
+    const matchedItem2 = list.find(item2 => `${item2.tblName}_${item2.dictCode}` === `${item1.tableName}_${item1.dictCode}`);
+    const matchedItem2s = list.find(item2 => item2.tblName === item1.tableName);
+    return {
+      seqPrefix: matchedItem2?.seqPrefix || matchedItem2s.seqPrefix,
+      tblName: matchedItem2?.tblName || matchedItem2s.seqPrefix,
+      columnComment: item1.columnComment,
+      columnName: item1.columnName
+    };
+  });
+
+  return result
+}
+
+/**
+ * 过滤数据中所用到的表数据
+ * @param tableList 拖拽表数据
+ * @param rangeFilterList 数据范围数据
+ * @param addEditData 新建表单页面数据
+ * @returns Array
+ */
+function filterTableData(tableList: any[] = [], rangeFilterList: any[] = [], addEditData: addEditDataType) {
+  // 所有节点表数据
+  const nodesAllDataList = addEditData.mindMapJSON.selectNodes.map(item => item.data)
+  // tableList 所用到的表数据
+  const tableNodesDataList = nodesAllDataList.filter(item1 =>
+    tableList.some(item2 => item1.tblName === item2.tableName)
+  );
+  // rangeFilter 所用到的表数据
+  const rangeFilterNodesDataList = nodesAllDataList.filter(item1 =>
+    rangeFilterList.some(item2 => item1.tblName === item2.tableName)
+  );
+  // 所用到的表数据
+  const tableUsedList = [...new Set([...tableNodesDataList, ...rangeFilterNodesDataList].filter(item => item.tblName))]
+
+  return tableUsedList || []
+}

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

@@ -18,7 +18,7 @@ function toPath(path: string) {
 onMounted(() => {
   const filterPath = route.meta?.parentPath
   const list = router.getRoutes().filter((item) => item.path == filterPath)[0].children
-  routerList.value = list.filter(item => !(['/biReport/addEdit', '/biReport/dragEdit'].includes(item.path)))
+  routerList.value = list.filter(item => !(['/biReport/addEdit', '/biReport/dragEdit', '/biReport/reportView'].includes(item.path)))
 })
 </script>
 

File diff suppressed because it is too large
+ 1152 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/list.ts


+ 25 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/reportView/index.vue

@@ -0,0 +1,25 @@
+<script lang="ts" setup>
+import { ref, onMounted, watch } from "vue";
+import { useRoute, useRouter } from 'vue-router'
+
+import previewTable from "../dragEdit/previewTable.vue";
+
+const previewTableRef = ref<InstanceType<typeof previewTable> | null>(null);
+
+onMounted(() => {
+  const reportViewJson = JSON.parse(sessionStorage.getItem('reportViewJson') || '{}')
+  const { SQL = '', rangeFilterData = [], tableData = [] } = reportViewJson
+  setTimeout(() => {
+    previewTableRef.value?.initializedData(SQL, rangeFilterData, tableData)
+  }, 100)
+})
+</script>
+
+<template>
+  <div class="w-full h-full bg-white rounded-md p-3">
+    <previewTable ref="previewTableRef"></previewTable>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+</style>

+ 22 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/biReport/type.d.ts

@@ -0,0 +1,22 @@
+type mindMapJSONType = {
+  clickedNodes: any[],
+  edges: any[],
+  nodes: any[],
+  selectNodes: any[]
+}
+
+type addEditDataType = {
+  addFormVal: any,
+  mindMapJSON: mindMapJSONType
+}
+
+type addEditReportFormVal = {
+  id?: number | string,
+  reportFormName: string,
+  privilege: boolean,
+  parentStoreId: number | string,
+  description: string,
+  deptAccessList: any[],
+  userAccessList: any[],
+  businessObject: any[]
+}

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

@@ -241,7 +241,7 @@ async function importBusiness(param: UploadRequestOptions) {
     allLoading.importLoading = false
   })
   if (res.code == 'ok') {
-    globalPopup?.showSuccess('导入成功' || '')
+    globalPopup?.showSuccess('导入成功')
     getContactPerson()
     return
   }

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

@@ -63,3 +63,7 @@ export const IMPORT_DATA = `${MOD}/importData`;//导入
 
 export const EXPORT_DATA = `${MOD}/exportData`;//按条件导出
 export const EXPORT_DATA_BY_TASK_ID = `${MOD}/exportDataByTaskIds`;//选中导出
+
+// 看板类型
+export const TABLE_VIEW = 'table'
+export const KANBAN_VIEW = 'view'

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

@@ -51,111 +51,128 @@
     </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="ml-auto p-3">
-          <el-button type="primary" v-permission="['tasksAdd']" @click="createTasks()">创建任务</el-Button>
-          <el-button type="primary" v-permission="['tasksDelete']" :disabled="len == 0" :loading="btnLoading" @click="deleteTasks()">批量删除</el-Button>
-          <el-button type="primary" v-permission="['tasksImport']" @click="openImportModal()">导入</el-Button>
-          <!-- <el-button type="primary" :loading="btnLoading" @click="exportTasks()">导出</el-Button> -->
-          <el-button type="primary" v-permission="['tasksExport']" :loading="btnLoading" @click="newExportTasks()">导出</el-Button>
+        <div class="p-3">
+          <div class="flex items-center w-full justify-between">
+            <div>
+              <el-radio-group v-model="layoutSingleChoice" @change="viewsSwitching">
+                <el-radio-button label="表格视图" :value="TABLE_VIEW" />
+                <el-radio-button label="日历视图" :value="KANBAN_VIEW" />
+              </el-radio-group>
+            </div>
+            <div class="justify-end flex">
+              <el-button type="primary" v-permission="['tasksAdd']" @click="createTasks()">创建任务</el-Button>
+              <el-button type="primary" v-permission="['tasksDelete']" :disabled="len == 0" :loading="btnLoading" @click="deleteTasks()">批量删除</el-Button>
+              <el-button type="primary" v-permission="['tasksImport']" @click="openImportModal()">导入</el-Button>
+              <!-- <el-button type="primary" :loading="btnLoading" @click="exportTasks()">导出</el-Button> -->
+              <el-button type="primary" v-permission="['tasksExport']" :loading="btnLoading" @click="newExportTasks()">导出</el-Button>
+            </div>
+          </div>
         </div>
-        <div class="flex-1 overflow-y-auto">
-          <el-table :data="tableData" :show-overflow-tooltip="tableShowOverflowTooltip" style="width: 100%;height: 100%;" ref="tableRef" v-loading="loading">
-            <el-table-column type="selection" width="55" />
-            <el-table-column prop="taskName" label="任务名称" header-align="center" align="center" show-overflow-tooltip
-              width="200" />
-            <el-table-column prop="priority" label="优先级" width="100" :sortable="true" header-align="center"
-              align="center">
-              <template #default="scope">
-                {{ PRIORITY.find(item => item.value == scope.row.priority)?.label }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="status" label="状态" width="100" header-align="center" align="center">
-              <template #default="scope">
-                <el-text :type="STATUS[scope.row.status]?.type">
-                  {{ STATUS[scope.row.status]?.label }}
-                </el-text>
-              </template>
-            </el-table-column>
-            <el-table-column prop="executorNames" label="执行人" width="120" header-align="center" align="center">
-              <template #default="scope">
-                <template v-for="(item, index) in (scope.row.taskExecutors || [])">
-                    <TextTranslation translationTypes="userName" :translationValue="item">
-                    </TextTranslation>
-                    <span v-if="index < (scope.row.taskExecutors || []).length - 1">,</span>
+        <template v-if="layoutSingleChoice == TABLE_VIEW">
+          <div class="flex-1 overflow-y-auto">
+            <el-table :data="tableData" :show-overflow-tooltip="tableShowOverflowTooltip" style="width: 100%;height: 100%;" ref="tableRef" v-loading="loading">
+              <el-table-column type="selection" width="55" />
+              <el-table-column prop="taskName" label="任务名称" header-align="center" align="center" show-overflow-tooltip
+                width="200" />
+              <el-table-column prop="priority" label="优先级" width="100" :sortable="true" header-align="center"
+                align="center">
+                <template #default="scope">
+                  {{ PRIORITY.find(item => item.value == scope.row.priority)?.label }}
                 </template>
-              </template>
-            </el-table-column>
-            <el-table-column prop="startDate" label="开始时间" width="200" :sortable="true" header-align="center"
-              align="center" value-format="YYYY-MM-DD" />
-            <el-table-column prop="endDate" label="截止时间" width="200" :sortable="true" header-align="center"
-              align="center" value-format="YYYY-MM-DD">
-              <template #default="scope">
-                <div :class="`${scope.row.endDate && scope.row.endDate < dateOfTheDay ? 'text-[#F56C6C]' : '' }`">{{ scope.row.endDate }}</div>
-              </template>
-            </el-table-column>
-            <el-table-column prop="customName" label="客户名称" header-align="center" align="center" width="120">
-              <template #default="scope">
-                <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'customer', 'customId')">
-                  {{ scope.row.customName }}
-                </el-link>
-              </template>
-            </el-table-column>
-            <el-table-column prop="businessName" :label="`${businessLabel}名称`" header-align="center" align="center" width="200">
-              <template #default="scope">
-                <el-link :underline="false" type="primary"
-                  @click="goDetail(scope.row, 'business', 'businessOpportunityId')">
-                  {{ scope.row.businessName }}
-                </el-link>
-              </template>
-            </el-table-column>
-            <el-table-column prop="orderName" label="销售订单" header-align="center" align="center" width="200">
-              <template #default="scope">
-                <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'order', 'orderId')">
-                  {{ scope.row.orderName }}
-                </el-link>
-              </template>
-            </el-table-column>
-            <el-table-column prop="clueName" label="线索名称" header-align="center" align="center" width="200">
-              <template #default="scope">
-                <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'thread', 'clueId')">
-                  {{ scope.row.clueName }}
-                </el-link>
-              </template>
-            </el-table-column>
-            <el-table-column prop="contactsName" label="联系人名称" header-align="center" align="center" width="120">
-              <template #default="scope">
-                <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'contacts', 'contactsId')">
-                  {{ scope.row.contactsName }}
-                </el-link>
-              </template>
-            </el-table-column>
-            <el-table-column prop="contactsTel" label="联系人号码" header-align="center" align="center" width="140" />
-
-            <el-table-column fixed="right" label="操作" header-align="center" align="center" width="160" v-permission="['tasksEdit']">
-
-              <template #default="scope">
-                <el-button link type="primary" @click.prevent="editRow(scope.row)">
-                  编辑
-                </el-button>
-                <el-button link type="primary" v-if="scope.row.status == '2'"
-                  @click.prevent="restart(scope.row)">
-                  重启
-                </el-button>
-                <el-button link type="primary" v-else @click.prevent="finishRow(scope.row)">
-                  完成
-                </el-button>
-
-                <el-button link type="danger" v-permission="['tasksDelete']" @click.prevent="deleteRow(scope.row)">
-                  删除
-                </el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-        </div>
-        <div class="ml-auto">
-          <el-pagination layout="total, prev, pager, next, sizes" :total="totalCount"
-            :current-page="searchForm.pageIndex" @size-change="sizeChage" @current-change="currentChange" />
-        </div>
+              </el-table-column>
+              <el-table-column prop="status" label="状态" width="100" header-align="center" align="center">
+                <template #default="scope">
+                  <el-text :type="STATUS[scope.row.status]?.type">
+                    {{ STATUS[scope.row.status]?.label }}
+                  </el-text>
+                </template>
+              </el-table-column>
+              <el-table-column prop="executorNames" label="执行人" width="120" header-align="center" align="center">
+                <template #default="scope">
+                  <template v-for="(item, index) in (scope.row.taskExecutors || [])">
+                      <TextTranslation translationTypes="userName" :translationValue="item">
+                      </TextTranslation>
+                      <span v-if="index < (scope.row.taskExecutors || []).length - 1">,</span>
+                  </template>
+                </template>
+              </el-table-column>
+              <el-table-column prop="startDate" label="开始时间" width="200" :sortable="true" header-align="center"
+                align="center" value-format="YYYY-MM-DD" />
+              <el-table-column prop="endDate" label="截止时间" width="200" :sortable="true" header-align="center"
+                align="center" value-format="YYYY-MM-DD">
+                <template #default="scope">
+                  <div :class="`${scope.row.endDate && scope.row.endDate < dateOfTheDay ? 'text-[#F56C6C]' : '' }`">{{ scope.row.endDate }}</div>
+                </template>
+              </el-table-column>
+              <el-table-column prop="customName" label="客户名称" header-align="center" align="center" width="120">
+                <template #default="scope">
+                  <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'customer', 'customId')">
+                    {{ scope.row.customName }}
+                  </el-link>
+                </template>
+              </el-table-column>
+              <el-table-column prop="businessName" :label="`${businessLabel}名称`" header-align="center" align="center" width="200">
+                <template #default="scope">
+                  <el-link :underline="false" type="primary"
+                    @click="goDetail(scope.row, 'business', 'businessOpportunityId')">
+                    {{ scope.row.businessName }}
+                  </el-link>
+                </template>
+              </el-table-column>
+              <el-table-column prop="orderName" label="销售订单" header-align="center" align="center" width="200">
+                <template #default="scope">
+                  <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'order', 'orderId')">
+                    {{ scope.row.orderName }}
+                  </el-link>
+                </template>
+              </el-table-column>
+              <el-table-column prop="clueName" label="线索名称" header-align="center" align="center" width="200">
+                <template #default="scope">
+                  <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'thread', 'clueId')">
+                    {{ scope.row.clueName }}
+                  </el-link>
+                </template>
+              </el-table-column>
+              <el-table-column prop="contactsName" label="联系人名称" header-align="center" align="center" width="120">
+                <template #default="scope">
+                  <el-link :underline="false" type="primary" @click="goDetail(scope.row, 'contacts', 'contactsId')">
+                    {{ scope.row.contactsName }}
+                  </el-link>
+                </template>
+              </el-table-column>
+              <el-table-column prop="contactsTel" label="联系人号码" header-align="center" align="center" width="140" />
+  
+              <el-table-column fixed="right" label="操作" header-align="center" align="center" width="160" v-permission="['tasksEdit']">
+  
+                <template #default="scope">
+                  <el-button link type="primary" @click.prevent="editRow(scope.row)">
+                    编辑
+                  </el-button>
+                  <el-button link type="primary" v-if="scope.row.status == '2'"
+                    @click.prevent="restart(scope.row)">
+                    重启
+                  </el-button>
+                  <el-button link type="primary" v-else @click.prevent="finishRow(scope.row)">
+                    完成
+                  </el-button>
+  
+                  <el-button link type="danger" v-permission="['tasksDelete']" @click.prevent="deleteRow(scope.row)">
+                    删除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+          <div class="ml-auto">
+            <el-pagination layout="total, prev, pager, next, sizes" :total="totalCount"
+              :current-page="searchForm.pageIndex" @size-change="sizeChage" @current-change="currentChange" />
+          </div>
+        </template>
+        <template v-if="layoutSingleChoice == KANBAN_VIEW">
+          <div class="w-full p-4 h-full overflow-auto scroll-bar">
+            <taskCalendar />
+          </div>
+        </template>
       </div>
     </div>
     <el-dialog v-model="restartPopUpWindowVisable" title="重启任务" width="500">
@@ -196,11 +213,12 @@
 import { computed, inject, onBeforeMount, onMounted, ref, } from 'vue';
 import { useRouter } from 'vue-router';
 import { useStore } from '@/store';
-import { MOD, PRIORITY, STATUS, defaultSearchForm, PAGE_LIST, ADD_TASK, DELETE_TASKS, UPDATE_TASK, UPDATE_TASK_STATUS, IMPORT_DATA, EXPORT_DATA, EXPORT_DATA_BY_TASK_ID } from './api';
+import { MOD, PRIORITY, STATUS, defaultSearchForm, PAGE_LIST, ADD_TASK, DELETE_TASKS, UPDATE_TASK, UPDATE_TASK_STATUS, IMPORT_DATA, EXPORT_DATA, EXPORT_DATA_BY_TASK_ID, TABLE_VIEW, KANBAN_VIEW } from './api';
 import { ElTable, dayjs } from 'element-plus';
 import TaskModal from '@/components/TaskModal/index.vue';
 import ImportModal from './ImportModal.vue';
 import ExportModal from './ExportModal.vue';
+import taskCalendar from './taskCalendar.vue';
 import { post, uploadFile } from '@/utils/request';
 import { getFromValue, confirmAction, downloadFile, createTaskFromType } from '@/utils/tools';
 import { tableShowOverflowTooltip } from '@/utils/globalVariables'
@@ -222,6 +240,11 @@ const restartFrom = ref<any>({});
 const dateOfTheDay = ref<any>(dayjs().format('YYYY-MM-DD'))
 const isExistBusiness = sessionStorage.getItem("isExistBusiness");
 const businessLabel = isExistBusiness === "1" ? "商机" : "项目"; 
+const layoutSingleChoice = ref(TABLE_VIEW)
+
+function viewsSwitching() {
+
+}
 
 function closeTaskModal() {
   taskModalVisible.value = false;

+ 55 - 0
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/tasks/taskCalendar.vue

@@ -0,0 +1,55 @@
+
+
+<template>
+  <div class="w-full h-full flex flex-col">
+    <div class="flex-1">
+      <FullCalendar ref="calendarRef" :options="calendarOptions" class="h-full w-full">
+        <template v-slot:eventContent='arg'>
+          <div>{{ arg.event.title }}123321</div>
+        </template>
+      </FullCalendar>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import FullCalendar from '@fullcalendar/vue3';
+import dayGridPlugin from '@fullcalendar/daygrid';
+import timeGridPlugin from '@fullcalendar/timegrid'
+import interactionPlugin from '@fullcalendar/interaction'
+import zhCnLocale from '@fullcalendar/core/locales/zh-cn' 
+
+// 当前选中的视图
+const currentView = ref('dayGridMonth')
+
+// 日历实例引用
+const calendarRef = ref()
+
+// FullCalendar 配置
+const calendarOptions = ref<any>({
+  plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
+  locale: zhCnLocale,
+  initialView: currentView.value,
+  slotMinTime: '06:00:00',  // 最早显示时间
+  slotMaxTime: '22:00:00',  // 最晚显示时间
+  slotDuration: '00:30:00', // 30分钟一格(timeGrid生效)
+  events: [
+    { title: '早会', start: '2025-03-16T09:00:00', end: '2025-03-20T10:00:00' },
+    { title: '午休', start: '2025-03-20T12:00:00', end: '2025-03-20T13:00:00' },
+    { title: 'AAA', start: '2025-03-17T12:00:00', end: '2025-03-19T13:00:00' },
+  ],
+  headerToolbar: {  // 自定义头部
+    left: 'today,prev,next',  // 左侧按钮:上一页,下一页,今天
+    center: 'title',
+    right: 'dayGridMonth,timeGridWeek,timeGridDay'  // 右侧显示视图切换按钮
+  }
+})
+
+</script>
+
+<style lang="scss" scoped>
+:deep(.fc-today-button) {
+  margin-right: 10px;
+} 
+</style> 

+ 63 - 234
fhKeeper/formulahousekeeper/customerBuler-crm/src/pages/test/index.vue

@@ -1,242 +1,71 @@
-<script lang="ts" setup>
-import { ref, onMounted } from 'vue'
-import { VueFlow, Handle, Position, type Node, Edge } from '@vue-flow/core'
-import { Background } from '@vue-flow/background'
-
-import '@vue-flow/core/dist/style.css';
-import '@vue-flow/core/dist/theme-default.css';
-
-interface selectedNode {
-  id: string;
-  data: any
-}
-
-const FixedConfiguration = {
-  type: 'custom',
-  style: {
-    width: '120px', padding: '5px 20px', fontSize: '14px',
-    border: '1px solid #dcdfe6', borderRadius: '8px'
-  },
-  sourcePosition: Position.Right,
-  targetPosition: Position.Left,
-  hidden: false
-}
-
-const fixedLabel = {
-  selected: false, // 是否选中
-}
-
-const connectionType = 'step'
-
-const nodes = ref<Node[]>([
-  {
-    id: '1', data: { label: '客户', ...fixedLabel }, position: { x: 100, y: 30 },
-    ...FixedConfiguration
-  },
-])
-
-const edges = ref<Edge[]>([
-  { id: 'e1-1', source: '1', target: '2', type: connectionType },
-])
-
-const clickedNodes = ref<selectedNode[]>([]) // 记录点击的节点
-const selectNodes = ref<any[]>([]) // 记录选中的节点
-
-function onNodeClick(event: any) {
-  const { x, y } = event.node.position // 点击的位置
-  const nodeId = event.node.id
-
-  // 点击的节点Id
-  const clickedNodesStr = clickedNodes.value.map(node => node.id)
-  // 选中的节点Id
-  const selectNodeIdList = selectNodes.value.map(node => node.id)
-  // 点击长度相同的节点Id
-  const clickedNodesLengthStr = clickedNodesStr.filter(strId => strId.length == nodeId.length)
-
-  if (clickedNodesStr.includes(nodeId)) {
-    if (selectNodeIdList.includes(nodeId)) {
-      for (let i in selectNodeIdList) {
-        if (nodeId !== selectNodeIdList[i] && nodeId.length === selectNodeIdList[i].length) {
-          expandNode(selectNodeIdList[i], true)
-        } else if (nodeId == selectNodeIdList[i]) {
-          expandNode(nodeId, false)
-        }
-      }
-
-      const notSelectedToDelete = clickedNodesLengthStr.filter(item => !selectNodeIdList.includes(item));
-      for (let i in notSelectedToDelete) {
-        deteleData(notSelectedToDelete[i])
-      }
-    }
-  } else {
-    clickedNodes.value.push({ id: nodeId, data: event.node.data })
-    clickedNodesStr.push(nodeId)
-
-    // 隐藏和删除其他节点
-    const hiddenNode = clickedNodesLengthStr.filter(item => selectNodeIdList.includes(item))
-    const deleteNode = clickedNodesLengthStr.filter(item => !selectNodeIdList.includes(item))
-
-    hiddenNode.forEach(item => expandNode(item, true))
-    deleteNode.forEach(item => deteleData(item))
-
-    const strList = ['拜访', '拜访计划', '协议', '合同订单', '工单', '项目', '报销费用']
-    AddData(strList, x, y, nodeId)
-  }
-}
-
-
-
-// 处理级之间的联动勾选
-function processSelectedNodes(nodeId: string) {
-  const item = nodes.value.find(item => item.id === nodeId)
-  if (item?.data?.selected) { // 选中状态将之前的节点选中
-    const selectNodes = item.id.split('-').map((_: any, index: number, arr: any) => arr.slice(0, index + 1).join('-'));
-    nodes.value = nodes.value.map(node => {
-      if (selectNodes.includes(node.id)) {
-        return {
-          ...node,
-          data: { ...node.data, selected: true }
-        }
-      } else {
-        return node
-      }
-    })
-  }
+<!-- <template>
+  <div class="w-full h-full flex flex-col">
+    <div>
+      <el-select v-model="currentView" placeholder="选择视图" @change="changeCalendarView">
+        <el-option label="月视图" value="dayGridMonth" />
+        <el-option label="周视图" value="timeGridWeek" />
+        <el-option label="日视图" value="timeGridDay" />
+      </el-select>
+    </div>
+    <div class="flex-1">
+      <FullCalendar ref="calendarRef" :options="calendarOptions">
+        <template v-slot:eventContent='arg'>
+          <div>{{ arg.event.title }}123321</div>
+        </template>
+      </FullCalendar>
+    </div>
+  </div>
+</template>
 
-  if(!item?.data?.selected) {
-    const cancelSelectNodes = nodes.value.filter(node => (node.id + '').indexOf((item?.id + '')) !== -1).map(node => node.id)
-    nodes.value = nodes.value.map(node => {
-      if (cancelSelectNodes.includes(node.id)) {
-        return {
-          ...node,
-          data: { ...node.data, selected: false }
-        }
-      } else {
-        return node
-      }
-    })
+<script setup lang="ts">
+import { ref } from 'vue';
+import FullCalendar from '@fullcalendar/vue3';
+import dayGridPlugin from '@fullcalendar/daygrid';
+import timeGridPlugin from '@fullcalendar/timegrid'
+import interactionPlugin from '@fullcalendar/interaction'
+import zhCnLocale from '@fullcalendar/core/locales/zh-cn' 
+
+
+
+// 当前选中的视图
+const currentView = ref('dayGridMonth')
+
+// 日历实例引用
+const calendarRef = ref()
+
+// FullCalendar 配置
+const calendarOptions = ref<any>({
+  plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
+  locale: zhCnLocale,
+  initialView: currentView.value,
+  slotMinTime: '06:00:00',  // 最早显示时间
+  slotMaxTime: '22:00:00',  // 最晚显示时间
+  slotDuration: '00:30:00', // 30分钟一格(timeGrid生效)
+  events: [
+    { title: '早会', start: '2025-03-16T09:00:00', end: '2025-03-20T10:00:00' },
+    { title: '午休', start: '2025-03-20T12:00:00', end: '2025-03-20T13:00:00' },
+    { title: 'AAA', start: '2025-03-17T12:00:00', end: '2025-03-19T13:00:00' },
+  ],
+  headerToolbar: {  // 自定义头部
+    left: 'today,prev,next',  // 左侧按钮:上一页,下一页,今天
+    center: 'title',
+    right: 'dayGridMonth,timeGridWeek,timeGridDay'  // 右侧显示视图切换按钮
   }
-  switchColors()
-  changeEdgeStyle()
-}
-
-// 更改连线样式
-function changeEdgeStyle() {
-  const selectNodeIdList = selectNodes.value.map(node => node.id)
-  edges.value = edges.value.map(edge => {
-    const { source, target } = edge
-    const isSelected = selectNodeIdList.includes(source) && selectNodeIdList.includes(target)
-    return {
-      ...edge,
-      style: {
-        ...edge.style,
-        strokeWidth: isSelected ? 4 : 1,
-        stroke: isSelected ? '#01517f' : '',
-        class: isSelected ? 'high-z-index' : ''
-      },
-    }
-  })
-}
-
-// 设置选中背景色
-function switchColors() {
-  selectNodes.value = nodes.value.filter(node => node.data.selected)
-  nodes.value = nodes.value.map(node => ({
-    ...node,
-    style: {
-      ...node.style,
-      backgroundColor: node.data.selected ? '#01517f' : '#fff',
-    },
-  }))
-}
-
-// 添加节点和连线数据
-function AddData(list: any[], x: number, y: number, nodeId: string) {
-  const newNodes: Node[] = []
-  const newEdges = [...edges.value]
-
-  const spacing = 80
-  const startY = y - (list.length * spacing) / 2
-
-  list.forEach((label, index) => {
-    const newNodeId = `${nodeId}-${index + 1}` // 生成唯一 ID
-    const newY = startY + index * spacing
-
-    newNodes.push({
-      id: newNodeId,
-      data: { label, ...fixedLabel },
-      position: { x: x + 240, y: newY },
-      ...FixedConfiguration,
-    })
-
-    newEdges.push({
-      id: `e${nodeId}-${newNodeId}`,
-      source: nodeId,
-      target: newNodeId,
-      type: connectionType
-    })
-  })
-
-  nodes.value = [...nodes.value, ...newNodes]
-  edges.value = newEdges
-}
+})
 
-// 删除节点和连线数据
-function deteleData(nodeId: string) {
-  clickedNodes.value = clickedNodes.value.filter(item => (item.id + '').indexOf(nodeId) === -1)
-  nodes.value = nodes.value.filter(node => (node.id.indexOf(nodeId) > 0 || (node.id.indexOf(nodeId) === -1 || node.id === nodeId)))
-  edges.value = edges.value.filter(edge => edge.source !== nodeId && edge.source.indexOf(nodeId) === -1)
+// 切换视图方法
+const changeCalendarView = (viewName: string) => {
+  const calendarApi = calendarRef.value.getApi()
+  calendarApi.changeView(viewName)
 }
-
-// 展开收起节点
-function expandNode(nodeId: string, val: boolean) {
-  const filterNodes = nodes.value.filter(node => !(node.id.indexOf(nodeId) > 0 || (node.id.indexOf(nodeId) === -1 || node.id === nodeId))).map(item => item.id)
-  nodes.value = nodes.value.map(node => {
-    if (filterNodes.includes(node.id)) {
-      return {
-        ...node,
-        hidden: val
-      }
-    } else {
-      return node
-    }
-  })
-}
-
-
 </script>
 
+<style lang="scss" scoped>
+:deep(.fc-today-button) {
+  margin-right: 10px;
+} 
+</style> -->
+
 <template>
-  <div style="width: 100%; height: 100%;">
-    <VueFlow :nodes="nodes" :edges="edges" @node-click="onNodeClick" fit-view-on-init :edge-updater-layer="true">
-      <template #node-custom="{ data, id }">
-        <div class="flex flex-row">
-          <Handle class="handle left-handle" type="target" :position="Position.Left" :id="`${id}`" />
-          <div class="flex items-center">
-            <el-checkbox v-model="data.selected" size="large" @change="processSelectedNodes(id)" @click.stop
-              class="pr-2 tops"></el-checkbox>
-            <span :class="{ 'text-white': data.selected }">{{ data.label }}</span>
-          </div>
-          <Handle class="handle right-handle" type="source" :position="Position.Right" :id="`${id}`" />
-        </div>
-      </template>
-      <Background />
-    </VueFlow>
-  </div>
+  <div>123312</div>
 </template>
-
-<style lang='scss' scoped>
-.tops {
-  top: 2px
-}
-
-::deep(.vue-flow__node-custom) {
-  position: relative;
-}
-
-.high-z-index {
-  position: relative;
-  z-index: 9999 !important;
-  /* 提高连线层级 */
-}
-</style>