浏览代码

小提交一波

刘洋 9 月之前
父节点
当前提交
b5b44a76d4

+ 66 - 0
src/api/report.js

@@ -292,4 +292,70 @@ export const getDetailByProcess = (params) =>
     url: '/api/admin/service/analyse/detail',
     url: '/api/admin/service/analyse/detail',
     params,
     params,
   });
   });
+
+export const exportByProcess = (data) =>
+  request({
+    url: '/api/admin/service/analyse/detail/export',
+    params: data,
+    download: true,
+    loading: true,
+  });
+
+//项目进度监控
+export const processByRegion = (params) =>
+  request({
+    url: '/api/admin/crm/process/monitor/region',
+    params,
+  });
+export const processBySupplier = (params) =>
+  request({
+    url: '/api/admin/crm/process/monitor/supplier',
+    params,
+  });
+export const processByCoordinator = (params) =>
+  request({
+    url: '/api/admin/crm/process/monitor/region',
+    params,
+  });
+
+//sop预警监控
+export const warnByRegion = (params) =>
+  request({
+    url: '/api/admin/sop/warn/monitor/region',
+    params,
+  });
+export const warnBySupplier = (params) =>
+  request({
+    url: '/api/admin/sop/warn/monitor/supplier',
+    params,
+  });
+export const warnByCoordinator = (params) =>
+  request({
+    url: '/api/admin/sop/warn/monitor/coordinator',
+    params,
+  });
+
+//资源保障分析
+
+export const deviceSupplier = (params) =>
+  request({
+    url: '/api/admin/resource/security/device_supplier/report',
+    params,
+  });
+export const humanSupplier = (params) =>
+  request({
+    url: '/api/admin/resource/security/human_supplier/report',
+    params,
+  });
+
+export const leadDingException = (params) =>
+  request({
+    url: '/api/admin/resource/security/area_ding_exception/report',
+    params,
+  });
+export const humanDingException = (params) =>
+  request({
+    url: '/api/admin/resource/security/human_ding_exception/report',
+    params,
+  });
 /******************************************************************** */
 /******************************************************************** */

+ 6 - 2
src/components/common/china-point-chart/index.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <my-chart :options="options"></my-chart>
+  <my-chart :options="options" @chartClick="chartClick"></my-chart>
 </template>
 </template>
 <script setup name="ChinaPointChart">
 <script setup name="ChinaPointChart">
 import { computed, onMounted } from 'vue';
 import { computed, onMounted } from 'vue';
@@ -16,6 +16,7 @@ onMounted(() => {
 
 
 const opacityColorList = ['rgba(22, 93, 255, 0.30)', 'rgba(0, 180, 42, 0.50)'];
 const opacityColorList = ['rgba(22, 93, 255, 0.30)', 'rgba(0, 180, 42, 0.50)'];
 const props = defineProps({ data: Object });
 const props = defineProps({ data: Object });
+const emit = defineEmits(['click']);
 const handleVal = (val) => {
 const handleVal = (val) => {
   if (val < 10) {
   if (val < 10) {
     return val * 2;
     return val * 2;
@@ -164,7 +165,6 @@ const options = computed(() => {
           color: '#999', //省份标签字体颜色
           color: '#999', //省份标签字体颜色
           fontSize: '12',
           fontSize: '12',
           formatter: (p) => {
           formatter: (p) => {
-            console.log('ppp', p);
             let target = props.data.find((item) => item.province === p.name);
             let target = props.data.find((item) => item.province === p.name);
             let str =
             let str =
               '\n' + (target?.sopSum || 0) + '/' + (target?.sopNum || 0);
               '\n' + (target?.sopSum || 0) + '/' + (target?.sopNum || 0);
@@ -225,5 +225,9 @@ const options = computed(() => {
     series: dataHandle(props.data),
     series: dataHandle(props.data),
   };
   };
 });
 });
+
+const chartClick = (params) => {
+  emit('click', params);
+};
 </script>
 </script>
 <style lang="less" scoped></style>
 <style lang="less" scoped></style>

+ 0 - 1
src/components/global/chart/index.vue

@@ -80,7 +80,6 @@ watch(hasData, () => {
 
 
 const renderChart = ref(false);
 const renderChart = ref(false);
 const handleClick = (params) => {
 const handleClick = (params) => {
-  console.log('params:', params);
   emits('chartClick', params);
   emits('chartClick', params);
 };
 };
 const handleZrClick = (params) => {
 const handleZrClick = (params) => {

+ 6 - 13
src/router/asyncRoutes.js

@@ -111,13 +111,6 @@ export const devPushMenuList = [
   //   type: 'MENU',
   //   type: 'MENU',
   // },
   // },
   // {
   // {
-  //   id: '2001',
-  //   name: '派单分析',
-  //   parentId: '2000',
-  //   url: 'crmAnalyse',
-  //   type: 'MENU',
-  // },
-  // {
   //   id: '2002',
   //   id: '2002',
   //   name: '服务单元分析',
   //   name: '服务单元分析',
   //   parentId: '2000',
   //   parentId: '2000',
@@ -125,17 +118,17 @@ export const devPushMenuList = [
   //   type: 'MENU',
   //   type: 'MENU',
   // },
   // },
   // {
   // {
-  //   id: '2003',
-  //   name: 'SOP预警监控',
+  //   id: '2004',
+  //   name: '项目进度监控',
   //   parentId: '2000',
   //   parentId: '2000',
-  //   url: 'sopAnalyse',
+  //   url: 'sopSchedule',
   //   type: 'MENU',
   //   type: 'MENU',
   // },
   // },
   // {
   // {
-  //   id: '2004',
-  //   name: '项目进度监控',
+  //   id: '2003',
+  //   name: 'SOP预警监控',
   //   parentId: '2000',
   //   parentId: '2000',
-  //   url: 'sopSchedule',
+  //   url: 'sopAnalyse',
   //   type: 'MENU',
   //   type: 'MENU',
   // },
   // },
   // {
   // {

+ 339 - 269
src/views/report/project-analysis/index.vue

@@ -1,263 +1,353 @@
 <template>
 <template>
   <div class="project-analysis">
   <div class="project-analysis">
-    <report-header
-      title="项目进度监控"
-      v-model:dateRange="curTimeRange"
-      @timeChange="timeChange"
-    >
-      <t-select
-        style="width: 200px"
-        :options="serviceOptions"
+    <report-header title="服务单元分析" hideTimePicker>
+      <select-service-unit
         v-model="serviceId"
         v-model="serviceId"
-        :keys="{ label: 'name', value: 'id' }"
-        filterable
-      ></t-select>
+        clearable
+        defaultSelect
+        style="width: 220px"
+      ></select-service-unit>
     </report-header>
     </report-header>
     <div class="page-main">
     <div class="page-main">
       <div class="scroll-content">
       <div class="scroll-content">
-        <div class="row1 flex items-center">
-          <div class="card">
-            <div class="title">
-              <span class="label">大区项目阶段分布及对比</span>
-              <FullscreenIcon
-                class="cursor-pointer"
-                @click="chart1?.maximize"
-                color="#595959"
-              />
-            </div>
-            <div class="chart-wrap">
-              <my-chart
-                :options="options1"
-                @chartClick="chart1Click"
-                ref="chart1"
-              ></my-chart>
-            </div>
-          </div>
-          <div class="card">
-            <div class="title">
-              <span class="label">项目进度阶段总体分布占比</span>
-            </div>
-            <div class="chart-wrap">
-              <my-chart :options="options2"></my-chart>
-            </div>
-          </div>
-          <div class="card">
-            <div class="title">
-              <span class="label">供应商项目阶段分布及对比</span>
-              <FullscreenIcon
-                class="cursor-pointer"
-                @click="chart3?.maximize"
-                color="#595959"
-              />
-            </div>
-            <div class="chart-wrap">
-              <my-chart
-                :options="options3"
-                @chartClick="chart3Click"
-                ref="chart3"
-              ></my-chart>
-            </div>
-          </div>
+        <div class="col1">
+          <div class="module-title">按大区进度统计</div>
+          <t-table
+            size="small"
+            row-key="leadId"
+            :columns="columns1"
+            :data="tableData1 || []"
+            :loading="loading1"
+            bordered
+          >
+            <template #leadName="{ col, row }">
+              <span v-if="row[col.colKey]">{{ row[col.colKey] }}</span>
+              <span v-else style="font-weight: bold">合计</span>
+            </template>
+            <template #total="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    leadId: row.leadId,
+                    process: '',
+                  })
+                "
+                >{{ row.total }}</t-link
+              >
+            </template>
+            <template #prepareSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'PREPARE',
+                    leadId: row.leadId,
+                  })
+                "
+                >{{ row.prepareSopNum }}({{ row.prepareSopRatio }})</t-link
+              >
+            </template>
+            <template #scanSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'SCAN',
+                    leadId: row.leadId,
+                  })
+                "
+                >{{ row.scanSopNum }}({{ row.scanSopRatio }})</t-link
+              >
+            </template>
+            <template #markSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'MARK',
+                    leadId: row.leadId,
+                  })
+                "
+                >{{ row.markSopNum }}({{ row.markSopRatio }})</t-link
+              >
+            </template>
+            <template #finalSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'FINAL',
+                    leadId: row.leadId,
+                  })
+                "
+                >{{ row.finalSopNum }}({{ row.finalSopRatio }})</t-link
+              >
+            </template>
+            <template #finishSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'FINISH',
+                    leadId: row.leadId,
+                  })
+                "
+                >{{ row.finishSopNum }}({{ row.finishSopRatio }})</t-link
+              >
+            </template>
+          </t-table>
+          <div class="module-title m-t-15px">按人力商进度统计</div>
+          <t-table
+            size="small"
+            row-key="leadId"
+            :columns="columns2"
+            :data="tableData2 || []"
+            bordered
+            :loading="loading2"
+          >
+            <template #supplierName="{ col, row }">
+              <span v-if="row[col.colKey]">{{ row[col.colKey] }}</span>
+              <span v-else style="font-weight: bold">合计</span>
+            </template>
+            <template #total="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    supplierId: row.supplierId,
+                    process: '',
+                  })
+                "
+                >{{ row.total }}</t-link
+              >
+            </template>
+            <template #prepareSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'PREPARE',
+                    supplierId: row.supplierId,
+                  })
+                "
+                >{{ row.prepareSopNum }}({{ row.prepareSopRatio }})</t-link
+              >
+            </template>
+            <template #scanSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'SCAN',
+                    supplierId: row.supplierId,
+                  })
+                "
+                >{{ row.scanSopNum }}({{ row.scanSopRatio }})</t-link
+              >
+            </template>
+            <template #markSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'MARK',
+                    supplierId: row.supplierId,
+                  })
+                "
+                >{{ row.markSopNum }}({{ row.markSopRatio }})</t-link
+              >
+            </template>
+            <template #finalSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'FINAL',
+                    supplierId: row.supplierId,
+                  })
+                "
+                >{{ row.finalSopNum }}({{ row.finalSopRatio }})</t-link
+              >
+            </template>
+            <template #finishSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'FINISH',
+                    supplierId: row.supplierId,
+                  })
+                "
+                >{{ row.finishSopNum }}({{ row.finishSopRatio }})</t-link
+              >
+            </template>
+          </t-table>
         </div>
         </div>
