Эх сурвалжийг харах

feat:站点信息增加流图显示窗口

Jerry Liang 3 сар өмнө
parent
commit
6c30754a12

+ 105 - 0
src/views/site-info-manage/component/powerFlowChart.vue

@@ -0,0 +1,105 @@
+<template>
+  <div ref="chartContainer" class="w-full"></div>
+</template>
+
+<script setup lang="ts">
+import { Chart, ChartEvent } from '@antv/g2'
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
+import { usePersonalization } from '@/composable/usePersonalization'
+
+const { isDark } = usePersonalization()
+
+const props = defineProps<{
+  height?: number
+}>()
+
+const chartContainer = ref<HTMLDivElement>()
+let chart: Chart | null = null
+
+export interface PowerChartData {
+  powerType: string
+  Time: string
+  Power?: number
+}
+
+let isVoidData = true
+const voidData: PowerChartData[] = (() => {
+  const times: string[] = []
+  const totalMinutes = 24 * 60
+
+  for (let m = 0; m < totalMinutes; m += 5) {
+    const hh = String(Math.floor(m / 60)).padStart(2, '0')
+    const mm = String(m % 60).padStart(2, '0')
+    times.push(`${hh}:${mm}`)
+  }
+
+  return times.map((t) => {
+    return [
+      { powerType: 'aPower', Time: t },
+      { powerType: 'Generator', Time: t },
+      { powerType: 'HomeLoad', Time: t },
+      { powerType: 'PV', Time: t },
+      { powerType: 'Grid', Time: t },
+    ]
+  }).flat()
+})()
+
+onMounted(() => {
+  chart = new Chart({
+    container: chartContainer.value!,
+    autoFit: true,
+    height: props.height || 300,
+  })
+
+  chart
+    .area()
+    .theme({ type: isDark ? 'dark' : 'light' })
+    .data(voidData)
+    .encode('x', 'Time')
+    .encode('y', 'Power')
+    .encode('color', 'powerType')
+    .transform({ type: 'stackY' })
+    .interaction('elementHighlight', { background: true })
+    .scale('color', {
+      palette: 'accent',
+      relations: [
+        ['aPower', '#41D187'],
+        ['Generator', '#5398FF'],
+        ['HomeLoad', '#0BF5FF'],
+        ['PV', '#F1A502'],
+        ['Grid', '#BB56FD'],
+      ],
+    })
+
+  chart.render()
+})
+
+onBeforeUnmount(() => {
+  chart?.destroy()
+})
+
+// 重绘数据
+const redraw = (data: PowerChartData[] | undefined) => {
+  if (chart === null) return
+
+  if (data === undefined) {
+    if (!isVoidData) {
+      chart.changeData(voidData)
+    }
+  } else {
+    isVoidData = false
+    chart.changeData(data)
+  }
+}
+
+watch(isDark, (newVal) => {
+  if (chart) {
+    chart.theme({ type: newVal ? 'dark' : 'light' })
+    chart.render()
+  }
+})
+
+defineExpose({
+  redraw,
+})
+</script>

+ 243 - 0
src/views/site-info-manage/component/siteDataModal.vue

