siteRuntimeChart.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. <template>
  2. <div class="flex p-x-4 gap-2">
  3. <NSlider v-model:value="timeRange" :max="getMaxRange()" :marks="dateMarks" range
  4. :format-tooltip="sliderFormatTooltip" @dragend="handleDragEnd" />
  5. <NButton quaternary circle @click="() => { showChartConfig = true }">
  6. <span class="i-ph:caret-circle-double-left-bold h-6 w-6" />
  7. </NButton>
  8. <NDrawer v-model:show="showChartConfig" :width="680" placement="right" :close-on-esc="false" :mask-closable="false">
  9. <NDrawerContent title="Chart配置" closable>
  10. <NForm class="h-full flex flex-col" :model="formModel" label-placement="top">
  11. <NFormItem label="使用数据类型">
  12. <NRadioGroup v-model:value="formModel.dataType" name="dataType">
  13. <NSpace>
  14. <NPopover trigger="hover">
  15. <template #trigger>
  16. <NRadio value="cycle">
  17. 周期
  18. </NRadio>
  19. </template>
  20. <span>只显示5min的标准周期数据</span>
  21. </NPopover>
  22. <NPopover trigger="hover">
  23. <template #trigger>
  24. <NRadio value="all">
  25. 全部
  26. </NRadio>
  27. </template>
  28. <span>显示全部数据,包括召唤和事件变化</span>
  29. </NPopover>
  30. </NSpace>
  31. </NRadioGroup>
  32. </NFormItem>
  33. <NFormItem class="flex-1" label="绘图选项">
  34. <NTransfer class="h-full" v-model:value="formModel.selectChartConfig"
  35. :options="optionsChartConfig.transferOption" :render-source-list="renderSourceList" source-filterable />
  36. </NFormItem>
  37. </NForm>
  38. <template #footer>
  39. <NButton>确定</NButton>
  40. </template>
  41. </NDrawerContent>
  42. </NDrawer>
  43. </div>
  44. <div ref="chartRuntime" class="w-full"></div>
  45. </template>
  46. <script setup lang="ts">
  47. import { Chart, type G2Spec, type Mark, type Composition } from '@antv/g2'
  48. import { NButton, NDrawer, NDrawerContent, NSlider, NTransfer, NTree, NForm, NFormItem, NRadio, NRadioGroup, NSpace, NPopover } from 'naive-ui';
  49. import type { TransferRenderSourceList, TreeOption, TransferOption } from 'naive-ui';
  50. import { ref, onMounted, onBeforeUnmount } from 'vue'
  51. import dayjs from 'dayjs'
  52. import { usePersonalization } from '@/composable/usePersonalization'
  53. import { useSiteRuntimeChartConfigStore } from '@/stores/modules/views/siteRuntimeChartConfig';
  54. import type { RuntimeItem } from '@/service/sitesAnalysis/interface';
  55. const props = defineProps<{
  56. height?: number
  57. data: RuntimeItem[] // 数据必须按正确的时间排序
  58. }>()
  59. const { isDark, color } = usePersonalization()
  60. const runtimeChartConfigStore = useSiteRuntimeChartConfigStore()
  61. interface FormModelType {
  62. dataType: string
  63. selectChartConfig: string[]
  64. }
  65. const formModel = ref<FormModelType>({
  66. dataType: 'cycle',
  67. selectChartConfig: runtimeChartConfigStore.getConfig()
  68. })
  69. interface ChartConfigOption extends TreeOption {
  70. label: string
  71. value: string
  72. children?: ChartConfigOption[]
  73. array?: false | { // 是否是数组
  74. key: string,
  75. index: number
  76. }
  77. }
  78. // TODO 暂时没包含aPower相关配置
  79. const chartConfigOption: ChartConfigOption[] = [
  80. {
  81. label: "状态信息",
  82. value: "stateInfo",
  83. children: [
  84. { label: "工作模式(mode)", value: "mode" },
  85. { label: "运行状态(run_status)", value: "run_status" },
  86. { label: "EMS-DSP运行状态(slaver_stat)", value: "slaver_stat" },
  87. { label: "市电状态(elecnet_state)", value: "elecnet_state" },
  88. { label: "系统SOC(soc)", value: "soc" },
  89. { label: "环境温度(t_amb)", value: "t_amb" },
  90. { label: "4G信号(signal)", value: "signal" },
  91. { label: "wifi信号(wifiSignal)", value: "wifiSignal" },
  92. { label: "连接方式(connType)", value: "connType" },
  93. { label: "发电机状态(genStat)", value: "genStat" },
  94. { label: "远端光伏接入数量(remoteSolarEn)", value: "remoteSolarEn" },
  95. { label: "主动离网(offgridreason)", value: "offgridreason" },
  96. { label: "调度策略(name)", value: "name" },
  97. { label: "电费类型(electricity_type)", value: "electricity_type" },
  98. { label: "市电相制(gridPhaseConSet)", value: "gridPhaseConSet" },
  99. { label: "紧急电源模式(v2lModeEnable)", value: "v2lModeEnable" },
  100. { label: "v2l电车运行状态(v2lRunState)", value: "v2lRunState" },
  101. ]
  102. },
  103. {
  104. label: "开关状态",
  105. value: "switchState",
  106. children: [
  107. { label: "市电开关状态(main_sw[0])", value: "main_sw_0", array: { key: "main_sw", index: 0 } },
  108. { label: "发电机开关状态(main_sw[1])", value: "main_sw_1", array: { key: "main_sw", index: 1 } },
  109. { label: "aGate-PV开关状态(main_sw[2])", value: "main_sw_2", array: { key: "main_sw", index: 2 } },
  110. { label: "智能负载1状态(pro_load[0])", value: "pro_load_0", array: { key: "pro_load", index: 0 } },
  111. { label: "智能负载2状态(pro_load[1])", value: "pro_load_1", array: { key: "pro_load", index: 1 } },
  112. { label: "智能负载3状态(pro_load[2])", value: "pro_load_2", array: { key: "pro_load", index: 2 } },
  113. { label: "输出干接点1状态(do[0])", value: "do_0", array: { key: "do", index: 0 } },
  114. { label: "输出干接点2状态(do[1])", value: "do_1", array: { key: "do", index: 1 } },
  115. { label: "输出干接点3状态(do[2])", value: "do_2", array: { key: "do", index: 2 } },
  116. { label: "输出干接点4状态(do[3])", value: "do_3", array: { key: "do", index: 3 } },
  117. { label: "输入干接点1状态(di[0])", value: "di_0", array: { key: "di", index: 0 } },
  118. { label: "输入干接点2状态(di[1])", value: "di_1", array: { key: "di", index: 1 } },
  119. { label: "输入干接点3状态(di[2])", value: "di_2", array: { key: "di", index: 2 } },
  120. { label: "输入干接点4状态(di[3])", value: "di_3", array: { key: "di", index: 3 } },
  121. { label: "光伏电表前Apbox继电器状态(BFPVApboxRelay)", value: "BFPVApboxRelay" },
  122. { label: "市电继电器2状态(gridRelay2)", value: "gridRelay2" },
  123. { label: "光伏继电器2状态(pvRelay2)", value: "pvRelay2" },
  124. { label: "黑启动继电器状态(blackStartRelay)", value: "blackStartRelay" },
  125. { label: "MPPT工作状态(mpptSta)", value: "mpptSta" },
  126. ]
  127. },
  128. {
  129. label: "功率数据",
  130. value: "powerData",
  131. children: [
  132. { label: "市电功率(p_uti)", value: "p_uti" },
  133. { label: "光伏发电功率(p_sun)", value: "p_sun" },
  134. { label: "发电机发电功率(p_gen)", value: "p_gen" },
  135. { label: "aPower功率(p_fhp)", value: "p_fhp" },
  136. { label: "家庭负载功率(p_load)", value: "p_load" },
  137. { label: "近端光伏功率(solarPower)", value: "solarPower" },
  138. { label: "远端光伏1功率(remoteSolar1Power)", value: "remoteSolar1Power" },
  139. { label: "远端光伏2功率(remoteSolar2Power)", value: "remoteSolar2Power" },
  140. { label: "DSP内置电表NEM光伏功率(DSPNEMPVPower)", value: "DSPNEMPVPower" },
  141. { label: "三相光伏功率(threePhasePvPower)", value: "threePhasePvPower" },
  142. { label: "MPPT光伏总功率(mpptAllPower)", value: "mpptAllPower" },
  143. { label: "aPwoerS逆变侧功率(acPower)", value: "acPower" },
  144. { label: "aPowerS交流侧光伏功率(aggAcPvPower)", value: "aggAcPvPower" },
  145. ]
  146. },
  147. {
  148. label: "电量数据",
  149. value: "energyData",
  150. children: [
  151. { label: "市电输入电量(kwh_uti_in)", value: "kwh_uti_in" },
  152. { label: "市电输出电量(kwh_uti_out)", value: "kwh_uti_out" },
  153. { label: "光伏发电量(kwh_sun)", value: "kwh_sun" },
  154. { label: "发电机发电量(kwh_gen)", value: "kwh_gen" },
  155. { label: "aPower放电电量(kwh_fhp_di)", value: "kwh_fhp_di" },
  156. { label: "aPower充电电量(kwh_fhp_chg)", value: "kwh_fhp_chg" },
  157. { label: "家庭负载用电量(kwh_load)", value: "kwh_load" },
  158. { label: "智能负载线路使用电量(kwhIqLoad1)", value: "kwhIqLoad1" },
  159. { label: "近端光伏日电量(proximalSolarWh)", value: "proximalSolarWh" },
  160. { label: "远端光伏1日电量(remoteSolar1Wh)", value: "remoteSolar1Wh" },
  161. { label: "远端光伏2日电量(remoteSolar2Wh)", value: "remoteSolar2Wh" },
  162. { label: "远端光伏3日电量(remoteSolar3Wh)", value: "remoteSolar3Wh" },
  163. { label: "meterkit日电量(meterkitPvWh)", value: "meterkitPvWh" },
  164. { label: "第二辅助电表日电量(secondaryPvWh)", value: "secondaryPvWh" },
  165. { label: "MPPT日电量(mpptWh)", value: "mpptWh" },
  166. ]
  167. },
  168. {
  169. label: "其他数据",
  170. value: "otherData",
  171. children: [
  172. { label: "油机电压(genVoltage)", value: "genVoltage" },
  173. ]
  174. }
  175. ]
  176. interface ChartConfigKeyMapValue {
  177. label: string
  178. array?: false | { // 是否是数组
  179. key: string,
  180. index: number
  181. }
  182. }
  183. interface ChartConfigKeyMap {
  184. [key: string]: ChartConfigKeyMapValue
  185. }
  186. function flattenTree(list: ChartConfigOption[]): {
  187. keyMap: ChartConfigKeyMap,
  188. transferOption: TransferOption[]
  189. } {
  190. const result: {
  191. keyMap: ChartConfigKeyMap,
  192. transferOption: TransferOption[]
  193. } = {
  194. keyMap: {},
  195. transferOption: []
  196. }
  197. function flatten(_list: ChartConfigOption[] = []) {
  198. _list.forEach((item) => {
  199. if (item.children != null) {
  200. flatten(item.children)
  201. } else {
  202. result.keyMap[item.value] = {
  203. label: item.label,
  204. array: item.array
  205. }
  206. result.transferOption.push(item)
  207. }
  208. })
  209. }
  210. flatten(list)
  211. return result
  212. }
  213. const optionsChartConfig = flattenTree(chartConfigOption)
  214. // 转换并解析运行时json数据
  215. const transformRuntimeData = (data?: RuntimeItem[]): RuntimeDataType[] => {
  216. if (data === undefined) return []
  217. return data.map(d => {
  218. const obj = JSON.parse(d.data_201);
  219. const dataArea = obj['dataArea']
  220. if (dataArea === null) {
  221. return {
  222. time: d.time
  223. }
  224. }
  225. // 从keyMap中获取数据配置信息
  226. const keyTab: Record<string, string | number> = {}
  227. for (const [key, value] of Object.entries(optionsChartConfig.keyMap)) {
  228. if (value.array != null && value.array !== false) {
  229. keyTab[key] = dataArea[value.array.key] ? dataArea[value.array.key][value.array.index] ?? undefined : undefined
  230. } else {
  231. keyTab[key] = dataArea[key] ?? undefined
  232. }
  233. }
  234. return {
  235. time: d.time,
  236. // ...(dataArea['mode'] != null && { RunMode: dataArea['mode'] }),
  237. // ...(dataArea['run_status'] != null && { RunStatus: dataArea['run_status'] }),
  238. // ...(dataArea['elecnet_state'] != null && { GridStatus: dataArea['elecnet_state'] })
  239. ...keyTab
  240. }
  241. })
  242. }
  243. const showChartConfig = ref(false)
  244. const renderSourceList: TransferRenderSourceList = function ({
  245. onCheck,
  246. pattern
  247. }) {
  248. return h(NTree, {
  249. style: 'margin: 0 4px;',
  250. keyField: 'value',
  251. cascade: true,
  252. checkable: true,
  253. selectable: false,
  254. blockLine: true,
  255. checkOnClick: true,
  256. data: chartConfigOption,
  257. pattern,
  258. checkedKeys: formModel.value.selectChartConfig,
  259. onUpdateCheckedKeys: (checkedKeys: Array<string>) => {
  260. runtimeChartConfigStore.updateConfig(checkedKeys)
  261. onCheck(checkedKeys)
  262. }
  263. })
  264. }
  265. const chartRuntime = ref<HTMLDivElement>()
  266. let runtimeChart: Chart | null = null
  267. interface RuntimeDataType {
  268. time: string // format YYYY-MM-DD HH:mm:ss
  269. [key: string]: string | number // number是曲线 string是事件
  270. }
  271. interface ChartDataType {
  272. time: number // 距离其实时间的秒数
  273. value: string | number // number是曲线 string是事件
  274. }
  275. // 模拟数据:10 分钟,每秒一个点
  276. // const data: RuntimeDataType[] = [];
  277. // const start = dayjs('2025-10-09 12:00:00');
  278. // for (let i = 0; i < 600; i++) {
  279. // const time = start.add(i, 'second').format('YYYY-MM-DD HH:mm:ss');
  280. // data.push({
  281. // time,
  282. // value: Math.sin(i / 10) * 10 + 20,
  283. // })
  284. // }
  285. // 计算时间相关信息
  286. const startTime = (props.data.length > 0) ? props.data[0].time : "2025-01-01"
  287. const startTimeDayjs = dayjs(startTime)
  288. const endTime = (props.data.length > 0) ? props.data[props.data.length - 1].time : "2025-01-01"
  289. const endTimeDayjs = dayjs(endTime)
  290. const diffSeconds = endTimeDayjs.diff(startTimeDayjs, 'second');
  291. // 计算某个点的时间 YYYY-MM-DD HH:mm:ss
  292. const getPointTime = (value: number): string => {
  293. if (value >= diffSeconds) return endTime
  294. return startTimeDayjs.add(value, 'second').format('YYYY-MM-DD HH:mm:ss')
  295. }
  296. // 计算最大选择范围
  297. const getMaxRange = () => {
  298. if (diffSeconds > 0) return diffSeconds
  299. return 0
  300. }
  301. // 计算选择数据范围
  302. const timeRange = ref<[number, number]>([0, (diffSeconds < 24 * 60 * 60) ? diffSeconds : 24 * 60 * 60])
  303. // 计算日期标注
  304. const dateMarks = computed<{ [markValue: number]: string }>(() => {
  305. const start = dayjs(startTime);
  306. const end = dayjs(endTime);
  307. const marks: Record<number, string> = {};
  308. if (!start.isValid() || !end.isValid() || !start.isBefore(end)) {
  309. return marks; // 无效输入
  310. }
  311. // 找出所有0点整
  312. let current = start.startOf('day'); // 当天0点
  313. if (current.isBefore(start)) {
  314. current = current.add(1, 'day'); // 如果start已经过了当天0点,取下一天
  315. }
  316. const midnights: dayjs.Dayjs[] = [];
  317. while (current.isBefore(end) || current.isSame(end)) {
  318. midnights.push(current);
  319. current = current.add(1, 'day');
  320. }
  321. // 若超过10个,均匀取样
  322. let selected = midnights;
  323. const MAX_COUNT = 10;
  324. if (midnights.length > MAX_COUNT) {
  325. const step = (midnights.length - 1) / (MAX_COUNT - 1);
  326. selected = Array.from({ length: MAX_COUNT }, (_, i) =>
  327. midnights[Math.round(i * step)]
  328. );
  329. }
  330. // 计算每个点相对起始时间的秒数差
  331. selected.forEach((d) => {
  332. const diffSec = d.diff(start, 'second'); // 秒差
  333. const dateStr = d.format('YYYY-MM-DD'); // 格式化成 "YYYY-MM-DD"
  334. marks[diffSec] = dateStr;
  335. });
  336. return marks;
  337. });
  338. const sliderFormatTooltip = (value: number) => {
  339. // console.log({ value, time: getPointTime(value) })
  340. return getPointTime(value)
  341. }
  342. interface RuntimeChartChildrenData {
  343. type: string
  344. data: ChartDataType[]
  345. }
  346. interface RuntimeChartOpt {
  347. childrenData: Record<string, RuntimeChartChildrenData>
  348. }
  349. // 生成Chart数据和子字段SET表
  350. const handleChartData = (objList: RuntimeDataType[]): RuntimeChartOpt => {
  351. const childrenData: Record<string, RuntimeChartChildrenData> = {};
  352. objList.map(obj => {
  353. Object.entries(obj).forEach(([key, value]) => {
  354. const keyType = typeof value
  355. if (key !== 'time' && (keyType === 'string' || keyType === 'number')) {
  356. if (childrenData[key] == null) {
  357. childrenData[key] = {
  358. type: keyType,
  359. data: []
  360. }
  361. }
  362. childrenData[key].data.push({
  363. time: dayjs(obj.time).diff(startTimeDayjs, 'second'),
  364. value
  365. })
  366. }
  367. });
  368. })
  369. return {
  370. childrenData
  371. }
  372. }
  373. const runtimeChartOpt = handleChartData(transformRuntimeData(props.data))
  374. console.log({ runtimeChartOpt })
  375. // 生成Chart子配置
  376. const getChildrenChartOpt = (xRange: [number, number]): (Mark | Composition)[] => {
  377. const childrenChart: (Mark | Composition)[] = []
  378. Object.entries(runtimeChartOpt.childrenData).forEach(([key, value]) => {
  379. if (value.type === 'number') {
  380. // 子Chart为曲线
  381. childrenChart.push(
  382. {
  383. type: 'line',
  384. height: 100,
  385. data: value.data,
  386. encode: {
  387. x: 'time',
  388. y: 'value'
  389. },
  390. axis: {
  391. x: false,
  392. y: {
  393. tickCount: 0,
  394. position: 'bottom'
  395. }
  396. },
  397. scale: {
  398. x: {
  399. domain: xRange,
  400. }
  401. },
  402. tooltip: {
  403. title: (d: ChartDataType) => {
  404. return getPointTime(d.time)
  405. },
  406. }
  407. }
  408. )
  409. } else if (value.type === 'string') {
  410. // 子Chart是事件点
  411. childrenChart.push(
  412. {
  413. type: 'point',
  414. height: 100,
  415. data: value.data,
  416. encode: {
  417. x: 'time',
  418. y: 'value'
  419. },
  420. axis: {
  421. x: false,
  422. y: {
  423. tickCount: 0,
  424. position: 'bottom'
  425. }
  426. },
  427. scale: {
  428. x: {
  429. domain: xRange,
  430. }
  431. },
  432. tooltip: {
  433. title: (d: ChartDataType) => {
  434. return getPointTime(d.time)
  435. },
  436. }
  437. }
  438. )
  439. }
  440. })
  441. return childrenChart
  442. }
  443. // 生成最新Chart配置
  444. const getChartOpt = (xRange: [number, number]): G2Spec => {
  445. return {
  446. type: 'spaceFlex',
  447. direction: 'col',
  448. // data: runtimeChartOpt.data,
  449. theme: { type: isDark ? 'dark' : 'light' },
  450. children: getChildrenChartOpt(xRange)
  451. }
  452. }
  453. const handleDragEnd = () => {
  454. if (runtimeChart === null) return
  455. const selectRang = timeRange.value
  456. if (selectRang[0] > selectRang[1]) {
  457. runtimeChart.options(getChartOpt([selectRang[1], selectRang[0]]))
  458. } else {
  459. runtimeChart.options(getChartOpt(timeRange.value))
  460. }
  461. console.log("render")
  462. runtimeChart.render()
  463. }
  464. onMounted(() => {
  465. runtimeChart = new Chart({
  466. container: chartRuntime.value!,
  467. autoFit: true,
  468. height: 100 * Object.keys(runtimeChartOpt.childrenData).length,
  469. });
  470. runtimeChart.options(getChartOpt(timeRange.value))
  471. runtimeChart.render()
  472. })
  473. onBeforeUnmount(() => {
  474. runtimeChart?.destroy()
  475. })
  476. </script>