-        <div class="row2">
-          <div class="card">
-            <div class="title">
-              <span class="label">项目燃尽图</span>
-            </div>
-            <div class="chart-wrap">
-              <my-chart :options="options4"></my-chart>
-            </div>
-          </div>
+        <div class="col2">
+          <div class="module-title">按区域协调人进度统计</div>
+
+          <t-table
+            size="small"
+            row-key="leadId"
+            :columns="columns3"
+            :data="tableData3 || []"
+            bordered
+            :loading="loading3"
+          >
+            <template #coordinatorName="{ col, row }">
+              <span v-if="row[col.colKey]">{{ row[col.colKey] }}</span>
+              <span v-else style="font-weight: bold">合计</span>
+            </template>
+            <template #total="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({ coordinatorId: row.coordinatorId, process: '' })
+                "
+                >{{ row.total }}</t-link
+              >
+            </template>
+            <template #prepareSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'PREPARE',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.prepareSopNum }}({{ row.prepareSopRatio }})</t-link
+              >
+            </template>
+            <template #scanSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'SCAN',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.scanSopNum }}({{ row.scanSopRatio }})</t-link
+              >
+            </template>
+            <template #markSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'MARK',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.markSopNum }}({{ row.markSopRatio }})</t-link
+              >
+            </template>
+            <template #finalSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'FINAL',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.finalSopNum }}({{ row.finalSopRatio }})</t-link
+              >
+            </template>
+            <template #finishSopNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    process: 'FINISH',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.finishSopNum }}({{ row.finishSopRatio }})</t-link
+              >
+            </template>
+          </t-table>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
-    <SopDetailDrillDialog
-      v-model:visible="showSopDetailDrill"
-      :data="sopDetailDrillData"
-      :footer="false"
-    ></SopDetailDrillDialog>
+    <DetailDrawer
+      v-model="showDetailDrawer"
+      :process="curProcess"
+      ref="DetailDrawerRef"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup name="ProjectAnalysis">
 <script setup name="ProjectAnalysis">
-import { ref, computed, onMounted, watch } from 'vue';
+import { ref, computed, watch } from 'vue';
+import ChinaPointChart from '@/components/common/china-point-chart/index.vue';
 import { useRequest } from 'vue-request';
 import { useRequest } from 'vue-request';
+import {} from '@/api/report';
+import { FullscreenIcon } from 'tdesign-icons-vue-next';
+import { cloneDeep } from 'lodash-es';
+import DetailDrawer from '../service-analysis/detail-drawer.vue';
 import {
 import {
-  createCakePieOption,
-  createPercentBarOption,
-  createLineOption,
-} from '@/utils/chart';
-import {
-  serviceServiceList,
-  projectStageAnalysis,
-  projectProcessAnalysis,
+  processByRegion,
+  processBySupplier,
+  processByCoordinator,
 } from '@/api/report';
 } from '@/api/report';