@@ -0,0 +1,243 @@
+<template>
+  <NModal v-model:show="show" preset="card" class="max-w-3xl" :title="currentSn">
+    <!-- 日期选择:← 日期 → -->
+    <div class="mb-3 flex items-center gap-2">
+      <NButton text :focusable="false" @click="prevDay">
+        <span class="i-ph:caret-left-bold text-lg" />
+      </NButton>
+      <NPopover trigger="click" placement="bottom" :show-arrow="false">
+        <template #trigger>
+          <span class="cursor-pointer select-none rounded px-2 py-1 text-sm font-medium hover:bg-neutral-100 dark:hover:bg-neutral-700">
+            {{ currentDateStr }}
+          </span>
+        </template>
+        <NDatePicker
+          v-model:value="currentDateValue"
+          type="date"
+          panel
+          :is-date-disabled="isDateDisabled"
+        />
+      </NPopover>
+      <NButton text :focusable="false" :disabled="isToday" @click="nextDay">
+        <span class="i-ph:caret-right-bold text-lg" />
+      </NButton>
+    </div>
+
+    <!-- 站点信息头部 -->
+    <div class="mb-2">
+      <div class="flex items-center gap-2">
+        <span class="font-bold">{{ currentSn }}</span>
+        <!-- 告警图标 -->
+        <NPopover v-if="alarmList.length > 0" placement="right-start" trigger="click">
+          <template #trigger>
+            <NButton
+              class="i-ph:warning-circle-fill text-red-500 hover:text-red-600 transition-colors duration-200"
+              size="small"
+              text
+            />
+          </template>
+          <NTimeline :icon-size="20">
+            <NTimelineItem
+              v-for="alarmItem in alarmList"
+              :key="alarmItem.alarmCode"
+              color="grey"
+              :content="alarmItem.logName"
+            >
+              <template #icon>
+                <i v-if="alarmItem.level === 2" class="i-ph:warning-circle-fill text-red-500" />
+                <i v-else class="i-ph:warning-fill text-yellow-500" />
+              </template>
+            </NTimelineItem>
+          </NTimeline>
+        </NPopover>
+        <!-- aPower SN -->
+        <template v-if="aPowerList.length > 0">
+          <NDivider vertical />
+          <span class="text-gray-500 text-sm">[{{ aPowerList.join(', ') }}]</span>
+        </template>
+      </div>
+
+      <!-- Tags: 工作模式 + 版本 -->
+      <NSpace v-if="siteTags.length > 0" size="small" class="mt-2">
+        <NTag v-for="tag in siteTags" :key="tag" :bordered="false" type="info" size="small">
+          {{ tag }}
+        </NTag>
+      </NSpace>
+    </div>
+
+    <!-- 功率流图表 -->
+    <NSpin :show="chartLoading" :delay="500">
+      <PowerFlowChart ref="powerFlowChartRef" class="h-[300px]" :height="300" />
+    </NSpin>
+  </NModal>
+</template>
+
+<script setup lang="ts">
+import {
+  NModal,
+  NDatePicker,
+  NSpin,
+  NPopover,
+  NButton,
+  NDivider,
+  NSpace,
+  NTag,
+  NTimeline,
+  NTimelineItem,
+} from 'naive-ui'
+import { ref, computed, watch, nextTick } from 'vue'
+import PowerFlowChart from './powerFlowChart.vue'
+import { useSitePowerFlowDataStore } from '@/stores/modules/data/sitePowerFlowData'
+import { useSiteInfoDataStore } from '@/stores/modules/data/siteInfoData'
+import { useSiteModeDataStore } from '@/stores/modules/data/siteModeData'
+import { useSiteAlarmListDataStore } from '@/stores/modules/data/siteAlarmList'
+import type { SiteAlarmList } from '@/views/sites-monitor/interface'
+
+defineOptions({
+  name: 'SiteDataModal',
+})
+
+const show = ref(false)
+const currentSn = ref('')
+const currentDateValue = ref<number>(Date.now())
+const chartLoading = ref(false)
+
+// 日期相关计算
+const ONE_DAY = 24 * 60 * 60 * 1000
+
+const currentDateStr = computed(() => formatDate(currentDateValue.value))
+
+const isToday = computed(() => {
+  const today = formatDate(Date.now())
+  return currentDateStr.value === today
+})
+
+function isDateDisabled(timestamp: number): boolean {
+  // 禁用今天之后的日期
+  const todayEnd = new Date()
+  todayEnd.setHours(23, 59, 59, 999)
+  return timestamp > todayEnd.getTime()
+}
+
+function prevDay() {
+  currentDateValue.value = currentDateValue.value - ONE_DAY
+}
+
+function nextDay() {
+  if (!isToday.value) {
+    currentDateValue.value = currentDateValue.value + ONE_DAY
+  }
+}
+
+// 站点附加信息
+const alarmList = ref<SiteAlarmList[]>([])
+const aPowerList = ref<string[]>([])
+const siteTags = ref<string[]>([])
+
+const powerFlowChartRef = ref<InstanceType<typeof PowerFlowChart>>()
+const sitePowerFlowDataStore = useSitePowerFlowDataStore()
+const siteInfoDataStore = useSiteInfoDataStore()
+const siteModeDataStore = useSiteModeDataStore()
+const siteAlarmListStore = useSiteAlarmListDataStore()
+
+function formatDate(timestamp: number): string {
+  const d = new Date(timestamp)
+  const y = d.getFullYear()
+  const m = String(d.getMonth() + 1).padStart(2, '0')
+  const day = String(d.getDate()).padStart(2, '0')
+  return `${y}-${m}-${day}`
+}
+
+// 加载功率流图表
+async function loadPowerFlow() {
+  const sn = currentSn.value
+  const date = formatDate(currentDateValue.value)
+  if (!sn || !date) return
+
+  chartLoading.value = true
+  try {
+    let powerData = sitePowerFlowDataStore.powerFlowChartGet(sn, date)
+    if (powerData === undefined) {
+      await sitePowerFlowDataStore.queryPowerFlow(sn, date)
+      powerData = sitePowerFlowDataStore.powerFlowChartGet(sn, date)
+    }
+    powerFlowChartRef.value?.redraw(powerData)
+  } finally {
+    chartLoading.value = false
+  }
+}
+
+// 加载站点信息(aPower SN、版本)
+async function loadSiteInfo(sn: string) {
+  let siteInfo = siteInfoDataStore.siteInfoGet(sn)
+  if (siteInfo === undefined) {
+    siteInfo = await siteInfoDataStore.querySiteInfo(sn)
+  }
+  if (siteInfo) {
+    // aPower SN
+    if (siteInfo.fhpSn) {
+      aPowerList.value = siteInfo.fhpSn.split(',').map((s) => s.trim()).filter(Boolean)
+    }
+    // 版本 tag
+    if (siteInfo.version) {
+      siteTags.value.push(siteInfo.version)
+    }
+  }
+}
+
+// 加载工作模式
+async function loadWorkMode(sn: string) {
+  let modeInfo = siteModeDataStore.siteModeInfoGet(sn)
+  if (modeInfo === undefined) {
+    modeInfo = await siteModeDataStore.querySiteModeInfo(sn)
+  }
+  if (modeInfo) {
+    const currentId = modeInfo.currendId
+    modeInfo.list.forEach((item) => {
+      if (item.id === currentId) {
+        siteTags.value.push(`${item.name} - (${item.soc}%)`)
+      }
+    })
+  }
+}
+
+// 加载告警列表
+async function loadAlarmList(sn: string) {
+  let list = siteAlarmListStore.siteAlarmListGet(sn)
+  if (list === undefined) {
+    list = await siteAlarmListStore.querySiteAlarmList(sn)
+  }
+  if (list) {
+    alarmList.value = [...list]
+  }
+}
+
+// 日期变化时重新加载图表
+watch(currentDateValue, () => {
+  if (show.value) loadPowerFlow()
+})
+
+function open(sn: string) {
+  // 重置状态
+  currentSn.value = sn
+  currentDateValue.value = Date.now()
+  alarmList.value = []
+  aPowerList.value = []
+  siteTags.value = []
+  show.value = true
+
+  // 加载数据
+  nextTick(() => {
+    loadPowerFlow()
+    loadSiteInfo(sn)
+    loadWorkMode(sn)
+    loadAlarmList(sn)
+  })
+}
+
+function close() {
+  show.value = false
+}
+
+defineExpose({ open, close })
+</script>

