Răsfoiți Sursa

Merge branch 'master' of http://1.94.244.160:3000/shuzhihua/pms-iot-vue

Zimo 17 ore în urmă
părinte
comite
3afb517ccc
3 a modificat fișierele cu 398 adăugiri și 19 ștergeri
  1. 1 1
      .env.local
  2. 8 0
      src/api/pms/qhse/index.ts
  3. 389 18
      src/views/pms/qhse/ptw/index.vue

+ 1 - 1
.env.local

@@ -3,7 +3,7 @@ NODE_ENV=development
 
 VITE_DEV=true
 
-# 请求路径  http://192.168.188.60:48080  https://iot.deepoil.cc:5443  http://172.26.0.56:48080  http://192.168.188.198:48080
+# 请求路径  http://192.168.188.86:48080  https://iot.deepoil.cc:5443  http://172.26.0.56:48080  http://192.168.188.198:48080
 VITE_BASE_URL='https://iot.deepoil.cc:5443'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务

+ 8 - 0
src/api/pms/qhse/index.ts

@@ -490,6 +490,14 @@ export const QHSEPtwApi = {
   // 导出PTW管理 Excel
   exportPtw: async (params) => {
     return await request.download({ url: `/rq/qhse-ptw/export-excel`, params })
+  },
+  // 获取PTW分类
+  getPtwCategory: async (id) => {
+    return await request.get({ url: `/rq/qhse-ptw/type?deptId=${id}` })
+  },
+  // 获取传入区间范围内的每天的数量
+  getPtwCountByDateRange: async (data) => {
+    return await request.post({ url: `/rq/qhse-ptw/time`, data })
   }
 }
 

+ 389 - 18
src/views/pms/qhse/ptw/index.vue

@@ -1,9 +1,27 @@
 <template>
-  <el-row :gutter="20">
+  <el-row :gutter="20" class="ptw-page-layout">
     <!-- 左侧部门树 -->
     <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
-      <div class="bg-white rounded-sm px-2 py-2">
+    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24" class="ptw-main-col">
+      <div ref="statsContainerRef" class="ptw-stats mb-15px">
+        <div class="ptw-stats-card" style="position: relative">
+          <div class="ptw-stats-card__label" style="position: absolute; top: 15px">作业票总数</div>
+          <div class="ptw-stats-card__value" style="text-align: center">{{ totalStaticCount }}</div>
+        </div>
+        <div
+          class="ptw-stats-chart-card"
+          style="background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%)">
+          <div class="ptw-stats-card__label">作业票分类统计</div>
+          <Echart ref="staticChartRef" :height="200" :options="staticChartOptions" />
+        </div>
+        <div
+          class="ptw-stats-chart-card"
+          style="background: linear-gradient(180deg, #ffffff 0%, #f7faff 100%)">
+          <div class="ptw-stats-card__label">作业票趋势统计</div>
+          <Echart ref="timeTrendChartRef" :height="200" :options="timeTrendChartOptions" />
+        </div>
+      </div>
+      <div class="bg-white rounded-sm px-2 py-2 shrink-0">
         <!-- 搜索工作栏 -->
         <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
           <el-form-item label="PTW编号" prop="ptwNo">
@@ -14,6 +32,17 @@
             <el-input placeholder="请输入PTW序号" v-model="queryParams.ptwXh" />
           </el-form-item>
 
+          <el-form-item label="创建日期" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-240px" />
+          </el-form-item>
+
           <el-form-item>
             <el-button @click="handleAdd" type="primary" v-hasPermi="['rq:qhse-ptw:create']"
               ><Icon icon="ep:plus" class="mr-5px" />新增</el-button
@@ -37,13 +66,17 @@
       </div>
 
       <!-- 列表 -->
-      <ContentWrap class="flex-1 overflow-hidden mt-15px" style="border: none">
-        <zm-table
-          :loading="loading"
-          :data="list"
-          :stripe="true"
-          height="calc(85vh - 130px)"
-          :show-overflow-tooltip="true">
+      <ContentWrap
+        class="ptw-table-wrap flex-1 overflow-hidden mt-15px !mb-0"
+        style="border: none"
+        :body-style="tableWrapBodyStyle">
+        <div class="ptw-table-scroll">
+          <zm-table
+            :loading="loading"
+            :data="list"
+            :stripe="true"
+            height="100%"
+            :show-overflow-tooltip="true">
           <zm-table-column :label="t('monitor.serial')" width="70" align="center" fixed="left">
             <template #default="scope">
               {{ scope.$index + 1 }}
@@ -146,14 +179,17 @@
               </el-button>
             </template>
           </zm-table-column>