-import SopDetailDrillDialog from './sop-detail-drill-dialog.vue';
-import { FullscreenIcon } from 'tdesign-icons-vue-next';
-const chart1 = ref();
-const chart3 = ref();
-const showSopDetailDrill = ref(false);
-const sopDetailDrillData = ref({
-  serviceId: '',
-  serviceName: '',
-  regionId: '',
-  supplierId: '',
-  regionName: '',
-  supplierName: '',
-});
-const curTimeRange = ref([]);
-const timeParams = computed(() => {
-  return {
-    startTime: new Date(curTimeRange.value[0] + ' 00:00:00').getTime(),
-    endTime: new Date(curTimeRange.value[1] + ' 23:59:59').getTime(),
-  };
-});
-const serviceOptions = ref([]);
-const serviceId = ref('');
-const timeChange = (time) => {
-  serviceServiceList(timeParams.value).then((res) => {
-    serviceOptions.value = res || [];
-    res?.length && (serviceId.value = res[0].id);
-  });
-};
-const sortKeys = ['prepare', 'scan', 'evaluation', 'summary', 'finish'];
-const legendData = ['准备', '扫描', '评卷', '收尾', '已完结'];
+const showDetailDrawer = ref(false);
+const curProcess = ref('');
 const {
 const {
-  data: result1,
-  loading: loading1,
+  data: tableData1,
   run: run1,
   run: run1,
-} = useRequest(projectStageAnalysis);
+  loading: loading1,
+} = useRequest(processByRegion);
 const {
 const {
-  data: result2,
-  loading: loading2,
+  data: tableData2,
   run: run2,
   run: run2,
-} = useRequest(projectStageAnalysis);
+  loading: loading2,
+} = useRequest(processBySupplier);
 const {
 const {
-  data: result3,
-  loading: loading3,
+  data: tableData3,
   run: run3,
   run: run3,
-} = useRequest(projectStageAnalysis);
-const {
-  data: result4,
-  loading: loading4,
-  run: run4,
-} = useRequest(projectProcessAnalysis);
-const options4 = computed(() => {
-  let actual = result4.value?.actual || [];
-  let plan = result4.value?.plan || [];
-
-  let xData = Array.from(
-    new Set([
-      ...actual.map((item) => item.datetime),
-      ...plan.map((item) => item.datetime),
-    ])
-  );
-  xData.sort();
-  let data1 = xData.map((datetime) => {
-    let find1 = actual.find((item) => item.datetime == datetime);
-    return find1 ? find1.count : 0;
-  });
-  let data2 = xData.map((datetime) => {
-    let find2 = plan.find((item) => item.datetime == datetime);
-    return find2 ? find2.count : 0;
-  });
-  // let data1 = actual.map((item) => {
-  //   return xData.includes(item.datetime) ? item.count : 0;
-  // });
-  // let data2 = plan.map((item) => {
-  //   return xData.includes(item.datetime) ? item.count : 0;
-  // });
-  let sData = [
-    { name: '实际', data: data1 },
-    { name: '计划', data: data2 },
-  ];
-  return createLineOption({ xData, sData }, { yAxis: { name: '单位:人/天' } });
-});
-
-watch(serviceId, (serviceId) => {
-  run1({ serviceId, group: 'REGION' });
-  run2({ serviceId, group: 'POPULATION' });
-  run3({ serviceId, group: 'SUPPLIER' });
-  run4({ serviceId });
-});
-const options1 = computed(() => {
-  let res = result1.value || {};
-  let xData = Object.keys(res);
+  loading: loading3,
+} = useRequest(processByCoordinator);
 
 
-  let originData = Object.values(res).map((item) => {
-    return sortKeys.map((v) => item[v] || 0);
-  });
-  return createPercentBarOption({
-    xData: xData,
-    originData: originData,
-    legendData: legendData,
-  });
-});
-const chart1Click = (params) => {
-  let obj = result1.value?.[params.name];
-  sopDetailDrillData.value = {
-    serviceId: serviceId.value,
-    serviceName: serviceOptions.value.find((item) => item.id == serviceId.value)
-      ?.name,
-    regionId: obj.region_id,
-    supplierId: '',
-    regionName: params.name,
-    supplierName: '',
-  };
-  showSopDetailDrill.value = true;
-};
-const chart3Click = (params) => {
-  let obj = result3.value?.[params.name];
-  sopDetailDrillData.value = {
-    serviceId: serviceId.value,
-    serviceName: serviceOptions.value.find((item) => item.id == serviceId.value)
-      ?.name,
-    regionId: '',
-    supplierId: obj.supplier_id,
-    regionName: '',
-    supplierName: params.name,
-  };
-  showSopDetailDrill.value = true;
+const DetailDrawerRef = ref();
+const searchByX = (obj) => {
+  DetailDrawerRef.value.searchByOther(obj);
 };
 };
-const options2 = computed(() => {
-  let obj = result2.value?.POPULATION || {};
-  let data = sortKeys.map((prop, index) => {
-    return {
-      name: legendData[index],
-      value: obj[prop] || 0,
-    };
-  });
-  return createCakePieOption(
-    {
-      data: data,
-      radius: [0, 70],
-      center: ['50%', '40%'],
-      seriesName: '总体分布占比',
-      showLegendValue: false,
-    },
-    {
-      legend: {
-        orient: 'horizontal',
-        top: null,
-        right: null,
-        bottom: '0%',
-        left: 'center',
-      },
-    }
-  );
-});
-const options3 = computed(() => {
-  let res = result3.value || {};
-  let xData = Object.keys(res);
-  let originData = Object.values(res).map((item) => {
-    return sortKeys.map((v) => item[v] || 0);
-  });
-  return createPercentBarOption({
-    xData: xData,
-    originData: originData,
-    legendData: legendData,
-  });
+const serviceId = ref('');
+watch(serviceId, (serviceUnitId) => {
+  run1({ serviceUnitId });
+  run2({ serviceUnitId });
+  run3({ serviceUnitId });
 });
 });
+const columns1 = [
+  { colKey: 'leadName', title: '大区经理' },
+  { colKey: 'total', title: '总数' },
+  { colKey: 'prepareSopNum', title: '准备阶段' },
+  { colKey: 'scanSopNum', title: '扫描阶段' },
+  { colKey: 'markSopNum', title: '评卷阶段' },
+  { colKey: 'finalSopNum', title: '收尾阶段' },
+  { colKey: 'finishSopNum', title: '已完成' },
+];
+const columns2 = [
+  { colKey: 'supplierName', title: '人力商' },
+  { colKey: 'total', title: '总数' },
+  { colKey: 'prepareSopNum', title: '准备阶段' },
+  { colKey: 'scanSopNum', title: '扫描阶段' },
+  { colKey: 'markSopNum', title: '评卷阶段' },
+  { colKey: 'finalSopNum', title: '收尾阶段' },
+  { colKey: 'finishSopNum', title: '已完成' },
+];
+const columns3 = [
+  { colKey: 'coordinatorName', title: '区域协调人' },
+  { colKey: 'total', title: '总数' },
+  { colKey: 'prepareSopNum', title: '准备阶段' },
+  { colKey: 'scanSopNum', title: '扫描阶段' },
+  { colKey: 'markSopNum', title: '评卷阶段' },
+  { colKey: 'finalSopNum', title: '收尾阶段' },
+  { colKey: 'finishSopNum', title: '已完成' },
+];
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
@@ -268,47 +358,27 @@ const options3 = computed(() => {
     overflow: auto;
     overflow: auto;
     .scroll-content {
     .scroll-content {
       height: 100%;
       height: 100%;
-      min-height: 600px;
       min-width: 1000px;
       min-width: 1000px;
-      .row1 {
-        height: calc(60% - 16px);
-        .card {
-          width: calc((100% - 30px) / 3);
-          height: 100%;
-          &:not(:first-child) {
-            margin-left: 15px;
+      display: flex;
+      justify-content: space-between;
+      .col1,
+      .col2 {
+        width: calc(50% - 8px);
+        :deep(.t-table__body) {
+          font-size: 12px !important;
+          .t-link {
+            font-size: 12px !important;
           }
           }
         }
         }
-      }
-      .row2 {
-        margin-top: 15px;
-        height: 40%;
-        .card {
-          height: 100%;
+        .module-title {
+          height: 30px;
+          font-size: 16px;
+          font-weight: bold;
+          color: #000;
+          text-align: center;
         }
         }
       }
       }
     }
     }
-
-    .card {
-      padding: 4px 10px 10px 10px;
-      background-color: #fff;
-      border: 1px solid #e5e5e5;
-      border-radius: 4px;
-      .title {
-        height: 36px;
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        .label {
-          color: #262626;
-          font-size: 14px;
-          // font-weight: bold;
-        }
-      }
-      .chart-wrap {
-        height: calc(100% - 36px);
-      }
-    }
   }
   }
 }
 }
 </style>
 </style>

+ 55 - 0
src/views/report/resource-analysis/index.vue

