@ledvance/ui-biz-bundle 1.1.156 → 1.1.158

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.156",
7
+ "version": "1.1.158",
8
8
  "scripts": {},
9
9
  "dependencies": {
10
10
  "@ledvance/base": "^1.x",
@@ -1,80 +1,144 @@
1
- import { StyleSheet, View, Text, ViewStyle, Image, TouchableOpacity } from 'react-native'
2
- import React from 'react'
3
1
  import Card from '@ledvance/base/src/components/Card'
4
- import { Utils } from 'tuya-panel-kit'
5
2
  import MoodColorsLine from '@ledvance/base/src/components/MoodColorsLine'
6
3
  import Spacer from '@ledvance/base/src/components/Spacer'
7
4
  import ThemeType from '@ledvance/base/src/config/themeType'
8
5
  import res from '@ledvance/base/src/res'
6
+ import React, { useMemo, useState } from 'react' // 重新引入 useState 和 LayoutChangeEvent
7
+ import { Image, LayoutChangeEvent, StyleSheet, Text, TouchableOpacity, View, ViewStyle } from 'react-native'
8
+ import { Utils } from 'tuya-panel-kit'
9
9
 
10
- const cx = Utils.RatioUtils.convertX
10
+ const { convertX: cx } = Utils.RatioUtils
11
11
  const { withTheme } = Utils.ThemeUtils
12
12
 
13
- interface RecommendMoodItemProps {
14
- theme?: ThemeType
15
- enable: boolean
16
- title: string
17
- colors: string[]
18
- icon?: number | string
19
- style?: ViewStyle
20
- onPress?: () => void
21
- onSwitch: (enable: boolean) => void
13
+ const CONTENT_HORIZONTAL_MARGIN = cx(16)
14
+
15
+ interface FlagItemProps {
16
+ theme?: ThemeType;
17
+ enable: boolean;
18
+ title: string;
19
+ colors: string[];
20
+ icon?: number | string;
21
+ style?: ViewStyle;
22
+ containerWidth?: number; // 1. 将 containerWidth 变为可选属性
23
+ onPress?: () => void;
24
+ onSwitch: (enable: boolean) => void;
22
25
  }
23
26
 
24
- function FlagItem(props: RecommendMoodItemProps) {
25
- const icon = typeof props.icon === 'number' ? props.icon : { uri: props.icon }
27
+ const getStyles = (theme?: ThemeType) => StyleSheet.create({
28
+ // ... 样式定义保持不变 ...
29
+ card: {
30
+ marginHorizontal: cx(24),
31
+ // 移除 Card 的水平内边距,以便我们能测量到完整的宽度
32
+ paddingHorizontal: 0,
33
+ },
34
+ headline: {
35
+ flexDirection: 'row',
36
+ marginHorizontal: CONTENT_HORIZONTAL_MARGIN,
37
+ justifyContent: 'space-between',
38
+ alignItems: 'center',
39
+ },
40
+ headText: {
41
+ color: theme?.global.fontColor,
42
+ fontSize: cx(16),
43
+ lineHeight: cx(20),
44
+ flex: 1,
45
+ },
46
+ checkbox: {
47
+ width: cx(45),
48
+ height: cx(45),
49
+ justifyContent: 'center',
50
+ alignItems: 'flex-end',
51
+ },
52
+ checkboxImage: {
53
+ width: cx(44),
54
+ height: cx(44),
55
+ },
56
+ lineContainer: {
57
+ marginHorizontal: CONTENT_HORIZONTAL_MARGIN,
58
+ minHeight: cx(24),
59
+ },
60
+ icon: {
61
+ width: cx(60),
62
+ aspectRatio: 2.14,
63
+ marginRight: cx(10),
64
+ },
65
+ titleContainer: {
66
+ flexDirection: 'row',
67
+ flex: 1,
68
+ paddingRight: cx(5),
69
+ justifyContent: 'flex-start',
70
+ alignItems: 'center',
71
+ }
72
+ })
73
+
74
+ function FlagItem(props: FlagItemProps) {
75
+ const { theme, containerWidth, enable, title, colors, icon: iconProp, style, onPress, onSwitch } = props
76
+ const styles = useMemo(() => getStyles(theme), [theme])
77
+ const icon = typeof iconProp === 'number' ? iconProp : { uri: iconProp }
78
+
79
+ // 2. 引入 state 用于存储内部测量的宽度
80
+ const [selfMeasuredWidth, setSelfMeasuredWidth] = useState(0)
81
+
82
+ // 3. 创建 onLayout 回调,用于自我测量
83
+ const handleLayout = (event: LayoutChangeEvent) => {
84
+ // 如果父组件已经提供了宽度,则忽略内部测量,避免冲突
85
+ if (containerWidth && containerWidth > 0) {
86
+ return
87
+ }
88
+ const { width: cardWidth } = event.nativeEvent.layout
89
+ if (cardWidth > 0 && cardWidth !== selfMeasuredWidth) {
90
+ setSelfMeasuredWidth(cardWidth)
91
+ }
92
+ }
26
93
 
27
- const styles = StyleSheet.create({
28
- card: {
29
- marginHorizontal: cx(24),
30
- },
31
- headline: {
32
- flexDirection: 'row',
33
- marginHorizontal: cx(16),
34
- justifyContent: 'space-between',
35
- alignItems: 'center',
36
- },
37
- headText: {
38
- color: props.theme?.global.fontColor,
39
- fontSize: cx(16),
40
- // fontFamily: 'helvetica_neue_lt_std_bd',
41
- lineHeight: cx(20),
42
- flex: 1
43
- },
44
- checkbox: {
45
- width: cx(45),
46
- height: cx(45),
47
- marginTop: cx(-5),
48
- marginBottom: cx(-10),
49
- },
50
- gradientItem: {
51
- alignItems: 'center',
52
- },
53
- })
94
+ // 4. 决定最终使用的宽度
95
+ const finalLineWidth = useMemo(() => {
96
+ // 优先使用父组件传入的宽度(适用于 FlatList)
97
+ if (containerWidth && containerWidth > 0) {
98
+ // 这里的计算需要注意:父组件传来的 containerWidth 是整个屏幕的宽度
99
+ // 所以需要减去 Card 的外边距
100
+ const cardOuterMargin = cx(24)
101
+ return containerWidth - (cardOuterMargin * 2) - (CONTENT_HORIZONTAL_MARGIN * 2)
102
+ }
103
+ // 其次使用自我测量的宽度(适用于普通 View)
104
+ if (selfMeasuredWidth > 0) {
105
+ // selfMeasuredWidth 是 Card 的宽度,所以只需减去内容的内边距
106
+ return selfMeasuredWidth - (CONTENT_HORIZONTAL_MARGIN * 2)
107
+ }
108
+ // 如果都没有,则返回 0
109
+ return 0
110
+ }, [containerWidth, selfMeasuredWidth])
54
111
 
55
112
  return (
56
- <Card
57
- style={[styles.card, props.style]}
58
- onPress={props.onPress}>
59
- <View accessibilityLabel={'FlagItem'} accessibilityHint={`${props.title}`}>
60
- <Spacer height={cx(16)} />
113
+ // 5. 将 onLayout 绑定到 Card 上,让组件能自我测量
114
+ <Card style={[styles.card, style]} onPress={onPress} onLayout={handleLayout}>
115
+ <View accessibilityLabel={'FlagItem'} accessibilityHint={`${title}`}>
116
+ <Spacer height={cx(8)}/>
61
117
  <View style={styles.headline}>
62
- <View style={{ flexDirection: 'row', flex: 1, paddingRight: cx(5), justifyContent: 'flex-start' }}>
63
- {props.title ? <Text style={styles.headText}>{props.title}</Text> : undefined}
64
- {props.icon ? <Image source={icon} style={{ width: cx(60), aspectRatio: 2.14, marginRight: cx(10) }} /> : undefined}
118
+ <View style={styles.titleContainer}>
119
+ {title ? <Text style={styles.headText}>{title}</Text> : null}
120
+ {iconProp ? <Image source={icon} style={styles.icon}/> : null}
65
121
  </View>
66
- <TouchableOpacity style={styles.checkbox} onPress={() => props.onSwitch(!props.enable)}>
67
- <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}]} />
122
+ <TouchableOpacity style={styles.checkbox} onPress={() => onSwitch(!enable)}>
123
+ <Image
124
+ source={{ uri: res.ic_check }}
125
+ style={[styles.checkboxImage, { tintColor: enable ? theme?.icon.primary : theme?.icon.disable }]}
126
+ />
68
127
  </TouchableOpacity>
69
128
  </View>
70
- <Spacer />
71
- <View style={styles.gradientItem}>
72
- <MoodColorsLine
73
- type={'separate'}
74
- nodeStyle={{ borderColor: props.theme?.card.border, borderWidth: 1 }}
75
- colors={props.colors} />
129
+ <Spacer/>
130
+ <View style={styles.lineContainer}>
131
+ {/* 6. 使用最终计算出的宽度进行渲染 */}
132
+ {finalLineWidth > 0 && (
133
+ <MoodColorsLine
134
+ type={'separate'}
135
+ width={finalLineWidth}
136
+ nodeStyle={{ borderColor: theme?.card.border, borderWidth: 1 }}
137
+ colors={colors}
138
+ />
139
+ )}
76
140
  </View>
77
- <Spacer height={cx(16)} />
141
+ <Spacer height={cx(16)}/>
78
142
  </View>
79
143
  </Card>
80
144
  )
@@ -1,28 +1,33 @@
1
- import React, { useCallback, useEffect, useMemo } from "react";
2
- import Page from "@ledvance/base/src/components/Page";
3
- import { useDeviceId, useDeviceInfo, useFlagMode, useFlags } from "@ledvance/base/src/models/modules/NativePropsSlice";
4
- import { FlatList, Image, TouchableOpacity, View } from "react-native";
5
- import Spacer from "@ledvance/base/src/components/Spacer";
6
- import { Utils } from "tuya-panel-kit";
7
- import FlagItem from "./FlagItem";
8
- import { FlagUiInfo } from "./FlagInfo";
9
- import { getRemoteFlag, saveFlag, saveFlagMode, useFlag } from "./FlagActions";
10
- import { useRoute, useNavigation } from '@react-navigation/core'
11
- import I18n from "@ledvance/base/src/i18n";
12
- import { useReactive, useUpdateEffect } from "ahooks";
13
- import { cloneDeep, difference, isEqual, last, map, range } from "lodash";
14
- import { ui_biz_routerKey } from "../../navigation/Routers";
15
- import res from "@ledvance/base/src/res";
16
- import { hsv2Hex } from "@ledvance/base/src/utils";
17
- import { SceneNodeTransitionMode } from "@ledvance/ui-biz-bundle/src/modules/scene/SceneInfo";
18
- import { Result } from "@ledvance/base/src/models/modules/Result";
19
- import { useDps } from "@ledvance/base/src/models/modules/NativePropsSlice";
20
- import { ColorUtils, WORK_MODE } from "@tuya/tuya-panel-lamp-sdk/lib/utils";
21
- import TextField from "@ledvance/base/src/components/TextField";
22
- import { showDialog } from "@ledvance/base/src/utils/common";
23
- import { defFlagHash, fnv1aHash } from "./FlagHash";
24
- import { NativeApi } from "@ledvance/base/src/api/native";
1
+ import { NativeApi } from '@ledvance/base/src/api/native'
2
+ import Page from '@ledvance/base/src/components/Page'
3
+ import Spacer from '@ledvance/base/src/components/Spacer'
4
+ import TextField from '@ledvance/base/src/components/TextField'
25
5
  import ThemeType from '@ledvance/base/src/config/themeType'
6
+ import I18n from '@ledvance/base/src/i18n'
7
+ import {
8
+ useDeviceId,
9
+ useDeviceInfo,
10
+ useDps,
11
+ useFlagMode,
12
+ useFlags
13
+ } from '@ledvance/base/src/models/modules/NativePropsSlice'
14
+ import { Result } from '@ledvance/base/src/models/modules/Result'
15
+ import res from '@ledvance/base/src/res'
16
+ import { hsv2Hex } from '@ledvance/base/src/utils'
17
+ import { showDialog } from '@ledvance/base/src/utils/common'
18
+ import { useNavigation, useRoute } from '@react-navigation/core'
19
+ import { ColorUtils, WORK_MODE } from '@tuya/tuya-panel-lamp-sdk/lib/utils'
20
+ import { useReactive, useUpdateEffect } from 'ahooks'
21
+ import { cloneDeep, difference, isEqual, last, map, range } from 'lodash'
22
+ import React, { useCallback, useEffect, useMemo, useState } from 'react'
23
+ import { FlatList, Image, TouchableOpacity, View } from 'react-native'
24
+ import { Utils } from 'tuya-panel-kit'
25
+ import { ui_biz_routerKey } from '../../navigation/Routers'
26
+ import { SceneNodeTransitionMode } from '../scene/SceneInfo'
27
+ import { getRemoteFlag, saveFlag, saveFlagMode, useFlag } from './FlagActions'
28
+ import { defFlagHash, fnv1aHash } from './FlagHash'
29
+ import { FlagUiInfo } from './FlagInfo'
30
+ import FlagItem from './FlagItem'
26
31
 
27
32
  const cx = Utils.RatioUtils.convertX
28
33
  const { withTheme } = Utils.ThemeUtils
@@ -100,6 +105,16 @@ const FlagPage = (props: { theme?: ThemeType }) => {
100
105
  state.filterFlags = state.searchText !== '' ? cloneDeep(state.flags).filter(flag => (flag.name ?? '').toLowerCase().includes(state.searchText.toLowerCase())) : cloneDeep(state.flags)
101
106
  }, [state.searchText, state.flags])
