@ledvance/group-ui-biz-bundle 1.0.146 → 1.0.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "name": "@ledvance/group-ui-biz-bundle",
5
5
  "pid": [],
6
6
  "uiid": "",
7
- "version": "1.0.146",
7
+ "version": "1.0.148",
8
8
  "scripts": {},
9
9
  "dependencies": {
10
10
  "@ledvance/base": "^1.x",
@@ -608,6 +608,42 @@ export const defFlagList: FlagUiInfo[] = [
608
608
  ...defFlagConfig,
609
609
  colors: [{"h":0,"s":0,"v":100}, {"h":160,"s":80,"v":34}],
610
610
  },
611
+ {
612
+ id: 160,
613
+ name: I18n.getLang('country_KR'),
614
+ ...defFlagConfig,
615
+ colors: [{"h":0,"s":0,"v":100}, {"h":355,"s":78,"v":80}, {"h":213,"s":93,"v":80}],
616
+ },
617
+ {
618
+ id: 159,
619
+ name: I18n.getLang('country_HT'),
620
+ ...defFlagConfig,
621
+ colors: [{"h":228,"s":100,"v":62}, {"h":349,"s":92,"v":82}, {"h":0,"s":0,"v":100}],
622
+ },
623
+ {
624
+ id: 158,
625
+ name: I18n.getLang('country_CW'),
626
+ ...defFlagConfig,
627
+ colors: [{"h":220,"s":100,"v":50}, {"h":56,"s":92,"v":98}, {"h":0,"s":0,"v":100}],
628
+ },
629
+ {
630
+ id: 157,
631
+ name: I18n.getLang('country_SN'),
632
+ ...defFlagConfig,
633
+ colors: [{"h":148,"s":100,"v":52}, {"h":56,"s":74,"v":99}, {"h":358,"s":88,"v":89}],
634
+ },
635
+ {
636
+ id: 156,
637
+ name: I18n.getLang('country_DZ'),
638
+ ...defFlagConfig,
639
+ colors: [{"h":150,"s":100,"v":40}, {"h":0,"s":0,"v":100}, {"h":349,"s":92,"v":82}],
640
+ },
641
+ {
642
+ id: 155,
643
+ name: I18n.getLang('country_UZ'),
644
+ ...defFlagConfig,
645
+ colors: [{"h":207,"s":100,"v":81}, {"h":0,"s":0,"v":100}, {"h":109,"s":76,"v":69}],
646
+ },
611
647
  ]
612
648
 
613
649
  export const def2Pids = ['k588eygmfkdzmpzm']
@@ -1,81 +1,143 @@
1
- import { StyleSheet, View, Text, ViewStyle, Image, TouchableOpacity } from 'react-native'
2
- import React from 'react'
3
- import Card from '@ledvance/base/src/components/Card'
4
- import { Utils } from 'tuya-panel-kit'
5
- import MoodColorsLine from '@ledvance/base/src/components/MoodColorsLine'
6
- import Spacer from '@ledvance/base/src/components/Spacer'
7
- import ThemeType from '@ledvance/base/src/config/themeType'
8
- import res from "@ledvance/base/src/res"
1
+ import { StyleSheet, View, Text, ViewStyle, Image, TouchableOpacity, LayoutChangeEvent } from 'react-native';
2
+ import React, { useMemo, useState } from 'react'; // 引入 Hooks
3
+ import Card from '@ledvance/base/src/components/Card';
4
+ import { Utils } from 'tuya-panel-kit';
5
+ import MoodColorsLine from '@ledvance/base/src/components/MoodColorsLine';
6
+ import Spacer from '@ledvance/base/src/components/Spacer';
7
+ import ThemeType from '@ledvance/base/src/config/themeType';
8
+ import res from "@ledvance/base/src/res";
9
9
 
10
- const cx = Utils.RatioUtils.convertX
11
- const { withTheme } = Utils.ThemeUtils
10
+ const cx = Utils.RatioUtils.convertX;
11
+ const { withTheme } = Utils.ThemeUtils;
12
12
 
13
+ // 1. 将边距定义为常量,便于计算和维护
14
+ const CONTENT_HORIZONTAL_MARGIN = cx(16);
15
+
16
+ // 2. 更新 Props 接口,添加可选的 containerWidth
13
17
  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
