@ledvance/ui-biz-bundle 1.1.168 → 1.1.170

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";
@@ -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
- const dataPriceY = data?.map(item =>
27
- ((isNaN(Number(item.value)) ? 0 : Number(item.value)) * price).toFixed(2)
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(price ? maxValue * price * 1.4 : 0),
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, price, unit, maxValue, gridRight, theme])
221
+ }, [dataX, dataKwhY, dataPriceY, unit, maxValue, maxPriceValue, gridRight, theme])
164
222
 
165
223
  return (
166
224
  <View style={{ flex: 1 }}>
@@ -78,16 +78,25 @@ const PowerLineChart = (props: PowerLineChartProps) => {
78
78
  triggerOn: 'mousemove|click',
79
79
  trigger: 'axis',
80
80
  position: ['30%', '50%'],
81
- formatter: function (params) {
82
- if (!params || !params.length) return ''
83
- var item = params[0]
84
- var date = new Date(item.value[0])
85
- function pad(num) {
86
- return num < 10 ? '0' + num : num
81
+ axisPointer: {
82
+ label: {
83
+ backgroundColor: '#6a7985',
84
+ // 这个 formatter 只负责格式化时间
85
+ formatter: (params: { value: number }) => {
86
+ const date = new Date(params.value);
87
+
88
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
89
+ const day = date.getDate().toString().padStart(2, '0');
90
+ const year = date.getFullYear();
91
+
92
+ const hours = date.getHours().toString().padStart(2, '0');
93
+ const minutes = date.getMinutes().toString().padStart(2, '0');
94
+ const seconds = date.getSeconds().toString().padStart(2, '0');
95
+
96
+ // 返回你指定的 DD/MM/YYYY HH:mm:SS 格式
97
+ return day + '/' + month + '/' + year + ' ' + hours + ':' + minutes + ':' + seconds;
98
+ }
87
99
  }
88
- var time = pad(date.getMonth() + 1) + '/' + pad(date.getDate()) + '/' + date.getFullYear() + ' ' +
89
- pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds())
90
- return (time + '<br/>' + item.marker + item.seriesName + ': ' + item.value[1])
91
100
  }
92
101
  },
93
102
  grid: {
@@ -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)