@@ -0,0 +1,55 @@
+<template>
+  <div class="resource-analysis">
+    <report-header title="服务单元分析" hideTimePicker>
+      <select-service-unit
+        v-model="serviceId"
+        clearable
+        defaultSelect
+        style="width: 220px"
+      ></select-service-unit>
+    </report-header>
+    <div class="page-main">
+      <div class="scroll-content">
+        <div class="col1"></div>
+        <div class="col2"></div>
+        <div class="col3"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup name="ResourceAnalysis">
+import { ref, computed, watch } from 'vue';
+import { useRequest } from 'vue-request';
+import { FullscreenIcon } from 'tdesign-icons-vue-next';
+import { cloneDeep } from 'lodash-es';
+import {} from '@/api/report';
+
+const serviceId = ref('');
+</script>
+
+<style lang="less" scoped>
+.resource-analysis {
+  .page-main {
+    height: calc(100vh - 57px);
+    padding: 15px;
+    overflow: auto;
+    .scroll-content {
+      height: 100%;
+      display: flex;
+      .col1,
+      .col2,
+      .col3 {
+        width: calc((100% - 45px) / 3);
+      }
+      .col1 {
+        height: 100%;
+        .level1,
+        .level2 {
+          height: calc((100% - 30px) / 2);
+        }
+      }
+    }
+  }
+}
+</style>

+ 170 - 30
src/views/report/service-analysis/detail-drawer.vue

@@ -24,7 +24,8 @@
         <t-table
         <t-table
           size="small"
           size="small"
           :columns="columns"
           :columns="columns"
-          :data="tableData"
+          row-key="key"
+          :data="data"
           bordered
           bordered
           :pagination="{
           :pagination="{
             defaultCurrent: 1,
             defaultCurrent: 1,
@@ -35,53 +36,136 @@
             total: pagination.total,
             total: pagination.total,
             current: pagination.pageNumber,
             current: pagination.pageNumber,
           }"
           }"
-          v-loading="tableLoading"
-        ></t-table>
+        >
+          <template #fieldObj="{ row, col }">
+            {{ fieldObjMap[row[col.colKey]] || '-' }}
+          </template>
+          <template #regionCoordinator="{ row, col }">
+            {{ row[col.colKey]?.userName }}
+          </template>
+          <template #projectManager="{ row, col }">
+            {{ row[col.colKey]?.userName }}
+          </template>
+          <template #engineerList="{ row, col }">
+            {{ row[col.colKey]?.map((item) => item.userName).join('、') }}
+          </template>
+
+          <template #operate="{ row }">
+            <div class="table-operations" @click.stop>
+              <t-link
+                theme="primary"
+                hover="color"
+                @click="showSopDrawer(row.sopNo)"
+              >
+                查阅sop详情
+              </t-link>
+              <t-link
+                v-if="type === 'warn'"
+                theme="primary"
+                hover="color"
+                @click="toWarnInfo(row)"
+              >
+                查阅预警信息
+              </t-link>
+            </div>
+          </template>
+        </t-table>
       </div>
       </div>
     </div>
     </div>
+    <sop-step-dialog
+      v-model:visible="showSopStepDialog"
+      :sop="curSopData"
+      :type="curSopType"
+    ></sop-step-dialog>
+    <delay-warn-flow-dialog
+      v-model:visible="showDelayWarnFlowDialog"
+      :curRow="curWarnData"
+      type="view"
+      :enumFilter="enumFilter"
+    ></delay-warn-flow-dialog>
   </t-drawer>
   </t-drawer>
 </template>
 </template>
 <script name="DetailDrawer" setup>
 <script name="DetailDrawer" setup>
 import { useVModel } from '@vueuse/core';
 import { useVModel } from '@vueuse/core';
-import { ref, reactive, watch } from 'vue';
+import { ref, reactive, watch, computed } from 'vue';
 import useFetchTable from '@/hooks/useFetchTable';
 import useFetchTable from '@/hooks/useFetchTable';
-import { getDetailByProcess } from '@/api/report';
+import { getDetailByProcess, exportByProcess } from '@/api/report';
 import { PROJECT_PROCESS } from '@/config/constants';
 import { PROJECT_PROCESS } from '@/config/constants';
 import { dictToOptionList } from '@/utils/tool';
 import { dictToOptionList } from '@/utils/tool';
+import { MessagePlugin } from 'tdesign-vue-next';
+import { getSopDataBySopNo, getDelayWarnList } from '@/api/sop';
+import { omit } from 'lodash-es';
+import SopStepDialog from '@/views/sop/sop-manage/sop-step/sop-step-dialog.vue';
+import DelayWarnFlowDialog from '@/views/sop/sop-monitor/delay-warning/flow-dialog.vue';
 
 
+const showDelayWarnFlowDialog = ref(false);
+const fieldObjMap = {
+  scan_start_time: '扫描开始时间',
+  scan_end_time: '扫描结束时间',
+  mark_start_time: '评卷开始时间',
+  mark_end_time: '评卷结束时间',
+};
+const enumFilter = (val) => {
+  return fieldObjMap[val] || '--';
+};
 const emit = defineEmits(['update:modelValue']);
 const emit = defineEmits(['update:modelValue']);
 const props = defineProps({
 const props = defineProps({
   modelValue: { type: Boolean, default: false },
   modelValue: { type: Boolean, default: false },
-  process: { type: String, default: '' },
+  supplierId: { type: String, default: '' },
+  coordinatorId: { type: String, default: '' },
+  type: { type: String, default: '' },
+});
+const curSopType = ref('');
+const curSopData = ref({});
+const showSopStepDialog = ref(false);
+const showSopDrawer = async (sopNo, type = 'view') => {
+  curSopType.value = type;
+  getSopDataBySopNo(sopNo).then((res) => {
+    if (res?.records?.length) {
+      curSopData.value = res.records[0];
+      showSopStepDialog.value = true;
+    }
+  });
+};
+const columns = computed(() => {
+  return [
+    { colKey: 'customName', title: '客户名称' },
+    { colKey: 'crmName', title: '项目名称' },
+    props.type === 'warn'
+      ? { colKey: 'fieldObj', title: '预警字段' }
+      : { colKey: 'courseName', title: '"科目名称' },
+    { colKey: 'process', title: '当前进度' },
+    { colKey: 'leadName', title: '大区经理' },
+    {
+      colKey: 'regionCoordinator',
+      title: '区域协调人',
+      cell: 'regionCoordinator',
+    },
+    { colKey: 'projectManager', title: '项目经理', cell: 'projectManager' },
+    { colKey: 'engineerList', title: '工程师', cell: 'engineerList' },
+    {
+      title: '操作',
+      colKey: 'operate',
+      fixed: 'right',
+      width: props.type === 'warn' ? 240 : 150,
+    },
+  ];
 });
 });
-watch(
-  () => props.process,
-  (val) => {
-    params.process = val;
-  }
-);
-const columns = [
-  { colKey: 'customName', title: '客户名称' },
-  { colKey: 'crmName', title: '项目名称' },
-  { colKey: 'courseName', title: '"科目名称' },
-  { colKey: 'process', title: '当前进度' },
-  { colKey: 'leadName', title: '大区经理' },
-  {
-    colKey: 'regionCoordinator',
-    title: '区域协调人',
-    cell: 'regionCoordinator',
-  },
-  { colKey: 'projectManager', title: '项目经理', cell: 'projectManager' },
-  { colKey: 'engineerList', title: '工程师', cell: 'engineerList' },
-];
 const visible = useVModel(props, 'modelValue', emit);
 const visible = useVModel(props, 'modelValue', emit);
 const params = reactive({
 const params = reactive({
   serviceUnitId: '',
   serviceUnitId: '',
   crmUserId: '',
   crmUserId: '',
   customName: '',
   customName: '',
-  process: '',
   leadId: '',
   leadId: '',
+  province: '',
+  supplierId: '',
+  coordinatorId: '',
+  type: '',
+  fieldObj: '',
 });
 });
