@ledvance/ui-biz-bundle 1.1.147 → 1.1.149

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.147",
7
+ "version": "1.1.149",
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"
@@ -150,7 +150,7 @@ export interface PowerDataItem {
150
150
  }
151
151
 
152
152
  // 常量抽离(便于维护)
153
- const DATA_POINT_INTERVAL_SEC = 10; // 预设数据间隔(秒)
153
+ const DATA_POINT_INTERVAL_SEC = 30; // 预设数据间隔(秒)
154
154
  const TIME_UNIT = 'minute' as const; // 时间单位(与 interval 配合)
155
155
  const RETRY_CONFIG = {
156
156
  maxRetries: 5,
@@ -160,11 +160,26 @@ const RETRY_CONFIG = {
160
160
  } as const;
161
161
 
162
162
  /**
163
- * 获取设备功率数据(保留所有实际数据,补充10秒间隔预设数据)
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值)
164
179
  * @param devId 设备ID
165
180
  * @param powerDpCode 功率数据点编码
166
181
  * @param interval 时间区间(分钟)
167
- * @returns 按时间排序的完整数据(预设数据 + 所有实际数据)
182
+ * @returns 按时间排序的完整数据
168
183
  */
169
184
  export async function getPowerData(
170
185
  devId: string,
@@ -173,29 +188,12 @@ export async function getPowerData(
173
188
  ): Promise<PowerDataItem[]> {
174
189
  try {
175
190
  const now = dayjs();
176
- const startTime = now.add(-interval, TIME_UNIT); // 起始时间:当前时间 - interval 分钟
177
- const endTime = now; // 结束时间:当前时间
178
- const startTimeMs = startTime.valueOf();
191
+ const endTime = now;
192
+ const startTime = now.add(-interval, TIME_UNIT);
179
193
  const endTimeMs = endTime.valueOf();
180
-
181
- // 1. 生成预设数据(10秒间隔,仅覆盖 [startTime, endTime] 区间)
182
- const totalSeconds = interval * 60;
183
- const expectedDataPoints = Math.floor(totalSeconds / DATA_POINT_INTERVAL_SEC);
184
- const presetData: PowerDataItem[] = [];
185
- for (let i = 0; i < expectedDataPoints; i++) {
186
- const timePoint = startTime.add(i * DATA_POINT_INTERVAL_SEC, 'second');
187
- const timeMs = timePoint.valueOf();
188
- // 确保预设数据不超出查询区间(防止边界计算误差)
189
- if (timeMs > endTimeMs) break;
190
- presetData.push({
191
- key: timePoint.format('HH:mm:ss'),
192
- chartTitle: timePoint.format('MM/DD/YYYY HH:mm:ss'),
193
- time: timeMs,
194
- value: 0,
195
- });
196
- }
197
-
198
- // 2. 请求并处理实际数据(保留所有实际数据,不过滤)
194
+ const startTimeMs = startTime.valueOf();
195
+ const paddingIntervalMs = DATA_POINT_INTERVAL_SEC * 1000;
196
+ // 1. 请求已排序的实际数据
199
197
  const dpResult = await getSpecifiedTimeDpReportLogs(
200
198
  devId,
201
199
  [powerDpCode],
@@ -204,37 +202,54 @@ export async function getPowerData(
204
202
  endTimeMs.toString(),
205
203
  RETRY_CONFIG
206
204
  );
207
- // 类型校验 + 格式转换(避免异常)
208
205
  const validDpResult = Array.isArray(dpResult) ? (dpResult as DpReportSataData[]) : [];
209
206
  const actualData: PowerDataItem[] = validDpResult
210
207
  .map((dp) => {
211
- const timeMs = dp.timeStamp * 1000; // 秒级转毫秒级
212
- // 过滤超出查询区间的实际数据(可选,根据业务需求)
208
+ const timeMs = dp.timeStamp * 1000;
213
209
  if (timeMs < startTimeMs || timeMs > endTimeMs) return null;
214
210
  return {
215
- key: dayjs.unix(dp.timeStamp).format('HH:mm:ss'), // 统一 key 格式
211
+ key: dayjs.unix(dp.timeStamp).format('HH:mm:ss'),
216
212
  chartTitle: dayjs.unix(dp.timeStamp).format('MM/DD/YYYY HH:mm:ss'),
217
213
  time: timeMs,
218
- value: parseFloat(dp.value) / 10, // 业务数值转换
214
+ value: parseFloat(dp.value) / 10,
219
215
  };
220
216
  })
221
- .filter((item): item is PowerDataItem => item !== null); // 过滤无效数据
222
-
223
- // 3. 合并预设数据和实际数据(去重:实际数据优先覆盖预设数据)
224
- // Map 实现 O(n) 合并(key:时间戳,value:数据项,实际数据覆盖预设)
225
- const mergedMap = new Map<number, PowerDataItem>();
226
- // 先加预设数据(后续实际数据相同时间戳会覆盖)
227
- presetData.forEach((item) => mergedMap.set(item.time, item));
228
- // 再加实际数据(覆盖相同时间戳的预设数据)
229
- actualData.forEach((item) => mergedMap.set(item.time, item));
230
-
231
- // 4. 按时间戳排序(Map 迭代顺序是插入顺序,必须显式排序)
232
- const result = Array.from(mergedMap.values()).sort((a, b) => a.time - b.time);
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;
227
+ }
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
+ }
233
239
 
234
- return result;
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;
249
+ }
250
+ return finalData;
235
251
  } catch (error) {
236
252
  console.error(`[getPowerData] 失败:devId=${devId}, powerDpCode=${powerDpCode}`, error);
237
- // 兜底返回空数组(或仅返回预设数据,根据业务容错需求调整)
238
253
  return [];
239
254
  }
240
255
  }
