@ledvance/group-ui-biz-bundle 1.0.140 → 1.0.142
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/modules/biorhythm/IconSelect.tsx +92 -29
- package/src/modules/energyConsumption/EnergyConsumptionActions.ts +42 -16
- package/src/modules/energyConsumption/EnergyConsumptionCard.tsx +3 -3
- package/src/modules/energyConsumption/EnergyConsumptionChart/ChartSection.tsx +103 -15
- package/src/modules/energyConsumption/EnergyConsumptionChart/LandscapeView.tsx +1 -0
- package/src/modules/energyConsumption/EnergyConsumptionChart/PortraitView.tsx +3 -27
- package/src/modules/energyConsumption/EnergyConsumptionChart/styles.ts +13 -4
- package/src/modules/energyConsumption/EnergyConsumptionChart/useEnergyData.ts +28 -12
- package/src/modules/energyConsumption/EnergyConsumptionChart.tsx +4 -0
- package/src/modules/energyConsumption/EnergyConsumptionDetail.tsx +41 -31
- package/src/modules/energyConsumption/EnergyConsumptionPage.tsx +52 -3
- package/src/modules/energyConsumption/component/DateSelectedItem.tsx +1 -1
- package/src/modules/energyConsumption/component/DateSwitch.tsx +10 -8
- package/src/modules/energyConsumption/component/DateTypeItem.tsx +1 -1
- package/src/modules/energyConsumption/component/EnergyModal.tsx +2 -2
- package/src/modules/energyConsumption/component/NewBarChart.tsx +221 -153
- package/src/modules/energyConsumption/component/Overview.tsx +1 -1
- package/src/modules/flags/FlagInfo.tsx +1204 -1054
- package/src/modules/flags/FlagItem.tsx +1 -2
- package/src/modules/flags/FlagPage.tsx +19 -15
- package/src/modules/mood_new/MoodItem.tsx +0 -1
package/package.json
CHANGED
|
@@ -1,33 +1,43 @@
|
|
|
1
|
-
import {useNavigation} from '@react-navigation/native'
|
|
2
|
-
import React, {useEffect, useState} from 'react'
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
1
|
+
import { useNavigation, useRoute } from '@react-navigation/native'
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
3
|
+
// 导入 Dimensions API 来获取屏幕宽度
|
|
4
|
+
import { Dimensions, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
|
|
5
|
+
import { Utils } from 'tuya-panel-kit'
|
|
6
|
+
import iconList from '../biorhythm/iconListData'
|
|
6
7
|
import LDVTopBar from '@ledvance/base/src/components/ldvTopBar'
|
|
7
8
|
import I18n from '@ledvance/base/src/i18n'
|
|
8
|
-
import { useParams } from '@ledvance/base/src/hooks/Hooks'
|
|
9
9
|
import ThemeType from '@ledvance/base/src/config/themeType'
|
|
10
|
+
import { xLog } from '@ledvance/base/src/utils'
|
|
10
11
|
|
|
11
|
-
const cx = Utils.RatioUtils
|
|
12
|
+
const { convertX: cx } = Utils.RatioUtils
|
|
12
13
|
const { withTheme } = Utils.ThemeUtils
|
|
13
14
|
|
|
14
|
-
interface
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
interface SceneDetailPageParams {
|
|
16
|
+
backText?: string
|
|
17
|
+
id: any
|
|
18
|
+
setIcon: (id) => void,
|
|
17
19
|
iconIdList: any
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
// --- 新增:定义常量,方便维护 ---
|
|
23
|
+
const ICON_WIDTH = cx(32)
|
|
24
|
+
const ICON_MARGIN = cx(10)
|
|
25
|
+
const CONTAINER_HORIZONTAL_PADDING = cx(24)
|
|
26
|
+
// 每个图标占据的总宽度(图片宽度 + 左右margin)
|
|
27
|
+
const ITEM_TOTAL_WIDTH = ICON_WIDTH + ICON_MARGIN * 2
|
|
28
|
+
|
|
29
|
+
function IconSelect(props: { theme?: ThemeType }) {
|
|
21
30
|
const [list, setList] = useState(iconList)
|
|
22
31
|
const navigation = useNavigation()
|
|
23
|
-
const params =
|
|
32
|
+
const params = useRoute().params as SceneDetailPageParams
|
|
33
|
+
|
|
24
34
|
const setColor = id => {
|
|
25
35
|
const newList = list?.map(item => {
|
|
26
36
|
return {
|
|
27
37
|
...item,
|
|
28
38
|
selectStatus: item?.id === id,
|
|
29
39
|
}
|
|
30
|
-
})
|
|
40
|
+
}) as typeof list
|
|
31
41
|
setList(newList)
|
|
32
42
|
}
|
|
33
43
|
useEffect(() => {
|
|
@@ -39,24 +49,74 @@ function IconSelectPage(props: { theme?: ThemeType }) {
|
|
|
39
49
|
selectStatus: item?.id === iconId,
|
|
40
50
|
disabled: iconIdList?.some(val => val === item?.id && val !== iconId),
|
|
41
51
|
}
|
|
42
|
-
})
|
|
52
|
+
}) as typeof list
|
|
43
53
|
setList(newList)
|
|
44
54
|
}, [])
|
|
45
55
|
|
|
56
|
+
const getStyles = useCallback((theme?: ThemeType) => StyleSheet.create({
|
|
57
|
+
container: { flex: 1, flexDirection: 'column' },
|
|
58
|
+
scrollView: { marginHorizontal: CONTAINER_HORIZONTAL_PADDING },
|
|
59
|
+
titleView: { marginTop: cx(40), marginBottom: cx(20) },
|
|
60
|
+
title: { fontSize: cx(24), color: theme?.global.brand },
|
|
61
|
+
iconContainer: {
|
|
62
|
+
flexDirection: 'row',
|
|
63
|
+
flex: 1,
|
|
64
|
+
flexWrap: 'wrap',
|
|
65
|
+
justifyContent: 'space-between',
|
|
66
|
+
alignItems: 'center'
|
|
67
|
+
},
|
|
68
|
+
ghostItem: {
|
|
69
|
+
width: ICON_WIDTH,
|
|
70
|
+
height: 0,
|
|
71
|
+
margin: ICON_MARGIN,
|
|
72
|
+
}
|
|
73
|
+
}), [CONTAINER_HORIZONTAL_PADDING, ICON_WIDTH, ICON_MARGIN])
|
|
74
|
+
|
|
75
|
+
const styles = useMemo(() => getStyles(props.theme), [props.theme])
|
|
76
|
+
|
|
77
|
+
// --- 新增:动态计算需要渲染的幽灵元素数量 ---
|
|
78
|
+
const ghostElements = useMemo(() => {
|
|
79
|
+
|
|
80
|
+
// 1. 获取容器的可用宽度
|
|
81
|
+
const containerWidth = cx(Dimensions.get('window').width) - CONTAINER_HORIZONTAL_PADDING * 2
|
|
82
|
+
|
|
83
|
+
// 2. 计算每行可以容纳多少个元素
|
|
84
|
+
const itemsPerRow = Math.ceil(containerWidth / ITEM_TOTAL_WIDTH)
|
|
85
|
+
// 如果无法容纳任何元素,则不渲染幽灵元素
|
|
86
|
+
if (itemsPerRow <= 0) {
|
|
87
|
+
xLog('No ghost elements needed.')
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. 计算需要补充的幽灵元素数量
|
|
92
|
+
const numberOfItems = list.length
|
|
93
|
+
const itemsInLastRow = numberOfItems % itemsPerRow
|
|
94
|
+
|
|
95
|
+
// 如果最后一行为空或已满,则不需要幽灵元素
|
|
96
|
+
if (itemsInLastRow === 0) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const numberOfGhosts = itemsPerRow - itemsInLastRow
|
|
101
|
+
|
|
102
|
+
// 4. 返回一个包含正确数量幽灵元素的数组
|
|
103
|
+
return Array.from({ length: numberOfGhosts }).map((_, index) => (
|
|
104
|
+
<View key={`ghost-${index}`} style={styles.ghostItem}/>
|
|
105
|
+
))
|
|
106
|
+
}, [list.length]) // 依赖项:当图标总数变化时重新计算
|
|
107
|
+
|
|
46
108
|
return (
|
|
47
|
-
<View style={
|
|
109
|
+
<View style={styles.container}>
|
|
48
110
|
<LDVTopBar
|
|
49
|
-
title={I18n.getLang('add_new_trigger_time_system_back_text')}
|
|
50
|
-
onBackPress={() =>
|
|
51
|
-
navigation.goBack()
|
|
52
|
-
}}
|
|
111
|
+
title={params.backText ?? I18n.getLang('add_new_trigger_time_system_back_text')}
|
|
112
|
+
onBackPress={() => navigation.goBack()}
|
|
53
113
|
/>
|
|
54
|
-
<ScrollView nestedScrollEnabled={true} style={
|
|
55
|
-
<View style={
|
|
56
|
-
<Text style={
|
|
114
|
+
<ScrollView nestedScrollEnabled={true} style={styles.scrollView}>
|
|
115
|
+
<View style={styles.titleView}>
|
|
116
|
+
<Text style={styles.title}>{I18n.getLang('add_new_trigger_time_icon_selection_headline_text')}</Text>
|
|
57
117
|
</View>
|
|
58
|
-
<View
|
|
59
|
-
|
|
118
|
+
<View style={styles.iconContainer}>
|
|
119
|
+
{/* 渲染真实的图标 */}
|
|
60
120
|
{list?.map(item => {
|
|
61
121
|
return <TouchableOpacity
|
|
62
122
|
onPress={() => {
|
|
@@ -67,20 +127,23 @@ function IconSelectPage(props: { theme?: ThemeType }) {
|
|
|
67
127
|
key={item.id}
|
|
68
128
|
>
|
|
69
129
|
<Image
|
|
70
|
-
source={{uri: item?.icon}}
|
|
130
|
+
source={{ uri: item?.icon }}
|
|
71
131
|
style={{
|
|
72
|
-
width:
|
|
73
|
-
height: cx(32),
|
|
74
|
-
margin:
|
|
132
|
+
width: ICON_WIDTH,
|
|
133
|
+
height: cx(32), // 高度保持不变
|
|
134
|
+
margin: ICON_MARGIN,
|
|
75
135
|
tintColor: item?.selectStatus ? props.theme?.icon.primary : item?.disabled && props.theme?.icon.disable || props.theme?.icon.normal,
|
|
76
136
|
}}
|
|
77
137
|
/>
|
|
78
138
|
</TouchableOpacity>
|
|
79
139
|
})}
|
|
140
|
+
|
|
141
|
+
{/* 渲染精确计算出的幽灵元素 */}
|
|
142
|
+
{ghostElements}
|
|
80
143
|
</View>
|
|
81
144
|
</ScrollView>
|
|
82
145
|
</View>
|
|
83
146
|
)
|
|
84
147
|
}
|
|
85
148
|
|
|
86
|
-
export default withTheme(
|
|
149
|
+
export default withTheme(IconSelect)
|
|
@@ -12,8 +12,8 @@ import { OverviewItem } from "./EnergyConsumptionPage";
|
|
|
12
12
|
import dayjs from "dayjs";
|
|
13
13
|
import { isNumber } from "lodash";
|
|
14
14
|
import { EnergyData, UnitList } from "./component/EnergyModal";
|
|
15
|
-
import I18n from "@ledvance/base/src/i18n";
|
|
16
15
|
import {NativeApi} from "@ledvance/base/src/api/native";
|
|
16
|
+
import {xLog, retryWithBackoff} from "@ledvance/base/src/utils"
|
|
17
17
|
|
|
18
18
|
interface LightConfig {
|
|
19
19
|
energyConsumption?: EnergyData
|
|
@@ -50,17 +50,18 @@ export async function getElectricity(devIdGroup: string[], addEleDpCode: string,
|
|
|
50
50
|
return res
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
let dpResultByMonthCache: DpResultByMonthResData[]
|
|
53
|
+
let dpResultByMonthCache: {key: DpResultByMonthResData[]} = {}
|
|
54
54
|
const getDpResultByYear = async (devIdGroup: string[], addEleDpCode: string, dateStr: string): Promise<OverviewItem[]> => {
|
|
55
55
|
const year = dateStr;
|
|
56
|
-
|
|
56
|
+
const key = devIdGroup.join()
|
|
57
|
+
let successGroup = dpResultByMonthCache[key];
|
|
57
58
|
if (!successGroup) {
|
|
58
59
|
const promiseGroup = devIdGroup.map(devId =>
|
|
59
60
|
getDpResultByMonth(devId, addEleDpCode, 'sum').catch(error => ({ error }))
|
|
60
61
|
);
|
|
61
62
|
// @ts-ignore
|
|
62
|
-
dpResultByMonthCache = (await Promise.all(promiseGroup)).filter(v => !v.error);
|
|
63
|
-
successGroup = dpResultByMonthCache
|
|
63
|
+
dpResultByMonthCache[key] = (await Promise.all(promiseGroup)).filter(v => !v.error);
|
|
64
|
+
successGroup = dpResultByMonthCache[key]
|
|
64
65
|
}
|
|
65
66
|
if (!successGroup) {
|
|
66
67
|
return []
|
|
@@ -79,7 +80,7 @@ const getDpResultByYear = async (devIdGroup: string[], addEleDpCode: string, dat
|
|
|
79
80
|
})
|
|
80
81
|
}
|
|
81
82
|
})
|
|
82
|
-
const curMonthList = Object.keys(mergedData).sort((a, b) => parseInt(
|
|
83
|
+
const curMonthList = Object.keys(mergedData).sort((a, b) => parseInt(a) - parseInt(b));
|
|
83
84
|
return curMonthList.map(month => {
|
|
84
85
|
return {
|
|
85
86
|
key: `${monthFormat(month)} ${year}`,
|
|
@@ -134,11 +135,12 @@ const getDpResultByYearMonth = async (deviceIdGroup: string[], addEleDpCode: str
|
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
const getDpResultByDate = async (deviceIdGroup: string[], addEleDpCode: string, date: string): Promise<OverviewItem[]> => {
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
const newDate = date.replaceAll('/', '')
|
|
139
|
+
if (overDays(newDate, 7)) {
|
|
140
|
+
xLog("getDpResultByDate overDays true")
|
|
139
141
|
return []
|
|
140
142
|
}
|
|
141
|
-
const promiseGroup = deviceIdGroup.map(devId => getDpResultByHour(devId, addEleDpCode,
|
|
143
|
+
const promiseGroup = deviceIdGroup.map(devId => getDpResultByHour(devId, addEleDpCode, newDate, 'sum').catch(error => ({ error })))
|
|
142
144
|
const res = await Promise.all(promiseGroup);
|
|
143
145
|
// @ts-ignore
|
|
144
146
|
const successGroup: DpResultByDataWithSpecifiedResData[] = res.filter(v => !v.error);
|
|
@@ -174,8 +176,7 @@ const getDpResultByDate = async (deviceIdGroup: string[], addEleDpCode: string,
|
|
|
174
176
|
return list
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
export const exportEnergyCsv = (values: any[][]
|
|
178
|
-
const headers = [I18n.getLang('date'), `${I18n.getLang('consumption_data_annual_bar_chart_system_back_text')} (kWh)`, `Price(${unit})`]
|
|
179
|
+
export const exportEnergyCsv = (headers: string[], values: any[][]) => {
|
|
179
180
|
const functionName = 'EnergyConsumption'
|
|
180
181
|
exportCsvFile(headers, values, functionName)
|
|
181
182
|
}
|
|
@@ -195,12 +196,37 @@ export interface EnergyGeneration {
|
|
|
195
196
|
history: EnergyHistory[]
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
async function getJsonWithError(devId: string, key: string) {
|
|
200
|
+
const res = await NativeApi.getJson(devId, key)
|
|
201
|
+
if (!res.success) {
|
|
202
|
+
xLog(`getJsonWithError ${devId} ${key} failed`, res)
|
|
203
|
+
throw new Error(`API失败: ${res.msg || '未知错误'}`, { cause: res })
|
|
204
|
+
}
|
|
205
|
+
return res
|
|
206
|
+
}
|
|
207
|
+
|
|
198
208
|
export async function getEnergyGenerationValue(devId: string): Promise<EnergyGeneration | undefined> {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
try {
|
|
210
|
+
const res = await retryWithBackoff(
|
|
211
|
+
() => getJsonWithError(devId, EnergyGenerationId),
|
|
212
|
+
{
|
|
213
|
+
// 自定义判断哪些错误需要重试
|
|
214
|
+
shouldRetry: (error) => {
|
|
215
|
+
// 网络错误、超时错误或服务器错误(5xx)通常需要重试
|
|
216
|
+
const isNetworkError = !error.cause?.success && error.cause?.msg === 'connection closed'
|
|
217
|
+
|
|
218
|
+
return isNetworkError;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
if (res.success && res.data) {
|
|
223
|
+
return JSON.parse(res.data)
|
|
224
|
+
} else {
|
|
225
|
+
return undefined
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
xLog('getEnergyGenerationValue error', error.cause)
|
|
204
229
|
return undefined
|
|
205
230
|
}
|
|
231
|
+
|
|
206
232
|
}
|
|
@@ -174,7 +174,7 @@ const EnergyConsumptionCard = (props: EnergyConsumptionProp) => {
|
|
|
174
174
|
consumedEnergyItemUnit: {
|
|
175
175
|
color: props.theme?.global.secondFontColor,
|
|
176
176
|
fontSize: cx(14),
|
|
177
|
-
fontFamily: 'helvetica_neue_lt_std_roman',
|
|
177
|
+
// fontFamily: 'helvetica_neue_lt_std_roman',
|
|
178
178
|
textAlign: 'center'
|
|
179
179
|
},
|
|
180
180
|
subContent: {
|
|
@@ -188,13 +188,13 @@ const EnergyConsumptionCard = (props: EnergyConsumptionProp) => {
|
|
|
188
188
|
color: props.theme?.global.secondBrand,
|
|
189
189
|
},
|
|
190
190
|
titleText: {
|
|
191
|
-
fontFamily: 'helvetica_neue_lt_std_roman',
|
|
191
|
+
// fontFamily: 'helvetica_neue_lt_std_roman',
|
|
192
192
|
fontSize: cx(14),
|
|
193
193
|
color: props.theme?.global.secondFontColor,
|
|
194
194
|
textAlign: 'center',
|
|
195
195
|
},
|
|
196
196
|
unitText: {
|
|
197
|
-
fontFamily: 'helvetica_neue_lt_std_roman',
|
|
197
|
+
// fontFamily: 'helvetica_neue_lt_std_roman',
|
|
198
198
|
fontSize: cx(14),
|
|
199
199
|
color: props.theme?.global.secondFontColor,
|
|
200
200
|
},
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import I18n from '@ledvance/base/src/i18n'
|
|
2
2
|
import React, { useCallback } from 'react'
|
|
3
|
-
import { View } from 'react-native'
|
|
3
|
+
import { Image, TouchableOpacity, View } from 'react-native'
|
|
4
4
|
import { DateType } from '../co2Data'
|
|
5
|
-
import DateSelectedItem from '../component/DateSelectedItem'
|
|
6
5
|
import DateTypeItem from '../component/DateTypeItem'
|
|
6
|
+
import DateSwitch from '../component/DateSwitch'
|
|
7
7
|
import NewBarChart from '../component/NewBarChart'
|
|
8
8
|
import { EmptyDataView } from './EmptyDataView'
|
|
9
|
+
import { exportEnergyCsv } from '../EnergyConsumptionActions'
|
|
10
|
+
import { Utils } from 'tuya-panel-kit'
|
|
11
|
+
import res from '@ledvance/base/src/res'
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
const { convertX: cx } = Utils.RatioUtils
|
|
14
|
+
|
|
15
|
+
export const ChartSection = ({ isLandscape, state, actions, params, styles, theme, chartHeight }) => {
|
|
11
16
|
const getEmptyDataTip = useCallback(() => {
|
|
12
17
|
if (state.over365Days && state.dateType !== DateType.Day) {
|
|
13
18
|
return I18n.getLang('energyconsumption_Daylimit')
|
|
@@ -18,7 +23,77 @@ export const ChartSection = ({ state, actions, params, styles, theme, chartHeigh
|
|
|
18
23
|
return I18n.getLang('energyconsumption_emptydata')
|
|
19
24
|
}, [state.dateType, state.over365Days, state.over7Days])
|
|
20
25
|
|
|
21
|
-
const
|
|
26
|
+
const handleExportCsv = useCallback(() => {
|
|
27
|
+
const displayMode = state.displayMode || 'consumption'
|
|
28
|
+
const consumedData = state.consumptionChartData
|
|
29
|
+
const generatedData = state.generationChartData
|
|
30
|
+
const price = params.price
|
|
31
|
+
const unit = params.unit
|
|
32
|
+
const consumedName = I18n.getLang('chart_legend_consumption')
|
|
33
|
+
const generatedName = I18n.getLang('chart_legend_generation')
|
|
34
|
+
// 1. 创建CSV头部 (与之前相同)
|
|
35
|
+
const header = [I18n.getLang('date')]
|
|
36
|
+
if (displayMode === 'consumption' || displayMode === 'both') {
|
|
37
|
+
header.push(`${consumedName} (kWh)`, `${consumedName} (${unit})`)
|
|
38
|
+
}
|
|
39
|
+
if (displayMode === 'generation' || displayMode === 'both') {
|
|
40
|
+
header.push(`${generatedName} (kWh)`, `${generatedName} (${unit})`)
|
|
41
|
+
}
|
|
42
|
+
const rows = []
|
|
43
|
+
let i = 0 // 指向 consumedData 的指针
|
|
44
|
+
let j = 0 // 指向 generatedData 的指针
|
|
45
|
+
// 2. 双指针合并算法
|
|
46
|
+
while (i < consumedData.length || j < generatedData.length) {
|
|
47
|
+
const consumedItem = consumedData[i]
|
|
48
|
+
const generatedItem = generatedData[j]
|
|
49
|
+
const consumedKey = consumedItem?.headlineText
|
|
50
|
+
const generatedKey = generatedItem?.headlineText
|
|
51
|
+
let rowData = []
|
|
52
|
+
let dateKey = ''
|
|
53
|
+
if (consumedKey === generatedKey) {
|
|
54
|
+
dateKey = consumedItem.key
|
|
55
|
+
const consumedValue = Number(consumedItem.value)
|
|
56
|
+
const generatedValue = Number(generatedItem.value)
|
|
57
|
+
if (displayMode === 'consumption' || displayMode === 'both') {
|
|
58
|
+
rowData.push(consumedValue.toFixed(2), (consumedValue * price).toFixed(2))
|
|
59
|
+
}
|
|
60
|
+
if (displayMode === 'generation' || displayMode === 'both') {
|
|
61
|
+
rowData.push(generatedValue.toFixed(2), (generatedValue * price).toFixed(2))
|
|
62
|
+
}
|
|
63
|
+
i++
|
|
64
|
+
j++
|
|
65
|
+
} else if (consumedKey > generatedKey || !generatedKey) {
|
|
66
|
+
dateKey = consumedItem.key
|
|
67
|
+
const consumedValue = Number(consumedItem.value)
|
|
68
|
+
if (displayMode === 'consumption' || displayMode === 'both') {
|
|
69
|
+
rowData.push(consumedValue.toFixed(2), (consumedValue * price).toFixed(2))
|
|
70
|
+
}
|
|
71
|
+
if (displayMode === 'generation' || displayMode === 'both') {
|
|
72
|
+
// 在 'generation' 或 'both' 模式下,为缺失的产生数据补0
|
|
73
|
+
rowData.push('0.00', '0.00')
|
|
74
|
+
}
|
|
75
|
+
i++
|
|
76
|
+
} else {
|
|
77
|
+
dateKey = generatedItem.key
|
|
78
|
+
const generatedValue = Number(generatedItem.value)
|
|
79
|
+
if (displayMode === 'consumption' || displayMode === 'both') {
|
|
80
|
+
// 在 'consumption' 或 'both' 模式下,为缺失的消耗数据补0
|
|
81
|
+
rowData.push('0.00', '0.00')
|
|
82
|
+
}
|
|
83
|
+
if (displayMode === 'generation' || displayMode === 'both') {
|
|
84
|
+
rowData.push(generatedValue.toFixed(2), (generatedValue * price).toFixed(2))
|
|
85
|
+
}
|
|
86
|
+
j++
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 将日期和处理好的数据行组合起来
|
|
90
|
+
rows.push([dateKey, ...rowData])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
exportEnergyCsv(header, rows)
|
|
94
|
+
}, [state.displayMode, state.consumptionChartData, state.generationChartData, params.price, params.unit])
|
|
95
|
+
|
|
96
|
+
const isDataEmpty = state.consumptionChartData.length <= 0 && state.generationChartData.length <= 0
|
|
22
97
|
|
|
23
98
|
return (
|
|
24
99
|
<>
|
|
@@ -28,10 +103,11 @@ export const ChartSection = ({ state, actions, params, styles, theme, chartHeigh
|
|
|
28
103
|
dateType={state.dateType}
|
|
29
104
|
onDateTypeChange={actions.setDateType}
|
|
30
105
|
/>
|
|
31
|
-
<
|
|
32
|
-
style={
|
|
33
|
-
dateType={state.dateType}
|
|
106
|
+
<DateSwitch
|
|
107
|
+
style={{ flex: 1 }}
|
|
34
108
|
date={state.date}
|
|
109
|
+
dateType={state.dateType}
|
|
110
|
+
headlineText={state.headlineText}
|
|
35
111
|
onDateChange={actions.setDate}
|
|
36
112
|
/>
|
|
37
113
|
</View>
|
|
@@ -39,14 +115,26 @@ export const ChartSection = ({ state, actions, params, styles, theme, chartHeigh
|
|
|
39
115
|
{isDataEmpty ? (
|
|
40
116
|
<EmptyDataView text={getEmptyDataTip()} theme={theme} styles={styles}/>
|
|
41
117
|
) : (
|
|
42
|
-
!state.loading &&
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
118
|
+
!state.loading && <>
|
|
119
|
+
{!isLandscape && <View style={styles.downloadContainer}>
|
|
120
|
+
<TouchableOpacity
|
|
121
|
+
style={{ width: cx(30) }}
|
|
122
|
+
onPress={handleExportCsv}>
|
|
123
|
+
<Image
|
|
124
|
+
style={styles.downloadIcon}
|
|
125
|
+
source={{ uri: res.download_icon }}/>
|
|
126
|
+
</TouchableOpacity>
|
|
127
|
+
</View>}
|
|
128
|
+
<NewBarChart
|
|
129
|
+
height={chartHeight}
|
|
130
|
+
data={state.chartData}
|
|
131
|
+
displayMode={state.displayMode}
|
|
132
|
+
consumedData={state.consumptionChartData}
|
|
133
|
+
generatedData={state.generationChartData}
|
|
134
|
+
price={state.price}
|
|
135
|
+
unit={params.unit}
|
|
136
|
+
/>
|
|
137
|
+
</>
|
|
50
138
|
)}
|
|
51
139
|
</>
|
|
52
140
|
)
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import Page from '@ledvance/base/src/components/Page'
|
|
2
2
|
import res from '@ledvance/base/src/res'
|
|
3
|
-
import React
|
|
4
|
-
import {
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { ScrollView, } from 'react-native'
|
|
5
5
|
import { Utils } from 'tuya-panel-kit'
|
|
6
|
-
import DateSwitch from '../component/DateSwitch'
|
|
7
|
-
import { exportEnergyCsv } from '../EnergyConsumptionActions'
|
|
8
6
|
import { ChartSection } from './ChartSection'
|
|
9
7
|
|
|
10
8
|
const { convertX: cx } = Utils.RatioUtils
|
|
11
9
|
|
|
12
10
|
export const PortraitView = ({ state, actions, params, styles, theme }) => {
|
|
13
|
-
|
|
14
|
-
const handleExportCsv = useCallback(() => {
|
|
15
|
-
const values = state.chartData.map(item => [item.key, item.value, (Number(params.price) * Number(item.value)).toFixed(2)])
|
|
16
|
-
exportEnergyCsv(values, params.unit)
|
|
17
|
-
}, [state.chartData, params.price, params.unit])
|
|
18
|
-
|
|
19
11
|
return (
|
|
20
12
|
<Page
|
|
21
13
|
backText={params.backTitle}
|
|
@@ -25,26 +17,10 @@ export const PortraitView = ({ state, actions, params, styles, theme }) => {
|
|
|
25
17
|
rightButtonIcon={state.isSupportLandscape ? res.screen_full : undefined}
|
|
26
18
|
rightButtonStyle={styles.fullScreenIcon}
|
|
27
19
|
rightButtonIconClick={actions.toggleLandscape}
|
|
28
|
-
headlineContent={
|
|
29
|
-
<View style={styles.headlineContainer}>
|
|
30
|
-
<DateSwitch
|
|
31
|
-
style={styles.dateSwitch}
|
|
32
|
-
date={state.date}
|
|
33
|
-
dateType={state.dateType}
|
|
34
|
-
headlineText={state.headlineText}
|
|
35
|
-
onDateChange={actions.setDate}
|
|
36
|
-
/>
|
|
37
|
-
<TouchableOpacity style={styles.downloadButton} onPress={handleExportCsv}>
|
|
38
|
-
<Image
|
|
39
|
-
style={styles.downloadIcon}
|
|
40
|
-
source={{ uri: !(state.chartData.length <= 0) ? res.download_icon : undefined }}
|
|
41
|
-
/>
|
|
42
|
-
</TouchableOpacity>
|
|
43
|
-
</View>
|
|
44
|
-
}
|
|
45
20
|
>
|
|
46
21
|
<ScrollView nestedScrollEnabled={true} style={styles.scrollViewContent}>
|
|
47
22
|
<ChartSection
|
|
23
|
+
isLandscape={false}
|
|
48
24
|
state={state}
|
|
49
25
|
actions={actions}
|
|
50
26
|
params={params}
|
|
@@ -9,11 +9,16 @@ export const getStyles = (theme: ThemeType | undefined) => StyleSheet.create({
|
|
|
9
9
|
scrollViewContent: {
|
|
10
10
|
marginHorizontal: cx(24),
|
|
11
11
|
},
|
|
12
|
+
dateSwitchContainer: {
|
|
13
|
+
width: '100%',
|
|
14
|
+
flexDirection: 'row',
|
|
15
|
+
marginVertical: cx(15),
|
|
16
|
+
},
|
|
12
17
|
dateControlsContainer: {
|
|
13
18
|
flexDirection: 'row',
|
|
14
19
|
},
|
|
15
20
|
dateTypeItem: {
|
|
16
|
-
flex:
|
|
21
|
+
flex: 0.5,
|
|
17
22
|
},
|
|
18
23
|
dateSelectedItem: {
|
|
19
24
|
flex: 1,
|
|
@@ -72,12 +77,16 @@ export const getStyles = (theme: ThemeType | undefined) => StyleSheet.create({
|
|
|
72
77
|
downloadButton: {
|
|
73
78
|
width: cx(30),
|
|
74
79
|
},
|
|
80
|
+
downloadContainer: {
|
|
81
|
+
flexDirection: 'row',
|
|
82
|
+
justifyContent: 'flex-end',
|
|
83
|
+
// marginHorizontal: cx(24),
|
|
84
|
+
marginTop: cx(15),
|
|
85
|
+
marginBottom: cx(-15)
|
|
86
|
+
},
|
|
75
87
|
downloadIcon: {
|
|
76
88
|
width: cx(24),
|
|
77
89
|
height: cx(24),
|
|
78
90
|
tintColor: theme?.global.brand,
|
|
79
|
-
position: 'absolute',
|
|
80
|
-
right: 0,
|
|
81
|
-
top: cx(10),
|
|
82
91
|
},
|
|
83
92
|
})
|
|
@@ -3,13 +3,17 @@ import { overDays } from '@ledvance/base/src/utils'
|
|
|
3
3
|
import { loopsText, monthFormat } from '@ledvance/base/src/utils/common'
|
|
4
4
|
import { useReactive, useUpdateEffect } from 'ahooks'
|
|
5
5
|
import dayjs from 'dayjs'
|
|
6
|
-
import { useCallback } from 'react'
|
|
6
|
+
import { useCallback, useEffect } from 'react'
|
|
7
7
|
import { DateType } from '../co2Data'
|
|
8
8
|
import { getElectricity } from '../EnergyConsumptionActions'
|
|
9
9
|
import { EnergyConsumptionChartProps } from '../EnergyConsumptionChart'
|
|
10
|
+
import {useIsPad} from "@ledvance/base/src/models/modules/NativePropsSlice"
|
|
11
|
+
|
|
12
|
+
export type EnergyDisplayMode = 'consumption' | 'generation' | 'both';
|
|
10
13
|
|
|
11
14
|
export const useEnergyData = (params: EnergyConsumptionChartProps) => {
|
|
12
|
-
const
|
|
15
|
+
const isPad = useIsPad()
|
|
16
|
+
const { addEleDpCode, price, unit, date, over365Days, over7Days, chartData, deviceIdGroup, consumptionDeviceIds, generationDeviceIds } = params
|
|
13
17
|
|
|
14
18
|
const getDateType = useCallback((d: string) => {
|
|
15
19
|
const datejs = dayjs(d)
|
|
@@ -24,13 +28,17 @@ export const useEnergyData = (params: EnergyConsumptionChartProps) => {
|
|
|
24
28
|
const initialDateType = getDateType(date)
|
|
25
29
|
const state = useReactive({
|
|
26
30
|
loading: false,
|
|
27
|
-
isSupportLandscape: OrientationService.isSupported(),
|
|
31
|
+
isSupportLandscape: OrientationService.isSupported() && !isPad,
|
|
28
32
|
isLandscape: false,
|
|
29
33
|
dateType: initialDateType,
|
|
30
34
|
date: date,
|
|
31
35
|
headlineText: initialDateType === DateType.Year ? date : params.headlineText,
|
|
32
36
|
chartData: chartData.filter((item) => initialDateType !== DateType.Year || item.headlineText.startsWith(date)),
|
|
37
|
+
displayMode: params.displayMode || 'consumption',
|
|
38
|
+
consumptionChartData: [],
|
|
39
|
+
generationChartData: [],
|
|
33
40
|
price: isNaN(Number(price)) ? 0 : Number(price),
|
|
41
|
+
unit: unit,
|
|
34
42
|
over365Days: over365Days,
|
|
35
43
|
over7Days: over7Days,
|
|
36
44
|
})
|
|
@@ -53,16 +61,17 @@ export const useEnergyData = (params: EnergyConsumptionChartProps) => {
|
|
|
53
61
|
}
|
|
54
62
|
}, [state.dateType])
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
useEffect(() => {
|
|
57
65
|
state.loading = true
|
|
58
|
-
getElectricity(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const consumptionData = getElectricity(consumptionDeviceIds, addEleDpCode, state.date, state.dateType)
|
|
67
|
+
const generationData = getElectricity(generationDeviceIds, addEleDpCode, state.date, state.dateType)
|
|
68
|
+
Promise.all([consumptionData, generationData]).then(([consumptionRes, generationRes]) => {
|
|
69
|
+
state.consumptionChartData = consumptionRes
|
|
70
|
+
state.generationChartData = generationRes
|
|
71
|
+
state.loading = false
|
|
72
|
+
}).catch(() => {
|
|
73
|
+
state.loading = false
|
|
74
|
+
})
|
|
66
75
|
}, [state.date])
|
|
67
76
|
|
|
68
77
|
useUpdateEffect(() => {
|
|
@@ -96,6 +105,9 @@ export const useEnergyData = (params: EnergyConsumptionChartProps) => {
|
|
|
96
105
|
setDateType: useCallback((type: DateType) => {
|
|
97
106
|
state.dateType = type
|
|
98
107
|
}, []),
|
|
108
|
+
setDisplayMode: useCallback((mode: EnergyDisplayMode) => {
|
|
109
|
+
state.displayMode = mode
|
|
110
|
+
}, []),
|
|
99
111
|
toggleLandscape: useCallback(() => {
|
|
100
112
|
if (!state.isSupportLandscape) return
|
|
101
113
|
const newIsLandscape = !state.isLandscape
|
|
@@ -106,6 +118,10 @@ export const useEnergyData = (params: EnergyConsumptionChartProps) => {
|
|
|
106
118
|
OrientationService.lockToPortrait()
|
|
107
119
|
}
|
|
108
120
|
}, []),
|
|
121
|
+
setPriceAndUnit: useCallback((newPrice: string, newUnit: string) => {
|
|
122
|
+
state.price = isNaN(Number(newPrice)) ? 0 : Number(newPrice)
|
|
123
|
+
state.unit = newUnit
|
|
124
|
+
}, []),
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
return { state, actions }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OverviewItem } from './EnergyConsumptionPage'
|
|
2
|
+
import { EnergyDisplayMode } from './EnergyConsumptionChart/useEnergyData'
|
|
2
3
|
|
|
3
4
|
export interface EnergyConsumptionChartProps {
|
|
4
5
|
addEleDpCode: string
|
|
@@ -8,6 +9,9 @@ export interface EnergyConsumptionChartProps {
|
|
|
8
9
|
over365Days?: boolean
|
|
9
10
|
over7Days?: boolean,
|
|
10
11
|
deviceIdGroup: string[];
|
|
12
|
+
displayMode?: EnergyDisplayMode;
|
|
13
|
+
consumptionDeviceIds: string[];
|
|
14
|
+
generationDeviceIds: string[];
|
|
11
15
|
price: string,
|
|
12
16
|
unit: string,
|
|
13
17
|
date: string,
|