|
|
@@ -3,12 +3,10 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { Chart, ChartEvent, register } from '@antv/g2'
|
|
|
-import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
|
+import { Chart, ChartEvent } from '@antv/g2'
|
|
|
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
|
|
import { usePersonalization } from '@/composable/usePersonalization'
|
|
|
|
|
|
-const message = useMessage()
|
|
|
-
|
|
|
const { isDark } = usePersonalization()
|
|
|
|
|
|
const props = defineProps<{
|
|
|
@@ -29,96 +27,123 @@ export interface PowerChartData {
|
|
|
Power?: number
|
|
|
}
|
|
|
|
|
|
+export interface SocChartData {
|
|
|
+ Time: string
|
|
|
+ Soc?: number
|
|
|
+}
|
|
|
+
|
|
|
let isVoidData = true
|
|
|
-const voidData: PowerChartData[] = (() => {
|
|
|
- const times: string[] = []
|
|
|
- const totalMinutes = 24 * 60 // 一天 = 1440 分钟
|
|
|
+// 缓存最近一次数据,供主题切换时复用
|
|
|
+let lastPowerData: PowerChartData[] | undefined = undefined
|
|
|
+let lastSocData: SocChartData[] | undefined = undefined
|
|
|
|
|
|
+// 生成一天的时间刻度(每 5 分钟)
|
|
|
+const buildTimes = (): string[] => {
|
|
|
+ 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
|
|
|
+}
|
|
|
|
|
|
- return times.map((t) => {
|
|
|
- return [
|
|
|
- {
|
|
|
- powerType: 'aPower',
|
|
|
- Time: t,
|
|
|
- },
|
|
|
+// 空功率数据
|
|
|
+const voidPowerData: PowerChartData[] = buildTimes()
|
|
|
+ .map((t) => [
|
|
|
+ { powerType: 'aPower', Time: t },
|
|
|
+ { powerType: 'Generator', Time: t },
|
|
|
+ { powerType: 'HomeLoad', Time: t },
|
|
|
+ { powerType: 'PV', Time: t },
|
|
|
+ { powerType: 'Grid', Time: t },
|
|
|
+ ])
|
|
|
+ .flat()
|
|
|
+
|
|
|
+// 空 SOC 数据
|
|
|
+const voidSocData: SocChartData[] = buildTimes().map((t) => ({ Time: t }))
|
|
|
+
|
|
|
+// 颜色映射(功率类型)
|
|
|
+const powerColorRelations: [string, string][] = [
|
|
|
+ ['aPower', '#41D187'],
|
|
|
+ ['Generator', '#5398FF'],
|
|
|
+ ['HomeLoad', '#0BF5FF'],
|
|
|
+ ['PV', '#F1A502'],
|
|
|
+ ['Grid', '#BB56FD'],
|
|
|
+]
|
|
|
+
|
|
|
+// 构建图表 options(上:功率堆叠面积;下:SOC 折线;共享 X 轴 Time)
|
|
|
+const buildOptions = (powerData: PowerChartData[], socData: SocChartData[]) => {
|
|
|
+ return {
|
|
|
+ type: 'spaceFlex' as const,
|
|
|
+ direction: 'col' as const,
|
|
|
+ ratio: [2, 1],
|
|
|
+ theme: { type: isDark.value ? 'classicDark' : 'classic' },
|
|
|
+ children: [
|
|
|
{
|
|
|
- powerType: 'Generator',
|
|
|
- Time: t,
|
|
|
+ // 上图:功率堆叠面积
|
|
|
+ type: 'area' as const,
|
|
|
+ data: powerData,
|
|
|
+ // 与下图左内边距对齐,保证 X 轴严格对齐
|
|
|
+ paddingLeft: 60,
|
|
|
+ paddingBottom: 0,
|
|
|
+ // 压缩与下图的间隔,仅留一点缝隙
|
|
|
+ marginBottom: 6,
|
|
|
+ transform: [{ type: 'stackY' as const }],
|
|
|
+ encode: { x: 'Time', y: 'Power', color: 'powerType' },
|
|
|
+ scale: {
|
|
|
+ color: { palette: 'accent', relations: powerColorRelations },
|
|
|
+ },
|
|
|
+ axis: { x: false },
|
|
|
+ interaction: { elementHighlight: { background: true } },
|
|
|
},
|
|
|
{
|
|
|
- powerType: 'HomeLoad',
|
|
|
- Time: t,
|
|
|
+ // 下图:SOC 折线
|
|
|
+ type: 'line' as const,
|
|
|
+ data: socData,
|
|
|
+ paddingLeft: 60,
|
|
|
+ paddingTop: 0,
|
|
|
+ // 压缩与上图的间隔,仅留一点缝隙
|
|
|
+ marginTop: 6,
|
|
|
+ encode: { x: 'Time', y: 'Soc' },
|
|
|
+ scale: { y: { domain: [0, 100], nice: false } },
|
|
|
+ style: { stroke: '#F5A623', lineWidth: 2 },
|
|
|
+ axis: { y: { title: 'SOC (%)' } },
|
|
|
},
|
|
|
- {
|
|
|
- powerType: 'PV',
|
|
|
- Time: t,
|
|
|
- },
|
|
|
- {
|
|
|
- powerType: 'Grid',
|
|
|
- Time: t,
|
|
|
- },
|
|
|
- ]
|
|
|
- }).flat()
|
|
|
-})()
|
|
|
-
|
|
|
-// 自定义色板
|
|
|
-// PV: #F1A502
|
|
|
-// GEN: #5398FF
|
|
|
-// Grid: #BB56FD
|
|
|
-// aPower: #41D187
|
|
|
-// Home: #0BF5FF
|
|
|
-// register('palette.customPowerFlow', customPalette);
|
|
|
-// function customPalette() {
|
|
|
-// return ['#F1A502', '#5398FF', '#BB56FD', '#41D187', '#0BF5FF'];
|
|
|
-// }
|
|
|
+ ],
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 绑定图例交互事件(功率类型图例)
|
|
|
+const bindLegendEvents = () => {
|
|
|
+ if (chart === null) return
|
|
|
+
|
|
|
+ // 监听图例点击事件 - 当用户通过图例进行筛选时触发
|
|
|
+ chart.on('legend:filter', (e: any) => {
|
|
|
+ const { nativeEvent, data } = e
|
|
|
+ if (!nativeEvent) return
|
|
|
+ emit('changeLegend', data?.values ?? [])
|
|
|
+ })
|
|
|
+
|
|
|
+ // 监听图例点击事件 - 当所有图例项都被选中时触发(重置状态)
|
|
|
+ chart.on('legend:reset', (e: any) => {
|
|
|
+ const { nativeEvent } = e
|
|
|
+ if (!nativeEvent) return
|
|
|
+ emit('changeLegend', [])
|
|
|
+ })
|
|
|
+}
|
|
|
|
|
|
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();
|
|
|
+ height: props.height || 300,
|
|
|
+ })
|
|
|
|
|
|
- // 监听图例点击事件 - 当用户通过图例进行筛选时触发
|
|
|
- chart.on('legend:filter', (e) => {
|
|
|
- const { nativeEvent, data } = e;
|
|
|
- if (!nativeEvent) return;
|
|
|
- emit('changeLegend', data.values ?? [])
|
|
|
- });
|
|
|
+ chart.options(buildOptions(voidPowerData, voidSocData))
|
|
|
+ chart.render()
|
|
|
|
|
|
- // 监听图例点击事件 - 当所有图例项都被选中时触发(重置状态)
|
|
|
- chart.on('legend:reset', (e) => {
|
|
|
- const { nativeEvent } = e;
|
|
|
- if (!nativeEvent) return;
|
|
|
- emit('changeLegend', [])
|
|
|
- });
|
|
|
+ bindLegendEvents()
|
|
|
})
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
@@ -126,21 +151,34 @@ onBeforeUnmount(() => {
|
|
|
})
|
|
|
|
|
|
// 重绘数据
|
|
|
-const redraw = (data: PowerChartData[] | undefined, legend?: string[]) => {
|
|
|
+const redraw = (
|
|
|
+ powerData: PowerChartData[] | undefined,
|
|
|
+ socData: SocChartData[] | undefined,
|
|
|
+ legend?: string[],
|
|
|
+) => {
|
|
|
if (chart === null) return
|
|
|
|
|
|
+ // 渲染完成后恢复图例筛选状态
|
|
|
if (legend !== undefined && legend.length > 0) {
|
|
|
- chart.on(ChartEvent.AFTER_RENDER, () => {
|
|
|
- changeLegend(legend)
|
|
|
- }, true)
|
|
|
+ chart.on(
|
|
|
+ ChartEvent.AFTER_RENDER,
|
|
|
+ () => {
|
|
|
+ changeLegend(legend)
|
|
|
+ },
|
|
|
+ true,
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- if (data === undefined) {
|
|
|
+ if (powerData === undefined) {
|
|
|
if (!isVoidData) {
|
|
|
- chart.changeData(voidData)
|
|
|
+ isVoidData = true
|
|
|
+ chart.options(buildOptions(voidPowerData, voidSocData))
|
|
|
+ chart.render()
|
|
|
}
|
|
|
} else {
|
|
|
- chart.changeData(data)
|
|
|
+ isVoidData = false
|
|
|
+ chart.options(buildOptions(powerData, socData ?? voidSocData))
|
|
|
+ chart.render()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -148,26 +186,35 @@ const redraw = (data: PowerChartData[] | undefined, legend?: string[]) => {
|
|
|
const changeLegend = (legend: string[]): boolean => {
|
|
|
if (chart === null) return false
|
|
|
if (legend.length === 0) {
|
|
|
- chart.emit('legend:reset', {});
|
|
|
+ chart.emit('legend:reset', {})
|
|
|
} else {
|
|
|
chart.emit('legend:filter', {
|
|
|
data: { channel: 'color', values: [...legend] },
|
|
|
- });
|
|
|
+ })
|
|
|
}
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
-watch(isDark, (newVal) => {
|
|
|
+watch(isDark, () => {
|
|
|
if (chart) {
|
|
|
- chart.theme({ type: newVal ? 'dark' : 'light' })
|
|
|
- chart.render()
|
|
|
+ // 主题变化时重建 options 以应用新主题
|
|
|
+ redraw(lastPowerData, lastSocData)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+const redrawWithCache = (
|
|
|
+ powerData: PowerChartData[] | undefined,
|
|
|
+ socData: SocChartData[] | undefined,
|
|
|
+ legend?: string[],
|
|
|
+) => {
|
|
|
+ lastPowerData = powerData
|
|
|
+ lastSocData = socData
|
|
|
+ redraw(powerData, socData, legend)
|
|
|
+}
|
|
|
+
|
|
|
// 暴露给父组件
|
|
|
defineExpose({
|
|
|
- redraw, // 重绘数据
|
|
|
- changeLegend // 更新图例显示
|
|
|
+ redraw: redrawWithCache, // 重绘数据
|
|
|
+ changeLegend, // 更新图例显示
|
|
|
})
|
|
|
-
|
|
|
</script>
|