+if (props.type === 'warn') {
+  params.type = props.type;
+}
 const fields = ref([
 const fields = ref([
   {
   {
     prop: 'crmUserId',
     prop: 'crmUserId',
@@ -119,13 +203,29 @@ const fields = ref([
         type: 'button',
         type: 'button',
         text: '搜索',
         text: '搜索',
         onClick: () => {
         onClick: () => {
-          search();
+          mixSearch();
         },
         },
       },
       },
       {
       {
         type: 'button',
         type: 'button',
         text: '导出',
         text: '导出',
-        onClick: () => {},
+        onClick: () => {
+          let p = {};
+          if (otherParamActive.value) {
+            p = { ...params };
+          } else {
+            p = omit(params, [
+              'province',
+              'supplierId',
+              'coordinatorId',
+              'fieldObj',
+            ]);
+          }
+          exportByProcess(p).then(() => {
+            MessagePlugin.success('导出成功');
+            otherParamActive.value = false;
+          });
+        },
       },
       },
     ],
     ],
   },
   },
@@ -135,8 +235,48 @@ const { pagination, tableData, fetchData, search, onChange } = useFetchTable(
   getDetailByProcess,
   getDetailByProcess,
   {
   {
     params: params,
     params: params,
-  }
+  },
+  false
 );
 );
+const data = computed(() => {
+  return (tableData.value || []).map((item) => ({
+    key: Math.random(),
+    ...item,
+  }));
+});
+const mixSearch = () => {
+  if (otherParamActive.value) {
+    otherParamActive.value = false;
+  }
+  search();
+};
+const otherParamActive = ref(false);
+watch(visible, (val) => {
+  if (val) {
+    // mixSearch();
+  } else {
+    otherParamActive.value = false;
+  }
+});
+const searchByOther = (obj) => {
+  Object.keys(obj).forEach((key) => {
+    params[key] = obj[key];
+  });
+  search();
+  otherParamActive.value = true;
+  visible.value = true;
+};
+defineExpose({ searchByOther });
+
+const curWarnData = ref({});
+const toWarnInfo = (row) => {
+  getDelayWarnList({ id: row.warnId, pageNumber: 1, pageSize: 1 }).then(
+    (res) => {
+      curWarnData.value = res.records[0];
+      showDelayWarnFlowDialog.value = true;
+    }
+  );
+};
 </script>
 </script>
 <style lang="less" scoped>
 <style lang="less" scoped>
 .table-search {
 .table-search {

+ 12 - 4
src/views/report/service-analysis/index.vue

@@ -50,19 +50,19 @@
                 <div class="td">{{ item.sopNum }}</div>
                 <div class="td">{{ item.sopNum }}</div>
                 <div class="td">{{ item.ratio }}</div>
                 <div class="td">{{ item.ratio }}</div>
                 <div class="td">
                 <div class="td">
-                  <t-link theme="primary">详情</t-link>
+                  <t-link theme="primary" @click="toDetail(item)">详情</t-link>
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
         <div class="col2">
         <div class="col2">
-          <ChinaPointChart :data="mapData"></ChinaPointChart>
+          <ChinaPointChart :data="mapData" @click="mapClick"></ChinaPointChart>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
 
 
-    <DetailDrawer v-model="showDetailDrawer" />
+    <DetailDrawer v-model="showDetailDrawer" ref="DetailDrawerRef" />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -79,7 +79,7 @@ import { FullscreenIcon } from 'tdesign-icons-vue-next';
 import { cloneDeep } from 'lodash-es';
 import { cloneDeep } from 'lodash-es';
 import { PROJECT_PROCESS } from '@/config/constants';
 import { PROJECT_PROCESS } from '@/config/constants';
 import DetailDrawer from './detail-drawer.vue';
 import DetailDrawer from './detail-drawer.vue';
-const showDetailDrawer = ref(true);
+const showDetailDrawer = ref(false);
 const serviceId = ref('');
 const serviceId = ref('');
 const overviewData = ref({});
 const overviewData = ref({});
 const table2data = ref([]);
 const table2data = ref([]);
@@ -105,6 +105,14 @@ watch(serviceId, (serviceUnitId) => {
   _getProjectProgress(serviceUnitId);
   _getProjectProgress(serviceUnitId);
   _getChinaMapData(serviceUnitId);
   _getChinaMapData(serviceUnitId);
 });
 });
+
+const DetailDrawerRef = ref();
+const mapClick = (params) => {
+  DetailDrawerRef.value.searchByOther({ province: params.name });
+};
+const toDetail = (item) => {
+  DetailDrawerRef.value.searchByOther({ process: item.process });
+};
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>

+ 285 - 678
src/views/report/sop-analysis/index.vue

@@ -1,573 +1,310 @@
 <template>
 <template>
   <div class="sop-analysis">
   <div class="sop-analysis">
-    <report-header
-      title="SOP预警监控"
-      v-model:dateRange="curTimeRange"
-      @timeChange="timeChange"
-    >
-      <t-select
-        style="width: 200px"
-        :options="serviceOptions"
+    <report-header title="服务单元分析" hideTimePicker>
+      <select-service-unit
         v-model="serviceId"
         v-model="serviceId"
-        :keys="{ label: 'name', value: 'id' }"
-        filterable
-      ></t-select>
+        clearable
+        defaultSelect
+        style="width: 220px"
+      ></select-service-unit>
     </report-header>
     </report-header>
     <div class="page-main">
     <div class="page-main">
       <div class="scroll-content">
       <div class="scroll-content">
         <div class="col1">
         <div class="col1">
-          <div class="card">
-            <div class="title">
-              <t-select
-                v-model="sort1"
-                style="width: calc(100% - 50px)"
-                @change="changeSort('CRM', 1)"
+          <div class="module-title">按大区预警统计</div>
+          <t-table
+            size="small"
+            row-key="leadId"
+            :columns="columns1"
+            :data="tableData1 || []"
+            bordered
+            :loading="loading1"
+          >
+            <template #leadName="{ col, row }">
+              <span v-if="row[col.colKey]">{{ row[col.colKey] }}</span>
+              <span v-else style="font-weight: bold">合计</span>
+            </template>
+            <template #total="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    leadId: row.leadId,
+                    fieldObj: '',
+                  })
+                "
+                >{{ row.total }}</t-link
+              >
+            </template>
+            <template #scanStartTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'scan_start_time',
+                    leadId: row.leadId,
+                  })
+                "
+                >{{ row.scanStartTimeNum }}({{
+                  row.scanStarTimeRatio
+                }})</t-link
+              >
+            </template>
+            <template #scanEndTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'scan_end_time',
+                    leadId: row.leadId,
+                  })
+                "
+                >{{ row.scanEndTimeNum }}({{ row.scanEndTimeRatio }})</t-link
+              >
+            </template>
+            <template #markStartTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'mark_start_time',
+                    leadId: row.leadId,
+                  })
+                "
+                >{{ row.markStartTimeNum }}({{
+                  row.markStartTimeRatio
+                }})</t-link
               >
               >
-                <t-option
-                  value="PENDING"
-                  label="项目预警待处理TOP10"
-                ></t-option>
-                <t-option
-                  value="SLOWEST"
-                  label="项目预警处理最慢TOP10"
-                ></t-option>
-                <t-option
-                  value="FASTEST"
-                  label="项目预警处理最快TOP10"
-                ></t-option>
-              </t-select>
-              <FullscreenIcon
-                class="cursor-pointer"
-                @click="chart1?.maximize"
-                color="#595959"
-              />
-            </div>
-            <div class="chart-wrap">
-              <table-loop
-                ref="chart1"
-                :data="tableDataHandle(result1)"
-                :columns="tableColumns1"
-                :title="
-                  sort1 == 'PENDING'
-                    ? '项目预警待处理TOP10'
-                    : sort1 == 'SLOWEST'
-                    ? '项目预警处理最慢TOP10'
-                    : '项目预警处理最快TOP10'
+            </template>
+            <template #markEndTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'mark_end_time',
+                    leadId: row.leadId,
+                  })
                 "
                 "
-              ></table-loop>
-            </div>
-          </div>
-          <div class="card">
-            <div class="title">
-              <t-select
-                v-model="sort2"
-                style="width: calc(100% - 50px)"
-                @change="changeSort('REGION', 2)"
+                >{{ row.markEndTimeNum }}({{ row.markEndTimeRatio }})</t-link
               >
               >