18
+ theme?: ThemeType;
19
+ enable: boolean;
20
+ title: string;
21
+ colors: string[];
22
+ icon?: number | string;
23
+ style?: ViewStyle;
24
+ containerWidth?: number; // 变为可选属性
25
+ onPress?: () => void;
26
+ onSwitch: (enable: boolean) => void;
22
27
  }
23
28
 
29
+ // 3. 将样式创建移到外部,提升性能
30
+ const getStyles = (theme?: ThemeType) => StyleSheet.create({
31
+ card: {
32
+ marginHorizontal: cx(24),
33
+ // 移除内边距,以便测量 Card 自身的准确宽度
34
+ paddingHorizontal: 0,
35
+ },
36
+ headline: {
37
+ flexDirection: 'row',
38
+ marginHorizontal: CONTENT_HORIZONTAL_MARGIN,
39
+ justifyContent: 'space-between',
40
+ alignItems: 'center',
41
+ },
42
+ headText: {
43
+ color: theme?.global.fontColor,
44
+ fontSize: cx(16),
45
+ lineHeight: cx(20),
46
+ flex: 1
47
+ },
48
+ checkbox: {
49
+ width: cx(45),
50
+ height: cx(45),
51
+ justifyContent: 'center',
52
+ alignItems: 'flex-end',
53
+ },
54
+ checkboxImage: {
55
+ width: cx(44),
56
+ height: cx(44),
57
+ },
58
+ // 这个容器只负责布局,不负责测量
59
+ lineContainer: {
60
+ marginHorizontal: CONTENT_HORIZONTAL_MARGIN,
61
+ minHeight: cx(24), // 设定最小高度防止布局跳动
62
+ },
63
+ icon: {
64
+ width: cx(60),
65
+ aspectRatio: 2.14,
66
+ marginRight: cx(10),
67
+ },
68
+ titleContainer: {
69
+ flexDirection: 'row',
70
+ flex: 1,
71
+ paddingRight: cx(5),
72
+ justifyContent: 'flex-start',
73
+ alignItems: 'center',
74
+ }
75
+ });
76
+
77
+
24
78
  const FlagItem = (props: RecommendMoodItemProps) => {
25
- const icon = typeof props.icon === 'number' ? props.icon : { uri: props.icon }
79
+ const { theme, containerWidth, enable, title, colors, icon: iconProp, style, onPress, onSwitch } = props;
80
+ const styles = useMemo(() => getStyles(theme), [theme]);
81
+ const icon = typeof iconProp === 'number' ? iconProp : { uri: iconProp };
26
82
 
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
- gradient: {
54
- borderRadius: cx(8),
55
- },
56
- })
83
+ // 4. 添加 state 用于自我测量
84
+ const [selfMeasuredWidth, setSelfMeasuredWidth] = useState(0);
85
+
86
+ // 5. 创建 onLayout 回调
87
+ const handleLayout = (event: LayoutChangeEvent) => {
88
+ // 如果父组件已提供宽度,则忽略自我测量
89
+ if (containerWidth && containerWidth > 0) {
90
+ return;
91
+ }
92
+ const { width: cardWidth } = event.nativeEvent.layout;
93
+ if (cardWidth > 0 && cardWidth !== selfMeasuredWidth) {
94
+ setSelfMeasuredWidth(cardWidth);
95
+ }
96
+ };
97
+
98
+ // 6. 智能决策最终宽度
99
+ const finalLineWidth = useMemo(() => {
100
+ const cardOuterMargin = cx(24);
101
+ // 优先使用父组件传入的宽度 (用于 FlatList)
102
+ if (containerWidth && containerWidth > 0) {
103
+ return containerWidth - (cardOuterMargin * 2) - (CONTENT_HORIZONTAL_MARGIN * 2);
104
+ }
105
+ // 其次使用自我测量的宽度 (用于普通 View)
106
+ if (selfMeasuredWidth > 0) {
107
+ return selfMeasuredWidth - (CONTENT_HORIZONTAL_MARGIN * 2);
108
+ }
109
+ return 0;
110
+ }, [containerWidth, selfMeasuredWidth]);
57
111
 
