Bläddra i källkod

调整连油看板

Zimo 4 dagar sedan
förälder
incheckning
c1a1ccedb5

+ 283 - 54
src/views/oli-connection/monitoring-board/chart.vue

@@ -45,10 +45,10 @@ const props = defineProps({
     type: String,
     required: true
   },
-  // isRealTime: {
-  //   type: Boolean,
-  //   default: true
-  // },
+  isRealTime: {
+    type: Boolean,
+    default: true
+  },
   date: {
     type: Array as PropType<Array<string>>,
     required: true
@@ -64,8 +64,14 @@ const selectedDimension = ref<Record<string, boolean>>({})
 
 const { connect, destroy, isConnected, subscribe } = useMqtt()
 
+const REALTIME_WINDOW_MS = 5 * 60 * 1000
+let isProgrammaticDataZoomUpdate = false
+
 const handleMessageUpdate = (_topic: string, data: any) => {
   const valueMap = new Map<string, number>()
+  const nextUpdatedNames = new Set<string>()
+  const wasFollowingFullRange = isFollowingFullRange.value
+  const previousVisibleRange = { ...visibleTimeRange.value }
 
   for (const item of data) {
     const { id: identity, value: logValue, remark } = item
@@ -79,18 +85,46 @@ const handleMessageUpdate = (_topic: string, data: any) => {
     const modelName = dimensions.value.find((item) => item.identifier === identity)?.name
 
     if (modelName && chartData.value[modelName]) {
-      chartData.value[modelName].push({
-        ts: dayjs.unix(remark).valueOf(),
+      const ts = dayjs.unix(remark).valueOf()
+
+      appendChartPoint(modelName, {
+        ts,
         value
       })
 
-      if (isFollowingFullRange.value) {
-        resetVisibleTimeRange()
-      }
-
-      updateSeriesByNames([modelName])
+      nextUpdatedNames.add(modelName)
     }
   }
+
+  if (!nextUpdatedNames.size) return
+
+  const { maxTs } = getGlobalTimeExtent()
+  if (maxTs === null) return
+
+  const fullRange = getRealtimeTimeRange(maxTs)
+  const trimmedNames = trimChartDataToRealtimeWindow(maxTs)
+  const selectedRange = wasFollowingFullRange
+    ? fullRange
+    : clampTimeRangeToFullRange(visibleTimeRange.value, fullRange)
+
+  visibleTimeRange.value = selectedRange
+  isFollowingFullRange.value = isSameTimeRange(selectedRange, fullRange)
+
+  trimmedNames.forEach((name) => nextUpdatedNames.add(name))
+
+  const didVisibleRangeChange =
+    previousVisibleRange.startTs === null ||
+    previousVisibleRange.endTs === null ||
+    Math.abs(previousVisibleRange.startTs - selectedRange.startTs) > 1 ||
+    Math.abs(previousVisibleRange.endTs - selectedRange.endTs) > 1
+
+  applyChartRangeAndSeries(
+    fullRange,
+    selectedRange,
+    wasFollowingFullRange || didVisibleRangeChange
+      ? getSelectedSeriesNames()
+      : Array.from(nextUpdatedNames)
+  )
 }
 
 watch(isConnected, (newVal) => {
@@ -140,8 +174,13 @@ async function loadDimensions() {
   }
 }
 
+interface ChartPoint {
+  ts: number
+  value: number
+}
+
 interface ChartData {
-  [key: Dimensions['name']]: { ts: number; value: number }[]
+  [key: Dimensions['name']]: ChartPoint[]
 }
 
 const chartData = ref<ChartData>({})
@@ -158,6 +197,7 @@ const visibleTimeRange = ref<{
 })
 
 const isFollowingFullRange = ref(true)
+const isRealtimeMode = ref(true)
 
 const TREND_AXIS_MIN = 0
 const TREND_AXIS_MAX = 100
@@ -202,30 +242,187 @@ const getGlobalTimeExtent = () => {
 }
 
 function resetVisibleTimeRange() {
-  const { minTs, maxTs } = getGlobalTimeExtent()
+  const { startTs, endTs } = getFullTimeRange()
 
   visibleTimeRange.value = {
-    startTs: minTs,
-    endTs: maxTs
+    startTs,
+    endTs
   }
 
   isFollowingFullRange.value = true
 }
 
-function syncVisibleTimeRangeFromChart() {
+function getFullTimeRange() {
   const { minTs, maxTs } = getGlobalTimeExtent()
 
-  if (!chart || minTs === null || maxTs === null) {
+  if (isRealtimeMode.value && maxTs !== null) {
+    return getRealtimeTimeRange(maxTs)
+  }
+
+  return {
+    startTs: minTs,
+    endTs: maxTs
+  }
+}
+
+function getRealtimeTimeRange(endTs: number) {
+  return {
+    startTs: endTs - REALTIME_WINDOW_MS,
+    endTs
+  }
+}
+
+function isSameTimeRange(
+  range: { startTs: number | null; endTs: number | null },
+  fullRange: { startTs: number; endTs: number }
+) {
+  if (range.startTs === null || range.endTs === null) return false
+
+  const tolerance = Math.max((fullRange.endTs - fullRange.startTs) * 0.001, 1000)
+
+  return (
+    Math.abs(range.startTs - fullRange.startTs) <= tolerance &&
+    Math.abs(range.endTs - fullRange.endTs) <= tolerance
+  )
+}
+
+function clampTimeRangeToFullRange(
+  range: { startTs: number | null; endTs: number | null },
+  fullRange: { startTs: number; endTs: number }
+) {
+  if (range.startTs === null || range.endTs === null) return fullRange
+
+  const startTs = Math.max(Math.min(range.startTs, range.endTs), fullRange.startTs)
+  const endTs = Math.min(Math.max(range.startTs, range.endTs), fullRange.endTs)
+
+  if (startTs > endTs) return fullRange
+
+  return {
+    startTs,
+    endTs
+  }
+}
+
+function appendChartPoint(name: string, point: ChartPoint) {
+  const dataset = chartData.value[name]
+  const lastPoint = dataset.at(-1)
+
+  if (!lastPoint || point.ts >= lastPoint.ts) {
+    dataset.push(point)
+    return
+  }
+
+  const insertIndex = dataset.findIndex(({ ts }) => ts > point.ts)
+  dataset.splice(insertIndex === -1 ? dataset.length : insertIndex, 0, point)
+}
+
+function trimDatasetToRealtimeWindow(dataset: ChartPoint[], startTs: number, endTs: number) {
+  const boundedDataset = dataset.filter(({ ts }) => Number.isFinite(ts) && ts <= endTs)
+
+  if (!boundedDataset.length) return []
+
+  const firstVisibleIndex = boundedDataset.findIndex(({ ts }) => ts >= startTs)
+
+  if (firstVisibleIndex === -1) {
+    return boundedDataset.slice(-1)
+  }
+
+  return boundedDataset.slice(Math.max(firstVisibleIndex - 1, 0))
+}
+
+function trimChartDataToRealtimeWindow(endTs: number | null) {
+  if (endTs === null || !Number.isFinite(endTs)) return []
+
+  const { startTs } = getRealtimeTimeRange(endTs)
+  const trimmedNames: string[] = []
+
+  Object.entries(chartData.value).forEach(([name, dataset]) => {
+    const nextDataset = trimDatasetToRealtimeWindow(dataset, startTs, endTs)
+
+    if (nextDataset.length !== dataset.length || nextDataset[0] !== dataset[0]) {
+      chartData.value[name] = nextDataset
+      trimmedNames.push(name)
+    }
+  })
+
+  return trimmedNames
+}
+
+function getDataZoomRangeOptions(range: { startTs: number; endTs: number }) {
+  return [
+    {
+      id: 'monitoring-board-inside-zoom',
+      startValue: range.startTs,
+      endValue: range.endTs
+    },
+    {
+      id: 'monitoring-board-slider-zoom',
+      startValue: range.startTs,
+      endValue: range.endTs
+    }
+  ]
+}
+
+function applyChartRangeAndSeries(
+  fullRange: { startTs: number; endTs: number },
+  selectedRange: { startTs: number; endTs: number },
+  names: string[]
+) {
+  if (!chart) render()
+  if (!chart) return
+
+  isProgrammaticDataZoomUpdate = true
+
+  chart.setOption(
+    {
+      xAxis: {
+        min: fullRange.startTs,
+        max: fullRange.endTs
+      },
+      dataZoom: getDataZoomRangeOptions(selectedRange),
+      series: names.map((name) => ({
+        name,
+        data: buildSeriesData(name)
+      }))
+    },
+    {
+      lazyUpdate: true,
+      silent: true
+    }
+  )
+
+  window.setTimeout(() => {
+    isProgrammaticDataZoomUpdate = false
+  })
+}
+
+function getDataZoomPayload(params?: any) {
+  const payload = Array.isArray(params?.batch) ? params.batch[0] : params
+
+  if (
+    payload &&
+    ['startValue', 'endValue', 'start', 'end'].some((key) => payload[key] !== undefined)
+  ) {
+    return payload
+  }
+
+  const dataZoomOptions = chart?.getOption().dataZoom
+  return Array.isArray(dataZoomOptions) ? (dataZoomOptions[0] as any) : undefined
+}
+
+function syncVisibleTimeRangeFromChart(params?: any) {
+  const { startTs: fullStartTs, endTs: fullEndTs } = getFullTimeRange()
+
+  if (!chart || fullStartTs === null || fullEndTs === null) {
     visibleTimeRange.value = {
-      startTs: minTs,
-      endTs: maxTs
+      startTs: fullStartTs,
+      endTs: fullEndTs
     }
     isFollowingFullRange.value = true
     return
   }
 
-  const dataZoomOptions = chart.getOption().dataZoom
-  const primaryDataZoom = Array.isArray(dataZoomOptions) ? (dataZoomOptions[0] as any) : undefined
+  const primaryDataZoom = getDataZoomPayload(params)
 
   if (!primaryDataZoom) {
     resetVisibleTimeRange()
@@ -235,8 +432,8 @@ function syncVisibleTimeRangeFromChart() {
   const startValue = Number(primaryDataZoom.startValue)
   const endValue = Number(primaryDataZoom.endValue)
 
-  let nextStartTs = minTs
-  let nextEndTs = maxTs
+  let nextStartTs = fullStartTs
+  let nextEndTs = fullEndTs
 
   if (Number.isFinite(startValue) && Number.isFinite(endValue)) {
     nextStartTs = Math.min(startValue, endValue)
@@ -244,23 +441,25 @@ function syncVisibleTimeRangeFromChart() {
   } else {
     const startPercent = Number(primaryDataZoom.start ?? 0)
     const endPercent = Number(primaryDataZoom.end ?? 100)
-    const range = maxTs - minTs
+    const range = fullEndTs - fullStartTs
 
-    nextStartTs = minTs + (range * Math.min(startPercent, endPercent)) / 100
-    nextEndTs = minTs + (range * Math.max(startPercent, endPercent)) / 100
+    nextStartTs = fullStartTs + (range * Math.min(startPercent, endPercent)) / 100
+    nextEndTs = fullStartTs + (range * Math.max(startPercent, endPercent)) / 100
   }
 
-  visibleTimeRange.value = {
+  const nextRange = {
     startTs: nextStartTs,
     endTs: nextEndTs
   }
 
-  const tolerance = Math.max((maxTs - minTs) * 0.001, 1)
-  isFollowingFullRange.value =
-    Math.abs(nextStartTs - minTs) <= tolerance && Math.abs(nextEndTs - maxTs) <= tolerance
+  visibleTimeRange.value = nextRange
+  isFollowingFullRange.value = isSameTimeRange(nextRange, {
+    startTs: fullStartTs,
+    endTs: fullEndTs
+  })
 }
 
-const getVisibleDataset = (dataset: { ts: number; value: number }[]) => {
+const getVisibleDataset = (dataset: ChartPoint[]) => {
   const { startTs, endTs } = visibleTimeRange.value
 
   if (startTs === null || endTs === null) {
@@ -270,7 +469,7 @@ const getVisibleDataset = (dataset: { ts: number; value: number }[]) => {
   return dataset.filter(({ ts }) => ts >= startTs && ts <= endTs)
 }
 
-const getTrendStats = (dataset: { ts: number; value: number }[]): TrendStats => {
+const getTrendStats = (dataset: ChartPoint[]): TrendStats => {
   const values = getVisibleDataset(dataset)
     .map(({ value }) => Number(value))
     .filter((value) => Number.isFinite(value))
@@ -375,16 +574,20 @@ function updateSeriesByNames(names: string[]) {
   if (!chart) render()
   if (!chart || !names.length) return
 
-  chart.setOption({
-    series: names.map((name) => ({
-      name,
-      data: buildSeriesData(name)
-    }))
-  })
+  chart.setOption(
+    {
+      series: names.map((name) => ({
+        name,
+        data: buildSeriesData(name)
+      }))
+    },
+    {
+      lazyUpdate: true
+    }
+  )
 }
 
 const refreshVisibleSeries = useDebounceFn(() => {
-  syncVisibleTimeRangeFromChart()
   updateSeriesByNames(getSelectedSeriesNames())
 }, 50)
 
@@ -403,7 +606,10 @@ function chartInit() {
   })
 
   chart.off('datazoom')
-  chart.on('datazoom', () => {
+  chart.on('datazoom', (params: any) => {
+    if (isProgrammaticDataZoomUpdate) return
+
+    syncVisibleTimeRangeFromChart(params)
     refreshVisibleSeries()
   })
 
@@ -420,10 +626,10 @@ function render() {
 
   chart.setOption({
     color: neonColors,
-    animation: true,
-    animationDuration: 200,
+    animation: false,
+    animationDuration: 0,
     animationEasing: 'linear',
-    animationDurationUpdate: 200,
+    animationDurationUpdate: 0,
     animationEasingUpdate: 'linear',
     grid: {
       left: '3%',
@@ -491,8 +697,15 @@ function render() {
       }
     },
     dataZoom: [
-      { type: 'inside', xAxisIndex: 0, filterMode: 'none', throttle: 50 },
       {
+        id: 'monitoring-board-inside-zoom',
+        type: 'inside',
+        xAxisIndex: 0,
+        filterMode: 'none',
+        throttle: 50
+      },
+      {
+        id: 'monitoring-board-slider-zoom',
         type: 'slider',
         xAxisIndex: 0,
         filterMode: 'none',
@@ -585,11 +798,11 @@ function render() {
 
       emphasis: {
         focus: 'series',
-        lineStyle: { width: 4 }
+        lineStyle: { width: 3 }
       },
 
       lineStyle: {
-        width: 3,
+        width: 2,
         shadowColor: 'rgba(0, 0, 0, 0.5)',
         shadowBlur: 10,
         shadowOffsetY: 5
@@ -608,6 +821,7 @@ const chartLoading = ref(false)
 async function initLoadChartData(real_time: boolean = true) {
   if (!dimensions.value.length) return
 
+  isRealtimeMode.value = real_time
   chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
   isFollowingFullRange.value = true
   resetVisibleTimeRange()
@@ -650,6 +864,17 @@ async function initLoadChartData(real_time: boolean = true) {
   }
 
   if (real_time) {
+    const { maxTs } = getGlobalTimeExtent()
+
+    if (maxTs !== null) {
+      trimChartDataToRealtimeWindow(maxTs)
+      const fullRange = getRealtimeTimeRange(maxTs)
+
+      visibleTimeRange.value = fullRange
+      isFollowingFullRange.value = true
+      applyChartRangeAndSeries(fullRange, fullRange, getSelectedSeriesNames())
+    }
+
     connect(`wss://aims.deepoil.cc/mqtt`, { password: props.token }, handleMessageUpdate)
   }
 }
@@ -665,20 +890,23 @@ onMounted(() => {
 })
 
 watch(
-  () => props.date,
-  async (newDate, oldDate) => {
+  () => [props.date, props.isRealTime] as const,
+  async ([newDate, isRealTime], [oldDate, oldIsRealTime]) => {
     if (!newDate || newDate.length !== 2) return
 
-    if (oldDate && newDate[0] === oldDate[0] && newDate[1] === oldDate[1]) return
+    if (
+      oldDate &&
+      newDate[0] === oldDate[0] &&
+      newDate[1] === oldDate[1] &&
+      isRealTime === oldIsRealTime
+    ) {
+      return
+    }
 
     await cancelAllRequests()
 
     destroy()
 
-    const endTime = dayjs(newDate[1])
-    const now = dayjs()
-    const isRealTime = endTime.isAfter(now.subtract(1, 'minute'))
-
     if (chart) chart.clear()
 
     if (isRealTime) initfn(false)
@@ -705,7 +933,8 @@ function handleDetailClick() {
       name: props.deviceName,
       code: props.deviceCode,
       dept: props.deptName,
-      vehicle: props.vehicleName
+      vehicle: props.vehicleName,
+      mqttUrl: props.mqttUrl
     }
   })
 }

+ 156 - 14
src/views/oli-connection/monitoring-board/index.vue

@@ -45,16 +45,22 @@ interface Query {
   time: string[]
 }
 
-const originalQuery: Query = {
-  deviceCodes: [],
-  time: [
+function getRealtimeTimeRange() {
+  return [
     dayjs().subtract(5, 'minute').format('YYYY-MM-DD HH:mm:ss'),
     dayjs().format('YYYY-MM-DD HH:mm:ss')
   ]
-  // time: [...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
 }
 
-const query = ref<Query>({ ...originalQuery })
+function getOriginalQuery(): Query {
+  return {
+    deviceCodes: [],
+    time: getRealtimeTimeRange()
+  }
+}
+
+const query = ref<Query>(getOriginalQuery())
+const isRealTime = ref(true)
 
 const deptLoading = ref(false)
 const deptOptions = ref<any[]>([])
@@ -125,6 +131,8 @@ const chartOption = ref<EChartsOption>({})
 const pageSize = 5
 const pageIndex = ref(0)
 const activeMainCardId = ref<number | null>(null)
+const boardMotion = ref<'page-prev' | 'page-next' | 'promote' | ''>('')
+let boardMotionTimer: number | undefined
 
 const totalPages = computed(() => Math.max(1, Math.ceil(deviceList.value.length / pageSize)))
 
@@ -143,24 +151,46 @@ const sideCards = computed(() => {
   return currentPageCards.value.filter((item) => item.id !== mainCard.value?.id)
 })
 
+const boardMotionClass = computed(() => (boardMotion.value ? `is-${boardMotion.value}` : ''))
+
+function triggerBoardMotion(type: 'page-prev' | 'page-next' | 'promote') {
+  if (boardMotionTimer) {
+    window.clearTimeout(boardMotionTimer)
+  }
+
+  boardMotion.value = ''
+
+  nextTick(() => {
+    boardMotion.value = type
+    boardMotionTimer = window.setTimeout(() => {
+      boardMotion.value = ''
+    }, 520)
+  })
+}
+
 function syncPageMainCard() {
   activeMainCardId.value = currentPageCards.value[0]?.id ?? null
 }
 
 function setMainCard(card: DeviceData) {
+  if (activeMainCardId.value === card.id) return
+
   activeMainCardId.value = card.id
+  triggerBoardMotion('promote')
 }
 
 function goPrevGroup() {
   if (pageIndex.value <= 0) return
   pageIndex.value -= 1
   syncPageMainCard()
+  triggerBoardMotion('page-prev')
 }
 
 function goNextGroup() {
   if (pageIndex.value >= totalPages.value - 1) return
   pageIndex.value += 1
   syncPageMainCard()
+  triggerBoardMotion('page-next')
 }
 
 async function handleDeviceChange(selectedIds: number[]) {
@@ -201,8 +231,15 @@ onMounted(() => {
   loadDeviceOptions()
 })
 
+onUnmounted(() => {
+  if (boardMotionTimer) {
+    window.clearTimeout(boardMotionTimer)
+  }
+})
+
 function handleDeptChange() {
-  query.value = { ...originalQuery }
+  query.value = getOriginalQuery()
+  isRealTime.value = true
   deviceList.value = []
   chartOption.value = {}
   pageIndex.value = 0
@@ -216,7 +253,8 @@ const { toggle, isFullscreen } = useFullscreen(targetArea)
 
 function handleRest() {
   deviceQuery.value = { ...originalDeviceQuery }
-  query.value = { ...originalQuery }
+  query.value = getOriginalQuery()
+  isRealTime.value = true
   pageIndex.value = 0
   activeMainCardId.value = null
   loadDeptOptions()
@@ -227,6 +265,10 @@ function handleInlineChange() {
   loadDeviceOptions()
 }
 
+function handleTimeChange() {
+  isRealTime.value = false
+}
+
 const token = ref('')
 const showSearchDialog = ref(false)
 
@@ -321,14 +363,20 @@ onMounted(() => {
         </div>
       </div>
 
-      <div v-if="mainCard" class="monitor-board-scroll-area">
+      <div v-if="mainCard" class="monitor-board-scroll-area" :class="boardMotionClass">
         <div class="monitor-board-layout">
           <div
             v-if="sideCards[0]"
             class="monitor-card-shell monitor-card-side monitor-card-left-top"
             @click="setMainCard(sideCards[0])"
           >
-            <chart v-bind="sideCards[0]" :date="query.time" :token="token" />
+            <chart
+              :key="sideCards[0].id"
+              v-bind="sideCards[0]"
+              :date="query.time"
+              :is-real-time="isRealTime"
+              :token="token"
+            />
           </div>
 
           <div
@@ -336,11 +384,23 @@ onMounted(() => {
             class="monitor-card-shell monitor-card-side monitor-card-left-bottom"
             @click="setMainCard(sideCards[1])"
           >
-            <chart v-bind="sideCards[1]" :date="query.time" :token="token" />
+            <chart
+              :key="sideCards[1].id"
+              v-bind="sideCards[1]"
+              :date="query.time"
+              :is-real-time="isRealTime"
+              :token="token"
+            />
           </div>
 
           <div class="monitor-card-shell monitor-card-main" @click="setMainCard(mainCard)">
-            <chart v-bind="mainCard" :date="query.time" :token="token" />
+            <chart
+              :key="mainCard.id"
+              v-bind="mainCard"
+              :date="query.time"
+              :is-real-time="isRealTime"
+              :token="token"
+            />
           </div>
 
           <div
@@ -348,7 +408,13 @@ onMounted(() => {
             class="monitor-card-shell monitor-card-side monitor-card-right-top"
             @click="setMainCard(sideCards[2])"
           >
-            <chart v-bind="sideCards[2]" :date="query.time" :token="token" />
+            <chart
+              :key="sideCards[2].id"
+              v-bind="sideCards[2]"
+              :date="query.time"
+              :is-real-time="isRealTime"
+              :token="token"
+            />
           </div>
 
           <div
@@ -356,7 +422,13 @@ onMounted(() => {
             class="monitor-card-shell monitor-card-side monitor-card-right-bottom"
             @click="setMainCard(sideCards[3])"
           >
-            <chart v-bind="sideCards[3]" :date="query.time" :token="token" />
+            <chart
+              :key="sideCards[3].id"
+              v-bind="sideCards[3]"
+              :date="query.time"
+              :is-real-time="isRealTime"
+              :token="token"
+            />
           </div>
         </div>
       </div>
@@ -367,7 +439,6 @@ onMounted(() => {
       title="筛选条件"
       width="1120px"
       class="monitor-search-dialog"
-      append-to-body
       destroy-on-close
     >
       <el-form size="default" class="search-container grid grid-cols-12 gap-6">
@@ -418,6 +489,7 @@ onMounted(() => {
             :teleported="false"
             popper-class="poper"
             class="w-full time-range-picker"
+            @change="handleTimeChange"
           />
         </el-form-item>
         <el-form-item class="col-span-12" label="设备">
@@ -492,6 +564,59 @@ onMounted(() => {
   }
 }
 
+@keyframes board-page-next {
+  0% {
+    opacity: 0;
+    transform: translateX(56px) scale(0.985);
+  }
+
+  100% {
+    opacity: 1;
+    transform: translateX(0) scale(1);
+  }
+}
+
+@keyframes board-page-prev {
+  0% {
+    opacity: 0;
+    transform: translateX(-56px) scale(0.985);
+  }
+
+  100% {
+    opacity: 1;
+    transform: translateX(0) scale(1);
+  }
+}
+
+@keyframes card-promote-main {
+  0% {
+    opacity: 0.72;
+    transform: scale(0.94);
+  }
+
+  55% {
+    opacity: 1;
+    transform: scale(1.018);
+  }
+
+  100% {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
+@keyframes card-promote-side {
+  0% {
+    opacity: 0.62;
+    transform: scale(1.04);
+  }
+
+  100% {
+    opacity: 1;
+    transform: scale(1);
+  }
+}
+
 .animate-scan-line {
   animation: scan-line 3s cubic-bezier(0.4, 0, 0.2, 1) infinite alternate;
 }
@@ -558,6 +683,7 @@ onMounted(() => {
   min-width: 0;
   min-height: 0;
   cursor: pointer;
+  will-change: transform, opacity;
   transition:
     transform 0.25s ease,
     box-shadow 0.25s ease,
@@ -568,6 +694,22 @@ onMounted(() => {
   transform: translateY(-2px);
 }
 
+.monitor-board-scroll-area.is-page-next .monitor-board-layout {
+  animation: board-page-next 0.46s cubic-bezier(0.2, 0.8, 0.2, 1);
+}
+
+.monitor-board-scroll-area.is-page-prev .monitor-board-layout {
+  animation: board-page-prev 0.46s cubic-bezier(0.2, 0.8, 0.2, 1);
+}
+
+.monitor-board-scroll-area.is-promote .monitor-card-main {
+  animation: card-promote-main 0.48s cubic-bezier(0.2, 0.8, 0.2, 1);
+}
+
+.monitor-board-scroll-area.is-promote .monitor-card-side {
+  animation: card-promote-side 0.38s cubic-bezier(0.2, 0.8, 0.2, 1);
+}
+
 .monitor-card-main {
   grid-column: 2;
   grid-row: 1 / span 2;

+ 7 - 10
src/views/oli-connection/monitoring/detail.vue

@@ -79,7 +79,7 @@ const handleMessageUpdate = (_topic: string, data: any) => {
 }
 
 watch(isConnected, (newVal) => {
-  if (newVal) {
+  if (newVal && data.value.mqttUrl) {
     subscribe(data.value.mqttUrl as string)
   }
 })
@@ -466,16 +466,12 @@ const chartLoading = ref(false)
 
 const token = ref('')
 
-async function getToken() {
-  const res = await IotDeviceApi.getToken()
+async function ensureToken() {
+  if (token.value) return
 
-  token.value = res
+  token.value = await IotDeviceApi.getToken()
 }
 
-onMounted(() => {
-  getToken()
-})
-
 async function initLoadChartData(real_time: boolean = true) {
   if (!dimensions.value.length) return
 
@@ -516,7 +512,8 @@ async function initLoadChartData(real_time: boolean = true) {
     }
   }
 
-  if (real_time) {
+  if (real_time && data.value.mqttUrl) {
+    await ensureToken()
     connect(`wss://aims.deepoil.cc/mqtt`, { password: token.value }, handleMessageUpdate)
   }
 }
@@ -524,7 +521,7 @@ async function initLoadChartData(real_time: boolean = true) {
 async function initfn(load: boolean = true, real_time: boolean = true) {
   if (load) await loadDimensions()
   render()
-  initLoadChartData(real_time)
+  await initLoadChartData(real_time)
 }
 
 onMounted(() => {