-        </zm-table>
+          </zm-table>
+        </div>
 
         <!-- 分页 -->
-        <Pagination
-          :total="total"
-          v-model:page="queryParams.pageNo"
-          v-model:limit="queryParams.pageSize"
-          @pagination="getList" />
+        <div class="ptw-pagination">
+          <Pagination
+            :total="total"
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            @pagination="getList" />
+        </div>
       </ContentWrap>
     </el-col>
   </el-row>
@@ -527,6 +563,7 @@ import DeptTree from '@/views/system/user/DeptTree2.vue'
 import { handleTree, defaultProps } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import { ElMessageBox, ElMessage } from 'element-plus'
+import type { EChartsOption } from 'echarts'
 const deptList2 = ref<Tree[]>([]) // 树形结构
 import { formatDate } from '@/utils/formatTime'
 import { useUserStore } from '@/store/modules/user'
@@ -551,9 +588,19 @@ const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   ptwXh: '',
-  ptwNo: ''
+  ptwNo: '',
+  createTime: null,
+  deptId: ''
 })
 
+type StaticItem = {
+  classify: string
+  count: number
+  sort?: number
+}
+
+type TimeStaticItem = Record<string, number>
+
 const queryParams2 = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -566,6 +613,17 @@ const queryParams3 = reactive({
   jsaXh: ''
 })
 const queryFormRef = ref(null) // 搜索的表单
+const statsContainerRef = ref<HTMLElement | null>(null)
+const staticChartRef = ref<any>(null)
+const timeTrendChartRef = ref<any>(null)
+let statsResizeObserver: ResizeObserver | null = null
+const tableWrapBodyStyle = {
+  padding: '10px',
+  height: '100%',
+  display: 'flex',
+  flexDirection: 'column',
+  overflow: 'hidden'
+}
 
 // 对话框相关
 const dialogVisible = ref(false)
@@ -673,6 +731,8 @@ const handleExport = async () => {
 /** 首页处理部门被点击 */
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
+  await getStatic()
+  await getTimeStaticRes()
   await getList()
 }
 
@@ -686,6 +746,7 @@ const handleQuery = () => {
 const resetQuery = () => {
   queryParams.deptId = ''
   queryFormRef.value?.resetFields()
+  getStatic()
   handleQuery()
 }
 
@@ -852,13 +913,241 @@ const extractFileName = (url: string): string => {
   }
 }
 
