#13 feat: 巡检工单、故障上报、设备台账报表

Zlúčené
yanghao mergnuté 3 commitov z shuzhihua/device_book do shuzhihua/master 15 hodín pred

+ 10 - 0
src/api/pms/inspect/order/index.ts

@@ -75,5 +75,15 @@ export const IotInspectOrderApi = {
   // 设备状态接口
   getInspectItemStatus: async (params: any) => {
     return await request.get({ url: `/rq/stat/report/inspect/status`, params })
+  },
+
+  // 故障上报
+  // 状态接口
+  getFaultReportStatus: async (params: any) => {
+    return await request.get({ url: `/rq/report/failure/status`, params })
+  },
+  // 列表接口
+  getFaultReportList: async (params: any) => {
+    return await request.get({ url: `/rq/report/failure/page`, params })
   }
 }

+ 1 - 0
src/views/report-statistics/device_book/index2.vue

@@ -124,6 +124,7 @@
               </el-link>
             </template>
           </el-table-column>
+          <el-table-column label="公司" align="center" prop="company" min-width="150" />
           <el-table-column
             :label="t('iotDevice.dept')"
             align="center"

+ 378 - 0
src/views/report-statistics/fault_report/index.vue

@@ -0,0 +1,378 @@
+<template>
+  <el-row :gutter="20">
+    <el-col :span="4" :xs="24">
+      <ContentWrap class="h-1/1" style="border: 0">
+        <DeptTree @node-click="handleDeptNodeClick" />
+      </ContentWrap>
+    </el-col>
+    <el-col :span="20" :xs="24">
+      <!-- 统计卡片 -->
+      <el-row :gutter="18" class="mb-4">
+        <el-col :span="5">
+          <div style="background-color: #fff; border-radius: 10px; cursor: pointer">
+            <div class="stat-card bg-blue-gradient">
+              <Icon icon="ep:histogram" :size="40" />
+              <div class="card-title">故障总数</div>
+              <div class="card-value pt-5">{{
+                statusCount.finished + statusCount.trans + statusCount.reporting
+              }}</div>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="4">
+          <div style="background-color: #fff; border-radius: 10px">
+            <div class="stat-card bg-green-gradient">
+              <Icon icon="ep:finished" :size="40" />
+              <div class="card-title">维修完成</div>
+              <div class="card-value pt-5">{{ statusCount.finished }}</div>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="4">
+          <div style="background-color: #fff; border-radius: 10px">
+            <div class="stat-card bg-orange-gradient">
+              <Icon icon="ep:more-filled" :size="40" />
+              <div class="card-title">未完成</div>
+              <div class="card-value pt-5">{{ statusCount.trans }}</div>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="4">
+          <div style="background-color: #fff; border-radius: 10px">
+            <div class="stat-card bg-red-gradient">
+              <Icon icon="ep:hide" :size="40" />
+              <div class="card-title">上报中</div>
+              <div class="card-value pt-5">{{ statusCount.reporting }}</div>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="7">
+          <div class="bg-[#fff] p-2 py-4 rounded-lg">
+            <el-form ref="queryFormRef" :model="queryParams">
+              <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="t('operationFill.start')"
+                  :end-placeholder="t('operationFill.end')"
+                  :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                  class="!w-220px"
+                />
+              </el-form-item>
+
+              <el-form-item>
+                <el-button @click="handleQuery"
+                  ><Icon icon="ep:search" class="mr-3px" />{{
+                    t('operationFill.search')
+                  }}</el-button
+                >
+              </el-form-item>
+
+              <el-form-item>
+                <el-button @click="resetQuery"
+                  ><Icon icon="ep:refresh" class="mr-3px" />
+                  {{ t('operationFill.reset') }}</el-button
+                >
+              </el-form-item>
+            </el-form>
+          </div>
+        </el-col>
+      </el-row>
+
+      <!-- 列表 -->
+      <ContentWrap style="border: 0; margin-top: 10px">
+        <el-table
+          v-loading="loading"
+          :data="list"
+          height="48vh"
+          :stripe="true"
+          :show-overflow-tooltip="true"
+        >
+          <el-table-column :label="t('fault.serial')" width="70" align="center">
+            <template #default="scope">
+              {{ scope.$index + 1 }}
+            </template>
+          </el-table-column>
+
+          <el-table-column
+            :label="t('fault.faultTitle')"
+            prop="failureName"
+            min-width="200"
+            align="center"
+          />
+          <el-table-column
+            :label="t('iotMaintain.deviceCode')"
+            align="center"
+            prop="deviceCode"
+            width="200"
+          />
+          <el-table-column
+            :label="t('fault.deviceName')"
+            align="center"
+            prop="deviceName"
+            min-width="200"
+          />
+          <el-table-column :label="t('fault.status')" align="center" prop="status" min-width="110">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.PMS_FAILURE_STATUS" :value="scope.row.status" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('faultForm.failureType')"
+            align="center"
+            prop="failureType"
+            min-width="110"
+          >
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.FAILURE_TYPE" :value="scope.row.failureType" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="故障频次" align="center" prop="failureNum" min-width="200" />
+          <el-table-column
+            :label="t('fault.approvalStatus')"
+            align="center"
+            prop="auditStatus"
+            min-width="130"
+          >
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('fault.solve')" align="center" prop="ifDeal">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.ifDeal" />
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('fault.failureTime')"
+            align="center"
+            prop="failureTime"
+            :formatter="dateFormatter"
+            min-width="180px"
+          />
+          <el-table-column :label="t('fault.solveTime')" align="center" prop="dealHour">
+            <template #default="scope">
+              {{ scope.row.dealHour && scope.row.dealHour > 0 ? scope.row.dealHour + 'H' : '' }}
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="上报时间"
+            align="center"
+            prop="createTime"
+            :formatter="dateFormatter"
+            min-width="180px"
+          />
+        </el-table>
+
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
+</template>
+
+<script setup lang="ts">
+import { IotInspectOrderApi, IotInspectOrderVO } from '@/api/pms/inspect/order'
+
+import { DICT_TYPE } from '@/utils/dict'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+import { dateFormatter } from '@/utils/formatTime'
+
+const { params } = useRoute()
+/** 巡检工单 列表 */
+defineOptions({ name: 'IotInspectOrder' })
+
+const { t } = useI18n() // 国际化
+const loading = ref(true) // 列表的加载中
+const list = ref<IotInspectOrderVO[]>([]) // 列表的数据
+const deptId = params.deptId
+const createTime = params.createTime
+const total = ref(0) // 列表的总页数
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+
+  createTime: [],
+  deptId: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 部门树节点点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  await getList()
+  await getCounts()
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotInspectOrderApi.getFaultReportList(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+let statusCount = ref({
+  finished: 0,
+  trans: 0,
+  reporting: 0
+})
+async function getCounts() {
+  const res = await IotInspectOrderApi.getFaultReportStatus({
+    deptId: queryParams.deptId,
+    createTime: queryParams.createTime
+  })
+
+  statusCount.value = res
+
+  console.log('statusCount', statusCount.value)
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getCounts()
+
+  if (deptId != null) {
+    queryParams.deptId = deptId
+  }
+
+  if (createTime) {
+    queryParams.createTime = createTime
+  }
+  getList()
+})
+</script>
+<style scoped>
+/* 添加异常行高亮样式 */
+:deep(.exception-row) {
+  background-color: #f29a90 !important;
+}
+
+:deep(.exception-row:hover) {
+  background-color: #fff0e0 !important;
+}
+
+.reason-confirm-container {
+  margin: 20px;
+}
+
+/* 调整文本框样式 */
+:deep(.el-textarea__inner) {
+  font-size: 14px;
+}
+
+/* 调整表单项间距 */
+:deep(.el-form-item) {
+  margin-bottom: 15px;
+}
+
+.stat-card {
+  padding: 20px;
+  border-radius: 10px;
+  color: white;
+  text-align: center;
+  font-size: 14px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+  transition:
+    transform 0.3s ease,
+    box-shadow 0.3s ease;
+  backdrop-filter: blur(12px);
+  height: 200px;
+  cursor: pointer;
+}
+.stat-card::before {
+  position: absolute;
+  filter: blur(20px);
+  z-index: -1;
+}
+
+.stat-card:hover {
+  transform: translateY(-4px);
+  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
+  cursor: pointer;
+}
+
+.card-title {
+  margin: 8px 0;
+  font-size: 16px;
+}
+
+.card-value {
+  font-size: 28px;
+  font-weight: bold;
+  margin: 8px 0;
+}
+
+.card-trend {
+  font-size: 12px;
+  opacity: 0.9;
+}
+
+/* 毛玻璃渐变背景 —— 不再使用 background-image */
+.bg-blue-gradient {
+  background: linear-gradient(135deg, rgba(77, 147, 255, 0.5), rgba(75, 132, 254));
+}
+
+.bg-green-gradient {
+  background: linear-gradient(135deg, rgba(101, 226, 136, 0.1), #52d7a2);
+  background-color: rgba(76, 175, 80, 0.1);
+}
+
+.bg-orange-gradient {
+  background: linear-gradient(135deg, rgba(152, 82, 4, 0.5), rgba(255, 152, 4, 0.6));
+}
+
+.bg-red-gradient {
+  background: linear-gradient(135deg, rgba(232, 65, 51), rgba(252, 242, 236));
+}
+
+.bg-warn-gradient {
+  background: linear-gradient(135deg, rgba(255, 201, 103), rgba(243, 162, 152));
+}
+
+/* 确保内容不溢出 */
+:deep(.el-row) {
+  flex-wrap: wrap;
+}
+
+.text-truncate {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+::v-deep .el-table__header-wrapper {
+  position: sticky !important;
+  width: 100%;
+  top: 0px;
+  z-index: 2000;
+}
+</style>

+ 126 - 11
src/views/report-statistics/inspection_order/index.vue

@@ -9,8 +9,12 @@
       <!-- 统计卡片 -->
       <el-row :gutter="18" class="mb-4">
         <el-col :span="4">
-          <div style="background-color: #fff; border-radius: 10px">
-            <div class="stat-card bg-blue-gradient">
+          <div style="border-radius: 10px">
+            <div
+              class="stat-card bg-blue-gradient"
+              :style="{ border: statusList.all ? '2px solid #0050b3' : '' }"
+              @click="getList('all')"
+            >
               <Icon icon="ep:histogram" :size="40" />
               <div class="card-title">工单数量</div>
               <div class="card-value pt-5">{{
@@ -21,7 +25,11 @@
         </el-col>
 
         <el-col :span="4">
-          <div style="background-color: #fff; border-radius: 10px">
+          <div
+            style="border-radius: 10px"
+            @click="getList('finished')"
+            :style="{ border: statusList.finished ? '2px solid #0050b3' : '' }"
+          >
             <div class="stat-card bg-green-gradient">
               <Icon icon="ep:finished" :size="40" />
               <div class="card-title">完成数量</div>
@@ -31,7 +39,11 @@
         </el-col>
 
         <el-col :span="4">
-          <div style="background-color: #fff; border-radius: 10px">
+          <div
+            style="border-radius: 10px"
+            @click="getList('todo')"
+            :style="{ border: statusList.todo ? '2px solid #0050b3' : '' }"
+          >
             <div class="stat-card bg-orange-gradient">
               <Icon icon="ep:more-filled" :size="40" />
               <div class="card-title">未完成数量</div>
@@ -41,7 +53,11 @@
         </el-col>
 
         <el-col :span="4">
-          <div style="background-color: #fff; border-radius: 10px">
+          <div
+            style="border-radius: 10px"
+            @click="getList('ignore')"
+            :style="{ border: statusList.ignore ? '2px solid #0050b3' : '' }"
+          >
             <div class="stat-card bg-green-gradient">
               <Icon icon="ep:hide" :size="40" />
               <div class="card-title">已忽略</div>
@@ -51,7 +67,7 @@
         </el-col>
 
         <el-col :span="4">
-          <div style="background-color: #fff; border-radius: 10px">
+          <div style="border-radius: 10px">
             <div class="stat-card bg-red-gradient">
               <Icon icon="ep:bell" :size="40" />
               <div class="card-title">异常设备数量</div>
@@ -61,8 +77,11 @@
         </el-col>
 
         <el-col :span="4">
-          <div style="background-color: #fff; border-radius: 10px">
-            <div class="stat-card bg-warn-gradient">
+          <div style="border-radius: 10px" @click="getExceptionPoint">
+            <div
+              class="stat-card bg-warn-gradient"
+              :style="{ border: statusList.exceptionPoint ? '2px solid #0050b3' : '' }"
+            >
               <Icon icon="ep:info-filled" :size="40" />
               <div class="card-title">异常点数量</div>
               <div class="card-value pt-5">{{ exceptionPoint.value }}</div>
@@ -84,6 +103,7 @@
       <!-- 列表 -->
       <ContentWrap style="border: 0; margin-top: 10px">
         <el-table
+          v-if="!isExceptionPoint"
           v-loading="loading"
           :row-class-name="tableRowClassName"
           :data="list"
@@ -103,8 +123,8 @@
             min-width="300"
           />
 
-          <el-table-column label="项目" align="center" prop="project" min-width="110" />
           <el-table-column label="公司" align="center" prop="company" min-width="110" />
+          <el-table-column label="项目" align="center" prop="project" min-width="110" />
 
           <el-table-column
             :label="t('route.orderType')"
@@ -123,6 +143,14 @@
             </template>
           </el-table-column>
 
+          <el-table-column
+            v-if="statusList.ignore"
+            label="忽略原因"
+            align="center"
+            prop="reason"
+            min-width="110"
+          />
+
           <el-table-column label="设备明细" align="center" prop="deviceInfo" min-width="110" />
 
           <el-table-column
@@ -161,12 +189,40 @@
           </el-table-column>
         </el-table>
 
+        <!-- 巡检异常点 -->
+        <el-table
+          v-else
+          v-loading="loading"
+          :data="list2"
+          :stripe="true"
+          :show-overflow-tooltip="true"
+        >
+          <el-table-column :label="t('iotDevice.serial')" width="70" align="center">
+            <template #default="scope">
+              {{ scope.$index + 1 }}
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('bomList.name')" align="center" prop="orderName" />
+          <el-table-column :label="t('iotDevice.code')" align="center" prop="deviceCode" />
+          <el-table-column :label="t('monitor.deviceName')" align="center" prop="deviceName" />
+          <el-table-column :label="t('operationFill.duty')" align="center" prop="charge" />
+          <el-table-column :label="t('inspect.InspectionItems')" align="center" prop="item" />
+          <el-table-column :label="t('inspect.isException')" align="center" prop="ifNormal">
+            <template #default="scope">
+              <span v-if="scope.row.ifNormal" style="color: dodgerblue">正常</span>
+              <span v-else-if="scope.row.ifNormal === null" style="color: #101010">待填写</span>
+              <span v-else-if="!scope.row.ifNormal" style="color: orangered">异常</span>
+            </template>
+          </el-table-column>
+          <el-table-column :label="t('inspect.exceptionDes')" align="center" prop="description" />
+        </el-table>
+
         <!-- 分页 -->
         <Pagination
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="getList"
+          @pagination="getAllList"
         />
       </ContentWrap>
     </el-col>
@@ -177,6 +233,8 @@
 import { IotInspectOrderApi, IotInspectOrderVO } from '@/api/pms/inspect/order'
 import { DICT_TYPE } from '@/utils/dict'
 import DeptTree from '@/views/system/user/DeptTree.vue'
+import { IotInspectItemVO, IotInspectOrderDetailApi } from '@/api/pms/inspect/order/detail'
+
 import { watch } from 'vue'
 
 const { push } = useRouter()
@@ -191,6 +249,15 @@ const deptId = params.deptId
 const createTime = params.createTime
 const total = ref(0) // 列表的总页数
 
+const statusList = ref({
+  all: false,
+  exception: false,
+  exceptionPoint: false,
+  finished: false,
+  ignore: false,
+  todo: false
+})
+
 let dateType = ref('year')
 const queryParams = reactive({
   pageNo: 1,
@@ -234,10 +301,49 @@ const handleRowClick = (row, column: any, event: Event) => {
     })
   }
 }
+
+let isExceptionPoint = ref(false)
+const list2 = ref<IotInspectItemVO[]>([]) // 列表的数据
+
+const getPointList = async () => {
+  loading.value = true
+  try {
+    queryParams.status = '异常'
+    const data = await IotInspectOrderDetailApi.getIotInspectItemStatusPage(queryParams)
+    list2.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+const getExceptionPoint = () => {
+  Object.keys(statusList.value).forEach((key) => {
+    statusList.value[key] = false
+  })
+  statusList.value['exceptionPoint'] = true
+
+  isExceptionPoint.value = true
+  getPointList()
+}
 /** 查询列表 */
-const getList = async () => {
+const getList = async (status: string = 'all') => {
+  isExceptionPoint.value = false
+  Object.keys(statusList.value).forEach((key) => {
+    statusList.value[key] = false
+  })
+  statusList.value[status] = true
   loading.value = true
   try {
+    if (status === 'all') {
+      queryParams.status = undefined
+    } else if (status === 'todo') {
+      queryParams.status = 'todo'
+    } else if (status === 'finished') {
+      queryParams.status = 'finished'
+    } else if (status === 'ignore') {
+      queryParams.status = 'ignore'
+    }
     const data = await IotInspectOrderApi.getIotInspectOrderList(queryParams)
     list.value = data.list
     total.value = data.total
@@ -246,6 +352,14 @@ const getList = async () => {
   }
 }
 
+const getAllList = async () => {
+  if (isExceptionPoint.value) {
+    await getPointList()
+  } else {
+    await getList()
+  }
+}
+
 watch(
   dateType,
   () => {
@@ -402,6 +516,7 @@ onMounted(() => {
     box-shadow 0.3s ease;
   backdrop-filter: blur(12px);
   height: 200px;
+  cursor: pointer;
 }
 .stat-card::before {
   position: absolute;