yanghao 8 stundas atpakaļ
vecāks
revīzija
c54af5ba7e

+ 5 - 2
src/components/Error/src/Error.vue

@@ -35,7 +35,8 @@ const errorMap: {
 }
 
 const props = defineProps({
-  type: propTypes.string.validate((v: string) => ['404', '500', '403'].includes(v)).def('404')
+  type: propTypes.string.validate((v: string) => ['404', '500', '403'].includes(v)).def('404'),
+  showButton: propTypes.bool.def(true)
 })
 
 const emit = defineEmits(['errorClick'])
@@ -51,7 +52,9 @@ const btnClick = () => {
       <img :src="errorMap[type].url" alt="" width="350" />
       <div class="text-14px text-[var(--el-color-info)]">{{ errorMap[type].message }}</div>
       <div class="mt-20px">
-        <ElButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</ElButton>
+        <ElButton type="primary" v-if="showButton" @click="btnClick">{{
+          errorMap[type].buttonText
+        }}</ElButton>
       </div>
     </div>
   </div>

+ 13 - 1
src/permission.ts

@@ -141,6 +141,12 @@ router.beforeEach(async (to, from, next) => {
 
       authUtil.setToken(res)
 
+      // const source = to.query.source
+      // if (source) {
+      //   sessionStorage.setItem('LOGIN_SOURCE', source as string)
+      //   next({ path: '/oli-connection/monitoring' })
+      // }
+
       next({ path: '/index' })
     } else if (whiteList.indexOf(to.path) !== -1) {
       const code = to.query.code
@@ -155,7 +161,13 @@ router.beforeEach(async (to, from, next) => {
       }
       // next()
     } else {
-      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
+      const source = to.query.source
+      if (source) {
+        sessionStorage.setItem('LOGIN_SOURCE', source as string)
+      }
+      if (source) {
+        next(`/login?redirect=/oli-connection/monitoring`)
+      } else next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
     }
   }
 })

+ 38 - 0
src/utils/useSocketBus.ts

@@ -77,3 +77,41 @@ export function useSocketBus(deviceCode: string) {
 
   return { ws, status, messages, on, off, sendEvent, open, close, onAny }
 }
+
+export const formatIotValue = (val: any) => {
+  if (val === null || val === undefined) return { value: '0', isText: true }
+  const strVal = String(val)
+
+  if (!/\d/.test(strVal)) {
+    return { value: strVal, isText: true }
+  }
+
+  const match = strVal.match(/^(-?\d+(?:\.\d+)?)(.*)$/)
+
+  if (match) {
+    return {
+      value: match[1],
+      suffix: match[2] || '',
+      isText: false
+    }
+  }
+  return { value: strVal, isText: true }
+}
+
+export interface HeaderItem {
+  label: string
+  key: string
+  judgment?: boolean
+}
+
+export interface Dimensions {
+  identifier: string
+  name: string
+  value: string | number
+  color: string
+  bgHover: string
+  bgActive: string
+  response?: boolean
+  suffix?: string
+  isText?: boolean
+}

+ 5 - 1
src/views/Error/404.vue

@@ -1,7 +1,11 @@
 <template>
-  <Error @error-click="push('/')" />
+  <Error :show-button="source !== 'zhly'" @error-click="push('/')" />
 </template>
 <script lang="ts" setup>
 defineOptions({ name: 'Error404' })
 const { push } = useRouter()
+
+const route = useRoute()
+
+const source = computed(() => sessionStorage.getItem('LOGIN_SOURCE') || route.query.source)
 </script>

+ 108 - 55
src/views/oli-connection/monitoring/detail.vue

@@ -14,7 +14,7 @@ import { neonColors } from '@/utils/td-color'
 import dayjs from 'dayjs'
 import * as echarts from 'echarts'
 import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
-import { useSocketBus } from '@/utils/useSocketBus'
+import { Dimensions, formatIotValue, HeaderItem, useSocketBus } from '@/utils/useSocketBus'
 import { rangeShortcuts } from '@/utils/formatTime'
 
 const { query } = useRoute()
@@ -72,6 +72,49 @@ onAny((msg) => {
   genderIntervalArr()
 })
 