102
107
 
108
+ // 3. 在父组件中创建 state 存储容器宽度
109
+ const [listContainerWidth, setListContainerWidth] = useState(0)
110
+ // 4. 创建 onLayout 回调函数
111
+ const handleLayout = (event: LayoutChangeEvent) => {
112
+ const { width } = event.nativeEvent.layout
113
+ if (width > 0 && width !== listContainerWidth) {
114
+ setListContainerWidth(width)
115
+ }
116
+ }
117
+
103
118
  const getRemoteFlagInfo = async (isRefresh?: boolean) => {
104
119
  const res = await getRemoteFlag(devInfo.devId, isRefresh)
105
120
  if (res.success) {
@@ -175,7 +190,7 @@ const FlagPage = (props: { theme?: ThemeType }) => {
175
190
  })
176
191
  }
177
192
 
178
- const getItemEnable = useCallback((flagItem: FlagUiInfo) =>{
193
+ const getItemEnable = useCallback((flagItem: FlagUiInfo) => {
179
194
  const workMode = params.isCeilingLight ? WORK_MODE.WHITE : params.isStripLight ? WORK_MODE.COLOUR : WORK_MODE.SCENE
180
195
  const idEnable = params.isSolarLight ? (flagMode?.flagId === flagItem.id) : (flagId === flagItem.id)
181
196
  return flagMode?.flagMode && dps[params.workModeCode] === workMode && idEnable && dps[params.switchLedCode]
@@ -198,67 +213,75 @@ const FlagPage = (props: { theme?: ThemeType }) => {
198
213
  }
199
214
  }}
200
215
  >
201
- <View style={{ flexDirection: 'row', display: 'flex', alignItems: 'center' }}>
202
- <TextField
203
- value={state.searchText}
204
- onChangeText={text => {
205
- state.searchText = text
206
- }}
207
- placeholder={I18n.getLang('country_selection_textfield_headline_search')}
208
- style={{ marginHorizontal: cx(24), flex: 1 }}
209
- />
210
- <TouchableOpacity
211
- style={{ paddingRight: cx(24) }}
212
- accessibilityLabel={'RefreshButton'}
213
- accessibilityHint={'RefreshButton'}
214
- onPress={() => {
215
- showDialog({
216
- method: 'confirm',
217
- title: I18n.getLang('flag_resetbutton'),
218
- subTitle: I18n.getLang('flag_resetdescription'),
219
- onConfirm: async (_, { close }) => {
220
- close()
221
- state.loading = true
222
- await getRemoteFlagInfo(true)
223
- state.loading = false
224
- }
225
- })
226
- }}
227
- >
228
- <Image source={{ uri: res.ic_refresh}} style={{ width: cx(24), height: cx(24), tintColor: props.theme?.global.fontColor }} />
229
- </TouchableOpacity>
216
+ <View style={{ flex: 1 }} onLayout={handleLayout}>
217
+ <View style={{ flexDirection: 'row', display: 'flex', alignItems: 'center' }}>
218
+ <TextField
219
+ value={state.searchText}
220
+ onChangeText={text => {
221
+ state.searchText = text
222
+ }}
223
+ placeholder={I18n.getLang('country_selection_textfield_headline_search')}
224
+ style={{ marginHorizontal: cx(24), flex: 1 }}
225
+ />
226
+ <TouchableOpacity
227
+ style={{ paddingRight: cx(24) }}
228
+ accessibilityLabel={'RefreshButton'}
229
+ accessibilityHint={'RefreshButton'}
230
+ onPress={() => {
231
+ showDialog({
232
+ method: 'confirm',
233
+ title: I18n.getLang('flag_resetbutton'),
234
+ subTitle: I18n.getLang('flag_resetdescription'),
235
+ onConfirm: async (_, { close }) => {
236
+ close()
237
+ state.loading = true
238
+ await getRemoteFlagInfo(true)
239
+ state.loading = false
240
+ }
241
+ })
242
+ }}
243
+ >
244
+ <Image source={{ uri: res.ic_refresh }}
245
+ style={{ width: cx(24), height: cx(24), tintColor: props.theme?.global.fontColor }}/>
246
+ </TouchableOpacity>
247
+ </View>
248
+ {/* 6. 只有在获取到有效宽度后才渲染 FlatList */}
249
+ {listContainerWidth > 0 && (
250
+ <FlatList
251
+ data={state.filterFlags}
252
+ renderItem={({ item }) => <FlagItem
253
+ enable={getItemEnable(item)}
254
+ title={item.name}
255
+ icon={item.icon}
256
+ colors={item.colors.map(item => hsv2Hex(item.h, item.s, item.v)).reverse()}
257
+ // 7. 【关键】将测量到的宽度传递给子组件
258
+ containerWidth={listContainerWidth}
259
+ onSwitch={async (enable) => {
260
+ if (enable) {
261
+ state.loading = true
262
+ await setFlag(item)
263
+ const flagHash = fnv1aHash(JSON.stringify(item.colors))
264
+ if (defFlagHash[flagHash]) {
265
+ NativeApi.putFlagFirebase({
266
+ country_flag: defFlagHash[flagHash].EN,
267
+ id: defFlagHash[flagHash].id
268
+ }).then()
269
+ }
270
+ state.loading = false
271
+ updateFlagMode(true, item)
272
+ }
273
+ }}
274
+ onPress={() => {
275
+ navigationRoute('edit', item)
276
+ }}
277
+ />}
278
+ keyExtractor={item => item.name}
279
+ ListHeaderComponent={() => (<Spacer height={cx(10)}/>)}
280
+ ItemSeparatorComponent={() => (<Spacer/>)}
281
+ ListFooterComponent={() => (<Spacer/>)}
282
+ />
283
+ )}
230
284
  </View>
231
- <FlatList
232
- data={state.filterFlags}
233
- renderItem={({ item }) => <FlagItem
234
- enable={getItemEnable(item)}
235
- title={item.name}
236
- icon={item.icon}
237
- colors={item.colors.map(item => hsv2Hex(item.h, item.s, item.v)).reverse()}
238
- onSwitch={async (enable) => {
239
- if (enable) {
240
- state.loading = true
241
- await setFlag(item)
242
- const flagHash = fnv1aHash(JSON.stringify(item.colors))
243
- if (defFlagHash[flagHash]) {
244
- NativeApi.putFlagFirebase({
245
- country_flag: defFlagHash[flagHash].EN,
246
- id: defFlagHash[flagHash].id
247
- }).then()
248
- }
249
- state.loading = false
250
- updateFlagMode(true, item)
251
- }
252
- }}
253
- onPress={() => {
254
- navigationRoute('edit', item)
255
- }}
256
- />}
257
- keyExtractor={item => item.name}
258
- ListHeaderComponent={() => (<Spacer height={cx(10)} />)}
259
- ItemSeparatorComponent={() => (<Spacer />)}
260
- ListFooterComponent={() => (<Spacer />)}
261
- />
262
285
  </Page>
263
286
  )
264
287
  }