58
112
  return (
113
+ // 7. 绑定 onLayout 以启用自我测量
59
114
  <Card
60
- style={[styles.card, props.style]}
61
- onPress={props.onPress}>
62
- <View accessibilityLabel={'FlagItem'} accessibilityHint={`${props.title}`}>
63
- <Spacer height={cx(16)} />
115
+ style={[styles.card, style]}
116
+ onPress={onPress}
117
+ onLayout={handleLayout}
118
+ >
119
+ <View accessibilityLabel={'FlagItem'} accessibilityHint={`${title}`}>
120
+ <Spacer height={cx(8)} />
64
121
  <View style={styles.headline}>
65
- <View style={{ flexDirection: 'row', flex: 1, paddingRight: cx(5), justifyContent: 'flex-start' }}>
66
- {props.title ? <Text style={styles.headText}>{props.title}</Text> : undefined}
67
- {props.icon ? <Image source={icon} style={{ width: cx(60), aspectRatio: 2.14, marginRight: cx(10) }} /> : undefined}
122
+ <View style={styles.titleContainer}>
123
+ {title ? <Text style={styles.headText}>{title}</Text> : null}
124
+ {iconProp ? <Image source={icon} style={styles.icon} /> : null}
68
125
  </View>
69
- <TouchableOpacity style={styles.checkbox} onPress={() => props.onSwitch(!props.enable)}>
70
- <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}]} />
126
+ <TouchableOpacity style={styles.checkbox} onPress={() => onSwitch(!enable)}>
127
+ <Image source={{ uri: res.ic_check}} style={[styles.checkboxImage, { tintColor: enable ? theme?.icon.primary : theme?.icon.disable}]} />
71
128
  </TouchableOpacity>
72
129
  </View>
73
130
  <Spacer />
74
- <View style={styles.gradientItem}>
75
- <MoodColorsLine
76
- type={'separate'}
77
- nodeStyle={{ borderColor: props.theme?.card.border, borderWidth: 1 }}
78
- colors={props.colors} />
131
+ <View style={styles.lineContainer}>
132
+ {/* 8. 使用最终计算的宽度进行渲染 */}
133
+ {finalLineWidth > 0 && (
134
+ <MoodColorsLine
135
+ type={'separate'}
136
+ width={finalLineWidth}
137
+ nodeStyle={{ borderColor: theme?.card.border, borderWidth: 1 }}
138
+ colors={colors}
139
+ />
140
+ )}
79
141
  </View>
80
142
  <Spacer height={cx(16)} />
81
143
  </View>
@@ -1,7 +1,7 @@
1
- import React, {useEffect} from "react";
1
+ import React, { useEffect, useState } from 'react'
2
2
  import Page from "@ledvance/base/src/components/Page";
3
3
  import {useFlags, useGroupDp, useUAGroupInfo} from "@ledvance/base/src/models/modules/NativePropsSlice";
4
- import {FlatList, Image, TouchableOpacity, View} from "react-native";
4
+ import { FlatList, Image, LayoutChangeEvent, TouchableOpacity, View } from 'react-native'
5
5
  import Spacer from "@ledvance/base/src/components/Spacer";
6
6
  import {Utils} from "tuya-panel-kit";
7
7
  import FlagItem from "./FlagItem";
@@ -60,6 +60,16 @@ const FlagPage = (props: { theme?: ThemeType }) => {
60
60
  state.filterFlags = state.searchText !== '' ? cloneDeep(state.flags).filter(flag => (flag.name ?? '').toLowerCase().includes(state.searchText.toLowerCase())) : cloneDeep(state.flags)
61
61
  }, [state.searchText, state.flags])
62
62
 
63
+ // 3. 在父组件中创建 state 存储容器宽度
64
+ const [listContainerWidth, setListContainerWidth] = useState(0);
65
+ // 4. 创建 onLayout 回调函数
66
+ const handleLayout = (event: LayoutChangeEvent) => {
67
+ const { width } = event.nativeEvent.layout;
68
+ if (width > 0 && width !== listContainerWidth) {
69
+ setListContainerWidth(width);
70
+ }
71
+ };
72
+
63
73
  const getRemoteFlagInfo = async (isRefresh?: boolean) => {
64
74
  const defNum = uaGroupInfo.groupDevices.filter(device => !(def2Pids.includes(device.tyPid) || def3Pids.includes(device.tyPid))).length
65
75
  const def2Num = uaGroupInfo.groupDevices.filter(device => def2Pids.includes(device.tyPid)).length
@@ -149,6 +159,7 @@ const FlagPage = (props: { theme?: ThemeType }) => {
149
159
  }}