+// onAny((msg) => {
+//   if (!Array.isArray(msg) || msg.length === 0) return
+
+//   const valueMap = new Map<string, { value: string; isText: boolean; suffix?: string }>()
+
+//   for (const item of msg) {
+//     const { identity, modelName, readTime, logValue } = item
+
+//     const { value, isText, suffix } = formatIotValue(logValue)
+
+//     if (identity) {
+//       valueMap.set(identity, { value, isText, suffix })
+//     }
+
+//     if (modelName && chartData.value[modelName]) {
+//       chartData.value[modelName].push({
+//         ts: dayjs(readTime).valueOf(),
+//         value: Number(value)
+//       })
+
+//       updateSingleSeries(modelName)
+//     }
+//   }
+
+//   const updateDimensions = (list) => {
+//     list.forEach((item) => {
+//       const v = valueMap.get(item.identifier)
+//       if (v !== undefined) {
+//         item.value = v.value
+//         item.suffix = v.suffix
+//         item.isText = v.isText
+//       }
+//     })
+//   }
+
+//   updateDimensions(dimensions.value)
+//   updateDimensions(gatewayDimensions.value)
+//   updateDimensions(carDimensions.value)
+
+//   // 3️⃣ 统一一次调用
+//   genderIntervalArr()
+// })
+
 function hexToRgba(hex: string, alpha: number) {
   const r = parseInt(hex.slice(1, 3), 16)
   const g = parseInt(hex.slice(3, 5), 16)
@@ -79,12 +122,6 @@ function hexToRgba(hex: string, alpha: number) {
   return `rgba(${r}, ${g}, ${b}, ${alpha})`
 }
 
-interface HeaderItem {
-  label: string
-  key: keyof typeof data.value
-  judgment?: boolean
-}
-
 const headerCenterContent: HeaderItem[] = [
   { label: '设备名称', key: 'deviceName' },
   { label: '所属部门', key: 'dept' },
@@ -99,16 +136,6 @@ const headerTagContent: HeaderItem[] = [
   { label: '北斗', key: 'carOnline', judgment: true }
 ]
 
-interface Dimensions {
-  identifier: string
-  name: string
-  value: string | number
-  color: string
-  bgHover: string
-  bgActive: string
-  response?: boolean
-}
-
 const dimensions = ref<Dimensions[]>([])
 const gatewayDimensions = ref<Dimensions[]>([])
 const carDimensions = ref<Dimensions[]>([])
@@ -141,36 +168,55 @@ async function loadDimensions() {
   try {
     const gateway = (((await IotDeviceApi.getIotDeviceTds(Number(query.id))) as any[]) ?? [])
       .sort((a, b) => b.modelOrder - a.modelOrder)
-      .map((item) => ({
-        identifier: item.identifier,
-        name: item.modelName,
-        value: item.value,
-        response: false
-      }))
+      .map((item) => {
+        const { value, suffix, isText } = formatIotValue(item.value)
+        return {
+          identifier: item.identifier,
+          name: item.modelName,
+          value: value,
+          suffix: suffix,
+          isText: isText,
+          response: false
+        }
+      })
 
     const car = (((await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))) as any[]) ?? [])
       .sort((a, b) => b.modelOrder - a.modelOrder)
-      .map((item) => ({
-        identifier: item.identifier,
-        name: item.modelName,
-        value: item.value,
-        response: false
-      }))
-
-    // 合并并分配霓虹色
-    dimensions.value = [...gateway, ...car]
-      .filter((item) => !disabledDimensions.value.includes(item.identifier))
-      .map((item, index) => {
-        const color = neonColors[index]
-
+      .map((item) => {
+        const { value, suffix, isText } = formatIotValue(item.value)
+        console.log(`${item.modelName} :>> `, value)
         return {
-          ...item,
-          color: color,
-          bgHover: hexToRgba(color, 0.08),
-          bgActive: hexToRgba(color, 0.12)
+          identifier: item.identifier,
+          name: item.modelName,
+          value: value,
+          suffix: suffix,
+          isText: isText,
+          response: false
         }
       })
 
+    const rawList = [...gateway, ...car]
+
+    const uniqueMap = new Map()
+
+    rawList.forEach((item) => {
+      const uniqueKey = `${item.identifier}|${item.name}`
+
+      // if (!uniqueMap.has(uniqueKey)) {
+      uniqueMap.set(uniqueKey, item)
+      // }
+    })
+
+    dimensions.value = Array.from(uniqueMap.values()).map((item, index) => {
+      const color = neonColors[index % neonColors.length]
+      return {
+        ...item,
+        color: color,
+        bgHover: hexToRgba(color, 0.08),
+        bgActive: hexToRgba(color, 0.12)
+      }
+    })
+
     gatewayDimensions.value = dimensions.value.filter((d) =>
       gateway.some((g) => g.identifier === d.identifier)
     )
@@ -199,15 +245,14 @@ async function loadDimensions() {
 //       IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))
 //     ])
 
-//     // 2. 创建一个 Map 用于快速查找 (Identifier -> Value)
-//     // 这样可以将复杂度从 O(N*M) 降低到 O(N)
-//     const newValueMap = new Map<string, any>()
+//     const newValueMap = new Map<string, { value: string; suffix?: string; isText?: boolean }>()
 
 //     const addToMap = (data: any[]) => {
 //       if (!data) return
 //       data.forEach((item) => {
 //         if (item.identifier) {
-//           newValueMap.set(item.identifier, item.value)
+//           const { value, suffix, isText } = formatIotValue(item.value)
+//           newValueMap.set(item.identifier, { value, suffix, isText })
 //         }
 //       })
 //     }
@@ -215,28 +260,30 @@ async function loadDimensions() {
 //     addToMap(gatewayRes as any[])
 //     addToMap(carRes as any[])
 
-//     // 3. 更新 dimensions.value (保留了之前的 color 和其他属性)
 //     dimensions.value.forEach((item) => {
 //       if (newValueMap.has(item.identifier)) {
-//         item.value = newValueMap.get(item.identifier)
+//         const { value, suffix, isText } = newValueMap.get(item.identifier) ?? {}
+//         item.value = value ?? ''
+//         item.suffix = suffix
+//         item.isText = isText
 //       }
 //     })
 
-//     // 4. 如果还需要同步更新 gatewayDimensions 和 carDimensions
-//     // (假设这些是引用类型,如果它们引用的是同一个对象,上面更新 dimensions 时可能已经同步了。
-//     // 如果它们是独立的对象数组,则需要显式更新)
-
-//     // 更新 Gateway 原始列表
 //     gatewayDimensions.value.forEach((item) => {
 //       if (newValueMap.has(item.identifier)) {
-//         item.value = newValueMap.get(item.identifier)
+//         const { value, suffix, isText } = newValueMap.get(item.identifier) ?? {}
+//         item.value = value ?? ''
+//         item.suffix = suffix
+//         item.isText = isText
 //       }
 //     })
 
-//     // 更新 Car 原始列表
 //     carDimensions.value.forEach((item) => {
 //       if (newValueMap.has(item.identifier)) {
-//         item.value = newValueMap.get(item.identifier)
+//         const { value, suffix, isText } = newValueMap.get(item.identifier) ?? {}
+//         item.value = value ?? ''
+//         item.suffix = suffix
+//         item.isText = isText
 //       }
 //     })
 //   } catch (error) {
@@ -730,10 +777,11 @@ onUnmounted(() => {
 
           <div class="px-3 pb-4 pt-2 space-y-3">
             <div
+              :data-disabled="disabledDimensions.includes(item.identifier)"
               v-for="item in citem.value"
               :key="item.identifier"
               @click="handleClickSpec(item.name)"
-              class="dimension-card group relative p-3 rounded-lg border border-solid bg-transparent border-gray-300 transition-all duration-300 cursor-pointer select-none"
+              class="dimension-card group relative p-3 rounded-lg border border-solid bg-transparent border-gray-300 transition-all duration-300 cursor-pointer select-none data-[disabled=true]:pointer-events-none"
               :class="{ 'is-active': selectedDimension[item.name] }"
               :style="{
                 '--theme-color': item.color,
@@ -757,10 +805,15 @@ onUnmounted(() => {
 
               <div class="flex items-baseline justify-between relative z-10">
                 <animated-count-to
+                  v-if="!item.isText"
                   :value="Number(item.value)"
                   :duration="500"
+                  :suffix="item.suffix"
                   class="text-lg font-bold font-mono tracking-tight text-slate-800"
                 />
+                <span v-else class="text-lg font-bold font-mono tracking-tight text-slate-800">
+                  {{ item.value }}
+                </span>
               </div>
               <div
                 class="absolute left-0 top-3 bottom-3 w-1 rounded-r transition-all duration-300"

+ 64 - 54
src/views/pms/device/monitor/TdDeviceInfo.vue

@@ -15,6 +15,7 @@ import dayjs from 'dayjs'
 import * as echarts from 'echarts'
 import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
 import { rangeShortcuts } from '@/utils/formatTime'
+import { Dimensions, formatIotValue, HeaderItem } from '@/utils/useSocketBus'
 
 const { query } = useRoute()
 
@@ -35,12 +36,6 @@ function hexToRgba(hex: string, alpha: number) {
   return `rgba(${r}, ${g}, ${b}, ${alpha})`
 }
 
-interface HeaderItem {
-  label: string
-  key: keyof typeof data.value
-  judgment?: boolean
-}
-
 const headerCenterContent: HeaderItem[] = [
   { label: '设备名称', key: 'deviceName' },
   { label: '所属部门', key: 'dept' },
@@ -55,16 +50,6 @@ const headerTagContent: HeaderItem[] = [
   { label: '北斗', key: 'carOnline', judgment: true }
 ]
 
-interface Dimensions {
-  identifier: string
-  name: string
-  value: string | number
-  color: string
-  bgHover: string
-  bgActive: string
-  response?: boolean
-}
-
 const dimensions = ref<Dimensions[]>([])
 const gatewayDimensions = ref<Dimensions[]>([])
 const carDimensions = ref<Dimensions[]>([])
@@ -97,36 +82,54 @@ async function loadDimensions() {
   try {
     const gateway = (((await IotDeviceApi.getIotDeviceTds(Number(query.id))) as any[]) ?? [])
       .sort((a, b) => b.modelOrder - a.modelOrder)
-      .map((item) => ({
-        identifier: item.identifier,
-        name: item.modelName,
-        value: item.value,
-        response: false
-      }))
+      .map((item) => {
+        const { value, suffix, isText } = formatIotValue(item.value)
+        return {
+          identifier: item.identifier,
+          name: item.modelName,
+          value: value,
+          suffix: suffix,
+          isText: isText,
+          response: false
+        }
+      })
 
     const car = (((await IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))) as any[]) ?? [])
       .sort((a, b) => b.modelOrder - a.modelOrder)
-      .map((item) => ({
-        identifier: item.identifier,
-        name: item.modelName,
-        value: item.value,
-        response: false
-      }))
-
-    // 合并并分配霓虹色
-    dimensions.value = [...gateway, ...car]
-      .filter((item) => !disabledDimensions.value.includes(item.identifier))
-      .map((item, index) => {
-        const color = neonColors[index]
-
+      .map((item) => {
+        const { value, suffix, isText } = formatIotValue(item.value)
         return {
-          ...item,
-          color: color,
-          bgHover: hexToRgba(color, 0.08),
-          bgActive: hexToRgba(color, 0.12)
+          identifier: item.identifier,
+          name: item.modelName,
+          value: value,
+          suffix: suffix,
+          isText: isText,
+          response: false
         }
       })
 
+    const rawList = [...gateway, ...car]
+
+    const uniqueMap = new Map()
+
+    rawList.forEach((item) => {
+      const uniqueKey = `${item.identifier}|${item.name}`
+
+      // if (!uniqueMap.has(uniqueKey)) {
+      uniqueMap.set(uniqueKey, item)
+      // }
+    })
+
+    dimensions.value = Array.from(uniqueMap.values()).map((item, index) => {
+      const color = neonColors[index % neonColors.length]
+      return {
+        ...item,
+        color: color,
+        bgHover: hexToRgba(color, 0.08),
+        bgActive: hexToRgba(color, 0.12)
+      }
+    })
+
     gatewayDimensions.value = dimensions.value.filter((d) =>
       gateway.some((g) => g.identifier === d.identifier)
     )
@@ -155,15 +158,14 @@ async function updateDimensionValues() {
       IotDeviceApi.getIotDeviceZHBDTds(Number(query.id))
     ])
 
-    // 2. 创建一个 Map 用于快速查找 (Identifier -> Value)
-    // 这样可以将复杂度从 O(N*M) 降低到 O(N)
-    const newValueMap = new Map<string, any>()
+    const newValueMap = new Map<string, { value: string; suffix?: string; isText?: boolean }>()
 
     const addToMap = (data: any[]) => {
       if (!data) return
       data.forEach((item) => {
         if (item.identifier) {
-          newValueMap.set(item.identifier, item.value)
+          const { value, suffix, isText } = formatIotValue(item.value)
+          newValueMap.set(item.identifier, { value, suffix, isText })
         }
       })
     }
@@ -171,28 +173,30 @@ async function updateDimensionValues() {
     addToMap(gatewayRes as any[])
     addToMap(carRes as any[])
 
-    // 3. 更新 dimensions.value (保留了之前的 color 和其他属性)
     dimensions.value.forEach((item) => {
       if (newValueMap.has(item.identifier)) {
-        item.value = newValueMap.get(item.identifier)
+        const { value, suffix, isText } = newValueMap.get(item.identifier) ?? {}
+        item.value = value ?? ''
+        item.suffix = suffix
+        item.isText = isText
       }
     })
 
-    // 4. 如果还需要同步更新 gatewayDimensions 和 carDimensions
-    // (假设这些是引用类型,如果它们引用的是同一个对象,上面更新 dimensions 时可能已经同步了。
-    // 如果它们是独立的对象数组,则需要显式更新)
-
-    // 更新 Gateway 原始列表
     gatewayDimensions.value.forEach((item) => {
       if (newValueMap.has(item.identifier)) {
-        item.value = newValueMap.get(item.identifier)
+        const { value, suffix, isText } = newValueMap.get(item.identifier) ?? {}
+        item.value = value ?? ''
+        item.suffix = suffix
+        item.isText = isText
       }
     })
 
-    // 更新 Car 原始列表
     carDimensions.value.forEach((item) => {
       if (newValueMap.has(item.identifier)) {
-        item.value = newValueMap.get(item.identifier)
+        const { value, suffix, isText } = newValueMap.get(item.identifier) ?? {}
+        item.value = value ?? ''
+        item.suffix = suffix
+        item.isText = isText
       }
     })
   } catch (error) {
@@ -676,10 +680,11 @@ onUnmounted(() => {
 
           <div class="px-3 pb-4 pt-2 space-y-3">
             <div
+              :data-disabled="disabledDimensions.includes(item.identifier)"
               v-for="item in citem.value"
               :key="item.identifier"
               @click="handleClickSpec(item.name)"
-              class="dimension-card group relative p-3 rounded-lg border border-solid bg-transparent border-gray-300 transition-all duration-300 cursor-pointer select-none"
+              class="dimension-card group relative p-3 rounded-lg border border-solid bg-transparent border-gray-300 transition-all duration-300 cursor-pointer select-none data-[disabled=true]:pointer-events-none"
               :class="{ 'is-active': selectedDimension[item.name] }"
               :style="{
                 '--theme-color': item.color,
@@ -703,10 +708,15 @@ onUnmounted(() => {
 
               <div class="flex items-baseline justify-between relative z-10">
                 <animated-count-to
+                  v-if="!item.isText"
                   :value="Number(item.value)"
                   :duration="500"
+                  :suffix="item.suffix"
                   class="text-lg font-bold font-mono tracking-tight text-slate-800"
                 />
+                <span v-else class="text-lg font-bold font-mono tracking-tight text-slate-800">
+                  {{ item.value }}
+                </span>
               </div>
               <div
                 class="absolute left-0 top-3 bottom-3 w-1 rounded-r transition-all duration-300"

+ 16 - 7
src/views/pms/iotrhdailyreport/approval.vue

@@ -293,15 +293,24 @@ function cellStyle(data: {
   const { row, column } = data
 
   if (column.property === 'transitTime') {
-    const originalValue = row.transitTime ?? 0
-
-    if (originalValue > 1.2)
-      return {
-        color: 'red',
-        fontWeight: 'bold'
+    // 1. 获取参与计算的字段,逻辑与 formatter 保持一致
+    const capacity = Number(row?.capacity)
+    const dailyGasInjection = Number(row?.dailyGasInjection)
+
+    // 2. 只有当两个值都有效(且 capacity 不为 0)时才进行计算
+    // 对应 formatter 中的 if (!capacity || !dailyGasInjection) 返回 '0.00%' 的情况
+    if (capacity && dailyGasInjection) {
+      const ratio = dailyGasInjection / capacity
+
+      // 3. 判断计算结果是否大于 1.2 (即 120%)
+      if (ratio > 1.0) {
+        return {
+          color: 'red',
+          fontWeight: 'bold'
+        }
       }
+    }
   }
-
   // const timeFields = ['dailyInjectGasTime', 'dailyInjectWaterTime', 'nonProductionTime']
   // if (timeFields.includes(column.property)) {
   //   if (!checkTimeSumEquals24(row)) {

+ 16 - 6
src/views/pms/iotrhdailyreport/fill.vue

@@ -293,13 +293,23 @@ function cellStyle(data: {
   const { row, column } = data
 
   if (column.property === 'transitTime') {
-    const originalValue = row.transitTime ?? 0
-
-    if (originalValue > 1.2)
-      return {
-        color: 'red',
-        fontWeight: 'bold'
+    // 1. 获取参与计算的字段,逻辑与 formatter 保持一致
+    const capacity = Number(row?.capacity)
+    const dailyGasInjection = Number(row?.dailyGasInjection)
+
+    // 2. 只有当两个值都有效(且 capacity 不为 0)时才进行计算
+    // 对应 formatter 中的 if (!capacity || !dailyGasInjection) 返回 '0.00%' 的情况
+    if (capacity && dailyGasInjection) {
+      const ratio = dailyGasInjection / capacity
+
+      // 3. 判断计算结果是否大于 1.2 (即 120%)
+      if (ratio > 1.2) {
+        return {
+          color: 'red',
+          fontWeight: 'bold'
+        }
       }
+    }
   }
 
   // const timeFields = ['dailyInjectGasTime', 'dailyInjectWaterTime', 'nonProductionTime']

+ 16 - 9
src/views/pms/iotrhdailyreport/index.vue

@@ -365,7 +365,7 @@
                 resizable
               />
               <el-table-column
-                label="用电量(万千瓦时)"
+                label="用电量(MWh)"
                 align="center"
                 prop="yearTotalPower"
                 :formatter="gasInjectionFormatter"
@@ -850,14 +850,21 @@ const cellStyle = ({
 }) => {
   // 只针对 transitTime 列进行处理
   if (column.property === 'transitTime') {
-    // 获取原始值(不是格式化后的百分比值)
-    const originalValue = row.transitTime
-
-    // 检查值是否大于120
-    if (originalValue !== null && originalValue !== undefined && parseFloat(originalValue) > 1.2) {
-      return {
-        color: 'red',
-        fontWeight: 'bold' // 可选:加粗以更突出显示
+    // 1. 获取参与计算的字段,逻辑与 formatter 保持一致
+    const capacity = Number(row?.capacity)
+    const dailyGasInjection = Number(row?.dailyGasInjection)
+
+    // 2. 只有当两个值都有效(且 capacity 不为 0)时才进行计算
+    // 对应 formatter 中的 if (!capacity || !dailyGasInjection) 返回 '0.00%' 的情况
+    if (capacity && dailyGasInjection) {
+      const ratio = dailyGasInjection / capacity
+
+      // 3. 判断计算结果是否大于 1.2 (即 120%)
+      if (ratio > 1.2) {
+        return {
+          color: 'red',
+          fontWeight: 'bold'
+        }
       }
     }
   }