@ledvance/group-ui-biz-bundle 1.0.160 → 1.0.161

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.
@@ -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";
@@ -9,6 +9,7 @@ import { Utils, Modal, Popup } from "tuya-panel-kit";
9
9
  import { useReactive, useUpdateEffect } from "ahooks";
10
10
  import { cloneDeep } from "lodash";
11
11
  import ThemeType from '@ledvance/base/src/config/themeType'
12
+ import EditPriceSegmentScreen from "../EditPriceSegmentScreen";
12
13
 
13
14
  const { convertX: cx, height, statusBarHeight } = Utils.RatioUtils
14
15
  const { withTheme } = Utils.ThemeUtils
@@ -29,28 +30,77 @@ export const UnitList = [
29
30
  I18n.getLang('consumption_data_price_per_kwh_currency_value13'),
30
31
  ]
31
32
 
33
+ export interface PriceSegment {
34
+ startMinute: number; // 0~1439
35
+ endMinute: number; // 0~1439
36
+ price: number;
37
+ }
38
+ export function calculateAveragePrice(
39
+ intervals: PriceSegment[],
40
+ ): number {
41
+ let totalPrice = 0
42
+ for(const item of intervals) {
43
+ const hrs = (item.endMinute - item.startMinute)/60
44
+ totalPrice += (hrs * item.price)
45
+ }
46
+ return totalPrice/24
47
+
48
+ }
49
+ export const parseMinutes = (totalMinutes: number | undefined) => {
50
+ if (!totalMinutes) {
51
+ return { hour: '00', minute: '00' };
52
+ }
53
+ const h = Math.floor(totalMinutes / 60);
54
+ const m = totalMinutes % 60;
55
+ return {
56
+ hour: h.toString().padStart(2, '0'),
57
+ minute: m.toString().padStart(2, '0')
58
+ }
59
+ }
60
+
61
+ export type EnergyType = 'fixed' | 'dynamic'
62
+ //固定价格能量模型
32
63
  export interface EnergyData {
33
64
  price: string
34
65
  unit: string
66
+ energyType?: EnergyType
67
+ generatePrice?: string
68
+ }
69
+ //动态价格能量模型
70
+ export interface DynamicEnergyData{
71
+ intervalPrices?: PriceSegment[]
72
+ generatePrices?: PriceSegment[]
35
73
  }
74
+
36
75
  interface EnergyModalProps {
37
- theme?: ThemeType
38
- visible: boolean
39
- popupType: 'money' | 'co2' | 'unit'
40
- title: string
41
- confirmText?: string
42
- cancelText?: string
43
- energyData?: EnergyData
44
- motionType?: 'none'
45
- onConfirm?: (data?: EnergyData) => void
46
- onCancel?: () => void
76
+ theme?: ThemeType
77
+ visible: boolean
78
+ popupType: 'money' | 'co2' | 'unit' | 'segment_price'
79
+ title: string
80
+ confirmText?: string
81
+ cancelText?: string
82
+ energyData?: EnergyData
83
+ motionType?: 'none'
84
+ editingPriceSegment?: PriceSegment
85
+ onConfirm?: (data?: EnergyData) => void
86
+ onPriceSegmentConfirm?: (segment?: PriceSegment) => void
87
+ onCancel?: () => void
47
88
  }