150
160
  loading={state.loading}
151
161
  >
162
+ <View style={{ flex: 1 }} onLayout={handleLayout}>
152
163
  <View style={{flexDirection: 'row', display: 'flex', alignItems: 'center'}}>
153
164
  <TextField
154
165
  value={state.searchText}
@@ -180,31 +191,36 @@ const FlagPage = (props: { theme?: ThemeType }) => {
180
191
  style={{ width: cx(24), height: cx(24), tintColor: props.theme?.global.fontColor }}/>
181
192
  </TouchableOpacity>
182
193
  </View>
183
- <FlatList
184
- data={state.filterFlags}
185
- renderItem={({ item }) => <FlagItem
186
- enable={flagMode?.flagId === item.id && flagMode?.flagMode && switch_led}
187
- title={item.name}
188
- icon={item.icon}
189
- colors={item.colors.map(item => hsv2Hex(item.h, item.s, item.v)).reverse()}
190
- onSwitch={async (enable) => {
191
- if (enable) {
192
- state.loading = true
193
- await setFlag({
194
- flag: cloneDeep(item),
195
- })
196
- state.loading = false
197
- }
198
- }}
199
- onPress={() => {
200
- navigationRoute('edit', item)
201
- }}
202
- />}
203
- keyExtractor={item => item.name}
204
- ListHeaderComponent={() => (<Spacer height={cx(10)} />)}
205
- ItemSeparatorComponent={() => (<Spacer />)}
206
- ListFooterComponent={() => (<Spacer />)}
207
- />
194
+ {listContainerWidth > 0 && (
195
+ <FlatList
196
+ data={state.filterFlags}
197
+ renderItem={({ item }) => <FlagItem
198
+ enable={flagMode?.flagId === item.id && flagMode?.flagMode && switch_led}
199
+ title={item.name}
200
+ icon={item.icon}
201
+ colors={item.colors.map(item => hsv2Hex(item.h, item.s, item.v)).reverse()}
202
+ // 7. 【关键】将测量到的宽度传递给子组件
203
+ containerWidth={listContainerWidth}
204
+ onSwitch={async (enable) => {
205
+ if (enable) {
206
+ state.loading = true
207
+ await setFlag({
208
+ flag: cloneDeep(item),
209
+ })
210
+ state.loading = false
211
+ }
212
+ }}
213
+ onPress={() => {
214
+ navigationRoute('edit', item)
215
+ }}
216
+ />}
217
+ keyExtractor={item => item.name}
218
+ ListHeaderComponent={() => (<Spacer height={cx(10)} />)}
219
+ ItemSeparatorComponent={() => (<Spacer />)}
220
+ ListFooterComponent={() => (<Spacer />)}
221
+ />
222
+ )}
223
+ </View>
208
224
  </Page>
209
225
  )
210
226
  }
@@ -1,39 +1,30 @@
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';
1
+ import React, { useCallback, useState } from 'react';
2
+ import { FlatList, StyleSheet, Text, View, LayoutChangeEvent } from 'react-native';
5
3
  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
4
  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'
5
+ import { useReactive } from 'ahooks';
18
6
  import { difference, head, range } from 'lodash';
7
+ import Page from '@ledvance/base/src/components/Page';
8
+ import Spacer from '@ledvance/base/src/components/Spacer';
9
+ import Strings from '@ledvance/base/src/i18n';
10
+ import ThemeType from '@ledvance/base/src/config/themeType';
11
+ import { Result } from '@ledvance/base/src/models/modules/Result';
12
+ import { ui_biz_routerKey } from '../../navigation/Routers';
13
+ import { MoodPageParams, MoodNodeTransitionMode, MoodUIInfo, MoodJumpGradientMode } from './Interface';
19
14
  import { RecommendMood, getRecommendMixMoods, getRecommendMoods } from './MoodInfo';
20
- import ThemeType from '@ledvance/base/src/config/themeType'
21
-
15
+ import RecommendMoodItem from './RecommendMoodItem';
22
16
  const cx = Utils.RatioUtils.convertX;