@@ -187,6 +187,7 @@ const SwitchHistoryPage = (props: { theme?: ThemeType }) => {
187
187
  backText={backText || deviceInfo.name}
188
188
  headlineText={headlineText || I18n.getLang('history_socket_headline_text')}
189
189
  headlineIcon={res.download_icon}
190
+ accessibilityHint={I18n.getLang('history_socket_headline_text')}
190
191
  onHeadlineIconClick={downFile}
191
192
  info={<FeatureInfo title={I18n.getLang('history_socket_headline_text')} content={I18n.getLang('infobutton_history')} />}
192
193
  >
@@ -553,16 +553,17 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
553
553
  {setTimer(item?.name)}
554
554
  </Text>
555
555
  </View>
556
- <View
557
- style={{
558
- width: cx(295),
559
- height: cx(24),
560
- backgroundColor: bgColor,
561
- marginLeft: cx(20),
562
- marginTop: cx(5),
563
- marginBottom: cx(24),
564
- borderRadius: cx(8),
565
- }}/>
556
+ <View style={{ width: '100%', paddingHorizontal: cx(20), }}>
557
+ <View
558
+ style={{
559
+ width: '100%',
560
+ height: cx(24),
561
+ backgroundColor: bgColor,
562
+ marginTop: cx(5),
563
+ marginBottom: cx(24),
564
+ borderRadius: cx(8),
565
+ }}/>
566
+ </View>
566
567
  </View>
567
568
  </Card>
568
569
  )
