@ledvance/ui-biz-bundle 1.1.146 → 1.1.148

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "name": "@ledvance/ui-biz-bundle",
5
5
  "pid": [],
6
6
  "uiid": "",
7
- "version": "1.1.146",
7
+ "version": "1.1.148",
8
8
  "scripts": {},
9
9
  "dependencies": {
10
10
  "@ledvance/base": "^1.x",
@@ -16,6 +16,7 @@
16
16
  "prop-types": "^15.6.1",
17
17
  "react": "16.8.3",
18
18
  "react-native": "0.59.10",
19
+ "react-native-orientation-locker": "^1.7.0",
19
20
  "react-native-svg": "5.5.1",
20
21
  "react-redux": "^7.2.1",
21
22
  "tuya-panel-kit": "^4.9.4"
@@ -1,10 +1,11 @@
1
- import { StyleSheet, View, Text, ViewStyle, Image } from 'react-native'
1
+ import { StyleSheet, View, Text, ViewStyle, Image, TouchableOpacity } from 'react-native'
2
2
  import React from 'react'
3
3
  import Card from '@ledvance/base/src/components/Card'
4
- import { SwitchButton, Utils } from 'tuya-panel-kit'
4
+ import { Utils } from 'tuya-panel-kit'
5
5
  import MoodColorsLine from '@ledvance/base/src/components/MoodColorsLine'
6
6
  import Spacer from '@ledvance/base/src/components/Spacer'
7
7
  import ThemeType from '@ledvance/base/src/config/themeType'
8
+ import res from '@ledvance/base/src/res'
8
9
 
9
10
  const cx = Utils.RatioUtils.convertX
10
11
  const { withTheme } = Utils.ThemeUtils
@@ -31,6 +32,7 @@ function FlagItem(props: RecommendMoodItemProps) {
31
32
  flexDirection: 'row',
32
33
  marginHorizontal: cx(16),
33
34
  justifyContent: 'space-between',
35
+ alignItems: 'center',
34
36
  },
35
37
  headText: {
36
38
  color: props.theme?.global.fontColor,
@@ -39,6 +41,13 @@ function FlagItem(props: RecommendMoodItemProps) {
39
41
  lineHeight: cx(20),
40
42
  flex: 1
41
43
  },
44
+ checkbox: {
45
+ width: cx(45),
46
+ height: cx(45),
47
+ marginTop: cx(-5),
48
+ marginBottom: cx(-10),
49
+ fontWeight: 'bold',
50
+ },
42
51
  gradientItem: {
43
52
  alignItems: 'center',
44
53
  },
@@ -55,10 +64,9 @@ function FlagItem(props: RecommendMoodItemProps) {
55
64
  {props.title ? <Text style={styles.headText}>{props.title}</Text> : undefined}
56
65
  {props.icon ? <Image source={icon} style={{ width: cx(60), aspectRatio: 2.14, marginRight: cx(10) }} /> : undefined}
57
66
  </View>
58
- <SwitchButton
59
- thumbStyle={{ elevation: 0 }}
60
- value={props.enable}
61
- onValueChange={props.onSwitch} />
67
+ <TouchableOpacity style={styles.checkbox} onPress={() => props.onSwitch(!props.enable)}>
68
+ <Image source={{ uri: res.ic_check}} width={cx(44)} height={cx(44)} style={[styles.checkbox, { tintColor: props.enable ? props.theme?.icon.primary : props.theme?.icon.disable}]} />
69
+ </TouchableOpacity>
62
70
  </View>
63
71
  <Spacer />
64
72
  <View style={styles.gradientItem}>
@@ -149,31 +149,109 @@ export interface PowerDataItem {
149
149
  value: number
150
150
  }
151
151
 
152
- export async function getPowerData(devId: string, powerDpCode: string, interval: number): Promise<PowerDataItem[]> {
153
- const now = dayjs()
154
- const startTime = now.add(-1 * interval, 'minute').valueOf().toString()
155
- const endTime = now.valueOf().toString()
156
- const dpResult = await getSpecifiedTimeDpReportLogs(
157
- devId,
158
- [powerDpCode],
159
- 'ASC',
160
- startTime,
161
- endTime,
162
- {
163
- maxRetries: 5,
164
- initialDelay: 1000,
165
- maxDelay: 30000,
166
- backoffFactor: 2
152
+ // 常量抽离(便于维护)
153
+ const DATA_POINT_INTERVAL_SEC = 30; // 预设数据间隔(秒)
154
+ const TIME_UNIT = 'minute' as const; // 时间单位(与 interval 配合)
155
+ const RETRY_CONFIG = {
156
+ maxRetries: 5,
157
+ initialDelay: 1000,
158
+ maxDelay: 30000,
159
+ backoffFactor: 2,
160
+ } as const;
161
+
162
+ /**
163
+ * 创建一个0值填充的数据点
164
+ * @param timePoint Dayjs对象
165
+ * @returns PowerDataItem
166
+ */
167
+ function createZeroPaddingPoint(timePoint: dayjs.Dayjs): PowerDataItem {
168
+ const timeMs = timePoint.valueOf();
169
+ return {
170
+ key: timePoint.format('HH:mm:ss'),
171
+ chartTitle: timePoint.format('MM/DD/YYYY HH:mm:ss'),
172
+ time: timeMs,
173
+ value: 0,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * 获取设备功率数据(智能填充,高性能,避免在小间隔内插入0值)
179
+ * @param devId 设备ID
180
+ * @param powerDpCode 功率数据点编码
181
+ * @param interval 时间区间(分钟)
182
+ * @returns 按时间排序的完整数据
183
+ */
184
+ export async function getPowerData(
185
+ devId: string,
186
+ powerDpCode: string,
187
+ interval: number
188
+ ): Promise<PowerDataItem[]> {
189
+ try {
190
+ const now = dayjs();
191
+ const endTime = now;
192
+ const startTime = now.add(-interval, TIME_UNIT);
193
+ const endTimeMs = endTime.valueOf();
194
+ const startTimeMs = startTime.valueOf();
195
+ const paddingIntervalMs = DATA_POINT_INTERVAL_SEC * 1000;
196
+ // 1. 请求已排序的实际数据
197
+ const dpResult = await getSpecifiedTimeDpReportLogs(
198
+ devId,
199
+ [powerDpCode],
200
+ 'ASC',
201
+ startTimeMs.toString(),
202
+ endTimeMs.toString(),
203
+ RETRY_CONFIG
204
+ );
205
+ const validDpResult = Array.isArray(dpResult) ? (dpResult as DpReportSataData[]) : [];
206
+ const actualData: PowerDataItem[] = validDpResult
207
+ .map((dp) => {
208
+ const timeMs = dp.timeStamp * 1000;
209
+ if (timeMs < startTimeMs || timeMs > endTimeMs) return null;
210
+ return {
211
+ key: dayjs.unix(dp.timeStamp).format('HH:mm:ss'),
212
+ chartTitle: dayjs.unix(dp.timeStamp).format('MM/DD/YYYY HH:mm:ss'),
213
+ time: timeMs,
214
+ value: parseFloat(dp.value) / 10,
215
+ };
216
+ })
217
+ .filter((item): item is PowerDataItem => item !== null);
218
+ // 如果没有任何真实数据,则生成完整的预设0值数据作为兜底
219
+ if (actualData.length === 0) {
220
+ const finalData: PowerDataItem[] = [];
221
+ let currentTime = startTime;
222
+ while (currentTime.valueOf() < endTimeMs) {
223
+ finalData.push(createZeroPaddingPoint(currentTime));
224
+ currentTime = currentTime.add(DATA_POINT_INTERVAL_SEC, 'second');
225
+ }
226
+ return finalData;
167
227
  }
168
- ) as DpReportSataData[]
169
- return dpResult.map(dp => {
170
- return {
171
- key: dp.timeStr,
172
- chartTitle: dayjs.unix(dp.timeStamp).format('MM/DD/YYYY HH:mm:ss'),
173
- time: dp.timeStamp * 1000,
174
- value: parseFloat(dp.value) / 10
228
+ // 2. 高性能线性填充
229
+ const finalData: PowerDataItem[] = [];
230
+ let lastTimeMs = startTimeMs; // 游标从查询区间的开始时间算起
231
+ // 遍历所有真实数据点
232
+ actualData.forEach((currentPoint) => {
233
+ // 从上一个点的时间开始,用 while 循环填充,直到下一个真实数据点之前
234
+ let paddingTimeMs = lastTimeMs + paddingIntervalMs;
235
+ while (paddingTimeMs < currentPoint.time) {
236
+ finalData.push(createZeroPaddingPoint(dayjs(paddingTimeMs)));
237
+ paddingTimeMs += paddingIntervalMs;
238
+ }
239
+
240
+ // 添加当前的真实数据点
241
+ finalData.push(currentPoint);
242
+ lastTimeMs = currentPoint.time; // 更新游标
243
+ });
244
+ // 3. 填充查询末尾的空白区域(从最后一个真实数据点到查询结束时间)
245
+ let paddingTimeMs = lastTimeMs + paddingIntervalMs;
246
+ while (paddingTimeMs < endTimeMs) {
247
+ finalData.push(createZeroPaddingPoint(dayjs(paddingTimeMs)));
248
+ paddingTimeMs += paddingIntervalMs;
175
249
  }
176
- })
250
+ return finalData;
251
+ } catch (error) {
252
+ console.error(`[getPowerData] 失败:devId=${devId}, powerDpCode=${powerDpCode}`, error);
253
+ return [];
254
+ }
177
255
  }
178
256
 
179
257
  export const exportEnergyCsv = (values: any[][], unit: string) => {
@@ -0,0 +1,21 @@
1
+ import InfoText from '@ledvance/base/src/components/InfoText'
2
+ import Spacer from '@ledvance/base/src/components/Spacer'
3
+ import res from '@ledvance/base/src/res'
4
+ import React from 'react'
5
+ import { Image, View } from 'react-native'
6
+ import { Utils } from 'tuya-panel-kit'
7
+
8
+ const { convertX: cx } = Utils.RatioUtils
9
+
10
+ export const EmptyDataView = ({ text, theme, styles, height }) => (
11
+ <View style={[styles.listEmptyView, { height }]}>
12
+ <Image style={styles.listEmptyImage} source={{ uri: res.ldv_timer_empty }}/>
13
+ <Spacer height={cx(5)}/>
14
+ <InfoText
15
+ text={text}
16
+ icon={res.ic_info}
17
+ textStyle={styles.listEmptyText}
18
+ contentColor={theme?.global.fontColor}
19
+ />
20
+ </View>
21
+ )
@@ -0,0 +1,75 @@
1
+ import I18n from '@ledvance/base/src/i18n'
2
+ import res from '@ledvance/base/src/res'
3
+ import React, { useCallback } from 'react'
4
+ import { Image, TouchableOpacity, View } from 'react-native'
5
+ import { Utils } from 'tuya-panel-kit'
6
+ import DateSelectedItem from '../component/DateSelectedItem'
7
+ import DateSwitch from '../component/DateSwitch'
8
+ import { DateType } from '../co2Data'
9
+ import DateTypeItem from '../component/DateTypeItem'
10
+ import NewBarChart from '../component/NewBarChart'
11
+ import { EmptyDataView } from './EmptyDataView'
12
+ import { exportEnergyCsv } from '../EnergyConsumptionActions'
13
+
14
+ const { convertX: cx } = Utils.RatioUtils
15
+
16
+ export const EnergyChartSection = ({ isLandscape, state, actions, params, styles, theme, chartHeight }) => {
17
+ const getEmptyDataTip = useCallback(() => {
18
+ if (state.over365Days && state.dateType !== DateType.Day) {
19
+ return I18n.getLang('energyconsumption_Daylimit')
20
+ }
21
+ if (state.dateType === DateType.Day && state.over7Days) {
22
+ return I18n.getLang('energyconsumption_hourlylimit')
23
+ }
24
+ return I18n.getLang('energyconsumption_emptydata')
25
+ }, [state.dateType, state.over365Days, state.over7Days])
26
+
27
+ const isDataEmpty = state.chartData.length <= 0
28
+
29
+ const handleExportCsv = useCallback(() => {
30
+ const values = state.chartData.map(item => [item.key, item.value, (Number(params.price) * Number(item.value)).toFixed(2)])
31
+ exportEnergyCsv(values, params.unit)
32
+ }, [state.chartData, params.price, params.unit])
33
+
34
+ return (
35
+ <>
36
+ {!isLandscape && (
37
+ <View style={styles.dateSwitchContainer}>
38
+ <DateSwitch
39
+ style={{ flex: 1 }}
40
+ date={state.date}
41
+ dateType={state.dateType}
42
+ headlineText={state.headlineText}
43
+ onDateChange={actions.setDate}
44
+ />
45
+ <TouchableOpacity style={{ width: cx(30) }} onPress={handleExportCsv}>
46
+ <Image
47
+ style={styles.downloadIcon}
48
+ source={{ uri: !isDataEmpty ? res.download_icon : undefined }}
49
+ />
50
+ </TouchableOpacity>
51
+ </View>
52
+ )}
53
+ <View style={styles.dateTypeContainer}>
54
+ <DateTypeItem
55
+ style={{ flex: 1, marginHorizontal: cx(5) }}
56
+ dateType={state.dateType}
57
+ onDateTypeChange={actions.setDateType}
58
+ />
59
+ <DateSelectedItem
60
+ style={{ flex: 1 }}
61
+ dateType={state.dateType}
62
+ date={state.date}
63
+ onDateChange={actions.setDate}
64
+ />
65
+ </View>
66
+
67
+ {(state.loading || isDataEmpty) ? (
68
+ <EmptyDataView text={getEmptyDataTip()} theme={theme} styles={styles}/>
69
+ ) : (
70
+ !state.loading &&
71
+ <NewBarChart height={chartHeight} data={state.chartData} price={state.price} unit={params.unit}/>
72
+ )}
73
+ </>
74
+ )
75
+ }
@@ -0,0 +1,46 @@
1
+ import I18n from '@ledvance/base/src/i18n'
2
+ import res from '@ledvance/base/src/res'
3
+ import React from 'react'
4
+ import { Image, Text, TouchableOpacity, View } from 'react-native'
5
+ import { Utils } from 'tuya-panel-kit'
6
+ import { ChartType } from '../co2Data'
7
+ import { EnergyChartSection } from './EnergyChartSection'
8
+ import { PowerChartSection } from './PowerChartSection'
9
+
10
+ const { convertX: cx, width } = Utils.RatioUtils
11
+
12
+ export const LandscapeView = ({ state, actions, params, styles, theme }) => (
13
+ <View style={styles.landscapeContainer}>
14
+ <View style={styles.landscapeHeader}>
15
+ <Text style={styles.landscapeTitle}>
16
+ {I18n.getLang(state.chartType === ChartType.kWh ? 'chartdisplay_energy' : 'chartdisplay_power')}
17
+ </Text>
18
+ <TouchableOpacity onPress={actions.toggleLandscape}>
19
+ <Image source={{ uri: res.screen_normal }} style={styles.normalScreenIcon}/>
20
+ </TouchableOpacity>
21
+ </View>
22
+
23
+ <View style={styles.landscapeContent}>
24
+ {state.chartType === ChartType.kWh ? (
25
+ <EnergyChartSection
26
+ isLandscape={true}
27
+ state={state}
28
+ actions={actions}
29
+ params={params}
30
+ styles={styles}
31
+ theme={theme}
32
+ chartHeight={cx(width - 110)}
33
+ />
34
+ ) : (
35
+ <PowerChartSection
36
+ isLandscape={true}
37
+ state={state}
38
+ actions={actions}
39
+ styles={styles}
40
+ theme={theme}
41
+ chartHeight={cx(width - 110)}
42
+ />
43
+ )}
44
+ </View>
45
+ </View>
46
+ )
@@ -0,0 +1,43 @@
1
+ import Page from '@ledvance/base/src/components/Page'
2
+ import Segmented from '@ledvance/base/src/components/Segmented'
3
+ import Spacer from '@ledvance/base/src/components/Spacer'
4
+ import I18n from '@ledvance/base/src/i18n'
5
+ import res from '@ledvance/base/src/res'
6
+ import React from 'react'
7
+ import { ScrollView } from 'react-native'
8
+ import { Utils } from 'tuya-panel-kit'
9
+ import { ChartType } from '../co2Data'
10
+ import { EnergyChartSection } from './EnergyChartSection'
11
+ import { PowerChartSection } from './PowerChartSection'
12
+
13
+ const { convertX: cx } = Utils.RatioUtils
14
+
15
+ export const PortraitView = ({ state, actions, params, styles, theme }) => (
16
+ <Page
17
+ backText={params.backTitle}
18
+ showGreenery={false}
19
+ loading={state.loading}
20
+ greeneryIcon={res.energy_consumption_greenery}
21
+ rightButtonIcon={state.isSupportLandscape ? res.screen_full : undefined}
22
+ rightButtonStyle={styles.fullScreenIcon}
23
+ rightButtonIconClick={actions.toggleLandscape}
24
+ >
25
+ <ScrollView nestedScrollEnabled={true} style={styles.scrollViewContent}>
26
+ <Spacer/>
27
+ <Segmented
28
+ options={[
29
+ { label: I18n.getLang('chartdisplay_energy'), value: ChartType.kWh },
30
+ { label: I18n.getLang('chartdisplay_power'), value: ChartType.Watt },
31
+ ]}
32
+ value={state.chartType}
33
+ onChange={actions.setChartType}
34
+ />
35
+ {state.chartType === ChartType.kWh ? (
36
+ <EnergyChartSection state={state} actions={actions} params={params} styles={styles} theme={theme}
37
+ chartHeight={cx(400)}/>
38
+ ) : (
39
+ <PowerChartSection state={state} actions={actions} styles={styles} theme={theme} chartHeight={cx(400)}/>
40
+ )}
41
+ </ScrollView>
42
+ </Page>
43
+ )
@@ -0,0 +1,42 @@
1
+ import I18n from '@ledvance/base/src/i18n'
2
+ import React from 'react'
3
+ import { Text, TouchableOpacity, View } from 'react-native'
4
+ import PowerLineChart from '../component/PowerLineChart'
5
+ import { EmptyDataView } from './EmptyDataView'
6
+
7
+ const INTERVAL_OPTIONS = [
8
+ { label: 'charttime_type1', value: 24 * 60 },
9
+ { label: 'charttime_type2', value: 6 * 60 },
10
+ { label: 'charttime_type3', value: 60 },
11
+ { label: 'charttime_type4', value: 5 },
12
+ ]
13
+
14
+ export const PowerChartSection = ({ isLandscape, state, actions, styles, theme, chartHeight }) => {
15
+ const isDataEmpty = state.powerData.length <= 0
16
+
17
+ return (
18
+ <>
19
+ {isDataEmpty ? (
20
+ <EmptyDataView text={I18n.getLang('power_chart_empty')} theme={theme} styles={styles} height={chartHeight}/>
21
+ ) : (
22
+ <PowerLineChart height={chartHeight} data={state.powerData}/>
23
+ )}
24
+ <View style={[styles.intervalContainer, isLandscape && styles.landscapeIntervalContainer]}>
25
+ {INTERVAL_OPTIONS.map(({ label, value }) => {
26
+ const isChecked = value === state.interval
27
+ return (
28
+ <TouchableOpacity
29
+ key={value}
30
+ style={[styles.intervalItem, isChecked && styles.intervalItemChecked]}
31
+ onPress={() => actions.setInterval(value)}
32
+ >
33
+ <Text style={isChecked ? styles.intervalTextChecked : styles.intervalText}>
34
+ {I18n.getLang(label)}
35
+ </Text>
36
+ </TouchableOpacity>
37
+ )
38
+ })}
39
+ </View>
40
+ </>
41
+ )
42
+ }
@@ -0,0 +1,33 @@
1
+ import ThemeType from '@ledvance/base/src/config/themeType'
2
+ import { useDeviceId } from '@ledvance/base/src/models/modules/NativePropsSlice'
3
+ import { useRoute } from '@react-navigation/core'
4
+ import React, { useMemo } from 'react'
5
+ import { Utils } from 'tuya-panel-kit'
6
+ import { EnergyConsumptionChartProps } from '../EnergyConsumptionChart'
7
+ import { LandscapeView } from './LandscapeView'
8
+ import { PortraitView } from './PortraitView'
9
+ import { getStyles } from './styles'
10
+
11
+ import { useEnergyData } from './useEnergyData'
12
+
13
+ const { withTheme } = Utils.ThemeUtils
14
+
15
+ const EnergyConsumptionChartComponent = (props: { theme?: ThemeType }) => {
16
+ const devId = useDeviceId()
17
+ const params = useRoute().params as EnergyConsumptionChartProps
18
+
19
+ // Use the custom Hook to get all state and logic
20
+ const { state, actions } = useEnergyData(params, devId)
21
+
22
+ // Use useMemo to prevent re-creating styles on every render
23
+ const styles = useMemo(() => getStyles(props.theme), [props.theme])
24
+
25
+ // Conditionally render the correct view based on orientation state
26
+ if (state.isLandscape) {
27
+ return <LandscapeView state={state} actions={actions} params={params} styles={styles} theme={props.theme}/>
28
+ }
29
+
30
+ return <PortraitView state={state} actions={actions} params={params} styles={styles} theme={props.theme}/>
31
+ }
32
+
33
+ export default withTheme(EnergyConsumptionChartComponent)
@@ -0,0 +1,95 @@
1
+ import ThemeType from '@ledvance/base/src/config/themeType'
2
+ import { StyleSheet } from 'react-native'
3
+ import { Utils } from 'tuya-panel-kit'
4
+
5
+ const { convertX: cx, width, height } = Utils.RatioUtils
6
+
7
+ export const getStyles = (theme: ThemeType | undefined) => StyleSheet.create({
8
+ listEmptyView: {
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ },
12
+ listEmptyImage: {
13
+ width: cx(180),
14
+ height: cx(180),
15
+ },
16
+ listEmptyText: {
17
+ flex: 0,
18
+ },
19
+ downloadIcon: {
20
+ width: cx(24),
21
+ height: cx(24),
22
+ tintColor: theme?.global.brand,
23
+ position: 'absolute',
24
+ right: 0,
25
+ top: cx(10),
26
+ },
27
+ intervalContainer: {
28
+ flexDirection: 'row',
29
+ justifyContent: 'center',
30
+ alignItems: 'center',
31
+ marginTop: cx(-10),
32
+ },
33
+ intervalItem: {
34
+ flex: 1,
35
+ marginHorizontal: cx(5),
36
+ padding: cx(5),
37
+ borderWidth: cx(1),
38
+ borderRadius: cx(25),
39
+ borderColor: theme?.icon.normal,
40
+ },
41
+ intervalItemChecked: {
42
+ borderColor: theme?.icon.primary,
43
+ },
44
+ intervalText: {
45
+ textAlign: 'center',
46
+ color: theme?.global.fontColor,
47
+ },
48
+ intervalTextChecked: {
49
+ textAlign: 'center',
50
+ color: theme?.icon.primary,
51
+ },
52
+ landscapeContainer: {
53
+ backgroundColor: theme?.global.background,
54
+ width: cx(height + 20), // In landscape, width is screen height
55
+ // height: width, // In landscape, height is screen width
56
+ paddingVertical: cx(30),
57
+ paddingHorizontal: cx(20),
58
+ },
59
+ landscapeHeader: {
60
+ flexDirection: 'row',
61
+ justifyContent: 'space-between',
62
+ alignItems: 'center',
63
+ },
64
+ landscapeTitle: {
65
+ color: theme?.global.fontColor,
66
+ fontSize: cx(16),
67
+ fontWeight: 'bold',
68
+ },
69
+ landscapeContent: {
70
+ height: cx(width - 120),
71
+ },
72
+ landscapeIntervalContainer: {
73
+ marginTop: cx(-10),
74
+ },
75
+ dateSwitchContainer: {
76
+ width: '100%',
77
+ flexDirection: 'row',
78
+ marginVertical: cx(15),
79
+ },
80
+ dateTypeContainer: {
81
+ flexDirection: 'row',
82
+ marginBottom: cx(15),
83
+ },
84
+ scrollViewContent: {
85
+ marginHorizontal: cx(24),
86
+ },
87
+ fullScreenIcon: {
88
+ tintColor: theme?.global.brand,
89
+ },
90
+ normalScreenIcon: {
91
+ width: cx(50),
92
+ height: cx(50),
93
+ tintColor: theme?.icon.normal
94
+ }
95
+ })