|
|
@@ -1,31 +1,297 @@
|
|
|
<template>
|
|
|
- <NSlider v-model:value="timeRange" :max="getMaxRange()" :marks="dateMarks" range :format-tooltip="sliderFormatTooltip"
|
|
|
- @dragend="handleDragEnd" />
|
|
|
+ <div class="flex p-x-4 gap-2">
|
|
|
+ <NSlider v-model:value="timeRange" :max="getMaxRange()" :marks="dateMarks" range
|
|
|
+ :format-tooltip="sliderFormatTooltip" @dragend="handleDragEnd" />
|
|
|
+ <NButton quaternary circle @click="() => { showChartConfig = true }">
|
|
|
+ <span class="i-ph:caret-circle-double-left-bold h-6 w-6" />
|
|
|
+ </NButton>
|
|
|
+ <NDrawer v-model:show="showChartConfig" :width="680" placement="right" :close-on-esc="false" :mask-closable="false">
|
|
|
+ <NDrawerContent title="Chart配置" closable>
|
|
|
+ <NForm class="h-full flex flex-col" :model="formModel" label-placement="top">
|
|
|
+ <NFormItem label="使用数据类型">
|
|
|
+ <NRadioGroup v-model:value="formModel.dataType" name="dataType">
|
|
|
+ <NSpace>
|
|
|
+ <NPopover trigger="hover">
|
|
|
+ <template #trigger>
|
|
|
+ <NRadio value="cycle">
|
|
|
+ 周期
|
|
|
+ </NRadio>
|
|
|
+ </template>
|
|
|
+ <span>只显示5min的标准周期数据</span>
|
|
|
+ </NPopover>
|
|
|
+ <NPopover trigger="hover">
|
|
|
+ <template #trigger>
|
|
|
+ <NRadio value="all">
|
|
|
+ 全部
|
|
|
+ </NRadio>
|
|
|
+ </template>
|
|
|
+ <span>显示全部数据,包括召唤和事件变化</span>
|
|
|
+ </NPopover>
|
|
|
+ </NSpace>
|
|
|
+ </NRadioGroup>
|
|
|
+ </NFormItem>
|
|
|
+ <NFormItem class="flex-1" label="绘图选项">
|
|
|
+ <NTransfer class="h-full" v-model:value="formModel.selectChartConfig"
|
|
|
+ :options="optionsChartConfig.transferOption" :render-source-list="renderSourceList" source-filterable />
|
|
|
+ </NFormItem>
|
|
|
+ </NForm>
|
|
|
+ <template #footer>
|
|
|
+ <NButton>确定</NButton>
|
|
|
+ </template>
|
|
|
+ </NDrawerContent>
|
|
|
+ </NDrawer>
|
|
|
+ </div>
|
|
|
<div ref="chartRuntime" class="w-full"></div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { Chart, type G2Spec, type Mark, type Composition } from '@antv/g2'
|
|
|
-import { NSlider } from 'naive-ui';
|
|
|
+import { NButton, NDrawer, NDrawerContent, NSlider, NTransfer, NTree, NForm, NFormItem, NRadio, NRadioGroup, NSpace, NPopover } from 'naive-ui';
|
|
|
+import type { TransferRenderSourceList, TreeOption, TransferOption } from 'naive-ui';
|
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
+import { usePersonalization } from '@/composable/usePersonalization'
|
|
|
+import { useSiteRuntimeChartConfigStore } from '@/stores/modules/views/siteRuntimeChartConfig';
|
|
|
+
|
|
|
+import type { RuntimeItem } from '@/service/sitesAnalysis/interface';
|
|
|
+
|
|
|
const props = defineProps<{
|
|
|
height?: number
|
|
|
- data: RuntimeDataType[] // 数据必须按正确的时间排序
|
|
|
+ data: RuntimeItem[] // 数据必须按正确的时间排序
|
|
|
}>()
|
|
|
|
|
|
+const { isDark, color } = usePersonalization()
|
|
|
+const runtimeChartConfigStore = useSiteRuntimeChartConfigStore()
|
|
|
+
|
|
|
+interface FormModelType {
|
|
|
+ dataType: string
|
|
|
+ selectChartConfig: string[]
|
|
|
+}
|
|
|
+
|
|
|
+const formModel = ref<FormModelType>({
|
|
|
+ dataType: 'cycle',
|
|
|
+ selectChartConfig: runtimeChartConfigStore.getConfig()
|
|
|
+})
|
|
|
+
|
|
|
+interface ChartConfigOption extends TreeOption {
|
|
|
+ label: string
|
|
|
+ value: string
|
|
|
+ children?: ChartConfigOption[]
|
|
|
+ array?: false | { // 是否是数组
|
|
|
+ key: string,
|
|
|
+ index: number
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TODO 暂时没包含aPower相关配置
|
|
|
+const chartConfigOption: ChartConfigOption[] = [
|
|
|
+ {
|
|
|
+ label: "状态信息",
|
|
|
+ value: "stateInfo",
|
|
|
+ children: [
|
|
|
+ { label: "工作模式(mode)", value: "mode" },
|
|
|
+ { label: "运行状态(run_status)", value: "run_status" },
|
|
|
+ { label: "EMS-DSP运行状态(slaver_stat)", value: "slaver_stat" },
|
|
|
+ { label: "市电状态(elecnet_state)", value: "elecnet_state" },
|
|
|
+ { label: "系统SOC(soc)", value: "soc" },
|
|
|
+ { label: "环境温度(t_amb)", value: "t_amb" },
|
|
|
+ { label: "4G信号(signal)", value: "signal" },
|
|
|
+ { label: "wifi信号(wifiSignal)", value: "wifiSignal" },
|
|
|
+ { label: "连接方式(connType)", value: "connType" },
|
|
|
+ { label: "发电机状态(genStat)", value: "genStat" },
|
|
|
+ { label: "远端光伏接入数量(remoteSolarEn)", value: "remoteSolarEn" },
|
|
|
+ { label: "主动离网(offgridreason)", value: "offgridreason" },
|
|
|
+ { label: "调度策略(name)", value: "name" },
|
|
|
+ { label: "电费类型(electricity_type)", value: "electricity_type" },
|
|
|
+ { label: "市电相制(gridPhaseConSet)", value: "gridPhaseConSet" },
|
|
|
+ { label: "紧急电源模式(v2lModeEnable)", value: "v2lModeEnable" },
|
|
|
+ { label: "v2l电车运行状态(v2lRunState)", value: "v2lRunState" },
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "开关状态",
|
|
|
+ value: "switchState",
|
|
|
+ children: [
|
|
|
+ { label: "市电开关状态(main_sw[0])", value: "main_sw_0", array: { key: "main_sw", index: 0 } },
|
|
|
+ { label: "发电机开关状态(main_sw[1])", value: "main_sw_1", array: { key: "main_sw", index: 1 } },
|
|
|
+ { label: "aGate-PV开关状态(main_sw[2])", value: "main_sw_2", array: { key: "main_sw", index: 2 } },
|
|
|
+ { label: "智能负载1状态(pro_load[0])", value: "pro_load_0", array: { key: "pro_load", index: 0 } },
|
|
|
+ { label: "智能负载2状态(pro_load[1])", value: "pro_load_1", array: { key: "pro_load", index: 1 } },
|
|
|
+ { label: "智能负载3状态(pro_load[2])", value: "pro_load_2", array: { key: "pro_load", index: 2 } },
|
|
|
+ { label: "输出干接点1状态(do[0])", value: "do_0", array: { key: "do", index: 0 } },
|
|
|
+ { label: "输出干接点2状态(do[1])", value: "do_1", array: { key: "do", index: 1 } },
|
|
|
+ { label: "输出干接点3状态(do[2])", value: "do_2", array: { key: "do", index: 2 } },
|
|
|
+ { label: "输出干接点4状态(do[3])", value: "do_3", array: { key: "do", index: 3 } },
|
|
|
+ { label: "输入干接点1状态(di[0])", value: "di_0", array: { key: "di", index: 0 } },
|
|
|
+ { label: "输入干接点2状态(di[1])", value: "di_1", array: { key: "di", index: 1 } },
|
|
|
+ { label: "输入干接点3状态(di[2])", value: "di_2", array: { key: "di", index: 2 } },
|
|
|
+ { label: "输入干接点4状态(di[3])", value: "di_3", array: { key: "di", index: 3 } },
|
|
|
+ { label: "光伏电表前Apbox继电器状态(BFPVApboxRelay)", value: "BFPVApboxRelay" },
|
|
|
+ { label: "市电继电器2状态(gridRelay2)", value: "gridRelay2" },
|
|
|
+ { label: "光伏继电器2状态(pvRelay2)", value: "pvRelay2" },
|
|
|
+ { label: "黑启动继电器状态(blackStartRelay)", value: "blackStartRelay" },
|
|
|
+ { label: "MPPT工作状态(mpptSta)", value: "mpptSta" },
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "功率数据",
|
|
|
+ value: "powerData",
|
|
|
+ children: [
|
|
|
+ { label: "市电功率(p_uti)", value: "p_uti" },
|
|
|
+ { label: "光伏发电功率(p_sun)", value: "p_sun" },
|
|
|
+ { label: "发电机发电功率(p_gen)", value: "p_gen" },
|
|
|
+ { label: "aPower功率(p_fhp)", value: "p_fhp" },
|
|
|
+ { label: "家庭负载功率(p_load)", value: "p_load" },
|
|
|
+ { label: "近端光伏功率(solarPower)", value: "solarPower" },
|
|
|
+ { label: "远端光伏1功率(remoteSolar1Power)", value: "remoteSolar1Power" },
|
|
|
+ { label: "远端光伏2功率(remoteSolar2Power)", value: "remoteSolar2Power" },
|
|
|
+ { label: "DSP内置电表NEM光伏功率(DSPNEMPVPower)", value: "DSPNEMPVPower" },
|
|
|
+ { label: "三相光伏功率(threePhasePvPower)", value: "threePhasePvPower" },
|
|
|
+ { label: "MPPT光伏总功率(mpptAllPower)", value: "mpptAllPower" },
|
|
|
+ { label: "aPwoerS逆变侧功率(acPower)", value: "acPower" },
|
|
|
+ { label: "aPowerS交流侧光伏功率(aggAcPvPower)", value: "aggAcPvPower" },
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "电量数据",
|
|
|
+ value: "energyData",
|
|
|
+ children: [
|
|
|
+ { label: "市电输入电量(kwh_uti_in)", value: "kwh_uti_in" },
|
|
|
+ { label: "市电输出电量(kwh_uti_out)", value: "kwh_uti_out" },
|
|
|
+ { label: "光伏发电量(kwh_sun)", value: "kwh_sun" },
|
|
|
+ { label: "发电机发电量(kwh_gen)", value: "kwh_gen" },
|
|
|
+ { label: "aPower放电电量(kwh_fhp_di)", value: "kwh_fhp_di" },
|
|
|
+ { label: "aPower充电电量(kwh_fhp_chg)", value: "kwh_fhp_chg" },
|
|
|
+ { label: "家庭负载用电量(kwh_load)", value: "kwh_load" },
|
|
|
+ { label: "智能负载线路使用电量(kwhIqLoad1)", value: "kwhIqLoad1" },
|
|
|
+ { label: "近端光伏日电量(proximalSolarWh)", value: "proximalSolarWh" },
|
|
|
+ { label: "远端光伏1日电量(remoteSolar1Wh)", value: "remoteSolar1Wh" },
|
|
|
+ { label: "远端光伏2日电量(remoteSolar2Wh)", value: "remoteSolar2Wh" },
|
|
|
+ { label: "远端光伏3日电量(remoteSolar3Wh)", value: "remoteSolar3Wh" },
|
|
|
+ { label: "meterkit日电量(meterkitPvWh)", value: "meterkitPvWh" },
|
|
|
+ { label: "第二辅助电表日电量(secondaryPvWh)", value: "secondaryPvWh" },
|
|
|
+ { label: "MPPT日电量(mpptWh)", value: "mpptWh" },
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "其他数据",
|
|
|
+ value: "otherData",
|
|
|
+ children: [
|
|
|
+ { label: "油机电压(genVoltage)", value: "genVoltage" },
|
|
|
+
|
|
|
+ ]
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+interface ChartConfigKeyMapValue {
|
|
|
+ label: string
|
|
|
+ array?: false | { // 是否是数组
|
|
|
+ key: string,
|
|
|
+ index: number
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+interface ChartConfigKeyMap {
|
|
|
+ [key: string]: ChartConfigKeyMapValue
|
|
|
+}
|
|
|
+
|
|
|
+function flattenTree(list: ChartConfigOption[]): {
|
|
|
+ keyMap: ChartConfigKeyMap,
|
|
|
+ transferOption: TransferOption[]
|
|
|
+} {
|
|
|
+ const result: {
|
|
|
+ keyMap: ChartConfigKeyMap,
|
|
|
+ transferOption: TransferOption[]
|
|
|
+ } = {
|
|
|
+ keyMap: {},
|
|
|
+ transferOption: []
|
|
|
+ }
|
|
|
+ function flatten(_list: ChartConfigOption[] = []) {
|
|
|
+ _list.forEach((item) => {
|
|
|
+ if (item.children != null) {
|
|
|
+ flatten(item.children)
|
|
|
+ } else {
|
|
|
+ result.keyMap[item.value] = {
|
|
|
+ label: item.label,
|
|
|
+ array: item.array
|
|
|
+ }
|
|
|
+ result.transferOption.push(item)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ flatten(list)
|
|
|
+ return result
|
|
|
+}
|
|
|
+
|
|
|
+const optionsChartConfig = flattenTree(chartConfigOption)
|
|
|
+
|
|
|
+// 转换并解析运行时json数据
|
|
|
+const transformRuntimeData = (data?: RuntimeItem[]): RuntimeDataType[] => {
|
|
|
+ if (data === undefined) return []
|
|
|
+ return data.map(d => {
|
|
|
+ const obj = JSON.parse(d.data_201);
|
|
|
+ const dataArea = obj['dataArea']
|
|
|
+ if (dataArea === null) {
|
|
|
+ return {
|
|
|
+ time: d.time
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从keyMap中获取数据配置信息
|
|
|
+ const keyTab: Record<string, string | number> = {}
|
|
|
+ for (const [key, value] of Object.entries(optionsChartConfig.keyMap)) {
|
|
|
+ if (value.array != null && value.array !== false) {
|
|
|
+ keyTab[key] = dataArea[value.array.key] ? dataArea[value.array.key][value.array.index] ?? undefined : undefined
|
|
|
+ } else {
|
|
|
+ keyTab[key] = dataArea[key] ?? undefined
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ time: d.time,
|
|
|
+ // ...(dataArea['mode'] != null && { RunMode: dataArea['mode'] }),
|
|
|
+ // ...(dataArea['run_status'] != null && { RunStatus: dataArea['run_status'] }),
|
|
|
+ // ...(dataArea['elecnet_state'] != null && { GridStatus: dataArea['elecnet_state'] })
|
|
|
+ ...keyTab
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const showChartConfig = ref(false)
|
|
|
+const renderSourceList: TransferRenderSourceList = function ({
|
|
|
+ onCheck,
|
|
|
+ pattern
|
|
|
+}) {
|
|
|
+ return h(NTree, {
|
|
|
+ style: 'margin: 0 4px;',
|
|
|
+ keyField: 'value',
|
|
|
+ cascade: true,
|
|
|
+ checkable: true,
|
|
|
+ selectable: false,
|
|
|
+ blockLine: true,
|
|
|
+ checkOnClick: true,
|
|
|
+ data: chartConfigOption,
|
|
|
+ pattern,
|
|
|
+ checkedKeys: formModel.value.selectChartConfig,
|
|
|
+ onUpdateCheckedKeys: (checkedKeys: Array<string>) => {
|
|
|
+ runtimeChartConfigStore.updateConfig(checkedKeys)
|
|
|
+ onCheck(checkedKeys)
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
const chartRuntime = ref<HTMLDivElement>()
|
|
|
let runtimeChart: Chart | null = null
|
|
|
|
|
|
-export interface RuntimeDataType {
|
|
|
+interface RuntimeDataType {
|
|
|
time: string // format YYYY-MM-DD HH:mm:ss
|
|
|
[key: string]: string | number // number是曲线 string是事件
|
|
|
}
|
|
|
|
|
|
interface ChartDataType {
|
|
|
time: number // 距离其实时间的秒数
|
|
|
- [key: string]: string | number // number是曲线 string是事件
|
|
|
+ value: string | number // number是曲线 string是事件
|
|
|
}
|
|
|
|
|
|
// 模拟数据:10 分钟,每秒一个点
|
|
|
@@ -48,7 +314,6 @@ const diffSeconds = endTimeDayjs.diff(startTimeDayjs, 'second');
|
|
|
|
|
|
// 计算某个点的时间 YYYY-MM-DD HH:mm:ss
|
|
|
const getPointTime = (value: number): string => {
|
|
|
- console.log({ startTime, endTime })
|
|
|
if (value >= diffSeconds) return endTime
|
|
|
return startTimeDayjs.add(value, 'second').format('YYYY-MM-DD HH:mm:ss')
|
|
|
}
|
|
|
@@ -105,54 +370,63 @@ const dateMarks = computed<{ [markValue: number]: string }>(() => {
|
|
|
});
|
|
|
|
|
|
const sliderFormatTooltip = (value: number) => {
|
|
|
- console.log({ value, time: getPointTime(value) })
|
|
|
+ // console.log({ value, time: getPointTime(value) })
|
|
|
return getPointTime(value)
|
|
|
}
|
|
|
|
|
|
-interface RuntimeChartOpt {
|
|
|
+interface RuntimeChartChildrenData {
|
|
|
+ type: string
|
|
|
data: ChartDataType[]
|
|
|
- childrenName: Record<string, string>
|
|
|
+}
|
|
|
+
|
|
|
+interface RuntimeChartOpt {
|
|
|
+ childrenData: Record<string, RuntimeChartChildrenData>
|
|
|
}
|
|
|
|
|
|
// 生成Chart数据和子字段SET表
|
|
|
const handleChartData = (objList: RuntimeDataType[]): RuntimeChartOpt => {
|
|
|
- const chartData: ChartDataType[] = []
|
|
|
- const childrenSet: Record<string, string> = {};
|
|
|
+ const childrenData: Record<string, RuntimeChartChildrenData> = {};
|
|
|
|
|
|
objList.map(obj => {
|
|
|
- chartData.push({
|
|
|
- ...obj,
|
|
|
- time: dayjs(obj.time).diff(startTimeDayjs, 'second'),
|
|
|
- })
|
|
|
Object.entries(obj).forEach(([key, value]) => {
|
|
|
- if (key !== 'time') {
|
|
|
- const keyType = typeof value
|
|
|
- childrenSet[key] = keyType
|
|
|
+ const keyType = typeof value
|
|
|
+ if (key !== 'time' && (keyType === 'string' || keyType === 'number')) {
|
|
|
+ if (childrenData[key] == null) {
|
|
|
+ childrenData[key] = {
|
|
|
+ type: keyType,
|
|
|
+ data: []
|
|
|
+ }
|
|
|
+ }
|
|
|
+ childrenData[key].data.push({
|
|
|
+ time: dayjs(obj.time).diff(startTimeDayjs, 'second'),
|
|
|
+ value
|
|
|
+ })
|
|
|
}
|
|
|
});
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
- data: chartData,
|
|
|
- childrenName: childrenSet
|
|
|
+ childrenData
|
|
|
}
|
|
|
}
|
|
|
-const runtimeChartOpt = handleChartData(props.data)
|
|
|
+const runtimeChartOpt = handleChartData(transformRuntimeData(props.data))
|
|
|
+console.log({ runtimeChartOpt })
|
|
|
|
|
|
// 生成Chart子配置
|
|
|
const getChildrenChartOpt = (xRange: [number, number]): (Mark | Composition)[] => {
|
|
|
const childrenChart: (Mark | Composition)[] = []
|
|
|
|
|
|
- Object.entries(runtimeChartOpt.childrenName).forEach(([key, value]) => {
|
|
|
- if (value === 'number') {
|
|
|
+ Object.entries(runtimeChartOpt.childrenData).forEach(([key, value]) => {
|
|
|
+ if (value.type === 'number') {
|
|
|
// 子Chart为曲线
|
|
|
childrenChart.push(
|
|
|
{
|
|
|
type: 'line',
|
|
|
height: 100,
|
|
|
+ data: value.data,
|
|
|
encode: {
|
|
|
x: 'time',
|
|
|
- y: key
|
|
|
+ y: 'value'
|
|
|
},
|
|
|
axis: {
|
|
|
x: false,
|
|
|
@@ -173,15 +447,16 @@ const getChildrenChartOpt = (xRange: [number, number]): (Mark | Composition)[] =
|
|
|
}
|
|
|
}
|
|
|
)
|
|
|
- } else if (value === 'string') {
|
|
|
+ } else if (value.type === 'string') {
|
|
|
// 子Chart是事件点
|
|
|
childrenChart.push(
|
|
|
{
|
|
|
type: 'point',
|
|
|
height: 100,
|
|
|
+ data: value.data,
|
|
|
encode: {
|
|
|
x: 'time',
|
|
|
- y: key
|
|
|
+ y: 'value'
|
|
|
},
|
|
|
axis: {
|
|
|
x: false,
|
|
|
@@ -213,7 +488,8 @@ const getChartOpt = (xRange: [number, number]): G2Spec => {
|
|
|
return {
|
|
|
type: 'spaceFlex',
|
|
|
direction: 'col',
|
|
|
- data: runtimeChartOpt.data,
|
|
|
+ // data: runtimeChartOpt.data,
|
|
|
+ theme: { type: isDark ? 'dark' : 'light' },
|
|
|
children: getChildrenChartOpt(xRange)
|
|
|
}
|
|
|
}
|
|
|
@@ -226,6 +502,7 @@ const handleDragEnd = () => {
|
|
|
} else {
|
|
|
runtimeChart.options(getChartOpt(timeRange.value))
|
|
|
}
|
|
|
+ console.log("render")
|
|
|
runtimeChart.render()
|
|
|
}
|
|
|
|
|
|
@@ -233,7 +510,7 @@ onMounted(() => {
|
|
|
runtimeChart = new Chart({
|
|
|
container: chartRuntime.value!,
|
|
|
autoFit: true,
|
|
|
- height: 100 * Object.keys(runtimeChartOpt.childrenName).length,
|
|
|
+ height: 100 * Object.keys(runtimeChartOpt.childrenData).length,
|
|
|
});
|
|
|
|
|
|
runtimeChart.options(getChartOpt(timeRange.value))
|