@@ -21,7 +21,8 @@ export const getStyles = (theme: ThemeType | undefined) => StyleSheet.create({
21
21
  justifyContent: 'flex-end',
22
22
  // marginHorizontal: cx(24),
23
23
  marginTop: cx(15),
24
- marginBottom: cx(-15)
24
+ marginBottom: cx(-15),
25
+ zIndex: 9999
25
26
  },
26
27
  downloadIcon: {
27
28
  width: cx(24),
@@ -1,32 +1,27 @@
1
- import Strings from '@ledvance/base/src/i18n';
2
- import Page from '@ledvance/base/src/components/Page';
3
- import React, { useCallback } from 'react';
4
- import { FlatList, StyleSheet, Text, View } from 'react-native';
5
- import { Utils } from 'tuya-panel-kit';
6
- import RecommendMoodItem from './RecommendMoodItem';
7
- import Spacer from '@ledvance/base/src/components/Spacer';
8
- import { useReactive } from 'ahooks';
9
- import { useNavigation, useRoute } from '@react-navigation/native';
10
- import {
11
- MoodPageParams,
12
- MoodNodeTransitionMode,
13
- MoodUIInfo,
14
- MoodJumpGradientMode,
15
- } from './Interface';
16
- import { Result } from '@ledvance/base/src/models/modules/Result';
17
- import { ui_biz_routerKey } from '../../navigation/Routers'
18
- import { difference, head, range } from 'lodash';
19
- import { RecommendMood, getRecommendMixMoods, getRecommendMoods } from './MoodInfo';
1
+ import Page from '@ledvance/base/src/components/Page'
2
+ import Spacer from '@ledvance/base/src/components/Spacer'
20
3
  import ThemeType from '@ledvance/base/src/config/themeType'
21
-
22
- const cx = Utils.RatioUtils.convertX;
4
+ import Strings from '@ledvance/base/src/i18n'
5
+ import { Result } from '@ledvance/base/src/models/modules/Result'
6
+ import { useNavigation, useRoute } from '@react-navigation/native'
7
+ import { useReactive } from 'ahooks'
8
+ import { difference, head, range } from 'lodash'
9
+ import React, { useCallback, useState } from 'react'
10
+ import { FlatList, LayoutChangeEvent, StyleSheet, Text, View } from 'react-native'
11
+ import { Utils } from 'tuya-panel-kit'
12
+ import { ui_biz_routerKey } from '../../navigation/Routers'
13
+ import { MoodJumpGradientMode, MoodNodeTransitionMode, MoodPageParams, MoodUIInfo, } from './Interface'
14
+ import { getRecommendMixMoods, getRecommendMoods, RecommendMood } from './MoodInfo'
15
+ import RecommendMoodItem from './RecommendMoodItem'
16
+ import {xLog} from "@ledvance/base/src/utils"
17
+ const cx = Utils.RatioUtils.convertX
23
18
  const { withTheme } = Utils.ThemeUtils
24
19
 
25
20
  export interface AddMoodPageParams {
26
21
  isStatic: boolean;
27
22
  moodIds: number[];
28
23
  moduleParams: MoodPageParams;
29
- nameRepeat: (mood: MoodUIInfo) => boolean
24
+ nameRepeat: (mood: MoodUIInfo) => boolean;
30
25
  modDeleteMood: (mode: 'add' | 'edit' | 'del', currentMood: MoodUIInfo) => Promise<Result<any>>;
31
26
  }
32
27
 
@@ -35,16 +30,23 @@ interface AddMoodPageState {
35
30
  }
36
31
 
37
32
  const AddMoodPage = (props: { theme?: ThemeType }) => {
38
- const navigation = useNavigation();
39
- const routeParams = useRoute().params as AddMoodPageParams;
40
- const moduleParams = routeParams.moduleParams;
41
- const isMix = !!(moduleParams.isMixLight || moduleParams.isCeilingLight);
33
+ const navigation = useNavigation()
34
+ const routeParams = useRoute().params as AddMoodPageParams
35
+ const moduleParams = routeParams.moduleParams
36
+ const isMix = !!(moduleParams.isMixLight || moduleParams.isCeilingLight)
37
+ xLog('isMix', isMix, moduleParams.isMixLight, moduleParams.isCeilingLight)
42
38
  const state = useReactive<AddMoodPageState>({
43
39
  data: isMix
44
40
  ? getRecommendMixMoods(routeParams.isStatic, moduleParams)
45
41
  : getRecommendMoods(routeParams.isStatic, moduleParams),
46
- });
47
-
42
+ })
43
+ const [containerWidth, setContainerWidth] = useState(0)
44
+ const handleLayout = (event: LayoutChangeEvent) => {
45
+ const { width } = event.nativeEvent.layout
46
+ if (width > 0 && width !== containerWidth) {
47
+ setContainerWidth(width)
48
+ }
49
+ }
48
50
  const getFormateItem = item => {
49
51
  return {
50
52
  ...item,
@@ -54,32 +56,34 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
54
56
  mode: 0,
55
57
  speed: 75,
56
58
  },
57
- };
58
- };
59
-
59
+ }
60
+ }
60
61
  const onMoodItemClick = useCallback(
61
62
  (moodItem: RecommendMood) => {
62
- const idRange = range(0, 256);
63
- const mainId: number = head(difference(idRange, routeParams.moodIds)) || 0;
63
+ const idRange = range(0, 256)
64
+ const mainId: number = head(difference(idRange, routeParams.moodIds)) || 0
64
65
  const secondaryId: number = moduleParams.isCeilingLight ? head(difference(idRange, [...routeParams.moodIds, mainId])) || 0 : 0
65
66
  const url = routeParams.isStatic
66
67
  ? ui_biz_routerKey.ui_biz_static_mood_edit
67
68
  : !!(moduleParams.isCeilingLight || moduleParams.isMixLight)
68
69
  ? ui_biz_routerKey.ui_biz_dynamic_mix_mood_edit
69
- : ui_biz_routerKey.ui_biz_dynamic_mood_edit;
70
+ : ui_biz_routerKey.ui_biz_dynamic_mood_edit
70
71
  const currentMood = moodItem.mainLamp
71
- ? { ...moodItem, id: mainId, mainLamp: { ...moodItem.mainLamp, id: mainId }, secondaryLamp: { ...moodItem.secondaryLamp, id: moodItem.secondaryLamp?.nodes?.length ? secondaryId : -1 } }
72
- : newMood(mainId, secondaryId, moduleParams.isSupportColor, routeParams.isStatic, moduleParams);
72
+ ? {
73
+ ...moodItem,
74
+ id: mainId,
75
+ mainLamp: { ...moodItem.mainLamp, id: mainId },
76
+ secondaryLamp: { ...moodItem.secondaryLamp, id: moodItem.secondaryLamp?.nodes?.length ? secondaryId : -1 }
77
+ }
78
+ : newMood(mainId, secondaryId, moduleParams.isSupportColor, routeParams.isStatic, moduleParams)
73
79
  navigation.navigate(url, {
74
80
  ...routeParams,
75
81
  mode: 'add',
76
82
  currentMood,
77
- });
83
+ })
78
84
  },
