@ledvance/ui-biz-bundle 1.1.167 → 1.1.169
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/navigation/Routers.ts +1 -0
- package/src/newModules/biorhythm/BiorhythmPage.tsx +1 -0
- package/src/newModules/energyConsumption/EditPriceSegmentContract.tsx +89 -0
- package/src/newModules/energyConsumption/EditPriceSegmentScreen.tsx +89 -0
- package/src/newModules/energyConsumption/EnergyConsumptionActions.ts +5 -1
- package/src/newModules/energyConsumption/EnergyConsumptionChart/EnergyChartSection.tsx +33 -5
- package/src/newModules/energyConsumption/EnergyConsumptionPage.tsx +228 -117
- package/src/newModules/energyConsumption/Router.ts +9 -0
- package/src/newModules/energyConsumption/SetSegmentPricesContract.tsx +423 -0
- package/src/newModules/energyConsumption/SetSegmentedPricesScreen.tsx +189 -0
- package/src/newModules/energyConsumption/component/DateSelectedItem.tsx +3 -1
- package/src/newModules/energyConsumption/component/DateSwitch.tsx +9 -3
- package/src/newModules/energyConsumption/component/EnergyModal.tsx +66 -5
- package/src/newModules/energyConsumption/component/NewBarChart.tsx +63 -5
- package/src/newModules/energyConsumption/component/VerticalDailyPricesIndicator.tsx +175 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
2
|
import Card from "@ledvance/base/src/components/Card";
|
|
3
3
|
import Spacer from "@ledvance/base/src/components/Spacer";
|
|
4
4
|
import TextFieldStyleButton from "@ledvance/base/src/components/TextFieldStyleButton";
|
|
@@ -11,6 +11,7 @@ import { cloneDeep } from "lodash";
|
|
|
11
11
|
import ThemeType from '@ledvance/base/src/config/themeType'
|
|
12
12
|
import dayjs from "dayjs";
|
|
13
13
|
import {EnergyHistory} from "../EnergyConsumptionActions";
|
|
14
|
+
import EditPriceSegmentScreen from "../EditPriceSegmentScreen";
|
|
14
15
|
|
|
15
16
|
const { convertX: cx, height, statusBarHeight } = Utils.RatioUtils
|
|
16
17
|
const { withTheme } = Utils.ThemeUtils
|
|
@@ -31,29 +32,78 @@ export const UnitList = [
|
|
|
31
32
|
I18n.getLang('consumption_data_price_per_kwh_currency_value13'),
|
|
32
33
|
]
|
|
33
34
|
|
|
35
|
+
export interface PriceSegment {
|
|
36
|
+
startMinute: number; // 0~1439
|
|
37
|
+
endMinute: number; // 0~1439
|
|
38
|
+
price: number;
|
|
39
|
+
}
|
|
40
|
+
export function calculateAveragePrice(
|
|
41
|
+
intervals: PriceSegment[],
|
|
42
|
+
): number {
|
|
43
|
+
let totalPrice = 0
|
|
44
|
+
for(const item of intervals) {
|
|
45
|
+
const hrs = (item.endMinute - item.startMinute)/60
|
|
46
|
+
totalPrice += (hrs * item.price)
|
|
47
|
+
}
|
|
48
|
+
return totalPrice/24
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
export const parseMinutes = (totalMinutes: number | undefined) => {
|
|
52
|
+
if (!totalMinutes) {
|
|
53
|
+
return { hour: '00', minute: '00' };
|
|
54
|
+
}
|
|
55
|
+
const h = Math.floor(totalMinutes / 60);
|
|
56
|
+
const m = totalMinutes % 60;
|
|
57
|
+
return {
|
|
58
|
+
hour: h.toString().padStart(2, '0'),
|
|
59
|
+
minute: m.toString().padStart(2, '0')
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type EnergyType = 'fixed' | 'dynamic'
|
|
64
|
+
//固定价格能量模型
|
|
34
65
|
export interface EnergyData {
|
|
35
66
|
price: string
|
|
36
67
|
unit: string
|
|
68
|
+
energyType?: EnergyType
|
|
69
|
+
generatePrice?: string
|
|
70
|
+
}
|
|
71
|
+
//动态价格能量模型
|
|
72
|
+
export interface DynamicEnergyData{
|
|
73
|
+
intervalPrices?: PriceSegment[]
|
|
74
|
+
generatePrices?: PriceSegment[]
|
|
37
75
|
}
|
|
38
76
|
|
|
39
77
|
interface EnergyModalProps {
|
|
40
78
|
theme?: ThemeType
|
|
41
79
|
visible: boolean
|
|
42
|
-
popupType: 'money' | 'co2' | 'unit' | 'history'
|
|
80
|
+
popupType: 'money' | 'co2' | 'unit' | 'history' | 'segment_price'
|
|
43
81
|
title: string
|
|
44
82
|
confirmText?: string
|
|
45
83
|
cancelText?: string
|
|
46
84
|
energyData?: EnergyData
|
|
47
85
|
energyHistory?: EnergyHistory[]
|
|
48
86
|
motionType?: 'none'
|
|
87
|
+
editingPriceSegment?: PriceSegment
|
|
49
88
|
onConfirm?: (data?: EnergyData) => void
|
|
89
|
+
onPriceSegmentConfirm?: (segment?: PriceSegment)=> void
|
|
50
90
|
onCancel?: () => void
|
|
51
91
|
}
|
|
52
92
|
const EnergyModal = (props: EnergyModalProps) => {
|
|
53
93
|
const state = useReactive({
|
|
54
94
|
energyData: cloneDeep(props.energyData),
|
|
55
|
-
unitPopup: false
|
|
95
|
+
unitPopup: false,
|
|
96
|
+
editingPriceSegment: cloneDeep(props.editingPriceSegment)
|
|
56
97
|
})
|
|
98
|
+
useEffect(()=>{
|
|
99
|
+
if(props.visible) {
|
|
100
|
+
if(!props.editingPriceSegment){
|
|
101
|
+
state.editingPriceSegment = { startMinute: 0, endMinute: 0, price: 0 }
|
|
102
|
+
} else {
|
|
103
|
+
state.editingPriceSegment = props.editingPriceSegment
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, [props.visible,props.editingPriceSegment])
|
|
57
107
|
|
|
58
108
|
const openLink = (url: string) => {
|
|
59
109
|
Linking.openURL(url).catch((error) => console.error('无法打开链接:', error));
|
|
@@ -237,7 +287,16 @@ const EnergyModal = (props: EnergyModalProps) => {
|
|
|
237
287
|
}
|
|
238
288
|
</View>
|
|
239
289
|
)
|
|
240
|
-
} else {
|
|
290
|
+
} else if(props.popupType === 'segment_price') {
|
|
291
|
+
return (
|
|
292
|
+
<EditPriceSegmentScreen
|
|
293
|
+
segment={state.editingPriceSegment}
|
|
294
|
+
onSegmentSet={(segment)=>{
|
|
295
|
+
state.editingPriceSegment = segment
|
|
296
|
+
}}/>
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
}else {
|
|
241
300
|
return (
|
|
242
301
|
<View>
|
|
243
302
|
<Spacer />
|
|
@@ -277,7 +336,7 @@ const EnergyModal = (props: EnergyModalProps) => {
|
|
|
277
336
|
<Spacer />
|
|
278
337
|
<Card>
|
|
279
338
|
<FlatList
|
|
280
|
-
data={UnitList}
|
|
339
|
+
data={ UnitList}
|
|
281
340
|
initialNumToRender={15}
|
|
282
341
|
renderItem={({ item }) => (
|
|
283
342
|
<TouchableOpacity
|
|
@@ -325,6 +384,8 @@ const EnergyModal = (props: EnergyModalProps) => {
|
|
|
325
384
|
<Text style={{ color: props.theme?.global.fontColor, fontSize: cx(16), fontWeight: 'bold' }}>{props.title}</Text>
|
|
326
385
|
<TouchableOpacity onPress={() => {
|
|
327
386
|
props.onConfirm && props.onConfirm(state.energyData)
|
|
387
|
+
console.log("wayne_check innner check segment ->", JSON.stringify(state.editingPriceSegment))
|
|
388
|
+
props.onPriceSegmentConfirm && props.onPriceSegmentConfirm(state.editingPriceSegment)
|
|
328
389
|
}}>
|
|
329
390
|
<Text style={{ color: props.theme?.button.primary, fontSize: cx(16) }}>{props.confirmText}</Text>
|
|
330
391
|
</TouchableOpacity>
|
|
@@ -9,22 +9,76 @@ import { OverviewItem } from '../EnergyConsumptionPage'
|
|
|
9
9
|
const { withTheme } = Utils.ThemeUtils
|
|
10
10
|
const cx = Utils.RatioUtils.convertX
|
|
11
11
|
|
|
12
|
+
import { calculateAveragePrice, PriceSegment, DynamicEnergyData } from './EnergyModal'
|
|
13
|
+
import { DateType } from '../co2Data'
|
|
14
|
+
|
|
12
15
|
interface BarChartProps {
|
|
13
16
|
theme?: ThemeType
|
|
14
17
|
data: OverviewItem[]
|
|
15
18
|
price: number
|
|
16
19
|
unit: string
|
|
17
20
|
height: number
|
|
21
|
+
energyType?: string
|
|
22
|
+
dynamicData?: DynamicEnergyData
|
|
23
|
+
isGeneration?: boolean
|
|
24
|
+
dateType?: DateType
|
|
25
|
+
}
|
|
26
|
+
//算出固定时间间隔内的平均价格,目前用于计算柱状图每一个小时的价格
|
|
27
|
+
export function calculateIntervalAveragePrice(
|
|
28
|
+
intervals: PriceSegment[],
|
|
29
|
+
startMinute: number,
|
|
30
|
+
endMinute: number
|
|
31
|
+
): number {
|
|
32
|
+
let totalWeightedPrice = 0;
|
|
33
|
+
let totalTimeOverlap = 0;
|
|
34
|
+
|
|
35
|
+
for (const item of intervals) {
|
|
36
|
+
const overlapStart = Math.max(item.startMinute, startMinute);
|
|
37
|
+
const overlapEnd = Math.min(item.endMinute, endMinute);
|
|
38
|
+
|
|
39
|
+
if (overlapStart < overlapEnd) {
|
|
40
|
+
const duration = overlapEnd - overlapStart;
|
|
41
|
+
totalWeightedPrice += duration * item.price;
|
|
42
|
+
totalTimeOverlap += duration;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (totalTimeOverlap > 0) {
|
|
47
|
+
return totalWeightedPrice / totalTimeOverlap;
|
|
48
|
+
}
|
|
49
|
+
return 0;
|
|
18
50
|
}
|
|
19
51
|
|
|
20
52
|
const BarChartWithTouch = (props: BarChartProps) => {
|
|
21
53
|
const echarts = useRef<any>()
|
|
22
|
-
const { data, height, price, unit, theme } = props
|
|
54
|
+
const { data, height, price, unit, theme, energyType, dynamicData, isGeneration, dateType } = props
|
|
23
55
|
|
|
24
56
|
const dataX = data?.map(item => item.chartTitle)
|
|
25
57
|
const dataKwhY = data?.map(item => Number(item.value))
|
|
26
|
-
|
|
27
|
-
|
|
58
|
+
|
|
59
|
+
const dataPrices = useMemo(() => {
|
|
60
|
+
if (energyType === 'dynamic' && dynamicData) {
|
|
61
|
+
const intervals = isGeneration ? dynamicData.generatePrices : dynamicData.intervalPrices;
|
|
62
|
+
if (intervals && intervals.length > 0) {
|
|
63
|
+
if (dateType === DateType.Day) {
|
|
64
|
+
return data?.map(item => {
|
|
65
|
+
const hour = parseInt(item.headlineText.split(':')[0]);
|
|
66
|
+
if (!isNaN(hour)) {
|
|
67
|
+
return calculateIntervalAveragePrice(intervals, hour * 60, (hour + 1) * 60);
|
|
68
|
+
}
|
|
69
|
+
return calculateAveragePrice(intervals);
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
const avgPrice = calculateAveragePrice(intervals);
|
|
73
|
+
return data?.map(() => avgPrice);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return data?.map(() => price);
|
|
78
|
+
}, [data, price, energyType, dynamicData, isGeneration, dateType]);
|
|
79
|
+
|
|
80
|
+
const dataPriceY = data?.map((item, index) =>
|
|
81
|
+
((isNaN(Number(item.value)) ? 0 : Number(item.value)) * (dataPrices?.[index] ?? price)).toFixed(2)
|
|
28
82
|
)
|
|
29
83
|
|
|
30
84
|
const maxValue = useMemo(() => {
|
|
@@ -42,6 +96,10 @@ const BarChartWithTouch = (props: BarChartProps) => {
|
|
|
42
96
|
return max
|
|
43
97
|
}, [dataKwhY])
|
|
44
98
|
|
|
99
|
+
const maxPriceValue = useMemo(() => {
|
|
100
|
+
return Math.max(...dataPriceY.map(it => Number(it)), 0);
|
|
101
|
+
}, [dataPriceY])
|
|
102
|
+
|
|
45
103
|
const gridRight = useMemo(() => {
|
|
46
104
|
const max = Math.max(...dataPriceY.map(it => Number(it)))
|
|
47
105
|
return max > 999 ? '15%' : '10%'
|
|
@@ -109,7 +167,7 @@ const BarChartWithTouch = (props: BarChartProps) => {
|
|
|
109
167
|
name: I18n.formatValue('format_unit', unit),
|
|
110
168
|
position: 'right',
|
|
111
169
|
alignTicks: true,
|
|
112
|
-
max: Math.ceil(
|
|
170
|
+
max: Math.ceil(maxPriceValue ? maxPriceValue * 1.4 : 0),
|
|
113
171
|
min: 0,
|
|
114
172
|
minInterval: 1,
|
|
115
173
|
axisLabel: {
|
|
@@ -160,7 +218,7 @@ const BarChartWithTouch = (props: BarChartProps) => {
|
|
|
160
218
|
zoomLock: true,
|
|
161
219
|
}
|
|
162
220
|
}
|
|
163
|
-
}, [dataX, dataKwhY, dataPriceY,
|
|
221
|
+
}, [dataX, dataKwhY, dataPriceY, unit, maxValue, maxPriceValue, gridRight, theme])
|
|
164
222
|
|
|
165
223
|
return (
|
|
166
224
|
<View style={{ flex: 1 }}>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import React, {useState} from 'react'
|
|
2
|
+
import { PriceSegment } from "./EnergyModal"
|
|
3
|
+
import { Utils } from 'tuya-panel-kit'
|
|
4
|
+
import {View, StyleSheet, StyleProp, ViewStyle, LayoutChangeEvent} from 'react-native'
|
|
5
|
+
import ThemeType from '@ledvance/base/src/config/themeType'
|
|
6
|
+
import _ from 'lodash'
|
|
7
|
+
|
|
8
|
+
const { withTheme } = Utils.ThemeUtils
|
|
9
|
+
const cx = Utils.RatioUtils.convertX
|
|
10
|
+
|
|
11
|
+
interface VerticalDailyPricesIndicatorProps {
|
|
12
|
+
theme?: ThemeType
|
|
13
|
+
data: PriceSegment[]
|
|
14
|
+
selectingData?: PriceSegment
|
|
15
|
+
style?: StyleProp<ViewStyle>
|
|
16
|
+
getSegmentColor:(startMinute: number, price: number)=>string,
|
|
17
|
+
}
|
|
18
|
+
interface DrawBlock {
|
|
19
|
+
startMinute: number,
|
|
20
|
+
endMinute: number,
|
|
21
|
+
price: number,
|
|
22
|
+
isEmpty: boolean,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const convertToDrawBlocks = (sortedSegments: PriceSegment[]): DrawBlock[] => {
|
|
26
|
+
const blocks: DrawBlock[] = [];
|
|
27
|
+
let ptr = 0;
|
|
28
|
+
const TOTAL_MINUTES = 1439; // (0 ~ 1439)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
for (const segment of sortedSegments) {
|
|
32
|
+
//早时补白
|
|
33
|
+
if (segment.startMinute > ptr) {
|
|
34
|
+
blocks.push({
|
|
35
|
+
startMinute: ptr,
|
|
36
|
+
endMinute: segment.startMinute, //直到现时起点
|
|
37
|
+
price:0,
|
|
38
|
+
isEmpty: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//现时色块
|
|
43
|
+
blocks.push({
|
|
44
|
+
startMinute: segment.startMinute,
|
|
45
|
+
endMinute: segment.endMinute,
|
|
46
|
+
price:segment.price,
|
|
47
|
+
isEmpty: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
//现时末点指针
|
|
51
|
+
ptr = segment.endMinute;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//未来时补白
|
|
55
|
+
if (ptr < TOTAL_MINUTES) {
|
|
56
|
+
blocks.push({
|
|
57
|
+
startMinute: ptr, //从现时末点
|
|
58
|
+
endMinute: TOTAL_MINUTES,
|
|
59
|
+
price:0,
|
|
60
|
+
isEmpty: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return blocks;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const VerticalDailyPricesIndicator = (props: VerticalDailyPricesIndicatorProps) => {
|
|
68
|
+
|
|
69
|
+
//计算百分比
|
|
70
|
+
const getSegmentHeightPercent = (segment: DrawBlock): number => {
|
|
71
|
+
const duration = segment.endMinute - segment.startMinute
|
|
72
|
+
return (duration / 1439) * 100
|
|
73
|
+
}
|
|
74
|
+
const [indicatorHeight, setIndicatorHeight] = useState<number>(0)
|
|
75
|
+
const markerHeight = indicatorHeight / 24
|
|
76
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
77
|
+
const { height } = event.nativeEvent.layout;
|
|
78
|
+
if (Math.abs(indicatorHeight - height) > 0.1) {
|
|
79
|
+
setIndicatorHeight(height);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const sortedSegments = convertToDrawBlocks([...props.data])
|
|
83
|
+
|
|
84
|
+
const styles = StyleSheet.create({
|
|
85
|
+
container: {
|
|
86
|
+
flexDirection: 'row',
|
|
87
|
+
alignItems: 'flex-start',
|
|
88
|
+
height: '100%',
|
|
89
|
+
},
|
|
90
|
+
indicatorWrapper: {
|
|
91
|
+
position: 'absolute',
|
|
92
|
+
top: 0,
|
|
93
|
+
left: 0,
|
|
94
|
+
height: indicatorHeight,
|
|
95
|
+
width: cx(30),
|
|
96
|
+
borderRadius: cx(4),
|
|
97
|
+
overflow: 'hidden',
|
|
98
|
+
alignItems: 'center',
|
|
99
|
+
},
|
|
100
|
+
segmentBlock: {
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
justifyContent: 'center',
|
|
103
|
+
},
|
|
104
|
+
expandedLabel: {
|
|
105
|
+
paddingVertical: cx(4),
|
|
106
|
+
paddingHorizontal: cx(2),
|
|
107
|
+
fontSize: cx(8),
|
|
108
|
+
fontWeight: 'bold',
|
|
109
|
+
color: '#fff',
|
|
110
|
+
},
|
|
111
|
+
timelineMarker: {
|
|
112
|
+
position: 'absolute',
|
|
113
|
+
left: -cx(28),
|
|
114
|
+
width: cx(20),
|
|
115
|
+
height: 1,
|
|
116
|
+
backgroundColor: props.theme?.global.fontColor,
|
|
117
|
+
opacity: 0.3,
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<View style={[styles.container, props.style]} onLayout={handleLayout}>
|
|
123
|
+
{/* Timeline Markers */}
|
|
124
|
+
<View style={{height: indicatorHeight, paddingVertical: 0 }}>
|
|
125
|
+
{_.times(24,(index)=>index).map((hour) => (
|
|
126
|
+
<View key={hour} style={{ alignItems: 'flex-start',}}>
|
|
127
|
+
<View style={{width: cx(8), height: markerHeight}}>
|
|
128
|
+
<View style={{width:'100%',height:cx(1), backgroundColor:props.theme?.global.secondFontColor}}></View>
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
))
|
|
132
|
+
}
|
|
133
|
+
</View>
|
|
134
|
+
<View style={{ height: indicatorHeight, width: cx(30), position: 'relative', alignItems: 'center', justifyContent: 'center' }}>
|
|
135
|
+
{/*Vertical bar */}
|
|
136
|
+
<View style={{
|
|
137
|
+
width: cx(10),
|
|
138
|
+
height: indicatorHeight,
|
|
139
|
+
backgroundColor: props.theme?.segment.background,
|
|
140
|
+
borderRadius: cx(1)
|
|
141
|
+
}} />
|
|
142
|
+
{/* Vertical Indicator Bar */}
|
|
143
|
+
<View style={[styles.indicatorWrapper]}>
|
|
144
|
+
{sortedSegments.map((segment) => {
|
|
145
|
+
const heightPercent = getSegmentHeightPercent(segment)
|
|
146
|
+
const height = (heightPercent / 100) * indicatorHeight
|
|
147
|
+
const isSelected = props.selectingData?.startMinute === segment.startMinute &&
|
|
148
|
+
props.selectingData?.endMinute === segment.endMinute
|
|
149
|
+
const color = segment.isEmpty ? 'transparent': props.getSegmentColor(segment.startMinute, segment.price)
|
|
150
|
+
|
|
151
|
+
if (isSelected) {
|
|
152
|
+
console.log(`wayne_check isSelected item inner start min: ${segment.startMinute},end min: ${segment.endMinute}`);
|
|
153
|
+
}
|
|
154
|
+
return (
|
|
155
|
+
<View
|
|
156
|
+
key={`${segment.startMinute}-${segment.endMinute}`}
|
|
157
|
+
style={[
|
|
158
|
+
styles.segmentBlock,
|
|
159
|
+
{
|
|
160
|
+
height,
|
|
161
|
+
backgroundColor: color,
|
|
162
|
+
width: isSelected ? cx(30) : cx(15),
|
|
163
|
+
zIndex: isSelected ? 10 : 1,
|
|
164
|
+
}
|
|
165
|
+
]}
|
|
166
|
+
/>
|
|
167
|
+
)
|
|
168
|
+
})}
|
|
169
|
+
</View>
|
|
170
|
+
</View>
|
|
171
|
+
</View>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default withTheme(VerticalDailyPricesIndicator)
|