-                <t-option
-                  value="PENDING"
-                  label="大区预警待处理TOP10"
-                ></t-option>
-                <t-option
-                  value="SLOWEST"
-                  label="大区预警处理最慢TOP10"
-                ></t-option>
-                <t-option
-                  value="FASTEST"
-                  label="大区预警处理最快TOP10"
-                ></t-option>
-              </t-select>
-              <FullscreenIcon
-                class="cursor-pointer"
-                @click="chart2?.maximize"
-                color="#595959"
-              />
-            </div>
-            <div class="chart-wrap">
-              <table-loop
-                ref="chart2"
-                :data="tableDataHandle(result2)"
-                :columns="tableColumns2"
-                :title="
-                  sort2 == 'PENDING'
-                    ? '大区预警待处理TOP10'
-                    : sort2 == 'SLOWEST'
-                    ? '大区预警处理最慢TOP10'
-                    : '大区预警处理最快TOP10'
+            </template>
+          </t-table>
+          <div class="module-title m-t-15px">按人力商进度统计</div>
+          <t-table
+            size="small"
+            row-key="leadId"
+            :columns="columns2"
+            :data="tableData2 || []"
+            bordered
+            :loading="loading2"
+          >
+            <template #supplierName="{ col, row }">
+              <span v-if="row[col.colKey]">{{ row[col.colKey] }}</span>
+              <span v-else style="font-weight: bold">合计</span>
+            </template>
+            <template #total="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    supplierId: row.supplierId,
+                    fieldObj: '',
+                  })
                 "
                 "
-              ></table-loop>
-            </div>
-          </div>
-          <div class="card">
-            <div class="title">
-              <t-select
-                v-model="sort3"
-                style="width: calc(100% - 50px)"
-                @change="changeSort('SUPPLIER', 3)"
+                >{{ row.total }}</t-link
               >
               >
-                <t-option
-                  value="PENDING"
-                  label="供应商预警待处理TOP10"
-                ></t-option>
-                <t-option
-                  value="SLOWEST"
-                  label="供应商预警处理最慢TOP10"
-                ></t-option>
-                <t-option
-                  value="FASTEST"
-                  label="供应商预警处理最快TOP10"
-                ></t-option>
-              </t-select>
-              <FullscreenIcon
-                class="cursor-pointer"
-                @click="chart3?.maximize"
-                color="#595959"
-              />
-            </div>
-            <div class="chart-wrap">
-              <table-loop
-                ref="chart3"
-                :data="tableDataHandle(result3)"
-                :columns="tableColumns3"
-                :title="
-                  sort3 == 'PENDING'
-                    ? '供应商预警待处理TOP10'
-                    : sort3 == 'SLOWEST'
-                    ? '供应商预警处理最慢TOP10'
-                    : '供应商预警处理最快TOP10'
+            </template>
+            <template #scanStartTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'scan_start_time',
+                    supplierId: row.supplierId,
+                  })
                 "
                 "
-              ></table-loop>
-            </div>
-          </div>
+                >{{ row.scanStartTimeNum }}({{
+                  row.scanStarTimeRatio
+                }})</t-link
+              >
+            </template>
+            <template #scanEndTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'scan_end_time',
+                    supplierId: row.supplierId,
+                  })
+                "
+                >{{ row.scanEndTimeNum }}({{ row.scanEndTimeRatio }})</t-link
+              >
+            </template>
+            <template #markStartTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'mark_start_time',
+                    supplierId: row.supplierId,
+                  })
+                "
+                >{{ row.markStartTimeNum }}({{
+                  row.markStartTimeRatio
+                }})</t-link
+              >
+            </template>
+            <template #markEndTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'mark_end_time',
+                    supplierId: row.supplierId,
+                  })
+                "
+                >{{ row.markEndTimeNum }}({{ row.markEndTimeRatio }})</t-link
+              >
+            </template>
+          </t-table>
         </div>
         </div>
         <div class="col2">
         <div class="col2">
-          <div class="card">
-            <div class="tab-box">
-              <div
-                class="tab"
-                @click="centerGroup = 'REGION'"
-                :class="{ active: centerGroup === 'REGION' }"
-                >按大区</div
+          <div class="module-title">按区域协调人预警统计</div>
+
+          <t-table
+            size="small"
+            row-key="leadId"
+            :columns="columns3"
+            :data="tableData3 || []"
+            bordered
+            :loading="loading3"
+          >
+            <template #coordinatorName="{ col, row }">
+              <span v-if="row[col.colKey]">{{ row[col.colKey] }}</span>
+              <span v-else style="font-weight: bold">合计</span>
+            </template>
+            <template #total="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({ coordinatorId: row.coordinatorId, fieldObj: '' })
+                "
+                >{{ row.total }}</t-link
               >
               >
-              <div
-                class="tab"
-                @click="centerGroup = 'SUPPLIER'"
-                :class="{ active: centerGroup === 'SUPPLIER' }"
-                >按人力供应商</div
+            </template>
+            <template #scanStartTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'scan_start_time',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.scanStartTimeNum }}({{
+                  row.scanStarTimeRatio
+                }})</t-link
               >
               >
-            </div>
-            <div class="clear-both"></div>
-            <div class="list-wrap" v-if="result7?.length">
-              <div
-                class="list-item"
-                v-for="(item, index) in result7"
-                :key="index"
+            </template>
+            <template #scanEndTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'scan_end_time',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.scanEndTimeNum }}({{ row.scanEndTimeRatio }})</t-link
               >
               >
-                <div class="item-head">
-                  {{
-                    centerGroup === 'SUPPLIER'
-                      ? item.supplierName
-                      : item.regionName
-                  }}
-                </div>
-                <div class="item-body">
-                  <div class="row1">
-                    <div class="grid-item">
-                      <div class="label">派单数</div>
-                      <p>{{ item.crmNum }}</p>
-                    </div>
-                    <div class="grid-item">
-                      <div class="label">完成进度(%)</div>
-                      <p>{{ division(item.finishCrmNum, item.crmNum) }}</p>
-                    </div>
-                    <div class="grid-item">
-                      <div class="label">平均处理时限(小时)</div>
-                      <p class="red">{{ (item.avgMinutes / 60).toFixed(2) }}</p>
-                    </div>
-                  </div>
-                  <div class="row2">
-                    <div class="label">预警处理进度</div>
-                    <div class="process-box">
-                      <div class="grid-item">
-                        <span>总体</span>
-                        <div class="bar-box">
-                          <div
-                            class="bar"
-                            :style="{
-                              width:
-                                division(
-                                  item.finishViolationNum + item.finishDelayNum,
-                                  item.violationNum + item.delayNum
-                                ) + '%',
-                            }"
-                          ></div>
-                        </div>
-                        <span>{{ item.violationNum + item.delayNum }}</span>
-                      </div>
-                      <div class="grid-item">
-                        <span>违规</span>
-                        <div class="bar-box">
-                          <div
-                            class="bar"
-                            :style="{
-                              width:
-                                division(
-                                  item.finishViolationNum,
-                                  item.violationNum
-                                ) + '%',
-                            }"
-                          ></div>
-                        </div>
-                        <span>{{ item.violationNum }}</span>
-                      </div>
-                      <div class="grid-item">
-                        <span>延期</span>
-                        <div class="bar-box">
-                          <div
-                            class="bar"
-                            :style="{
-                              width:
-                                division(item.finishDelayNum, item.delayNum) +
-                                '%',
-                            }"
-                          ></div>
-                        </div>
-                        <span>{{ item.delayNum }}</span>
-                      </div>
-                    </div>
-                  </div>
-                </div>
-              </div>
-            </div>
-            <div class="none-data" v-else>暂无数据</div>
-          </div>
-          <div class="card">
-            <div class="title">
-              <span class="label">供应商预警均值走势</span>
-            </div>
-            <div class="chart-wrap">
-              <my-chart :options="options8"></my-chart>
-            </div>
-          </div>
-        </div>
-        <div class="col3">
-          <div class="card">
-            <div class="title">
-              <span class="label">项目考勤异常TOP10</span>
-              <FullscreenIcon
-                class="cursor-pointer"
-                @click="chart4?.maximize"
-                color="#595959"
-              />
-            </div>
-            <div class="chart-wrap">
-              <table-loop
-                ref="chart4"
-                :data="tableDataHandle(result4)"
-                :columns="tableColumns4"
-                title="项目考勤异常TOP10"
-              ></table-loop>
-            </div>
-          </div>
-          <div class="card">
-            <div class="title">
-              <span class="label">大区考勤异常TOP10</span>
-              <FullscreenIcon
-                class="cursor-pointer"
-                @click="chart5?.maximize"
-                color="#595959"
-              />
-            </div>
-            <div class="chart-wrap">
-              <table-loop
-                ref="chart5"
-                :data="tableDataHandle(result5)"
-                :columns="tableColumns5"
-                title="大区考勤异常TOP10"
-              ></table-loop>
-            </div>
-          </div>
-          <div class="card">
-            <div class="title">
-              <span class="label">供应商考勤异常TOP5</span>
-              <FullscreenIcon
-                class="cursor-pointer"
-                @click="chart6?.maximize"
-                color="#595959"
-              />
-            </div>
-            <div class="chart-wrap">
-              <table-loop
-                ref="chart6"
-                :data="tableDataHandle(result6)"
-                :columns="tableColumns6"
-                title="供应商考勤异常TOP5"
-              ></table-loop>
-            </div>
-          </div>
+            </template>
+            <template #markStartTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'mark_start_time',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.markStartTimeNum }}({{
+                  row.markStartTimeRatio
+                }})</t-link
+              >
+            </template>
+            <template #markEndTimeNum="{ col, row }">
+              <t-link
+                theme="primary"
+                @click="
+                  searchByX({
+                    fieldObj: 'mark_end_time',
+                    coordinatorId: row.coordinatorId,
+                  })
+                "
+                >{{ row.markEndTimeNum }}({{ row.markEndTimeRatio }})</t-link
+              >
+            </template>
+          </t-table>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
+    <DetailDrawer
+      v-model="showDetailDrawer"
+      :process="curProcess"
+      ref="DetailDrawerRef"
+      type="warn"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup name="SopAnalysis">
 <script setup name="SopAnalysis">