23
- const { withTheme } = Utils.ThemeUtils
24
-
17
+ const { withTheme } = Utils.ThemeUtils;
25
18
  export interface AddMoodPageParams {
26
19
  isStatic: boolean;
27
20
  moodIds: number[];
28
21
  moduleParams: MoodPageParams;
29
- nameRepeat: (mood: MoodUIInfo) => boolean
22
+ nameRepeat: (mood: MoodUIInfo) => boolean;
30
23
  modDeleteMood: (mode: 'add' | 'edit' | 'del', currentMood: MoodUIInfo) => Promise<Result<any>>;
31
24
  }
32
-
33
25
  interface AddMoodPageState {
34
26
  data: RecommendMood[];
35
27
  }
36
-
37
28
  const AddMoodPage = (props: { theme?: ThemeType }) => {
38
29
  const navigation = useNavigation();
39
30
  const routeParams = useRoute().params as AddMoodPageParams;
@@ -44,31 +35,27 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
44
35
  ? getRecommendMixMoods(routeParams.isStatic, moduleParams)
45
36
  : getRecommendMoods(routeParams.isStatic, moduleParams),
46
37
  });
47
-
48
- const getFormateItem = item => {
49
- return {
50
- ...item,
51
- version: 0,
52
- secondaryLamp: item.secondaryLamp ?? {
53
- nodes: [],
54
- mode: 0,
55
- speed: 75,
56
- },
57
- };
38
+ // 1. 添加 state 用于存储容器宽度
39
+ const [containerWidth, setContainerWidth] = useState(0);
40
+ // 2. 添加 onLayout 回调函数,用于测量根视图宽度
41
+ const handleLayout = (event: LayoutChangeEvent) => {
42
+ const { width } = event.nativeEvent.layout;
43
+ if (width > 0 && width !== containerWidth) {
44
+ setContainerWidth(width);
45
+ }
58
46
  };
59
-
60
47
  const onMoodItemClick = useCallback(
61
48
  (moodItem: RecommendMood) => {
62
49
  const idRange = range(0, 256);
63
50
  const mainId: number = head(difference(idRange, routeParams.moodIds)) || 0;
64
- const secondaryId: number = moduleParams.isCeilingLight ? head(difference(idRange, [...routeParams.moodIds, mainId])) || 0 : 0
51
+ const secondaryId: number = moduleParams.isCeilingLight ? head(difference(idRange, [...routeParams.moodIds, mainId])) || 0 : 0;
65
52
  const url = routeParams.isStatic
66
53
  ? ui_biz_routerKey.group_ui_biz_static_mood_edit
67
54
  : !!(moduleParams.isCeilingLight || moduleParams.isMixLight)
68
55
  ? ui_biz_routerKey.group_ui_biz_dynamic_mix_mood_edit
69
56
  : ui_biz_routerKey.group_ui_biz_dynamic_mood_edit;
70
57
  const currentMood = moodItem.mainLamp
71
- ? { ...moodItem, id: mainId, mainLamp: { ...moodItem.mainLamp, id: mainId }, secondaryLamp: { ...moodItem.secondaryLamp, id: moodItem.secondaryLamp?.nodes?.length ? secondaryId : -1} }
58
+ ? { ...moodItem, id: mainId, mainLamp: { ...moodItem.mainLamp, id: mainId }, secondaryLamp: { ...moodItem.secondaryLamp, id: moodItem.secondaryLamp?.nodes?.length ? secondaryId : -1 } }
72
59
  : newMood(mainId, secondaryId, moduleParams.isSupportColor, routeParams.isStatic, moduleParams);
73
60
  navigation.navigate(url, {
74
61
  ...routeParams,
@@ -78,11 +65,9 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
78
65
  },
79
66
  [routeParams]
80
67
  );
81
-
82
68
  const styles = StyleSheet.create({
83
69
  root: {
84
70
  flex: 1,
85
- flexDirection: 'column',
86
71
  },
87
72
  desc: {
88
73
  color: props.theme?.global.fontColor,
@@ -90,8 +75,7 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
90
75
  marginHorizontal: cx(24),
91
76
  marginTop: cx(12),
92
77
  },
93
- })
94
-
78
+ });
95
79
  return (
96
80
  <Page
97
81
  backText={Strings.getLang('add_new_static_mood_system_back')}
@@ -101,7 +85,8 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
101
85
  : 'add_new_dynamic_mood_headline_text'
102
86
  )}
103
87
  >