79
85
  [routeParams]
80
- );
81
-
82
-
86
+ )
83
87
  const styles = StyleSheet.create({
84
88
  root: {
85
89
  flex: 1,
@@ -92,8 +96,6 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
92
96
  marginTop: cx(12),
93
97
  },
94
98
  })
95
-
96
-
97
99
  return (
98
100
  <Page
99
101
  backText={Strings.getLang('add_new_static_mood_system_back')}
@@ -103,7 +105,7 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
103
105
  : 'add_new_dynamic_mood_headline_text'
104
106
  )}
105
107
  >
106
- <View style={styles.root}>
108
+ <View style={styles.root} onLayout={handleLayout}>
107
109
  <Text style={styles.desc}>
108
110
  {Strings.getLang(
109
111
  routeParams.isStatic
@@ -111,32 +113,31 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
111
113
  : 'add_new_dynamic_mood_description_text'
112
114
  )}
113
115
  </Text>
114
- <FlatList
115
- style={{ flex: 1 }}
116
- data={state.data}
117
- renderItem={({ item }) => {
118
- return (
116
+ {containerWidth > 0 && (
117
+ <FlatList
118
+ style={{ flex: 1 }}
119
+ data={state.data}
120
+ renderItem={({ item }) => (
119
121
  <RecommendMoodItem
120
122
  title={item.name}
121
123
  isMix={isMix}
122
- mood={getFormateItem(item)}
124
+ mood={item}
123
125
  deviceTypeOption={moduleParams}
124
- onPress={() => {
125
- onMoodItemClick(item);
126
- }}
126
+ onPress={() => onMoodItemClick(item)}
127
+ // 5. 将测量到的宽度传递给子组件
128
+ containerWidth={containerWidth}
127
129
  />
128
- );
129
- }}
130
- ItemSeparatorComponent={() => <Spacer />}
131
- ListHeaderComponent={() => <Spacer />}
132
- ListFooterComponent={() => <Spacer />}
133
- keyExtractor={item => item.name}
134
- />
130
+ )}
131
+ ItemSeparatorComponent={() => <Spacer/>}
132
+ ListHeaderComponent={() => <Spacer/>}
133
+ ListFooterComponent={() => <Spacer/>}
134
+ keyExtractor={item => item.name}
135
+ />
136
+ )}
135
137
  </View>
136
138
  </Page>
137
- );
138
- };
139
-
139
+ )
140
+ }
140
141
  export default withTheme(AddMoodPage)
141
142
 
142
143
  const defStripConfig = {
@@ -161,7 +162,7 @@ function newMood(
161
162
  ? MoodJumpGradientMode.StringJump
162
163
  : moduleParams.isStripLight
163
164
  ? MoodJumpGradientMode.StripJump
164
- : MoodJumpGradientMode.SourceJump;
165
+ : MoodJumpGradientMode.SourceJump
165
166
  const node = {
166
167
  brightness: 100,
167
168
  colorTemp: 100,
@@ -169,7 +170,7 @@ function newMood(
169
170
  s: 100,
170
171
  v: 100,
171
172
  isColorNode: !!(moduleParams.isCeilingLight || moduleParams.isMixLight) ? false : isColorMode,
172
- };
173
+ }
173
174
  return {
174
175
  version: 0,
175
176
  name: '',
@@ -194,5 +195,5 @@ function newMood(
194
195
  version: 1,
195
196
  nodes: isStatic ? [] : [{ ...node, isColorNode: true }, { ...node, isColorNode: true }],
196
197
  },
197
- };
198
+ }
198
199
  }
@@ -1,171 +1,220 @@
1
- import React, { useMemo } from 'react';
2
- import { StyleSheet, Text, View, ViewProps, ViewStyle, Image, TouchableOpacity } from 'react-native';
3
- import { Utils } from 'tuya-panel-kit';
4
- import { hsv2Hex } from '@ledvance/base/src/utils';
5
- import { cctToColor } from '@ledvance/base/src/utils/cctUtils';
6
- import { mapFloatToRange } from '@ledvance/base/src/utils';
7
1
  import Card from '@ledvance/base/src/components/Card';
8
- import Spacer from '@ledvance/base/src/components/Spacer';
9
2
  import MoodColorsLine from '@ledvance/base/src/components/MoodColorsLine';
10
- import { MoodJumpGradientMode, MoodLampInfo, MoodUIInfo } from './Interface';
3
+ import Spacer from '@ledvance/base/src/components/Spacer';
4
+ import ThemeType from '@ledvance/base/src/config/themeType';
11
5
  import I18n from '@ledvance/base/src/i18n';
12
6
  import res from '@ledvance/base/src/res';
13
- import ThemeType from '@ledvance/base/src/config/themeType'
7
+ import { hsv2Hex, mapFloatToRange } from '@ledvance/base/src/utils';
8
+ import { cctToColor } from '@ledvance/base/src/utils/cctUtils';
9
+ import React, { useMemo, useState } from 'react';
10
+ import { Image, LayoutChangeEvent, StyleSheet, Text, TouchableOpacity, View, ViewProps, ViewStyle } from 'react-native';
11
+ import { Utils } from 'tuya-panel-kit';
12
+ import { MoodJumpGradientMode, MoodLampInfo, MoodUIInfo } from './Interface';
14
13
 
15
14
  const cx = Utils.RatioUtils.convertX;
16
- const { withTheme } = Utils.ThemeUtils
15
+ const { withTheme } = Utils.ThemeUtils;
17
16
 
18
17
  interface LightCategory {
19
- isMixLight?: boolean
20
- isStripLight?: boolean
21
- isStringLight?: boolean
22
- isCeilingLight?: boolean
18
+ isMixLight?: boolean;
19
+ isStripLight?: boolean;
20
+ isStringLight?: boolean;
21
+ isCeilingLight?: boolean;
23
22
  }
24
23
 
25
24
  interface MoodItemProps extends ViewProps {
26
- theme?: ThemeType
25
+ theme?: ThemeType;
27
26
  enable: boolean;
28
27
  isMix: boolean;
29
28
  mood: MoodUIInfo;
30
29
  style?: ViewStyle;
31
- deviceTypeOption?: LightCategory
30
+ deviceTypeOption?: LightCategory;
32
31
  onPress?: () => void;
33
32
  onSwitch: (enable: boolean) => void;
34
33
  }