-import { ref, computed, watch, onMounted } from 'vue';
-import TableLoop from '@/components/common/table-loop/index.vue';
+import { ref, computed, watch } from 'vue';
 import { useRequest } from 'vue-request';
 import { useRequest } from 'vue-request';
-import { division, dateFormat } from '@/utils/tool';
-import {
-  sopServiceList,
-  warningTop,
-  attendanceTop,
-  supWarningTrend,
-  sopWarningAnalysis,
-} from '@/api/report';
-import { createLineOption } from '@/utils/chart';
 import { FullscreenIcon } from 'tdesign-icons-vue-next';
 import { FullscreenIcon } from 'tdesign-icons-vue-next';
-const chart1 = ref();
-const chart2 = ref();
-const chart3 = ref();
-const chart4 = ref();
-const chart5 = ref();
-const chart6 = ref();
-const curTimeRange = ref([]);
-const serviceOptions = ref([]);
-
-const timeParams = computed(() => {
-  return {
-    startTime: new Date(curTimeRange.value[0] + ' 00:00:00').getTime(),
-    endTime: new Date(curTimeRange.value[1] + ' 23:59:59').getTime(),
-  };
-});
-const serviceId = ref('');
-const centerGroup = ref('REGION');
-const timeChange = (time) => {
-  sopServiceList(timeParams.value).then((res) => {
-    serviceOptions.value = res || [];
-    res?.length && (serviceId.value = res[0].id);
-  });
-};
-const sort1 = ref('PENDING');
-const sort2 = ref('PENDING');
-const sort3 = ref('PENDING');
-
-const { data: result1, loading: loading1, run: run1 } = useRequest(warningTop);
-const { data: result2, loading: loading2, run: run2 } = useRequest(warningTop);
-const { data: result3, loading: loading3, run: run3 } = useRequest(warningTop);
+import { cloneDeep } from 'lodash-es';
+import DetailDrawer from '../service-analysis/detail-drawer.vue';
+import { warnByRegion, warnBySupplier, warnByCoordinator } from '@/api/report';
+const showDetailDrawer = ref(false);
+const curProcess = ref('');
 const {
 const {
-  data: result4,
-  loading: loading4,
-  run: run4,
-} = useRequest(attendanceTop);
+  data: tableData1,
+  run: run1,
+  loading: loading1,
+} = useRequest(warnByRegion);
 const {
 const {
-  data: result5,
-  loading: loading5,
-  run: run5,
-} = useRequest(attendanceTop);
+  data: tableData2,
+  run: run2,
+  loading: loading2,
+} = useRequest(warnBySupplier);
 const {
 const {
-  data: result6,
-  loading: loading6,
-  run: run6,
-} = useRequest(attendanceTop);
-const {
-  data: result7,
-  loading: loading7,
-  run: run7,
-} = useRequest(sopWarningAnalysis);
-const {
-  data: result8,
-  loading: loading8,
-  run: run8,
-} = useRequest(supWarningTrend);
-
-const options8 = computed(() => {
-  let res = result8.value || [];
-  let xData = [],
-    sData = [];
-  if (res.length) {
-    xData = res.map((item) => item.timeStr);
-  }
-  if (res.length && res[0].sopSupplierAvgViewInfo?.length) {
-    res = res.map((item) => {
-      item.sopSupplierAvgViewInfo = item.sopSupplierAvgViewInfo || [];
-      item.sopSupplierAvgViewInfo.sort((a, b) => a.supplierId - b.supplierId);
-      return item;
-    });
-
-    sData = res[0].sopSupplierAvgViewInfo.map((item) => ({
-      name: item.supplierName,
-      data: res.map((v) => {
-        return (
-          v.sopSupplierAvgViewInfo.find((x) => x.supplierId == item.supplierId)
-            ?.rate || []
-        );
-      }),
-    }));
-  }
-  return createLineOption({ xData, sData });
-});
+  data: tableData3,
+  run: run3,
+  loading: loading3,
+} = useRequest(warnByCoordinator);
 
 
-const tableDataHandle = (data) => {
-  return data || [];
+const DetailDrawerRef = ref();
+const searchByX = (obj) => {
+  DetailDrawerRef.value.searchByOther(obj);
 };
 };
-const tableColumns1 = [
-  {
-    prop: 'crmName',
-    label: '客户名称',
-    style: { width: '60px' },
-  },
-  {
-    prop: 'region_name',
-    label: '大区',
-    style: { width: '70px' },
-  },
-  {
-    prop: 'real_name',
-    label: '区域协调人',
-    style: { width: '70px' },
-  },
-  { prop: 'avgWarn', label: '预警数', style: { width: '60px' } },
-];
-const tableColumns2 = [
-  {
-    prop: 'region_name',
-    label: '大区',
-    style: { width: '60px' },
-  },
-  {
-    prop: 'real_name',
-    label: '大区经理',
-    style: { width: '70px' },
-  },
-  {
-    prop: 'avgWarn',
-    label: '预警数',
-    style: { width: '70px' },
-  },
-  { prop: 'avgMinutes', label: '均值', style: { width: '60px' } },
-];
-
-const tableColumns3 = [
-  {
-    prop: 'supplier',
-    label: '供应商',
-    style: { width: '60px' },
-  },
-  {
-    prop: 'region_name',
-    label: '大区',
-    style: { width: '70px' },
-  },
-  {
-    prop: 'real_name',
-    label: '区域协调人',
-    style: { width: '70px' },
-  },
-  { prop: 'avgWarn', label: '预警数', style: { width: '70px' } },
-  { prop: 'avgMinutes', label: '均值', style: { width: '60px' } },
-];
-
-const tableColumns4 = [
-  {
-    prop: 'crmName',
-    label: '客户名称',
-    style: { width: '60px' },
-  },
-  {
-    prop: 'dname',
-    label: '考勤对象',
-    style: { width: '70px' },
-  },
-  {
-    prop: 'real_name',
-    label: '区域协调人',
-    style: { width: '70px' },
-  },
-  { prop: 'total', label: '异常数', style: { width: '60px' } },
-];
-const tableColumns5 = [
-  {
-    prop: 'region_name',
-    label: '大区',
-    style: { width: '60px' },
-  },
-  {
-    prop: 'real_name',
-    label: '大区经理',
-    style: { width: '70px' },
-  },
-  {
-    prop: 'total',
-    label: '异常数',
-    style: { width: '70px' },
-  },
-  { prop: 'avg', label: '均值', style: { width: '60px' } },
+const serviceId = ref('');
+watch(serviceId, (serviceUnitId) => {
+  run1({ serviceUnitId });
+  run2({ serviceUnitId });
+  run3({ serviceUnitId });
+});
+const baseColumns = [
+  { colKey: 'total', title: '预警总数' },
+  { colKey: 'scanStartTimeNum', title: '扫描开始时间预警' },
+  { colKey: 'scanEndTimeNum', title: '扫描结束时间预警' },
+  { colKey: 'markStartTimeNum', title: '评卷开始时间预警' },
+  { colKey: 'markEndTimeNum', title: '评卷结束时间预警' },
 ];
 ];