104
- <View style={styles.root}>
88
+ {/* 3. 将 onLayout 绑定到根视图 */}
89
+ <View style={styles.root} onLayout={handleLayout}>
105
90
  <Text style={styles.desc}>
106
91
  {Strings.getLang(
107
92
  routeParams.isStatic
@@ -109,33 +94,33 @@ const AddMoodPage = (props: { theme?: ThemeType }) => {
109
94
  : 'add_new_dynamic_mood_description_text'
110
95
  )}
111
96
  </Text>
112
- <FlatList
113
- style={{ flex: 1 }}
114
- data={state.data}
115
- renderItem={({ item }) => {
116
- return (
97
+ {/* 4. 仅在获取到有效宽度后渲染列表 */}
98
+ {containerWidth > 0 && (
99
+ <FlatList
100
+ style={{ flex: 1 }}
101
+ data={state.data}
102
+ renderItem={({ item }) => (
117
103
  <RecommendMoodItem
118
104
  title={item.name}
119
105
  isMix={isMix}
120
- mood={getFormateItem(item)}
106
+ mood={item}
121
107
  deviceTypeOption={moduleParams}
122
- onPress={() => {
123
- onMoodItemClick(item);
124
- }}
108
+ onPress={() => onMoodItemClick(item)}
109
+ // 5. 将测量到的宽度传递给子组件
110
+ containerWidth={containerWidth}
125
111
  />
126
- );
127
- }}
128
- ItemSeparatorComponent={() => <Spacer />}
129
- ListHeaderComponent={() => <Spacer />}
130
- ListFooterComponent={() => <Spacer />}
131
- keyExtractor={item => item.name}
132
- />
112
+ )}
113
+ ItemSeparatorComponent={() => <Spacer />}
114
+ ListHeaderComponent={() => <Spacer />}
115
+ ListFooterComponent={() => <Spacer />}
116
+ keyExtractor={item => item.name}
117
+ />
118
+ )}
133
119
  </View>
134
120
  </Page>
135
121
  );
136
122
  };
137
-
138
- export default withTheme(AddMoodPage)
123
+ export default withTheme(AddMoodPage);
139
124
 
140
125
  const defStripConfig = {
141
126
  version: 0,
@@ -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
- const styles = StyleSheet.create({
47
- card: {
48
- marginHorizontal: cx(24),
49
- },
50
- headline: {
51
- flexDirection: 'row',
52
- marginHorizontal: cx(16),
53
- alignItems: 'center',
54
- },
55
- headText: {
56
- flex: 1,
57
- color: props.theme?.global.fontColor,
58
- fontSize: cx(16),
59
- // fontFamily: 'helvetica_neue_lt_std_bd',
60
- lineHeight: cx(20),
61
- },
62
- checkbox: {
63
- width: cx(45),
64
- height: cx(45),
65
- marginTop: cx(-5),
66
- marginBottom: cx(-10),
67
- },
68
- moodTypeItem: {
69
- flexDirection: 'row',
70
- },
71
- moodTypeLabel: {
72
- marginStart: cx(16),
73
- paddingHorizontal: cx(12.5),
74
- backgroundColor: props.theme?.tag.background,
75
- borderRadius: cx(8),
76
- },
77
- moodTypeLabelText: {
78
- height: cx(16),
79
- color: props.theme?.tag.fontColor,
80
- fontSize: cx(10),
81
- textAlignVertical: 'center',
82
- // fontFamily: 'helvetica_neue_lt_std_roman',
83
- lineHeight: cx(16),
84
- },
85
- });
43
+ ), [MoodJumpGradientMode, deviceTypeOption]);
44
+
45
+ const styles = useMemo(() => getStyles(theme), [theme]);
86
46
 
87
47
  return (
88
48
  <Card style={[styles.card, props.style]} onPress={props.onPress}>
89
- <View>
90
- <Spacer height={cx(16)} />
91
- <View style={styles.headline}>
49
+ <View style={styles.contentContainer}>
50
+ <View style={styles.row}>
92
51
  <Text style={styles.headText}>{mood.name}</Text>
93
52
  <TouchableOpacity style={styles.checkbox} onPress={() => props.onSwitch(!props.enable)}>
94
- <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 }]}/>
95
55
  </TouchableOpacity>
96
56
  </View>