@@ -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, screenWidth, screenHeight }) => (
13
+ <View style={[styles.landscapeContainer, { width: screenWidth, height: screenHeight }]}>
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={screenHeight - cx(110)}
33
+ />
34
+ ) : (
35
+ <PowerChartSection
36
+ isLandscape={true}
37
+ state={state}
38
+ actions={actions}
39
+ styles={styles}
40
+ theme={theme}
41
+ chartHeight={screenHeight - cx(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,43 @@
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
+ import { useScreenDimensions } from './useScreenDimensions'
13
+
14
+ const { withTheme } = Utils.ThemeUtils
15
+
16
+ const EnergyConsumptionChartComponent = (props: { theme?: ThemeType }) => {
17
+ const devId = useDeviceId()
18
+ const params = useRoute().params as EnergyConsumptionChartProps
19
+
20
+ // Use the custom Hook to get all state and logic
21
+ const { state, actions } = useEnergyData(params, devId)
22
+
23
+ // Use useMemo to prevent re-creating styles on every render
24
+ const styles = useMemo(() => getStyles(props.theme), [props.theme])
25
+
26
+ const { width: screenWidth, height: screenHeight } = useScreenDimensions()
27
+ // Conditionally render the correct view based on orientation state
28
+ if (state.isLandscape) {
29
+ return <LandscapeView
30
+ state={state}
31
+ actions={actions}
32
+ params={params}
33
+ styles={styles}
34
+ theme={props.theme}
35
+ screenWidth={screenWidth}
36
+ screenHeight={screenHeight}
37
+ />
38
+ }
39
+
40
+ return <PortraitView state={state} actions={actions} params={params} styles={styles} theme={props.theme}/>
41
+ }
42
+
43
+ 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
+ })
@@ -0,0 +1,147 @@
1
+ import { queryDpIds } from '@ledvance/base/src/api/native'
2
+ import { OrientationService } from '@ledvance/base/src/api/OrientationService'
3
+ import { overDays } from '@ledvance/base/src/utils'
4
+ import { loopsText, monthFormat } from '@ledvance/base/src/utils/common'
5
+ import { useIsFocused } from '@react-navigation/native'
6
+ import { useInterval, useReactive, useUpdateEffect } from 'ahooks'
7
+ import dayjs from 'dayjs'
8
+ import { useCallback, useEffect } from 'react'
9
+ import { ChartType, DateType } from '../co2Data'
10
+ import { getElectricity, getPowerData, PowerDataItem } from '../EnergyConsumptionActions'
11
+ import { EnergyConsumptionChartProps } from '../EnergyConsumptionChart'
12
+
13
+ export const useEnergyData = (params: EnergyConsumptionChartProps, devId: string) => {
14
+ const isFocused = useIsFocused()
15
+ const { addEleDpCode, powerDpCode, price, date, over365Days, over7Days, chartData } = params
16
+
17
+ const getDateType = useCallback((d: string) => {
18
+ const datejs = dayjs(d)
19
+ if (datejs.isValid()) {
20
+ if (datejs.format('YYYY') === d) return DateType.Year
21
+ if (datejs.format('YYYYMM') === d) return DateType.Month
22
+ if (datejs.format('YYYY/MM/DD') === d) return DateType.Day
23
+ }
24
+ return DateType.Day
25
+ }, [])
26
+
27
+ const state = useReactive({
28
+ loading: false,
29
+ isSupportLandscape: OrientationService.isSupported(),
30
+ isLandscape: false,
31
+ chartType: ChartType.kWh as ChartType,
32
+ dateType: getDateType(date),
33
+ date: date,
34
+ headlineText: params.headlineText,
35
+ chartData: chartData.filter((item) => getDateType(date) !== DateType.Year || item.headlineText.startsWith(date)),
36
+ price: isNaN(Number(price)) ? 0 : Number(price),
37
+ over365Days: over365Days,
38
+ over7Days: over7Days,
39
+ interval: 5,
40
+ powerData: [] as PowerDataItem[],
41
+ })
42
+
43
+ // 定时查询 DP 点
44
+ useInterval(() => {
45
+ if (isFocused) {
46
+ const jsonData = JSON.stringify([powerDpCode])
47
+ queryDpIds(jsonData, devId).then()
48
+ }
49
+ }, 5000, { immediate: true })
50
+
51
+ // 副作用:获取数据
52
+ useUpdateEffect(() => {
53
+ state.loading = true
54
+ if (state.chartType === ChartType.kWh) {
55
+ getElectricity(devId, addEleDpCode, state.date, state.dateType)
56
+ .then(res => {
57
+ state.chartData = res
58
+ state.loading = false
59
+ })
60
+ .catch(() => {
61
+ state.loading = false
62
+ })
63
+ } else {
64
+ getPowerData(devId, powerDpCode, state.interval)
65
+ .then((res: PowerDataItem[]) => {
66
+ state.powerData = res
67
+ state.loading = false
68
+ })
69
+ .catch(() => {
70
+ state.loading = false
71
+ })
72
+ }
73
+ }, [state.chartType, state.date, state.interval])
74
+
75
+ // 副作用:更新日期相关的状态和标题
76
+ const updateHeadlineText = useCallback((d: dayjs.Dayjs) => {
77
+ const year = d.year().toString()
78
+ const month = (d.month() + 1).toString().padStart(2, '0')
79
+ const day = d.date().toString().padStart(2, '0')
80
+ const dayOfWeek = d.day() % 7
81
+ switch (state.dateType) {
82
+ case DateType.Year:
83
+ state.headlineText = year
84
+ break
85
+ case DateType.Month:
86
+ state.headlineText = `${monthFormat(month)} ${year}`
87
+ break
88
+ case DateType.Day:
89
+ state.headlineText = `${day}.${month}.${year} (${loopsText[dayOfWeek]})`
90
+ break
91
+ }
92
+ }, [state.dateType])
93
+
94
+ useEffect(() => {
95
+ state.over365Days = overDays(state.date, 365)
96
+ state.over7Days = overDays(state.date, 7)
97
+ updateHeadlineText(dayjs(state.date))
98
+ }, [state.date])
99
+
100
+ // 副作用:根据 dateType 自动切换到今天的日期
101
+ useUpdateEffect(() => {
102
+ const d = dayjs()
103
+ const year = d.year().toString()
104
+ const month = (d.month() + 1).toString().padStart(2, '0')
105
+ const day = d.date().toString().padStart(2, '0')
106
+ switch (state.dateType) {
107
+ case DateType.Year:
108
+ state.date = year
109
+ break
110
+ case DateType.Month:
111
+ state.date = `${year}${month}`
112
+ break
113
+ case DateType.Day:
114
+ state.date = `${year}${month}${day}`
115
+ break
116
+ }
117
+ }, [state.dateType])
118
+
119
+ // 封装所有修改状态的函数
120
+ const actions = {
121
+ setChartType: useCallback((type: ChartType) => {
122
+ state.chartType = type
123
+ }, []),
124
+ setDate: useCallback((newDate: string) => {
125
+ state.date = newDate
126
+ }, []),
127
+ setDateType: useCallback((type: DateType) => {
128
+ state.dateType = type
129
+ }, []),
130
+ setInterval: useCallback((newInterval: number) => {
131
+ state.interval = newInterval
132
+ state.powerData = []
133
+ }, []),
134
+ toggleLandscape: useCallback(() => {
135
+ if (!state.isSupportLandscape) return
136
+ const newIsLandscape = !state.isLandscape
137
+ state.isLandscape = newIsLandscape
138
+ if (newIsLandscape) {
139
+ OrientationService.lockToLandscape()
140
+ } else {
141
+ OrientationService.lockToPortrait()
142
+ }
143
+ }, []),
144
+ }
145
+
146
+ return { state, actions }
147
+ }
@@ -0,0 +1,36 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { Dimensions, ScaledSize, Platform } from 'react-native'
3
+ import { Utils } from 'tuya-panel-kit'
4
+
5
+ const { width, height, statusBarHeight } = Utils.RatioUtils
6
+
7
+ export const useScreenDimensions = () => {
8
+ const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
9
+ const [screenData, setScreenData] = useState({
10
+ width: screenWidth,
11
+ height: screenHeight,
12
+ })
13
+ useEffect(() => {
14
+ if (Platform.OS === 'android') {
15
+ setScreenData({
16
+ width: height,
17
+ height: width - statusBarHeight,
18
+ })
19
+ }
20
+ const onChange = (result: { window: ScaledSize }) => {
21
+ if (Platform.OS === 'ios') {
22
+ setScreenData(result.window)
23
+ }
24
+ }
25
+ // Subscribe to dimension changes
26
+ Dimensions.addEventListener('change', onChange)
27
+ // Don't forget to unsubscribe on cleanup
28
+ return () => {
29
+ Dimensions.removeEventListener('change', onChange)
30
+ }
31
+ }, [])
32
+ return {
33
+ ...screenData,
34
+ isLandscape: screenData.width > screenData.height,
35
+ }
36
+ }
@@ -1,33 +1,5 @@
1
- import { queryDpIds } from '@ledvance/base/src/api/native'
2
- import InfoText from '@ledvance/base/src/components/InfoText'
3
- import Page from '@ledvance/base/src/components/Page'
4
- import Segmented from '@ledvance/base/src/components/Segmented'
5
- import Spacer from '@ledvance/base/src/components/Spacer'
6
- import ThemeType from '@ledvance/base/src/config/themeType'
7
- import I18n from '@ledvance/base/src/i18n'
8
- import { useDeviceId } from '@ledvance/base/src/models/modules/NativePropsSlice'
9
- import res from '@ledvance/base/src/res'
10
- import { loopsText, monthFormat } from '@ledvance/base/src/utils/common'
11
- import { overDays } from '@ledvance/base/src/utils'
12
- import { useIsFocused } from '@react-navigation/core'
13
- import PowerLineChart from './component/PowerLineChart'
14
- import { useRoute } from '@react-navigation/core'
15
- import { useInterval, useReactive, useUpdateEffect } from 'ahooks'
16
- import dayjs from 'dayjs'
17
- import React, { useCallback, useMemo } from 'react'
18
- import { Image, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
19
- import { Utils } from 'tuya-panel-kit'
20
- import { ChartType, DateType } from './co2Data'
21
- import DateSelectedItem from './component/DateSelectedItem'
22
- import DateSwitch from './component/DateSwitch'
23
- import DateTypeItem from './component/DateTypeItem'
24
- import NewBarChart from './component/NewBarChart'
25
- import { exportEnergyCsv, getElectricity, getPowerData, PowerDataItem } from './EnergyConsumptionActions'
26
1
  import { OverviewItem } from './EnergyConsumptionPage'
27
2
 
28
- const { convertX: cx } = Utils.RatioUtils
29
- const { withTheme } = Utils.ThemeUtils
30
-
31
3
  export interface EnergyConsumptionChartProps {
32
4
  addEleDpCode: string
33
5
  powerDpCode: string
@@ -40,302 +12,3 @@ export interface EnergyConsumptionChartProps {
40
12
  unit: string,
41
13
  date: string,
42
14
  }
43
-
44
- const EnergyConsumptionChart = (props: { theme?: ThemeType }) => {
45
- const devId = useDeviceId()
46
- const isFocused = useIsFocused()
47
-
48
- const params = useRoute().params as EnergyConsumptionChartProps
49
- const { backTitle, price, unit, date, addEleDpCode, powerDpCode, over365Days, over7Days } = params
50
-
51
- useInterval(() => {
52
- if (isFocused) {
53
- const jsonData = JSON.stringify([powerDpCode])
54
- queryDpIds(jsonData, devId).then()
55
- }
56
- },
57
- 5000,
58
- {immediate: true}
59
- )
60
- const styles = StyleSheet.create({
61
- listEmptyView: {
62
- alignItems: 'center',
63
- },
64
- listEmptyImage: {
65
- width: cx(200),
66
- height: cx(200),
67
- },
68
- listEmptyText: {
69
- flex: 0
70
- },
71
- downloadIcon: {
72
- width: cx(24),
73
- height: cx(24),
74
- tintColor: props.theme?.global.brand,
75
- position: 'absolute',
76
- right: 0,
77
- top: cx(10)
78
- },
79
- intervalContainer: {
80
- flexDirection: 'row',
81
- justifyContent: 'center',
82
- alignItems: 'center',
83
- },
84
- intervalItem: {
85
- flex: 1,
86
- marginHorizontal: cx(5),
87
- padding: cx(5),
88
- borderWidth: cx(1),
89
- borderRadius: cx(25),
90
- borderColor: props.theme?.icon.normal,
91
- },
92
- intervalItemChecked: {
93
- borderColor: props.theme?.icon.primary,
94
- },
95
- intervalText: {
96
- textAlign: 'center',
97
- color: props.theme?.global.fontColor,
98
- },
99
- intervalTextChecked: {
100
- textAlign: 'center',
101
- color: props.theme?.icon.primary,
102
- }
103
- })
104
-
105
- const getDateType = useCallback((date: string) => {
106
- const datejs = dayjs(date)
107
- if (datejs.isValid()) {
108
- if (datejs.format('YYYY') === date) {
109
- return DateType.Year
110
- } else if (datejs.format('YYYYMM') === date) {
111
- return DateType.Month
112
- } else if (datejs.format('YYYY/MM/DD') === date) {
113
- return DateType.Day
114
- }
115
- }
116
- return DateType.Day
117
- }, [])
118
- const dateType = getDateType(date)
119
- const intervalLabels = useMemo(() => {
120
- return ['charttime_type1', 'charttime_type2', 'charttime_type3', 'charttime_type4']
121
- }, [])
122
- const state = useReactive({
123
- loading: false,
124
- chartType: ChartType.kWh,
125
- dateType: dateType,
126
- date: date,
127
- headlineText: dateType === DateType.Year ? date : params.headlineText,
128
- chartData: params.chartData.filter((item) => {
129
- return dateType !== DateType.Year || item.headlineText.startsWith(date)
130
- }),
131
- price: isNaN(Number(price)) ? 0 : Number(price),
132
- over365Days: over365Days,
133
- over7Days: over7Days,
134
- interval: 5,
135
- powerData: [] as PowerDataItem[]
136
- })
137
-
138
- useUpdateEffect(() => {
139
- state.over365Days = overDays(state.date, 365)
140
- state.over7Days = overDays(state.date, 7)
141
- updateHeadlineText(dayjs(state.date))
142
- state.loading = true
143
- if (state.chartType === ChartType.kWh) {
144
- getElectricity(devId, addEleDpCode, state.date, state.dateType).then((res) => {
145
- state.chartData = res
146
- state.loading = false
147
- }).catch(() => {
148
- state.loading = false
149
- })
150
- } else {
151
- getPowerData(devId, powerDpCode, state.interval).then((res: PowerDataItem[]) => {
152
- state.powerData = res
153
- state.loading = false
154
- }).catch(() => {
155
- state.loading = false
156
- })
157
- }
158
- }, [state.chartType, state.date])
159
-
160
- const getEmptyDataTip = useCallback(() => {
161
- if (state.over365Days && state.dateType !== DateType.Day) {
162
- return I18n.getLang('energyconsumption_Daylimit')
163
- }
164
- if (state.dateType === DateType.Day && state.over7Days) {
165
- return I18n.getLang('energyconsumption_hourlylimit')
166
- }
167
- return I18n.getLang('energyconsumption_emptydata')
168
- }, [state.dateType, state.over365Days, state.over7Days])
169
-
170
- const updateHeadlineText = useCallback((date: dayjs.Dayjs) => {
171
- const year = date.year().toString()
172
- const month = (date.month() + 1).toString().padStart(2, '0')
173
- const day = date.date().toString().padStart(2, '0')
174
- const dayOfWeek = date.day() % 7
175
- switch (state.dateType) {
176
- case DateType.Year:
177
- state.headlineText = year
178
- break
179
- case DateType.Month:
180
- state.headlineText = `${monthFormat(month)} ${year}`
181
- break
182
- case DateType.Day:
183
- state.headlineText = `${day}.${month}.${year} (${loopsText[dayOfWeek]})`
184
- break
185
- }
186
- }, [state.dateType, state.headlineText])
187
-
188
- useUpdateEffect(() => {
189
- const date = dayjs()
190
- const year = date.year().toString()
191
- const month = (date.month() + 1).toString().padStart(2, '0')
192
- const day = date.date().toString().padStart(2, '0')
193
- switch (state.dateType) {
194
- case DateType.Year:
195
- state.date = year
196
- break
197
- case DateType.Month:
198
- state.date = `${year}${month}`
199
- break
200
- case DateType.Day:
201
- state.date = `${year}${month}${day}`
202
- break
203
- }
204
- }, [state.dateType])
205
-
206
- const requestPowerData = useCallback((interval: number) => {
207
- state.loading = true
208
- getPowerData(devId, powerDpCode, interval).then((res: PowerDataItem[]) => {
209
- state.powerData = res
210
- state.loading = false
211
- }).catch(() => {
212
- state.loading = false
213
- })
214
- }, [])
215
-
216
- return (
217
- <Page
218
- backText={backTitle}
219
- showGreenery={false}
220
- loading={state.loading}
221
- greeneryIcon={res.energy_consumption_greenery}
222
- >
223
- <ScrollView nestedScrollEnabled={true} style={{ marginHorizontal: cx(24) }}>
224
- <Spacer/>
225
-
226
- <Segmented
227
- options={[
228
- { label: I18n.getLang('chartdisplay_energy'), value: ChartType.kWh },
229
- { label: I18n.getLang('chartdisplay_power'), value: ChartType.Watt }
230
- ]}
231
- value={state.chartType}
232
- onChange={(chartType: ChartType) => {
233
- state.chartType = chartType
234
- }}/>
235
- {
236
- state.chartType === ChartType.kWh ? (
237
- <>
238
- <View style={{ width: '100%', flexDirection: 'row', marginVertical: cx(15) }}>
239
- <DateSwitch
240
- style={{ flex: 1 }}
241
- date={state.date}
242
- dateType={state.dateType}
243
- headlineText={state.headlineText}
244
- onDateChange={(date) => {
245
- state.date = date
246
- }}/>
247
- <TouchableOpacity
248
- style={{ width: cx(30) }}
249
- onPress={() => {
250
- const values = state.chartData.map(item => [item.key, item.value, (Number(params.price) * Number(item.value)).toFixed(2)])
251
- exportEnergyCsv(values, params.unit)
252
- }}>
253
- <Image
254
- style={styles.downloadIcon}
255
- source={{ uri: state.chartData?.length ? res.download_icon : undefined }}/>
256
- </TouchableOpacity>
257
- </View>
258
- <View style={{ flexDirection: 'row', marginBottom: cx(15) }}>
259
- <DateTypeItem
260
- style={{ flex: 1, marginHorizontal: cx(5) }}
261
- dateType={state.dateType}
262
- onDateTypeChange={(dateType) => {
263
- state.dateType = dateType
264
- }}/>
265
- <DateSelectedItem
266
- style={{ flex: 1 }}
267
- dateType={state.dateType}
268
- date={state.date}
269
- onDateChange={date => {
270
- state.date = date
271
- }}
272
- />
273
- </View>
274
- {
275
- (state.chartData.length <= 0) ? (
276
- <View style={styles.listEmptyView}>
277
- <Spacer height={cx(26)}/>
278
- <Image
279
- style={styles.listEmptyImage}
280
- source={{ uri: res.ldv_timer_empty }}/>
281
- <Spacer height={cx(14)}/>
282
- <InfoText
283
- text={getEmptyDataTip()}
284
- icon={res.ic_info}
285
- textStyle={styles.listEmptyText}
286
- contentColor={props.theme?.global.fontColor}
287
- />
288
- </View>
289
- ) : (
290
- !state.loading && <NewBarChart height={cx(400)} data={state.chartData} price={state.price} unit={unit}/>
291
- )
292
- }
293
- </>
294
- ) : (
295
- <>
296
- {
297
- (state.loading || state.powerData.length <= 0) ? (
298
- <View style={[styles.listEmptyView, {marginVertical: cx(70)}]}>
299
- <Spacer height={cx(26)}/>
300
- <Image
301
- style={styles.listEmptyImage}
302
- source={{ uri: res.ldv_timer_empty }}/>
303
- <Spacer height={cx(14)}/>
304
- <InfoText
305
- text={I18n.getLang('power_chart_empty')}
306
- icon={res.ic_info}
307
- textStyle={styles.listEmptyText}
308
- contentColor={props.theme?.global.fontColor}
309
- />
310
- </View>
311
- ) : (
312
- <PowerLineChart height={cx(400)} data={state.powerData}/>
313
- )
314
- }
315
- <View style={styles.intervalContainer}>
316
- {
317
- [24 * 60, 6 * 60, 60, 5].map((item, index) => {
318
- const isChecked = item === state.interval
319
- return (
320
- <TouchableOpacity
321
- key={index}
322
- style={[styles.intervalItem, isChecked ? styles.intervalItemChecked : {}]}
323
- onPress={() => {
324
- state.interval = item
325
- requestPowerData(item)
326
- }}>
327
- <Text
328
- style={isChecked ? styles.intervalTextChecked : styles.intervalText}>{I18n.getLang(intervalLabels[index])}</Text>
329
- </TouchableOpacity>)
330
- })
331
- }
332
- </View>
333
- </>
334
- )
335
- }
336
- </ScrollView>
337
- </Page>
338
- )
339
- }
340
-
341
- export default withTheme(EnergyConsumptionChart)
@@ -389,32 +389,36 @@ const EnergyConsumptionPage = (props: {theme?: ThemeType}) => {
389
389
  <Card
390
390
  style={styles.cardContainer}
391
391
  >
392
- <View style={{ flexDirection: 'row', alignItems: 'center' }}>
393
- <Text style={styles.cardTitle}>{I18n.getLang('consumption_data_field1_headline_text')}</Text>
394
- <View style={{ flexDirection: 'row', alignItems: 'flex-end', marginLeft: cx(10) }}>
395
- <Text style={[styles.consumptionNum, { fontSize: cx(30), marginRight: cx(8) }]}>{localeNumber(state.todayElectricity)}</Text>
396
- <Text style={[styles.consumptionNum, { fontSize: cx(20), marginBottom: cx(4) }]}>kWh</Text>
397
- </View>
398
- </View>
399
- <View style={{ flexDirection: 'row', alignItems: 'center' }}>
400
- <View style={{ flexDirection: 'row', alignItems: 'center' }}>
401
- <Text style={styles.cardTitle}>{I18n.getLang('consumption_data_field3_headline_text')}</Text>
402
- { state.energyGeneration.history.length > 0 && <TouchableOpacity
403
- onPress={() => {
404
- state.showPopup = true
405
- state.popupType = 'history'
406
- }}
407
- >
408
- <Image
409
- source={{uri: res.co2_icon}}
410
- resizeMode="contain"
411
- style={{ height: cx(20), width: cx(20), tintColor: props.theme?.button.primary }}
412
- />
413
- </TouchableOpacity>}
392
+ <View style={{ flexDirection: 'row', alignItems: 'center'}}>
393
+ <View>
394
+ <View style={{height: cx(35), justifyContent: 'center'}}>
395
+ <Text style={[styles.cardTitle]}>{I18n.getLang('consumption_data_field1_headline_text')}</Text>
396
+ </View>
397
+ <View style={{ flexDirection: 'row', alignItems: 'center', height: cx(35) }}>
398
+ <Text style={styles.cardTitle}>{I18n.getLang('consumption_data_field3_headline_text')}</Text>
399
+ { state.energyGeneration.history.length > 0 && <TouchableOpacity
400
+ onPress={() => {
401
+ state.showPopup = true
402
+ state.popupType = 'history'
403
+ }}
404
+ >
405
+ <Image
406
+ source={{uri: res.co2_icon}}
407
+ resizeMode="contain"
408
+ style={{ height: cx(20), width: cx(20), tintColor: props.theme?.button.primary }}
409
+ />
410
+ </TouchableOpacity>}
411
+ </View>
414
412
  </View>
415
- <View style={{ flexDirection: 'row', alignItems: 'flex-end', marginLeft: cx(10) }}>
416
- <Text style={[styles.consumptionNum, { fontSize: cx(30), marginRight: cx(8) }]}>{localeNumber(totalElectricity)}</Text>
417
- <Text style={[styles.consumptionNum, { fontSize: cx(20), marginBottom: cx(4) }]}>kWh</Text>
413
+ <View>
414
+ <View style={{ flexDirection: 'row', alignItems: 'flex-end', marginLeft: cx(10) }}>
415
+ <Text style={[styles.consumptionNum, { fontSize: cx(30), marginRight: cx(8) }]}>{localeNumber(state.todayElectricity)}</Text>
416
+ <Text style={[styles.consumptionNum, { fontSize: cx(20), marginBottom: cx(4) }]}>kWh</Text>
417
+ </View>
418
+ <View style={{ flexDirection: 'row', alignItems: 'flex-end', marginLeft: cx(10) }}>
419
+ <Text style={[styles.consumptionNum, { fontSize: cx(30), marginRight: cx(8) }]}>{localeNumber(totalElectricity)}</Text>
420
+ <Text style={[styles.consumptionNum, { fontSize: cx(20), marginBottom: cx(4) }]}>kWh</Text>
421
+ </View>
418
422
  </View>
419
423
  </View>
420
424
  <Spacer />
@@ -1,7 +1,7 @@
1
1
  import {NavigationRoute} from "tuya-panel-kit";
2
2
  import EnergyConsumptionPage from "./EnergyConsumptionPage";
3
3
  import EnergyConsumptionDetail from "./EnergyConsumptionDetail";
4
- import EnergyConsumptionChart from "./EnergyConsumptionChart";
4
+ import EnergyConsumptionChart from "./EnergyConsumptionChart/index";
5
5
  import {ui_biz_routerKey} from "../../navigation/Routers";
6
6
 
7
7
  const EnergyConsumptionPageRouters: NavigationRoute[] = [
@@ -5,7 +5,7 @@ import {DatePicker, Modal, Utils} from "tuya-panel-kit";
5
5
  import {useReactive} from "ahooks";
6
6
  import I18n from "@ledvance/base/src/i18n/index";
7
7
  import dayjs from "dayjs";
8
- import { DateType } from "@ledvance/ui-biz-bundle/src/newModules/energyConsumption/co2Data";
8
+ import { DateType } from "../co2Data";
9
9
 
10
10
  const {convertX: cx} = Utils.RatioUtils
11
11
  const {withTheme} = Utils.ThemeUtils
@@ -29,7 +29,7 @@ const PowerLineChart = (props: PowerLineChartProps) => {
29
29
  position: ['30%', '50%']
30
30
  },
31
31
  grid: {
32
- right: 0
32
+ right: '5%'
33
33
  },
34
34
  xAxis: {
35
35
  type: 'time',
@@ -92,7 +92,7 @@ const PowerLineChart = (props: PowerLineChartProps) => {
92
92
  customMapData: {}
93
93
  }
94
94
  return (
95
- <View style={{ flex: 1 }}>
95
+ <View style={{ height: height, width: '100%' }}>
96
96
  <ECharts
97
97
  option={option}
98
98
  ref={echarts}