48
89
  const EnergyModal = (props: EnergyModalProps) => {
49
90
  const state = useReactive({
50
91
  energyData: cloneDeep(props.energyData),
51
- unitPopup: false
92
+ unitPopup: false,
93
+ editingPriceSegment: cloneDeep(props.editingPriceSegment)
52
94
  })
53
- console.log('running ~~~~~')
95
+ useEffect(()=>{
96
+ if(props.visible) {
97
+ if(!props.editingPriceSegment){
98
+ state.editingPriceSegment = { startMinute: 0, endMinute: 0, price: 0 }
99
+ } else {
100
+ state.editingPriceSegment = props.editingPriceSegment
101
+ }
102
+ }
103
+ }, [props.visible,props.editingPriceSegment])
54
104
 
55
105
  const openLink = (url: string) => {
56
106
  Linking.openURL(url).catch((error) => console.error('无法打开链接:', error));
@@ -217,6 +267,14 @@ const EnergyModal = (props: EnergyModalProps) => {
217
267
  </Card>
218
268
  </View>
219
269
  )
270
+ } else if(props.popupType === 'segment_price'){
271
+ return (
272
+ <EditPriceSegmentScreen
273
+ segment={state.editingPriceSegment}
274
+ onSegmentSet={(segment)=>{
275
+ state.editingPriceSegment = segment
276
+ }}/>
277
+ )
220
278
  } else {
221
279
  return (
222
280
  <View>
@@ -306,6 +364,8 @@ const EnergyModal = (props: EnergyModalProps) => {
306
364
  <Text style={{ color: props.theme?.global.fontColor, fontSize: cx(16), fontWeight: 'bold' }}>{props.title}</Text>
307
365
  <TouchableOpacity onPress={() => {
308
366
  props.onConfirm && props.onConfirm(state.energyData)
367
+ console.log("wayne_check innner check segment ->", JSON.stringify(state.editingPriceSegment))
368
+ props.onPriceSegmentConfirm && props.onPriceSegmentConfirm(state.editingPriceSegment)
309
369
  }}>
310
370
  <Text style={{ color: props.theme?.button.primary, fontSize: cx(16) }}>{props.confirmText}</Text>
311
371
  </TouchableOpacity>
@@ -7,6 +7,8 @@ import { View } from 'react-native'
7
7
  import { Utils } from 'tuya-panel-kit'
8
8
  import { OverviewItem } from '../EnergyConsumptionPage'
9
9
  import { EnergyDisplayMode } from '../EnergyConsumptionChart/useEnergyData'
10
+ import { calculateAveragePrice, PriceSegment, DynamicEnergyData } from './EnergyModal'
11
+ import { DateType } from '../co2Data'
10
12
 
11
13
  const { withTheme } = Utils.ThemeUtils
12
14
  interface BarChartProps {
@@ -19,6 +21,35 @@ interface BarChartProps {
19
21
  unit: string;
20
22
  height: number;
21
23
  displayMode?: EnergyDisplayMode;
24
+ energyType?: string;
25
+ dynamicData?: DynamicEnergyData;
26
+ dateType?: DateType;
27
+ }
28
+
29
+ //算出固定时间间隔内的平均价格,目前用于计算柱状图每一个小时的价格
30
+ export function calculateIntervalAveragePrice(
31
+ intervals: PriceSegment[],
32
+ startMinute: number,
33
+ endMinute: number
34
+ ): number {
35
+ let totalWeightedPrice = 0;
36
+ let totalTimeOverlap = 0;
37
+
38
+ for (const item of intervals) {
39
+ const overlapStart = Math.max(item.startMinute, startMinute);
40
+ const overlapEnd = Math.min(item.endMinute, endMinute);
41
+
42
+ if (overlapStart < overlapEnd) {
43
+ const duration = overlapEnd - overlapStart;
44
+ totalWeightedPrice += duration * item.price;
45
+ totalTimeOverlap += duration;
46
+ }
47
+ }
48
+
49
+ if (totalTimeOverlap > 0) {
50
+ return totalWeightedPrice / totalTimeOverlap;
51
+ }
52
+ return 0;
22
53
  }
23
54
 
24
55
  const BarChartWithTouch = (props: BarChartProps) => {
@@ -33,6 +64,9 @@ const BarChartWithTouch = (props: BarChartProps) => {
33
64
  height,
34
65
  theme,
35
66
  displayMode = 'consumption',
67
+ energyType,
68
+ dynamicData,
69
+ dateType,
36
70
  } = props
37
71
 
38
72
  const chartKey = useMemo(() => {
@@ -80,11 +114,45 @@ const BarChartWithTouch = (props: BarChartProps) => {
80
114
  const consumedVal = dataPoint.consumed || 0
81
115
  const generatedVal = dataPoint.generated || 0
82
116
 
117
+ let consumedPrice = price
118
+ let generatedPrice = price
119
+
120
+ if (energyType === 'dynamic' && dynamicData) {
121
+ // Consumed price
122
+ const costIntervals = dynamicData.intervalPrices || []
123
+ if (costIntervals.length > 0) {
124
+ if (dateType === DateType.Day) {
125
+ const hour = parseInt(key.split(':')[0]);
126
+ if (!isNaN(hour)) {
127
+ consumedPrice = calculateIntervalAveragePrice(costIntervals, hour * 60, (hour + 1) * 60);
128
+ } else {
129
+ consumedPrice = calculateAveragePrice(costIntervals);
130
+ }
131
+ } else {
132
+ consumedPrice = calculateAveragePrice(costIntervals);
133
+ }
134
+ }
135
+ // Generated price
136
+ const generateIntervals = dynamicData.generatePrices || []
137
+ if (generateIntervals.length > 0) {
138
+ if (dateType === DateType.Day) {
139
+ const hour = parseInt(key.split(':')[0]);
140
+ if (!isNaN(hour)) {
141
+ generatedPrice = calculateIntervalAveragePrice(generateIntervals, hour * 60, (hour + 1) * 60);
142
+ } else {
143
+ generatedPrice = calculateAveragePrice(generateIntervals);
144
+ }
145
+ } else {
146
+ generatedPrice = calculateAveragePrice(generateIntervals);
147
+ }
148
+ }
149
+ }
150
+
83
151
  finalDataX.push(dataPoint.title)
84
152
  finalAlignedConsumed.push(consumedVal)
85
153
  finalAlignedGenerated.push(generatedVal)
86
- finalAlignedConsumedPrice.push((consumedVal * price).toFixed(2))
87
- finalAlignedGeneratedPrice.push((generatedVal * price).toFixed(2))
154
+ finalAlignedConsumedPrice.push((consumedVal * consumedPrice).toFixed(2))
155
+ finalAlignedGeneratedPrice.push((generatedVal * generatedPrice).toFixed(2))
88
156
  })
89
157
 
90
158
  return {
@@ -94,7 +162,7 @@ const BarChartWithTouch = (props: BarChartProps) => {
94
162
  alignedConsumedPrice: finalAlignedConsumedPrice,
95
163
  alignedGeneratedPrice: finalAlignedGeneratedPrice,
96
164
  }
97
- }, [consumedData, generatedData, price])
165
+ }, [consumedData, generatedData, price, energyType, dynamicData, dateType])
98
166
 
99
167
  // 图例 & series 动态生成
100
168
  const { legendData, seriesData } = useMemo(() => {
@@ -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)
@@ -122,50 +122,52 @@ const FixedTimeForPlugPage = (props: { theme?: ThemeType}) => {
122
122
 
123
123
  const renderList = () => {
124
124
  return (
125
- <ScrollView nestedScrollEnabled={true}>
125
+ <>
126
126
  <Text style={styles.execTip}>{I18n.getLang('timeschedule_overview_description_text')}</Text>
127
127
  <Spacer height={cx(10)}/>
128
128
  {isMaxNum && <View style={{marginHorizontal: cx(24), flexDirection: 'row'}}>
129
129
  <Image style={styles.maxImg} source={{uri: res.ic_warning_amber}}/>
130
130
  <Text style={styles.maxTip}>{I18n.getLang('randomtimecycle_warning_max_number_text')}</Text>
131
131
  </View>}
132
- <FlatList
133
- data={fixedTimeList}
134
- renderItem={({item}) => (
135
- <ItemCard
136
- item={item}
137
- is24Hour={params.is24Hour}
138
- onSwitch={async (v) => {
139
- await onPost('set', {
140
- ...item,
141
- enable: v,
142
- settingTime: new Date().getTime()
143
- })
144
- }}
145
- onPress={() => {
146
- onAddOrEditItem('edit', item)
147
- }}
148
- onLongPress={async () =>{
149
- showDialog({
150
- method: 'confirm',
151
- title: I18n.getLang('cancel_dialog_delete_item_fixedtimecycle_titel'),
152
- subTitle: I18n.getLang('cancel_dialog_delete_item_fixedtimecycle_description'),
153
- onConfirm: async (_, {close}) => {
154
- close()
155
- state.loading = true
156
- await onPost('del', item)
157
- state.loading = false
158
- }
159
- })
160
- }}
161
- />
162
- )}
163
- keyExtractor={(item: any) => `${item?.index}`}
164
- ItemSeparatorComponent={() => <Spacer/>}
165
- ListHeaderComponent={<Spacer height={cx(10)}/>}
166
- ListFooterComponent={<Spacer/>}
167
- />
168
- </ScrollView>
132
+ <ScrollView nestedScrollEnabled={true}>
133
+ <FlatList
134
+ data={fixedTimeList}
135
+ renderItem={({item}) => (
136
+ <ItemCard
137
+ item={item}
138
+ is24Hour={params.is24Hour}
139
+ onSwitch={async (v) => {
140
+ await onPost('set', {
141
+ ...item,
142
+ enable: v,
143
+ settingTime: new Date().getTime()
144
+ })
145
+ }}
146
+ onPress={() => {
147
+ onAddOrEditItem('edit', item)
148
+ }}
149
+ onLongPress={async () => {
150
+ showDialog({
151
+ method: 'confirm',
152
+ title: I18n.getLang('cancel_dialog_delete_item_fixedtimecycle_titel'),
153
+ subTitle: I18n.getLang('cancel_dialog_delete_item_fixedtimecycle_description'),
154
+ onConfirm: async (_, {close}) => {
155
+ close()
156
+ state.loading = true
157
+ await onPost('del', item)
158
+ state.loading = false
159
+ }
160
+ })
161
+ }}
162
+ />
163
+ )}
164
+ keyExtractor={(item: any) => `${item?.index}`}
165
+ ItemSeparatorComponent={() => <Spacer/>}
166
+ ListHeaderComponent={<Spacer height={cx(10)}/>}
167
+ ListFooterComponent={<Spacer/>}
168
+ />
169
+ </ScrollView>
170
+ </>
169
171
  )
170
172
  }
171
173
  const renderEmpty = () => {
@@ -126,50 +126,52 @@ const FixedTimeForLightPage = (props: { theme?: ThemeType}) => {
126
126
 
127
127
  const renderList = () => {
128
128
  return (
129
- <ScrollView nestedScrollEnabled={true}>
129
+ <>
130
130
  <Text style={styles.execTip}>{I18n.getLang('timeschedule_overview_description_text')}</Text>
131
131
  <Spacer height={cx(10)}/>
132
132
  {isMaxNum && <View style={{marginHorizontal: cx(24), flexDirection: 'row'}}>
133
133
  <Image style={styles.maxImg} source={{uri: res.ic_warning_amber}}/>
134
134
  <Text style={styles.maxTip}>{I18n.getLang('fixedtimecycle_warning_max_number_text')}</Text>
135
135
  </View>}
136
- <FlatList
137
- data={fixedTimingList}
138
- renderItem={({item}) => (
139
- <ItemCard
140
- item={item}
141
- is24Hour={params.is24Hour}
142
- onSwitch={async (v) => {
143
- await onPost('set', {
144
- ...item,
145
- power: v,
146
- settingTime: new Date().getTime()
147
- })
148
- }}
149
- onPress={() => {
150
- onAddOrEditItem('edit', item)
151
- }}
152
- onLongPress={async () =>{
153
- showDialog({
154
- method: 'confirm',
155
- title: I18n.getLang('cancel_dialog_delete_item_fixedtimecycle_titel'),
156
- subTitle: I18n.getLang('cancel_dialog_delete_item_fixedtimecycle_description'),
157
- onConfirm: async (_, {close}) => {
158
- close()
159
- state.loading = true
160
- await onPost('del', item)
161
- state.loading = false
162
- }
163
- })
164
- }}
165
- />
166
- )}
167
- keyExtractor={(item: any) => `${item?.index}`}
168
- ItemSeparatorComponent={() => <Spacer/>}
169
- ListHeaderComponent={<Spacer height={cx(10)}/>}
170
- ListFooterComponent={<Spacer/>}
171
- />
172
- </ScrollView>
136
+ <ScrollView nestedScrollEnabled={true}>
137
+ <FlatList
138
+ data={fixedTimingList}
139
+ renderItem={({item}) => (
140
+ <ItemCard
141
+ item={item}
142
+ is24Hour={params.is24Hour}
143
+ onSwitch={async (v) => {
144
+ await onPost('set', {
145
+ ...item,
146
+ power: v,
147
+ settingTime: new Date().getTime()
148
+ })
149
+ }}
150
+ onPress={() => {
151
+ onAddOrEditItem('edit', item)
152
+ }}
153
+ onLongPress={async () => {
154
+ showDialog({
155
+ method: 'confirm',
156
+ title: I18n.getLang('cancel_dialog_delete_item_fixedtimecycle_titel'),
157
+ subTitle: I18n.getLang('cancel_dialog_delete_item_fixedtimecycle_description'),
158
+ onConfirm: async (_, {close}) => {
159
+ close()
160
+ state.loading = true
161
+ await onPost('del', item)
162
+ state.loading = false
163
+ }
164
+ })
165
+ }}
166
+ />
167
+ )}
168
+ keyExtractor={(item: any) => `${item?.index}`}
169
+ ItemSeparatorComponent={() => <Spacer/>}
170
+ ListHeaderComponent={<Spacer height={cx(10)}/>}
171
+ ListFooterComponent={<Spacer/>}
172
+ />
173
+ </ScrollView>
174
+ </>
173
175
  )
174
176
  }
175
177
  const renderEmpty = () => {