| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- <template>
- <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 { 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: 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
- interface RuntimeDataType {
- time: string // format YYYY-MM-DD HH:mm:ss
- [key: string]: string | number // number是曲线 string是事件
- }
- interface ChartDataType {
- time: number // 距离其实时间的秒数
- value: string | number // number是曲线 string是事件
- }
- // 模拟数据:10 分钟,每秒一个点
- // const data: RuntimeDataType[] = [];
- // const start = dayjs('2025-10-09 12:00:00');
- // for (let i = 0; i < 600; i++) {
- // const time = start.add(i, 'second').format('YYYY-MM-DD HH:mm:ss');
- // data.push({
- // time,
- // value: Math.sin(i / 10) * 10 + 20,
- // })
- // }
- // 计算时间相关信息
- const startTime = (props.data.length > 0) ? props.data[0].time : "2025-01-01"
- const startTimeDayjs = dayjs(startTime)
- const endTime = (props.data.length > 0) ? props.data[props.data.length - 1].time : "2025-01-01"
- const endTimeDayjs = dayjs(endTime)
- const diffSeconds = endTimeDayjs.diff(startTimeDayjs, 'second');
- // 计算某个点的时间 YYYY-MM-DD HH:mm:ss
- const getPointTime = (value: number): string => {
- if (value >= diffSeconds) return endTime
- return startTimeDayjs.add(value, 'second').format('YYYY-MM-DD HH:mm:ss')
- }
- // 计算最大选择范围
- const getMaxRange = () => {
- if (diffSeconds > 0) return diffSeconds
- return 0
- }
- // 计算选择数据范围
- const timeRange = ref<[number, number]>([0, (diffSeconds < 24 * 60 * 60) ? diffSeconds : 24 * 60 * 60])
- // 计算日期标注
- const dateMarks = computed<{ [markValue: number]: string }>(() => {
- const start = dayjs(startTime);
- const end = dayjs(endTime);
- const marks: Record<number, string> = {};
- if (!start.isValid() || !end.isValid() || !start.isBefore(end)) {
- return marks; // 无效输入
- }
- // 找出所有0点整
- let current = start.startOf('day'); // 当天0点
- if (current.isBefore(start)) {
- current = current.add(1, 'day'); // 如果start已经过了当天0点,取下一天
- }
- const midnights: dayjs.Dayjs[] = [];
- while (current.isBefore(end) || current.isSame(end)) {
- midnights.push(current);
- current = current.add(1, 'day');
- }
- // 若超过10个,均匀取样
- let selected = midnights;
- const MAX_COUNT = 10;
- if (midnights.length > MAX_COUNT) {
- const step = (midnights.length - 1) / (MAX_COUNT - 1);
- selected = Array.from({ length: MAX_COUNT }, (_, i) =>
- midnights[Math.round(i * step)]
- );
- }
- // 计算每个点相对起始时间的秒数差
- selected.forEach((d) => {
- const diffSec = d.diff(start, 'second'); // 秒差
- const dateStr = d.format('YYYY-MM-DD'); // 格式化成 "YYYY-MM-DD"
- marks[diffSec] = dateStr;
- });
- return marks;
- });
- const sliderFormatTooltip = (value: number) => {
- // console.log({ value, time: getPointTime(value) })
- return getPointTime(value)
- }
- interface RuntimeChartChildrenData {
- type: string
- data: ChartDataType[]
- }
- interface RuntimeChartOpt {
- childrenData: Record<string, RuntimeChartChildrenData>
- }
- // 生成Chart数据和子字段SET表
- const handleChartData = (objList: RuntimeDataType[]): RuntimeChartOpt => {
- const childrenData: Record<string, RuntimeChartChildrenData> = {};
- objList.map(obj => {
- Object.entries(obj).forEach(([key, value]) => {
- 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 {
- childrenData
- }
- }
- const runtimeChartOpt = handleChartData(transformRuntimeData(props.data))
- console.log({ runtimeChartOpt })
- // 生成Chart子配置
- const getChildrenChartOpt = (xRange: [number, number]): (Mark | Composition)[] => {
- const childrenChart: (Mark | Composition)[] = []
- 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: 'value'
- },
- axis: {
- x: false,
- y: {
- tickCount: 0,
- position: 'bottom'
- }
- },
- scale: {
- x: {
- domain: xRange,
- }
- },
- tooltip: {
- title: (d: ChartDataType) => {
- return getPointTime(d.time)
- },
- }
- }
- )
- } else if (value.type === 'string') {
- // 子Chart是事件点
- childrenChart.push(
- {
- type: 'point',
- height: 100,
- data: value.data,
- encode: {
- x: 'time',
- y: 'value'
- },
- axis: {
- x: false,
- y: {
- tickCount: 0,
- position: 'bottom'
- }
- },
- scale: {
- x: {
- domain: xRange,
- }
- },
- tooltip: {
- title: (d: ChartDataType) => {
- return getPointTime(d.time)
- },
- }
- }
- )
- }
- })
- return childrenChart
- }
- // 生成最新Chart配置
- const getChartOpt = (xRange: [number, number]): G2Spec => {
- return {
- type: 'spaceFlex',
- direction: 'col',
- // data: runtimeChartOpt.data,
- theme: { type: isDark ? 'dark' : 'light' },
- children: getChildrenChartOpt(xRange)
- }
- }
- const handleDragEnd = () => {
- if (runtimeChart === null) return
- const selectRang = timeRange.value
- if (selectRang[0] > selectRang[1]) {
- runtimeChart.options(getChartOpt([selectRang[1], selectRang[0]]))
- } else {
- runtimeChart.options(getChartOpt(timeRange.value))
- }
- console.log("render")
- runtimeChart.render()
- }
- onMounted(() => {
- runtimeChart = new Chart({
- container: chartRuntime.value!,
- autoFit: true,
- height: 100 * Object.keys(runtimeChartOpt.childrenData).length,
- });
- runtimeChart.options(getChartOpt(timeRange.value))
- runtimeChart.render()
- })
- onBeforeUnmount(() => {
- runtimeChart?.destroy()
- })
- </script>
|