97
- <Spacer />
98
- <MixMoodColorsLine mixSubLight={mood.mainLamp} isMix={isMix} type={mood.mainLamp.mode === gradientMode ? 'gradient' : 'separate'}/>
99
- {(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)) && (
100
64
  <>
101
- <Spacer height={cx(7)} />
102
- <MixMoodColorsLine mixSubLight={mood.secondaryLamp} isMix={isMix} type={mood.secondaryLamp.mode === (deviceTypeOption?.isCeilingLight ? MoodJumpGradientMode.StripGradient : MoodJumpGradientMode.SourceGradient) ? '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
+ />
103
71
  </>
104
72
  )}
105
- <Spacer height={cx(12)} />
106
- <View style={styles.moodTypeItem}>
73
+ <Spacer height={cx(12)}/>
74
+ <View style={styles.row}>
107
75
  <View style={styles.moodTypeLabel}>
108
76
  <Text style={styles.moodTypeLabelText}>
109
- {I18n.getLang(
110
- isDynamic ? 'mood_overview_field_chip_2' : 'mood_overview_field_chip_text'
111
- )}
77
+ {I18n.getLang(isDynamic ? 'mood_overview_field_chip_2' : 'mood_overview_field_chip_text')}
112
78
  </Text>
113
79
  </View>
114
80
  </View>
115
- <Spacer height={cx(16)} />
116
81
  </View>
117
82
  </Card>
118
83
  );
119
84
  };
120
85
 
121
- export default withTheme(MoodItem)
122
-
123
- export function MixMoodColorsLine(props: { mixSubLight: MoodLampInfo; isMix: boolean, type: 'gradient' | 'separate' }) {
124
- const { mixSubLight, isMix } = props;
125
- const lightColors = !!(mixSubLight.enable && mixSubLight.nodes.length) ? mixSubLight.nodes?.map(n => {
126
- const s = Math.round(mapFloatToRange(n.s / 100, 30, 100));
127
- return n.isColorNode
128
- ? hsv2Hex(n.h, s, Math.round(mapFloatToRange(n.v / 100, 50, 100)))
129
- : cctToColor(n.colorTemp.toFixed());
130
- }) : ['#eee'];
131
-
132
- const styles = StyleSheet.create({
133
- gradientItem: {
134
- flexDirection: 'row',
135
- },
136
- gradientItemIconView: {
137
- width: cx(24),
138
- height: cx(24),
139
- justifyContent: 'center',
140
- alignItems: 'center',
141
- backgroundColor: '#aaa',
142
- borderRadius: cx(8),
143
- },
144
- gradientItemIcon: {
145
- width: cx(16),
146
- height: cx(16),
147
- tintColor: '#fff',
148
- },
149
- })
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);
95
+
96
+ const handleLayout = (event: LayoutChangeEvent) => {
97
+ const { width } = event.nativeEvent.layout;
98
+ if (width > 0 && width !== measuredWidth) {
99
+ setMeasuredWidth(width);
100
+ }
101
+ };
150
102
 
151
- return (
152
- <View style={styles.gradientItem}>
153
- <Spacer height={0} width={cx(16)} />
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 && (
154
118
  <MoodColorsLine
155
119
  nodeStyle={{ borderColor: '#ccc', borderWidth: 1 }}
156
- width={isMix ? cx(264) : undefined}
157
- type={props.type}
120
+ width={finalWidth}
121
+ type={type}
158
122
  colors={lightColors}
159
123
  />
160
- {isMix && (
161
- <>
162
- <Spacer height={0} width={cx(7)} />
163
- <View style={styles.gradientItemIconView}>
164
- <Image style={styles.gradientItemIcon} source={{uri: mixSubLight.enable ? res.light_on : res.light_off}} />
165
- </View>
166
- </>
167
- )}
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()}
168
158
  </View>
169
159
  );
170
160
  }
171
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 ? 'gradient' : 'separate'}/>
75
- <Spacer height={cx(7)} />
76
- {(isMix && !!mood.secondaryLamp.nodes.length) && <MixMoodColorsLine mixSubLight={mood.secondaryLamp} isMix={isMix} type={mood.secondaryLamp.mode === (deviceTypeOption?.isCeilingLight ? MoodJumpGradientMode.StripGradient : MoodJumpGradientMode.SourceGradient) ? '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);