+let staticList = ref<StaticItem[]>([])
+const totalStaticCount = computed(() =>
+  staticList.value.reduce((sum: number, item: StaticItem) => sum + Number(item.count || 0), 0)
+)
+const staticChartOptions = computed<EChartsOption>(() => ({
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: 16,
+    right: 16,
+    top: 24,
+    bottom: 24,
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: staticList.value.map((item: StaticItem) => item.classify),
+    axisTick: {
+      show: false
+    },
+    axisLine: {
+      lineStyle: {
+        color: '#d9e2ef'
+      }
+    },
+    axisLabel: {
+      color: '#6b7280',
+      fontSize: 12,
+      interval: 0
+      // rotate: 30
+    }
+  },
+  yAxis: {
+    type: 'value',
+    axisLine: {
+      show: false
+    },
+    axisTick: {
+      show: false
+    },
+    axisLabel: {
+      color: '#94a3b8',
+      fontSize: 12
+    },
+    splitLine: {
+      lineStyle: {
+        color: '#edf2f7',
+        type: 'dashed'
+      }
+    }
+  },
+  series: [
+    {
+      type: 'bar',
+      barWidth: 32,
+      data: staticList.value.map((item: StaticItem) => item.count),
+      itemStyle: {
+        borderRadius: [8, 8, 0, 0],
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [
+            { offset: 0, color: '#79b8ff' },
+            { offset: 1, color: '#2f78f6' }
+          ]
+        }
+      },
+      label: {
+        show: true,
+        position: 'top',
+        color: '#2563eb',
+        fontSize: 12,
+        fontWeight: 700
+      }
+    }
+  ]
+}))
+
+const timeTrendDates = computed(() =>
+  Object.keys(timeStaticRes.value || {}).sort(
+    (a, b) => new Date(a).getTime() - new Date(b).getTime()
+  )
+)
+
+const timeTrendChartOptions = computed<EChartsOption>(() => ({
+  tooltip: {
+    trigger: 'axis'
+  },
+  grid: {
+    left: 16,
+    right: 24,
+    top: 24,
+    bottom: 24,
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: timeTrendDates.value,
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    },
+    axisLine: {
+      lineStyle: {
+        color: '#d9e2ef'
+      }
+    },
+    axisLabel: {
+      color: '#6b7280',
+      fontSize: 12
+    }
+  },
+  yAxis: {
+    type: 'value',
+    minInterval: 1,
+    axisLine: {
+      show: false
+    },
+    axisTick: {
+      show: false
+    },
+    axisLabel: {
+      color: '#94a3b8',
+      fontSize: 12
+    },
+    splitLine: {
+      lineStyle: {
+        color: '#edf2f7',
+        type: 'dashed'
+      }
+    }
+  },
+  series: [
+    {
+      type: 'line',
+      smooth: true,
+      symbol: 'circle',
+      symbolSize: 8,
+      data: timeTrendDates.value.map((date) => Number(timeStaticRes.value?.[date] || 0)),
+      lineStyle: {
+        width: 3,
+        color: '#10b981'
+      },
+      itemStyle: {
+        color: '#10b981',
+        borderColor: '#ffffff',
+        borderWidth: 2
+      },
+      areaStyle: {
+        color: {
+          type: 'linear',
+          x: 0,
+          y: 0,
+          x2: 0,
+          y2: 1,
+          colorStops: [
+            { offset: 0, color: 'rgba(16, 185, 129, 0.32)' },
+            { offset: 1, color: 'rgba(16, 185, 129, 0.04)' }
+          ]
+        }
+      }
+    }
+  ]
+}))
+
+async function getStatic() {
+  const deptId = queryParams.deptId || userStore.user.deptId
+  const res = await QHSEPtwApi.getPtwCategory(deptId)
+  staticList.value = Array.isArray(res)
+    ? [...res].sort((a: StaticItem, b: StaticItem) => (a.sort || 0) - (b.sort || 0))
+    : []
+}
+
+let timeStaticRes = ref<TimeStaticItem>({})
+async function getTimeStaticRes() {
+  const deptId = queryParams.deptId || userStore.user.deptId
+  const res = await QHSEPtwApi.getPtwCountByDateRange({
+    createTime: queryParams.createTime,
+    deptId: deptId
+  })
+
+  timeStaticRes.value = res || {}
+}
+
+const resizeStatsCharts = () => {
+  staticChartRef.value?.resize?.()
+  timeTrendChartRef.value?.resize?.()
+}
+
+watch(
+  () => queryParams.createTime,
+  () => {
+    getTimeStaticRes()
+  }
+)
+
+watch(
+  () => isLeftContentCollapsed.value,
+  () => {
+    nextTick(() => {
+      resizeStatsCharts()
+    })
+  }
+)
+
 onMounted(async () => {
   getList()
   deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
 
   const users = await getUserProfile()
   userInfo.value = users
-  console.log('xxxxxxxxxxxxxxxxxx', userInfo.value)
+  getStatic()
+  getTimeStaticRes()
+
+  nextTick(() => {
+    resizeStatsCharts()
+    if (statsContainerRef.value) {
+      statsResizeObserver = new ResizeObserver(() => {
+        resizeStatsCharts()
+      })
+      statsResizeObserver.observe(statsContainerRef.value)
+    }
+  })
+})
+
+onBeforeUnmount(() => {
+  statsResizeObserver?.disconnect()
+  statsResizeObserver = null
 })
 
 let personDialogVisible = ref(false)
@@ -1027,6 +1316,88 @@ const handleRadioChange2 = (row: any) => {
 </script>
 
 <style scoped>
+.ptw-page-layout {
+  align-items: stretch;
+}
+
+.ptw-main-col {
+  height: 85vh;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.ptw-stats {
+  display: grid;
+  grid-template-columns: 240px minmax(0, 1fr) minmax(0, 1fr);
+  gap: 16px;
+  flex-shrink: 0;
+}
+
+.ptw-table-wrap {
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.ptw-table-scroll {
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.ptw-pagination {
+  flex-shrink: 0;
+}
+
+.ptw-stats-card,
+.ptw-stats-chart-card {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+}
+
+.ptw-stats-card {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  min-height: 180px;
+  background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
+}
+
+.ptw-stats-card__label {
+  font-size: 14px;
+  color: #64748b;
+  margin-bottom: 12px;
+}
+
+.ptw-stats-card__value {
+  font-size: 42px;
+  line-height: 1;
+  font-weight: 700;
+  color: #1d4ed8;
+}
+
+.ptw-stats-chart-card {
+  min-width: 0;
+}
+
+@media (max-width: 768px) {
+  .ptw-main-col {
+    height: auto;
+  }
+
+  .ptw-stats {
+    grid-template-columns: 1fr;
+  }
+
+  .ptw-stats-card {
+    min-height: 130px;
+  }
+}
+
 .file-name-text {
   display: block;
   white-space: nowrap;