35
34
 
36
35
  const MoodItem = (props: MoodItemProps) => {
37
- const { mood, isMix, deviceTypeOption } = props;
36
+ const { mood, isMix, deviceTypeOption, theme } = props;
38
37
  const isDynamic = useMemo(() => {
39
38
  return mood.mainLamp.nodes?.length > 1 || mood.secondaryLamp.nodes?.length > 1;
40
39
  }, [mood.mainLamp.nodes, mood.secondaryLamp.nodes]);
41
40
 
42
41
  const gradientMode = useMemo(() => (
43
42
  deviceTypeOption?.isStringLight ? MoodJumpGradientMode.StringGradient : deviceTypeOption?.isStripLight ? MoodJumpGradientMode.StripGradient : MoodJumpGradientMode.SourceGradient
44
- ), [MoodJumpGradientMode, deviceTypeOption])
45
-
46
-
47
- const styles = StyleSheet.create({
48
- card: {
49
- marginHorizontal: cx(24),
50
- },
51
- headline: {
52
- flexDirection: 'row',
53
- marginHorizontal: cx(16),
54
- alignItems: 'center',
55
- },
56
- headText: {
57
- flex: 1,
58
- color: props.theme?.global.fontColor,
59
- fontSize: cx(16),
60
- // fontFamily: 'helvetica_neue_lt_std_bd',
61
- lineHeight: cx(20),
62
- },
63
- checkbox: {
64
- width: cx(45),
65
- height: cx(45),
66
- marginTop: cx(-5),
67
- marginBottom: cx(-10),
68
- },
69
- moodTypeItem: {
70
- flexDirection: 'row',
71
- },
72
- moodTypeLabel: {
73
- marginStart: cx(16),
74
- paddingHorizontal: cx(12.5),
75
- backgroundColor: props.theme?.tag.background,
76
- borderRadius: cx(8),
77
- },
78
- moodTypeLabelText: {
79
- height: cx(16),
80
- color: props.theme?.tag.fontColor,
81
- fontSize: cx(10),
82
- textAlignVertical: 'center',
83
- // fontFamily: 'helvetica_neue_lt_std_roman',
84
- lineHeight: cx(16),
85
- },
86
- })
43
+ ), [MoodJumpGradientMode, deviceTypeOption]);
44
+
45
+ const styles = useMemo(() => getStyles(theme), [theme]);
87
46
 
88
47
  return (
89
48
  <Card style={[styles.card, props.style]} onPress={props.onPress}>
90
- <View>
91
- <Spacer height={cx(16)} />
92
- <View style={styles.headline}>
49
+ <View style={styles.contentContainer}>
50
+ <View style={styles.row}>
93
51
  <Text style={styles.headText}>{mood.name}</Text>
94
52
  <TouchableOpacity style={styles.checkbox} onPress={() => props.onSwitch(!props.enable)}>
95
- <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}]} />
53
+ <Image source={{ uri: res.ic_check }}
54
+ style={[styles.checkboxImage, { tintColor: props.enable ? theme?.icon.primary : theme?.icon.disable }]}/>
96
55
  </TouchableOpacity>
97
56
  </View>
98
- <Spacer />
99
- <MixMoodColorsLine mixSubLight={mood.mainLamp} isMix={isMix} type={(mood.mainLamp.mode === gradientMode && !deviceTypeOption?.isCeilingLight) ? 'gradient' : 'separate'}/>
100
- {(deviceTypeOption?.isMixLight || (isMix && !!mood.secondaryLamp.nodes.length)) && (
57
+ <Spacer height={cx(8)}/>
58
+ <MixMoodColorsLine
59
+ mixSubLight={mood.mainLamp}
60
+ isMix={isMix}
61
+ type={mood.mainLamp.mode === gradientMode && !deviceTypeOption?.isCeilingLight ? 'gradient' : 'separate'}
62
+ />
63
+ {(deviceTypeOption?.isMixLight || (isMix && !!mood.secondaryLamp.nodes.length)) && (
101
64
  <>
102
- <Spacer height={cx(7)} />
103
- <MixMoodColorsLine mixSubLight={mood.secondaryLamp} isMix={isMix} type={mood.secondaryLamp.mode === (deviceTypeOption?.isMixLight ? MoodJumpGradientMode.SourceGradient : MoodJumpGradientMode.StripGradient) ? 'gradient' : 'separate'}/>
65
+ <Spacer height={cx(7)}/>
66
+ <MixMoodColorsLine
67
+ mixSubLight={mood.secondaryLamp}
68
+ isMix={isMix}
69
+ type={mood.secondaryLamp.mode === (deviceTypeOption?.isMixLight ? MoodJumpGradientMode.SourceGradient : MoodJumpGradientMode.StripGradient) ? 'gradient' : 'separate'}
70
+ />
104
71
  </>
105
72
  )}
106
- <Spacer height={cx(12)} />
107
- <View style={styles.moodTypeItem}>
73
+ <Spacer height={cx(12)}/>
74
+ <View style={styles.row}>
108
75
  <View style={styles.moodTypeLabel}>
109
76
  <Text style={styles.moodTypeLabelText}>
110
- {I18n.getLang(
111
- isDynamic ? 'mood_overview_field_chip_2' : 'mood_overview_field_chip_text'
112
- )}
77
+ {I18n.getLang(isDynamic ? 'mood_overview_field_chip_2' : 'mood_overview_field_chip_text')}
113
78
  </Text>
114
79
  </View>
115
80
  </View>
116
- <Spacer height={cx(16)} />
117
81
  </View>
118
82
  </Card>
119
83
  );
120
84
  };
121
85
 
