Bläddra i källkod

feat: 添加站点NBT分析功能

Jerry Liang 1 år sedan
förälder
incheckning
d82cffcbb0
3 ändrade filer med 218 tillägg och 3 borttagningar
  1. 70 0
      src/utils/downloadCVS.ts
  2. 129 0
      src/utils/extend/analysisNBT.ts
  3. 19 3
      src/views/sites-monitor/index.vue

+ 70 - 0
src/utils/downloadCVS.ts

@@ -0,0 +1,70 @@
+// 1) 安全地把对象数组转成 CSV 文本
+export function toCSV<T extends Record<string, any>>(
+  rows: T[],
+  opts?: {
+    headers?: string[] // 指定列顺序与表头;不传则用 rows[0] 的键
+    delimiter?: string // 默认逗号 ","
+    includeHeader?: boolean // 默认 true
+  },
+): string {
+  const delimiter = opts?.delimiter ?? ','
+  const includeHeader = opts?.includeHeader ?? true
+
+  if (!rows?.length) return ''
+
+  const headers = opts?.headers ?? Object.keys(rows[0])
+
+  // 处理每个单元格:转为字符串、转义双引号、若含分隔符/换行/引号则整体再包一层引号
+  const esc = (val: any) => {
+    let s = val == null ? '' : String(val)
+    s = s.replace(/"/g, '""')
+    if (s.includes('"') || s.includes(delimiter) || s.includes('\n') || s.includes('\r')) {
+      s = `"${s}"`
+    }
+    return s
+  }
+
+  const lines: string[] = []
+
+  if (includeHeader) {
+    lines.push(headers.map((h) => esc(h)).join(delimiter))
+  }
+
+  for (const row of rows) {
+    lines.push(headers.map((h) => esc(row[h])).join(delimiter))
+  }
+
+  // Windows/Excel 友好:使用 \r\n 作为换行
+  return lines.join('\r\n')
+}
+
+// 2) 触发浏览器下载(含 UTF-8 BOM,Excel 更稳)
+export function downloadCSV(filename: string, csv: string) {
+  // 加 BOM,避免 Excel 中文乱码
+  const BOM = '\uFEFF'
+  const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8;' })
+  const url = URL.createObjectURL(blob)
+
+  const a = document.createElement('a')
+  a.href = url
+  a.download = filename.endsWith('.csv') ? filename : `${filename}.csv`
+  document.body.appendChild(a)
+  a.click()
+  document.body.removeChild(a)
+  URL.revokeObjectURL(url)
+}
+
+// 3) 使用示例
+// type Row = { id: number; name: string; note?: string };
+
+// const data: Row[] = [
+//   { id: 1, name: "张三", note: "带,逗号" },
+//   { id: 2, name: "李四", note: "含\"引号\"与换行\n第二行" },
+// ];
+
+// const csv = toCSV(data, {
+//   headers: ["id", "name", "note"], // 指定列顺序(可选)
+//   includeHeader: true,
+// });
+
+// downloadCSV("示例数据.csv", csv);

+ 129 - 0
src/utils/extend/analysisNBT.ts

@@ -0,0 +1,129 @@
+import { toCSV, downloadCSV } from '@/utils/downloadCVS'
+import { useSiteModeDataStore } from '@/stores/modules/data/siteModeData'
+import { useSitePowerFlowDataStore } from '@/stores/modules/data/sitePowerFlowData'
+import type { SitePowerFlow } from '@/views/sites-monitor/interface'
+
+interface CsvRow {
+  id: number
+  sn: string
+  rates: string
+  description: string
+  '9-5(Weekday)': string
+  '9-6(Weekend)': string
+}
+
+const siteModeDataStore = useSiteModeDataStore()
+const sitePowerFlowDataStore = useSitePowerFlowDataStore()
+
+const getPowerFlowData = async (site: string, date: string) => {
+  let powerData = sitePowerFlowDataStore.powerFlowGet(site, date)
+  if (powerData === undefined) {
+    await sitePowerFlowDataStore.queryPowerFlow(site, date)
+    powerData = sitePowerFlowDataStore.powerFlowGet(site, date)
+  }
+  return powerData
+}
+
+const analysisPowerFlow = (data: SitePowerFlow | undefined, start: number, end: number): string => {
+  if (data === undefined) return '无数据'
+  if (data.powerFhpArray === undefined) return '无数据'
+  if (data.powerFhpArray === null) return '无数据'
+
+  for (let i = start; i <= end; i++) {
+    if (data.powerFhpArray.length <= i) return '数据不全'
+
+    const power = data.powerFhpArray[i]
+    if (power < 4) {
+      if (i - start < 6) {
+        return '未放电'
+      } else {
+        return '放电不足'
+      }
+    }
+  }
+  return '正常放电'
+}
+
+const getRatesDescriptionAndAnalysis = async (
+  sn: string,
+  rate: string | undefined,
+): Promise<{
+  description: string
+  analysis5: string
+  analysis6: string
+}> => {
+  console.log({ rate })
+  if (rate === 'E-ELEC (NBT 2025)') {
+    const p5 = await getPowerFlowData(sn, '2025-09-05')
+    const p6 = await getPowerFlowData(sn, '2025-09-06')
+    return {
+      description: '9月份每天放电',
+      analysis5: analysisPowerFlow(p5, 230, 238), // 230 - 19:10, 238 - 19: 50
+      analysis6: analysisPowerFlow(p6, 230, 238),
+    }
+  }
+
+  if (rate === 'TOU-D-PRIME (NBT 2025)') {
+    return {
+      description: '9月不放电',
+      analysis5: '无需放电',
+      analysis6: '无需放电',
+    }
+  }
+
+  if (rate === 'EV-TOU-5 (NBT 2025)') {
+    const p6 = await getPowerFlowData(sn, '2025-09-06')
+    return {
+      description: '9月节假日放电',
+      analysis5: '无需放电',
+      analysis6: analysisPowerFlow(p6, 218, 238), // 218 - 18:10, 238 - 19: 50
+    }
+  }
+
+  return {
+    description: '',
+    analysis5: rate ?? '',
+    analysis6: rate ?? '',
+  }
+}
+
+const createNbtResult = async (siteList: string[]): Promise<CsvRow[]> => {
+  let resCsv: CsvRow[] = []
+  for (const [index, site] of siteList.entries()) {
+    // 加载站点工作模式
+    let modeInfo = siteModeDataStore.siteModeInfoGet(site)
+    let modeName: string | undefined
+    modeInfo ??= await siteModeDataStore.querySiteModeInfo(site)
+    if (modeInfo !== undefined) {
+      const currentId = modeInfo.currendId
+      modeInfo.list.forEach((item) => {
+        if (item.id === currentId) {
+          modeName = `${item.name}`
+        }
+      })
+    }
+
+    const res = await getRatesDescriptionAndAnalysis(site, modeName)
+
+    resCsv.push({
+      id: index + 1,
+      sn: site,
+      rates: modeName ?? 'unknown',
+      description: res.description,
+      '9-5(Weekday)': res.analysis5,
+      '9-6(Weekend)': res.analysis6,
+    })
+  }
+  return resCsv
+}
+
+export const DownloadNbtCsv = async (siteList: string[]) => {
+  const data = await createNbtResult(siteList)
+  downloadCSV(
+    '9月份NBT放电结果.csv',
+    toCSV(data, {
+      headers: ['id', 'sn', 'rates', 'description', '9-5(Weekday)', '9-6(Weekend)'], // 指定列顺序(可选)
+      includeHeader: true,
+    }),
+  )
+}

+ 19 - 3
src/views/sites-monitor/index.vue

@@ -192,6 +192,10 @@ const dateSelectValue = sitesMonitorStateStore.monitorStateDateGet(monitorName)
 const dateSelect = computed(() => {
   return new Date(dateSelectValue.value).toLocaleDateString('en-CA');  // en-CA 默认是 YYYY-MM-DD
 })
+const updateDateSelectValue = (value: number | null, formattedValue: string | null) => {
+  if (value === null) return
+  sitesMonitorStateStore.setMonitorStateDate(monitorName, value)
+}
 
 interface VirtualItemType {
   key: string
@@ -239,6 +243,15 @@ onUnmounted(() => {
   siteAlarmListStore.clearSitesData(monitorSiteList.value)
 })
 
+import { DownloadNbtCsv } from '@/utils/extend/analysisNBT'
+
+const loadingDownloadCsv = ref<boolean>(false)
+const handleDownloadCsv = async () => {
+  loadingDownloadCsv.value = true
+  await DownloadNbtCsv(monitorSiteList.value)
+  loadingDownloadCsv.value = false
+}
+
 </script>
 
 <template>
@@ -265,12 +278,12 @@ onUnmounted(() => {
             :ellipsis-tag-popover-props="{ trigger: 'hover', placement: 'top' }" />
         </NFormItem>
         <NFormItem class="inline-block" :show-feedback="false" label="选择日期">
-          <NDatePicker v-model:value="dateSelectValue" type="date" />
+          <NDatePicker :value="dateSelectValue" type="date" @update:value="updateDateSelectValue" />
         </NFormItem>
       </NCard>
       <NCard>
         <NRow>
-          <NCol :span="12">
+          <NCol :span="10">
             <NStatistic label="统计数据" :value="monitorSiteList.length">
               <template #prefix>
                 <span class="inline-block size-5 i-ph:ghost-duotone"></span>
@@ -280,11 +293,14 @@ onUnmounted(() => {
               </template>
             </NStatistic>
           </NCol>
-          <NCol :span="12">
+          <NCol :span="10">
             <NStatistic label="活跃站点">
               {{ monitorSiteList.length }}
             </NStatistic>
           </NCol>
+          <NCol :span="4">
+            <NButton :loading="loadingDownloadCsv" @click="handleDownloadCsv">下载CSV</NButton>
+          </NCol>
         </NRow>
       </NCard>
     </NCollapseTransition>