-
-const tableColumns6 = [
-  {
-    prop: 'supplier',
-    label: '供应商',
-    style: { width: '80px' },
-  },
-  { prop: 'total', label: '异常数', style: { width: '70px' } },
-  { prop: 'avg', label: '均值', style: { width: '60px' } },
+const columns1 = [{ colKey: 'leadName', title: '大区经理' }, ...baseColumns];
+const columns2 = [{ colKey: 'supplierName', title: '人力商' }, ...baseColumns];
+const columns3 = [
+  { colKey: 'coordinatorName', title: '区域协调人' },
+  ...baseColumns,
 ];
 ];
-watch(serviceId, (serviceId) => {
-  sort1.value = sort2.value = sort3.value = 'PENDING';
-  run1({
-    group: 'CRM',
-    serviceId,
-    sort: sort1.value,
-  });
-  run2({
-    group: 'REGION',
-    serviceId,
-    sort: sort2.value,
-  });
-  run3({
-    group: 'SUPPLIER',
-    serviceId,
-    sort: sort3.value,
-  });
-  run4({
-    group: 'CRM',
-    serviceId,
-    ...timeParams.value,
-  });
-  run5({
-    group: 'REGION',
-    serviceId,
-    ...timeParams.value,
-  });
-  run6({
-    group: 'SUPPLIER',
-    serviceId,
-    ...timeParams.value,
-  });
-  run7({
-    group: centerGroup.value,
-    serviceId,
-  });
-  run8({ serviceId });
-});
-watch(centerGroup, () => {
-  run7({
-    group: centerGroup.value,
-    serviceId,
-  });
-});
-const changeSort = (group, index) => {
-  const sort =
-    index == 1 ? sort1.value : index == 2 ? sort2.value : sort3.value;
-  const run = index == 1 ? run1 : index == 2 ? run2 : run3;
-  run({
-    group,
-    serviceId,
-    sort,
-  });
-};
 </script>
 </script>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
@@ -578,154 +315,24 @@ const changeSort = (group, index) => {
     overflow: auto;
     overflow: auto;
     .scroll-content {
     .scroll-content {
       height: 100%;
       height: 100%;
-      min-height: 600px;
       min-width: 1000px;
       min-width: 1000px;
       display: flex;
       display: flex;
-
-      .chart-wrap {
-        height: calc(100% - 36px);
-      }
+      justify-content: space-between;
       .col1,
       .col1,
-      .col3 {
-        width: 268px;
-        height: 100%;
-        .card {
-          height: calc((100% - 30px) / 3);
-          &:not(:first-child) {
-            margin-top: 15px;
-          }
-        }
-      }
       .col2 {
       .col2 {
-        width: calc(100% - 566px);
-        margin-left: 15px;
-        margin-right: 15px;
-        .card {
-          &:first-child {
-            height: calc((100% - 30px) * 2 / 3 + 15px);
-            .none-data {
-              height: calc(100% - 42px);
-              margin-top: 10px;
-              display: flex;
-              justify-content: center;
-              align-items: center;
-              color: #ccc;
-              font-size: 14px;
-            }
-            .list-wrap {
-              height: calc(100% - 42px);
-              overflow: auto;
-              margin-top: 10px;
-              .list-item {
-                .item-head {
-                  height: 32px;
-                  background: #f7f7f7;
-                  padding: 0 10px;
-                  line-height: 32px;
-                  color: #262626;
-                }
-                .item-body {
-                  padding: 4px 8px;
-                  .label {
-                    color: #8c8c8c;
-                    font-size: 15px;
-                  }
-                  .row2 {
-                    margin-top: 5px;
-                    .process-box {
-                      display: flex;
-                      .grid-item {
-                        width: 33.33%;
-                        display: flex;
-                        align-items: center;
-                        span {
-                          font-size: 11px;
-                          color: #262626;
-                          &:last-child {
-                            padding-right: 20px;
-                          }
-                        }
-                        .bar-box {
-                          flex: 1;
-                          max-width: 100px;
-                          margin: 0 10px;
-                          height: 6px;
-                          border-radius: 3px;
-                          background: #d9d9d9;
-                          .bar {
-                            background: #4080ff;
-                            height: 100%;
-                          }
-                        }
-                      }
-                    }
-                  }
-                  .row1 {
-                    display: flex;
-                    .grid-item {
-                      width: 33.33%;
-                      p {
-                        font-size: 18px;
-                        font-weight: bold;
-                        color: #165dff;
-                        &.red {
-                          color: #f53f3f;
-                        }
-                        &.green {
-                          color: #00b42a;
-                        }
-                      }
-                    }
-                  }
-                }
-              }
-            }
-            .tab-box {
-              height: 32px;
-              padding: 5px 8px;
-              background: #f0f0f0;
-              border-radius: 3px;
-              float: left;
-              display: flex;
-              align-items: center;
-              .tab {
-                height: 22px;
-                padding: 0 6px;
-                line-height: 22px;
-                color: #8c8c8c;
-                cursor: pointer;
-                &.active {
-                  background-color: #fff;
-                  border-radius: 2px;
-                }
-                &:last-child {
-                  margin-left: 5px;
-                }
-              }
-            }
-          }
-          &:last-child {
-            height: calc((100% - 30px) / 3);
-            margin-top: 15px;
+        width: calc(50% - 8px);
+        :deep(.t-table__body) {
+          font-size: 12px !important;
+          .t-link {
+            font-size: 12px !important;
           }
           }
         }
         }
-      }
-    }
-
-    .card {
-      padding: 4px 10px 10px 10px;
-      background-color: #fff;
-      border: 1px solid #e5e5e5;
-      border-radius: 4px;
-      .title {
-        height: 36px;
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        .label {
-          color: #262626;
-          font-size: 14px;
-          // font-weight: bold;
+        .module-title {
+          height: 30px;
+          font-size: 16px;
+          font-weight: bold;
+          color: #000;
+          text-align: center;
         }
         }
       }
       }
     }
     }

+ 1 - 1
src/views/sop/components/dynamic-form-item/SELECT.vue

@@ -68,7 +68,7 @@ const getOptions = async () => {
       : [valueData.value];
       : [valueData.value];
     for (let i = 0; i < chooseValueArr.length; i++) {
     for (let i = 0; i < chooseValueArr.length; i++) {
       let value = chooseValueArr[i];
       let value = chooseValueArr[i];
-      if (!options.value.find((item) => item.userId === value)) {
+      if (!options.value.find((item) => item.userId === value) && !!value) {
         let personInfo = await getPersonInfoByUserId(value);
         let personInfo = await getPersonInfoByUserId(value);
         if (
         if (
           personInfo &&
           personInfo &&