122
- export default withTheme(MoodItem)
123
-
124
- export function MixMoodColorsLine(props: { mixSubLight: MoodLampInfo; isMix: boolean, type: 'gradient' | 'separate' }) {
125
- const { mixSubLight, isMix } = props;
126
- const lightColors = !!(mixSubLight.enable && mixSubLight.nodes.length) ? mixSubLight.nodes?.map(n => {
127
- const s = Math.round(mapFloatToRange(n.s / 100, 30, 100));
128
- return n.isColorNode
129
- ? hsv2Hex(n.h, s, Math.round(mapFloatToRange(n.v / 100, 50, 100)))
130
- : cctToColor(n.colorTemp.toFixed());
131
- }) : ['#eee'];
132
-
133
- const styles = StyleSheet.create({
134
- gradientItem: {
135
- flexDirection: 'row',
136
- },
137
- gradientItemIconView: {
138
- width: cx(24),
139
- height: cx(24),
140
- justifyContent: 'center',
141
- alignItems: 'center',
142
- backgroundColor: '#aaa',
143
- borderRadius: cx(8),
144
- },
145
- gradientItemIcon: {
146
- width: cx(16),
147
- height: cx(16),
148
- tintColor: '#fff',
149
- },
150
- })
86
+ // --- 【核心修正】 ---
87
+ export function MixMoodColorsLine(props: {
88
+ mixSubLight: MoodLampInfo;
89
+ isMix: boolean;
90
+ type: 'gradient' | 'separate';
91
+ width?: number; // 外部传入的宽度(父组件已计算好)
92
+ }) {
93
+ const { mixSubLight, isMix, type, width: propWidth } = props;
94
+ const [measuredWidth, setMeasuredWidth] = useState(0);
151
95
 
152
- return (
153
- <View style={styles.gradientItem}>
154
- <Spacer height={0} width={cx(16)} />
96
+ const handleLayout = (event: LayoutChangeEvent) => {
97
+ const { width } = event.nativeEvent.layout;
98
+ if (width > 0 && width !== measuredWidth) {
99
+ setMeasuredWidth(width);
100
+ }
101
+ };
102
+
103
+ const lightColors = (mixSubLight.enable && mixSubLight.nodes.length > 0)
104
+ ? mixSubLight.nodes.map(n => {
105
+ const s = Math.round(mapFloatToRange(n.s / 100, 30, 100));
106
+ return n.isColorNode
107
+ ? hsv2Hex(n.h, s, Math.round(mapFloatToRange(n.v / 100, 50, 100)))
108
+ : cctToColor(n.colorTemp.toFixed());
109
+ })
110
+ : ['#eee'];
111
+
112
+ // 如果父组件传入了精确宽度,直接使用;否则使用内部测量的宽度。
113
+ const finalWidth = propWidth || measuredWidth;
114
+
115
+ // 渲染颜色条的通用逻辑
116
+ const renderColorLine = () => (
117
+ finalWidth > 0 && (
155
118
  <MoodColorsLine
156
119
  nodeStyle={{ borderColor: '#ccc', borderWidth: 1 }}
157
- width={isMix ? cx(264) : undefined}
158
- type={props.type}
120
+ width={finalWidth}
121
+ type={type}
159
122
  colors={lightColors}
160
123
  />
161
- {isMix && (
162
- <>
163
- <Spacer height={0} width={cx(7)} />
164
- <View style={styles.gradientItemIconView}>
165
- <Image style={styles.gradientItemIcon} source={{uri: mixSubLight.enable ? res.light_on : res.light_off}} />
166
- </View>
167
- </>
168
- )}
124
+ )
125
+ );
126
+
127
+ // 渲染右侧图标的通用逻辑
128
+ const renderIcon = () => (
129
+ isMix && (
130
+ <>
131
+ <Spacer width={cx(7)}/>
132
+ <View style={styles.mixLineIconView}>
133
+ <Image style={styles.mixLineIcon} source={{ uri: mixSubLight.enable ? res.light_on : res.light_off }}/>
134
+ </View>
135
+ </>
136
+ )
137
+ );
138
+
139
+ // Case 1: 父组件已经计算并传入了宽度 (最高效)
140
+ if (propWidth) {
141
+ return (
142
+ <View style={styles.mixLineRow}>
143
+ {renderColorLine()}
144
+ {renderIcon()}
145
+ </View>
146
+ );
147
+ }
148
+
149
+ // Case 2: 自我测量模式 (回退方案)
150
+ return (
151
+ <View style={styles.mixLineRow}>
152
+ {/* 这个 View (测量器) 会自动收缩以填充'颜色条'应占的空间 */}
153
+ <View style={{ flex: 1 }} onLayout={handleLayout}>
154
+ {renderColorLine()}
155
+ </View>
156
+ {/* 图标作为测量器的兄弟节点,Flexbox 会先为它分配空间 */}
157
+ {renderIcon()}
169
158
  </View>
170
159
  );
171
160
  }
161
+
162
+ const getStyles = (theme?: ThemeType) => StyleSheet.create({
163
+ card: {
164
+ marginHorizontal: cx(24),
165
+ padding: 0,
166
+ borderRadius: cx(16),
167
+ },
168
+ contentContainer: {
169
+ paddingHorizontal: cx(16),
170
+ paddingTop: cx(8),
171
+ paddingBottom: cx(16),
172
+ },
173
+ row: {
174
+ flexDirection: 'row',
175
+ alignItems: 'center',
176
+ },
177
+ headText: {
178
+ flex: 1,
179
+ color: theme?.global.fontColor,
180
+ fontSize: cx(16),
181
+ lineHeight: cx(20),
182
+ },
183
+ checkbox: {
184
+ width: cx(45),
185
+ height: cx(45),
186
+ marginRight: cx(-10),
187
+ justifyContent: 'center',
188
+ alignItems: 'flex-end',
189
+ },
190
+ checkboxImage: {
191
+ width: cx(44),
192
+ height: cx(44),
193
+ },
194
+ moodTypeLabel: {
195
+ paddingHorizontal: cx(12.5),
196
+ backgroundColor: theme?.tag.background,
197
+ borderRadius: cx(8),
198
+ },
199
+ moodTypeLabelText: {
200
+ height: cx(16),
201
+ color: theme?.tag.fontColor,
202
+ fontSize: cx(10),
203
+ lineHeight: cx(16),
204
+ },
205
+ });
206
+
207
+ const styles = StyleSheet.create({
208
+ mixLineRow: { flexDirection: 'row', alignItems: 'center' },
209
+ mixLineIconView: {
210
+ width: cx(24),
211
+ height: cx(24),
212
+ justifyContent: 'center',
213
+ alignItems: 'center',
214
+ backgroundColor: '#aaa',
215
+ borderRadius: cx(8),
216
+ },
217
+ mixLineIcon: { width: cx(16), height: cx(16), tintColor: '#fff' },
218
+ });
219
+
220
+ export default withTheme(MoodItem);
@@ -1,85 +1,117 @@
1
1
  import React, { useMemo } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
2
3
  import { Utils } from 'tuya-panel-kit';
3
4
  import Card from '@ledvance/base/src/components/Card';
4
5
  import { CellContent } from '@ledvance/base/src/components/Cell';
5
- import { StyleSheet, View } from 'react-native';
6
6
  import Spacer from '@ledvance/base/src/components/Spacer';
7
+ import ThemeType from '@ledvance/base/src/config/themeType';
8
+
7
9
  import { MoodJumpGradientMode, MoodUIInfo } from './Interface';
8
- import { MixMoodColorsLine } from './MoodItem';
9
- import ThemeType from '@ledvance/base/src/config/themeType'
10
+ import { MixMoodColorsLine } from './MoodItem'; // 从 MoodItem.tsx 导入重构后的组件
10
11
 
11
12
  const cx = Utils.RatioUtils.convertX;
12
- const { withTheme } = Utils.ThemeUtils
13
+ const { withTheme } = Utils.ThemeUtils;
14
+
15
+ // 定义布局中使用的常量,便于维护和计算
16
+ const CARD_MARGIN_HORIZONTAL = cx(24);
17
+ const CONTENT_PADDING_HORIZONTAL = cx(16);
18
+ const ICON_WIDTH = cx(24);
19
+ const ICON_SPACING = cx(7);
13
20
 
14
21
  interface LightCategory {
15
- isMixLight?: boolean
16
- isStripLight?: boolean
17
- isStringLight?: boolean
18
- isCeilingLight?: boolean
22
+ isMixLight?: boolean;
23
+ isStripLight?: boolean;
24
+ isStringLight?: boolean;
25
+ isCeilingLight?: boolean;
19
26
  }
20
27
 
21
- interface RecommendMixMoodItemProps {
22
- theme?: ThemeType
28
+ interface RecommendMoodItemProps {
29
+ theme?: ThemeType;
23
30
  title: string;
24
31
  isMix: boolean;
25
32
  mood: MoodUIInfo;
26
- deviceTypeOption?: LightCategory
33
+ deviceTypeOption?: LightCategory;
27
34
  onPress: () => void;
35
+ containerWidth: number; // 1. 接收从父组件传来的容器宽度
28
36
  }
29
37
 
30
- const RecommendMixMoodItem = (props: RecommendMixMoodItemProps) => {
31
- const { mood, isMix, deviceTypeOption } = props;
38
+ const RecommendMoodItem = (props: RecommendMoodItemProps) => {
39
+ const { mood, isMix, deviceTypeOption, theme, containerWidth } = props;
32
40
 
33
41
  const gradientMode = useMemo(() => (
34
42
  deviceTypeOption?.isStringLight ? MoodJumpGradientMode.StringGradient : deviceTypeOption?.isStripLight ? MoodJumpGradientMode.StripGradient : MoodJumpGradientMode.SourceGradient
35
- ), [MoodJumpGradientMode, deviceTypeOption])
43
+ ), [MoodJumpGradientMode, deviceTypeOption]);
36
44
 
45
+ // 2. 使用 useMemo 根据传入的 prop 精确计算颜色条的宽度
46
+ const lineWidth = useMemo(() => {
47
+ if (containerWidth <= 0) return 0;
37
48
 
38
- const styles = StyleSheet.create({
39
- root: {
40
- flexDirection: 'row',
41
- alignItems: 'center',
42
- marginHorizontal: cx(24),
43
- },
44
- content: {
45
- height: cx(56),
46
- marginHorizontal: cx(16),
47
- width: cx(295)
48
- },
49
- title: {
50
- color: props.theme?.global.fontColor,
51
- fontSize: cx(16),
52
- // fontFamily: 'helvetica_neue_lt_std_bd',
53
- },
54
- lineStyle: {
55
- alignItems: 'center',
56
- },
57
- })
49
+ // 基础宽度 = 容器总宽 - 卡片外边距*2 - 内容内边距*2
50
+ const baseWidth = containerWidth - (CARD_MARGIN_HORIZONTAL * 2) - (CONTENT_PADDING_HORIZONTAL * 2);
51
+
52
+ // 如果是 isMix 模式,还需减去右侧图标的宽度和间距
53
+ return isMix ? baseWidth - ICON_WIDTH - ICON_SPACING : baseWidth;
54
+ }, [containerWidth, isMix]);
55
+
56
+ const styles = useMemo(() => getRecommendStyles(theme), [theme]);
58
57
 
59
58
  return (
60
- <Card style={styles.root} onPress={props.onPress}>
59
+ <Card style={styles.card} onPress={props.onPress}>
61
60
  <CellContent
62
61
  title={props.title}
63
62
  value={''}
64
63
  style={styles.content}
65
64
  titleStyle={styles.title}
66
- arrowStyle={{
67
- color: props.theme?.global.fontColor,
68
- size: cx(16),
69
- }}
65
+ arrowStyle={{ color: theme?.global.fontColor, size: cx(16) }}
70
66
  />
71
- {!!(mood.mainLamp) && (
72
- <>
73
- <View style={styles.lineStyle}>
74
- <MixMoodColorsLine mixSubLight={mood?.mainLamp} isMix={isMix} type={(mood.mainLamp.mode === gradientMode && !deviceTypeOption?.isCeilingLight) ? 'gradient' : 'separate'}/>
75
- <Spacer height={cx(7)} />
76
- {(deviceTypeOption?.isMixLight || (isMix && !!mood.secondaryLamp.nodes.length)) && <MixMoodColorsLine mixSubLight={mood.secondaryLamp} isMix={isMix} type={mood.secondaryLamp.mode === (deviceTypeOption?.isMixLight ? MoodJumpGradientMode.SourceGradient : MoodJumpGradientMode.StripGradient) ? 'gradient' : 'separate'}/>}
77
- </View>
78
- <Spacer height={cx(24)} />
79
- </>
67
+
68
+ {/* 3. 只有在计算出有效宽度后才渲染颜色条 */}
69
+ {lineWidth > 0 && mood.mainLamp && (
70
+ <View style={styles.lineContainer}>
71
+ {/* 主灯颜色条 */}
72
+ <MixMoodColorsLine
73
+ width={lineWidth} // 4. 将计算好的宽度传递下去
74
+ isMix={isMix}
75
+ mixSubLight={mood.mainLamp}
76
+ type={mood.mainLamp.mode === gradientMode ? 'gradient' : 'separate'}
77
+ />
78
+ {/* 副灯颜色条 (如果需要) */}
79
+ {isMix && mood.secondaryLamp && mood.secondaryLamp.nodes.length > 0 && (
80
+ <>
81
+ <Spacer height={cx(7)} />
82
+ <MixMoodColorsLine
83
+ width={lineWidth} // 同样传递计算好的宽度
84
+ isMix={isMix}
85
+ mixSubLight={mood.secondaryLamp}
86
+ type={mood.secondaryLamp.mode === (deviceTypeOption?.isCeilingLight ? MoodJumpGradientMode.StripGradient : MoodJumpGradientMode.SourceGradient) ? 'gradient' : 'separate'}
87
+ />
88
+ </>
89
+ )}
90
+ <Spacer height={cx(12)} />
91
+ </View>
80
92
  )}
81
93
  </Card>
82
94
  );
83
95
  };
84
96
 
85
- export default withTheme(RecommendMixMoodItem)
97
+ const getRecommendStyles = (theme?: ThemeType) => StyleSheet.create({
98
+ card: {
99
+ marginHorizontal: CARD_MARGIN_HORIZONTAL,
100
+ paddingVertical: 0,
101
+ paddingHorizontal: 0,
102
+ },
103
+ content: {
104
+ paddingHorizontal: CONTENT_PADDING_HORIZONTAL,
105
+ height: cx(56),
106
+ // 移除了硬编码的 width,使其自适应
107
+ },
108
+ title: {
109
+ color: theme?.global.fontColor,
110
+ fontSize: cx(16),
111
+ },
112
+ lineContainer: {
113
+ paddingHorizontal: CONTENT_PADDING_HORIZONTAL,
114
+ },
115
+ });
116
+
117
+ export default withTheme(RecommendMoodItem);
@@ -44,6 +44,7 @@ const OverchargeSwitchPage = (props: { theme?: ThemeType }) => {
44
44
 
45
45
  return (<Page
46
46
  backText={devInfo.name}
47
+ accessibilityHint={I18n.getLang('switch_overcharge_headline_text')}
47
48
  loading={state.loading}>
48
49
  <Spacer height={cx(24)}/>
49
50
  <Card style={{marginHorizontal: cx(24)}}>