+ 20 - 1
src/views/site-info-manage/index.vue

@@ -170,6 +170,7 @@
       </div>
     </NCard>
     <SiteInfoView ref="siteInfoViewRef" />
+    <SiteDataModal ref="siteDataModalRef" />
   </div>
 </template>
 
@@ -187,7 +188,7 @@ import {
   NDataTable,
   NNumberAnimation,
 } from 'naive-ui'
-import type { PaginationProps, DataTableColumns } from 'naive-ui'
+import type { PaginationProps, DataTableColumns, DataTableBaseColumn } from 'naive-ui'
 import { useFullCard } from '@/composable/comp/useFullCard'
 import {
   getSiteInfoManageList,
@@ -205,6 +206,7 @@ import type {
 import SiteInfoFilterComp from './component/siteInfoFilter.vue'
 import SiteInfoFieldsComp from './component/siteInfoFields.vue'
 import SiteInfoView from './component/siteInfoView.vue'
+import SiteDataModal from './component/siteDataModal.vue'
 import * as siteTabCfg from './config'
 import { useSiteInfoManageStateStore } from '@/stores/modules/views/siteInfoManageState'
 import { useUserStore } from '@/stores/user'
@@ -218,6 +220,7 @@ const userStore = useUserStore()
 const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
 const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
 const siteInfoViewRef = useTemplateRef<InstanceType<typeof SiteInfoView>>('siteInfoViewRef')
+const siteDataModalRef = useTemplateRef<InstanceType<typeof SiteDataModal>>('siteDataModalRef')
 
 const filterPopoverRef = useTemplateRef<InstanceType<typeof NPopover>>('filterPopoverRef')
 const filterPopoverContentRef = ref<HTMLDivElement | undefined>(undefined)
@@ -384,6 +387,22 @@ function createColumns(): DataTableColumns<SiteInfo> {
   )
   result.push(...columnsConfig)
 
+  // 给 SN 列添加点击打开功率流弹窗的 render
+  const snColumn = result.find((col) => 'key' in col && col.key === 'sn')
+  if (snColumn && 'key' in snColumn) {
+    (snColumn as DataTableBaseColumn<SiteInfo>).render = (rowData) => {
+      return h(
+        NButton,
+        {
+          text: true,
+          type: 'primary',
+          onClick: () => siteDataModalRef.value?.open(rowData.sn),
+        },
+        { default: () => rowData.sn },
+      )
+    }
+  }
+
   // filterSiteInfoKeys(tabOpt, validKeys).map((item, index, array) => {
   //   const columnConfig = siteTabCfg.columnTitleMap[item]
   //   if (index === (array.length - 1)) {