@ledvance/group-ui-biz-bundle 1.0.150 → 1.0.152

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.150",
7
+ "version": "1.0.152",
8
8
  "scripts": {},
9
9
  "dependencies": {
10
10
  "@ledvance/base": "^1.x",
@@ -17,6 +17,7 @@
17
17
  "prop-types": "^15.6.1",
18
18
  "react": "16.8.3",
19
19
  "react-native": "0.59.10",
20
+ "react-native-linear-gradient": "2.8.3",
20
21
  "react-native-orientation-locker": "^1.7.0",
21
22
  "react-native-svg": "5.5.1",
22
23
  "tuya-panel-kit": "^4.9.4"
@@ -1,3 +1,6 @@
1
+ import { xLog } from '@ledvance/base/src/utils'
2
+ import { cloneDeep } from 'lodash'
3
+ import { AsyncStorage } from 'react-native'
1
4
  import {
2
5
  BiorhythmBean,
3
6
  BiorhythmGradientType,
@@ -11,8 +14,9 @@ import {hex2Int, spliceByStep} from '@ledvance/base/src/utils/common'
11
14
  import I18n from '@ledvance/base/src/i18n'
12
15
  import res from '@ledvance/base/src/res'
13
16
  import { to16 } from '@tuya/tuya-panel-lamp-sdk/lib/utils'
14
- import { useCallback } from 'react'
17
+ import { useCallback, useEffect, useState } from 'react'
15
18
  import { Result } from '@ledvance/base/src/models/modules/Result'
19
+ import iconList from './iconListData'
16
20
 
17
21
  interface BiorhythmConfig {
18
22
  rhythm_mode: BiorhythmBean
@@ -179,6 +183,7 @@ function getRepeatPeriodTitleByIndex(index: number): string {
179
183
  let title = ''
180
184
  switch (index) {
181
185
  case 0:
186
+ case 7:
182
187
  title = I18n.getLang('bio_ryhthm_default_weekday7_text')
183
188
  break
184
189
  case 1:
@@ -231,3 +236,104 @@ function obj2Dp(obj: BiorhythmBean): string {
231
236
  .join('')
232
237
  return versionHex + enableHex + gradientHex + repeatPeriodHex + planCountHex + planListHex
233
238
  }
239
+
240
+ export const replaceImg = (img) => {
241
+ const item = iconList?.find(val => val.id === Number(img))
242
+ switch (img) {
243
+ case 'rhythm_icon1':
244
+ case '31':
245
+ return { icon: res.biorhythom_icon1, iconId: 1 }
246
+ case 'rhythm_icon2':
247
+ case '33':
248
+ return { icon: res.biorhythom_icon5, iconId: 5 }
249
+ case 'rhythm_icon3':
250
+ case '35':
251
+ return { icon: res.biorhythom_icon2, iconId: 2 }
252
+ case 'rhythm_icon4':
253
+ case '32':
254
+ return { icon: res.biorhythom_icon9, iconId: 9 }
255
+ case 'rhythm_icon12':
256
+ case '39':
257
+ return { icon: res.biorhythom_icon3, iconId: 3 }
258
+ default:
259
+ return { icon: item?.icon, iconId: item?.id }
260
+ }
261
+ }
262
+
263
+ export function useStorageBiorhythm(): [[], (key, enable: boolean, gradient: BiorhythmGradientType, repeatPeriod: Period[], planList: Plan[]) => void, (key) => void, (key) => {
264
+ repeatPeriod: Period[];
265
+ planList: Plan[];
266
+ enable: boolean;
267
+ gradient: BiorhythmGradientType
268
+ }] {
269
+ const [storageBiorhythms, setStorageBiorhythms] = useState([])
270
+ useEffect(() => {
271
+ AsyncStorage.getItem('BIORHYTHM_STORAGE').then(res => {
272
+ if (res) {
273
+ const storageBiorhythms = JSON.parse(res)
274
+ setStorageBiorhythms(storageBiorhythms)
275
+ xLog('AsyncStorage getBiorhythm', storageBiorhythms)
276
+ }
277
+ })
278
+ }, [])
279
+
280
+ const saveBiorhythm = useCallback((key, enable: boolean, gradient: BiorhythmGradientType, repeatPeriod: Period[], planList: Plan[]) => {
281
+ const newPlanList = planList?.map(item => {
282
+ return { ...item, icon: `${item.icon}` }
283
+ }).sort((a, b) => a.time - b.time)
284
+ const weeks = repeatPeriod.map(item => item.enabled ? 1 : 0)
285
+ const sun = weeks.splice(6, 1)[0]
286
+ weeks.unshift(sun)
287
+ weeks.push(0)
288
+ const biorhythm = cloneDeep({
289
+ key: key,
290
+ enable: enable,
291
+ weeks: weeks,
292
+ gradient: gradient,
293
+ planList: newPlanList,
294
+ })
295
+ const newStorageBiorhythms = storageBiorhythms.filter(item => item.key !== key)
296
+ newStorageBiorhythms.unshift(biorhythm)
297
+ setStorageBiorhythms(newStorageBiorhythms)
298
+ xLog('AsyncStorage saveBiorhythm', newStorageBiorhythms)
299
+ AsyncStorage.setItem('BIORHYTHM_STORAGE', JSON.stringify(newStorageBiorhythms), (error) => {
300
+ xLog('AsyncStorage saveBiorhythm error', error)
301
+ }).then()
302
+ }, [storageBiorhythms])
303
+
304
+ const removeBiorhythm = useCallback((key) => {
305
+ const newStorageBiorhythms = storageBiorhythms.filter(item => item.key !== key)
306
+ setStorageBiorhythms(newStorageBiorhythms)
307
+ AsyncStorage.setItem('BIORHYTHM_STORAGE', JSON.stringify(newStorageBiorhythms)).then(res => {
308
+ xLog('AsyncStorage removeBiorhythm res', res)
309
+ })
310
+ }, [storageBiorhythms])
311
+
312
+ const applyBiorhythm = useCallback((key) => {
313
+ const newBiorhythm = storageBiorhythms.find(item => item.key === key)
314
+ const planList = newBiorhythm.planList?.map((it, index) => {
315
+ return {
316
+ ...it,
317
+ id: it.id ?? index,
318
+ icon: replaceImg(it?.iconId || it?.icon)?.icon,
319
+ iconId: replaceImg(it?.iconId || it?.icon)?.iconId,
320
+ }
321
+ })
322
+ const repeatPeriod = newBiorhythm.weeks.slice(0, 7).map((it, index) => {
323
+ const idx = index === 0 ? 7 : index
324
+ return {
325
+ index: idx,
326
+ title: getRepeatPeriodTitleByIndex(idx),
327
+ enabled: it,
328
+ }
329
+ })
330
+ repeatPeriod.sort((a, b) => a.index - b.index)
331
+ return {
332
+ repeatPeriod: repeatPeriod,
333
+ planList: planList,
334
+ enable: newBiorhythm.enable,
335
+ gradient: newBiorhythm.gradient,
336
+ }
337
+ }, [storageBiorhythms])
338
+ return [storageBiorhythms, saveBiorhythm, removeBiorhythm, applyBiorhythm]
339
+ }
@@ -1,4 +1,5 @@
1
- import React, { useCallback, useEffect, useMemo } from 'react'
1
+ import { xLog } from '@ledvance/base/src/utils'
2
+ import React, { useCallback, useEffect, useMemo, useState } from 'react'
2
3
  import { FlatList, Image, Linking, ScrollView, Switch, Text, TouchableOpacity, View } from 'react-native'
3
4
  import { useNavigation } from '@react-navigation/native'
4
5
  import { useDebounceFn, useReactive, useUpdateEffect } from 'ahooks'
@@ -23,8 +24,11 @@ import res from '@ledvance/base/src/res'
23
24
  import { ui_biz_routerKey } from '../../navigation/Routers'
24
25
  import { cctToColor } from '@ledvance/base/src/utils/cctUtils'
25
26
  import { BiorhythmEditPageParams } from './BiorhythmDetailPage'
26
- import { useBiorhythm } from './BiorhythmActions'
27
- import { convertMinutesTo12HourFormat, showDialog as showCommonDialog, showDialog } from '@ledvance/base/src/utils/common'
27
+ import { replaceImg, useBiorhythm, useStorageBiorhythm } from './BiorhythmActions'
28
+ import {
29
+ convertMinutesTo12HourFormat,
30
+ showDialog
31
+ } from '@ledvance/base/src/utils/common'
28
32
  import { useParams } from '@ledvance/base/src/hooks/Hooks'
29
33
  import Page from '@ledvance/base/src/components/Page'
30
34
  import Spacer from '@ledvance/base/src/components/Spacer'
@@ -63,6 +67,8 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
63
67
  const is24Hour = useSystemTimeFormate()
64
68
  const { productId } = deviceInfo
65
69
  const devicesJudge = pIdList.some(val => val === productId)
70
+ const [storageBiorhythms, saveStorageBiorhythms, removeStorageBiorhythm, applyStorageBiorhythm] = useStorageBiorhythm()
71
+ const [biorhythmListVisible, setBiorhythmListVisible] = useState(false)
66
72
 
67
73
  const state = useReactive<UIState>({
68
74
  ...cloneDeep(biorhythm),
@@ -144,29 +150,6 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
144
150
  run()
145
151
  }, [state.flag])
146
152
 
147
- const replaceImg = (img) => {
148
- const item = iconList?.find(val => val.id === Number(img))
149
- switch (img) {
150
- case 'rhythm_icon1':
151
- case '31':
152
- return { icon: res.biorhythom_icon1, iconId: 1 }
153
- case 'rhythm_icon2':
154
- case '33':
155
- return { icon: res.biorhythom_icon5, iconId: 5 }
156
- case 'rhythm_icon3':
157
- case '35':
158
- return { icon: res.biorhythom_icon2, iconId: 2 }
159
- case 'rhythm_icon4':
160
- case '32':
161
- return { icon: res.biorhythom_icon9, iconId: 9 }
162
- case 'rhythm_icon12':
163
- case '39':
164
- return { icon: res.biorhythom_icon3, iconId: 3 }
165
- default:
166
- return { icon: item?.icon, iconId: item?.id }
167
- }
168
- }
169
-
170
153
  useUpdateEffect(() => {
171
154
  console.log('Redux 生物节律数据更新', biorhythm)
172
155
  const cloneBiorhym = cloneDeep(biorhythm)
@@ -262,6 +245,26 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
262
245
  <Page
263
246
  backText={uaGroupInfo.name}
264
247
  onBackClick={navigation.goBack}
248
+ headlineTopContent={<View style={{ flexDirection: 'row', width: '100%', justifyContent: 'space-between' }}>
249
+ <DeleteButton style={{flex: 1}} text={I18n.getLang('biorhythm_save_as')} onPress={() => {
250
+ Dialog.prompt({
251
+ title: I18n.getLang('biorhythm_save_title'),
252
+ placeholder: I18n.getLang('biorhythm_save_placeholder'),
253
+ defaultValue: `${uaGroupInfo.name}`,
254
+ cancelText: I18n.getLang('auto_scan_system_cancel'),
255
+ confirmText: I18n.getLang('auto_scan_system_wifi_confirm'),
256
+ inputWrapperStyle: {backgroundColor: props.theme?.textInput.background, borderRadius: cx(10)},
257
+ autoFocus: true,
258
+ onConfirm: (data, { close }) => {
259
+ saveStorageBiorhythms(data, state.enable, state.gradient, state.repeatPeriod, state.planList)
260
+ close()
261
+ }
262
+ })
263
+ }} />
264
+ <DeleteButton style={{flex: 1}} text={I18n.getLang('biorhythm_load')} onPress={() => {
265
+ setBiorhythmListVisible(true)
266
+ }} />
267
+ </View>}
265
268
  headlineText={I18n.getLang('add_new_trigger_time_system_back_text')}
266
269
  headlineIconContent={<Switch
267
270
  value={state.enable}
@@ -626,7 +629,7 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
626
629
  <DeleteButton
627
630
  text={I18n.getLang('bio_ryhthm_default_button_reset_text')}
628
631
  onPress={() => {
629
- showCommonDialog({
632
+ showDialog({
630
633
  method: 'confirm',
631
634
  title: I18n.getLang('bio_ryhthm_reset_description_text'),
632
635
  onConfirm: (_, { close }) => {
@@ -691,6 +694,54 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
691
694
  state.flag = Symbol()
692
695
  }}
693
696
  />
697
+ <Modal visible={biorhythmListVisible} onMaskPress={() => {setBiorhythmListVisible(false)}}>
698
+ <View style={{ height: cx(300), padding: cx(16), backgroundColor: props.theme?.card.background }}>
699
+ {
700
+ storageBiorhythms.length === 0 ? (
701
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
702
+ <InfoText
703
+ textStyle={{ flex: undefined }}
704
+ icon={res.ic_info}
705
+ text={I18n.getLang('energyconsumption_emptydata')}
706
+ />
707
+ </View>
708
+ ) : (
709
+ <FlatList
710
+ data={storageBiorhythms}
711
+ renderItem={({ item }) => {
712
+ return <View style={{ padding: cx(5), flexDirection: 'row' }}>
713
+ <Text style={{ flex: 1, color: props.theme?.global.fontColor }}>{item.key}</Text>
714
+ <TouchableOpacity style={{width: cx(24), height: cx(24), marginRight: cx(20)}} onPress={() => {
715
+ showDialog({
716
+ method: 'confirm',
717
+ title: I18n.getLang('biorhythm_delete_tips'),
718
+ onConfirm: (_, { close }) => {
719
+ removeStorageBiorhythm(item.key)
720
+ close()
721
+ }
722
+ })
723
+ }}>
724
+ <Image source={{ uri: res.delete}} style={{width: cx(24), height: cx(24), tintColor: props.theme?.global.warning}} />
725
+ </TouchableOpacity>
726
+ <TouchableOpacity style={{width: cx(24), height: cx(24), marginRight: cx(10)}} onPress={() => {
727
+ const newBiorhythm = applyStorageBiorhythm(item.key)
728
+ state.enable = newBiorhythm.enable
729
+ state.gradient = newBiorhythm.gradient
730
+ state.repeatPeriod = newBiorhythm.repeatPeriod
731
+ state.planList = newBiorhythm.planList
732
+ run()
733
+ setBiorhythmListVisible(false)
734
+ }}>
735
+ <Image source={{ uri:res.ic_checked}} style={{width: cx(24), height: cx(24), tintColor: props.theme?.icon.normal}} />
736
+ </TouchableOpacity>
737
+ </View>
738
+ }}
739
+ keyExtractor={(item) => `${item.key}`}
740
+ />
741
+ )
742
+ }
743
+ </View>
744
+ </Modal>
694
745
  </>
695
746
  </Page>
696
747
  )
@@ -0,0 +1,102 @@
1
+ import MoodColorsLine from '@ledvance/base/src/components/MoodColorsLine'
2
+ import Spacer from '@ledvance/base/src/components/Spacer'
3
+ import res from '@ledvance/base/src/res'
4
+ import { hsv2Hex, mapFloatToRange } from '@ledvance/base/src/utils'
5
+ import { cctToColor } from '@ledvance/base/src/utils/cctUtils'
6
+ import React, { useState } from 'react'
7
+ import { Image, LayoutChangeEvent, StyleSheet, View } from 'react-native'
8
+ import { Utils } from 'tuya-panel-kit'
9
+ import { MoodLampInfo } from './Interface'
10
+
11
+ const cx = Utils.RatioUtils.convertX;
12
+ const { withTheme } = Utils.ThemeUtils;
13
+
14
+ const MixMoodColorsLine = (props: {
15
+ mixSubLight: MoodLampInfo;
16
+ isMix: boolean;
17
+ type: 'gradient' | 'separate';
18
+ width?: number; // 外部传入的宽度(父组件已计算好)
19
+ }) => {
20
+ const { mixSubLight, isMix, type, width: propWidth } = props;
21
+ const [measuredWidth, setMeasuredWidth] = useState(0);
22
+
23
+ const handleLayout = (event: LayoutChangeEvent) => {
24
+ const { width } = event.nativeEvent.layout;
25
+ if (width > 0 && width !== measuredWidth) {
26
+ setMeasuredWidth(width);
27
+ }
28
+ };
29
+
30
+ const lightColors = (mixSubLight.enable && mixSubLight.nodes.length > 0)
31
+ ? mixSubLight.nodes.map(n => {
32
+ const s = Math.round(mapFloatToRange(n.s / 100, 30, 100));
33
+ return n.isColorNode
34
+ ? hsv2Hex(n.h, s, Math.round(mapFloatToRange(n.v / 100, 50, 100)))
35
+ : cctToColor(n.colorTemp.toFixed());
36
+ })
37
+ : ['#eee'];
38
+
39
+ // 如果父组件传入了精确宽度,直接使用;否则使用内部测量的宽度。
40
+ const finalWidth = propWidth || measuredWidth;
41
+
42
+ // 渲染颜色条的通用逻辑
43
+ const renderColorLine = () => (
44
+ finalWidth > 0 && (
45
+ <MoodColorsLine
46
+ nodeStyle={{ borderColor: '#ccc', borderWidth: 1 }}
47
+ width={finalWidth}
48
+ type={type}
49
+ colors={lightColors}
50
+ />
51
+ )
52
+ );
53
+
54
+ // 渲染右侧图标的通用逻辑
55
+ const renderIcon = () => (
56
+ isMix && (
57
+ <>
58
+ <Spacer width={cx(7)}/>
59
+ <View style={styles.mixLineIconView}>
60
+ <Image style={styles.mixLineIcon} source={{ uri: mixSubLight.enable ? res.light_on : res.light_off }}/>
61
+ </View>
62
+ </>
63
+ )
64
+ );
65
+
66
+ // Case 1: 父组件已经计算并传入了宽度 (最高效)
67
+ if (propWidth) {
68
+ return (
69
+ <View style={styles.mixLineRow}>
70
+ {renderColorLine()}
71
+ {renderIcon()}
72
+ </View>
73
+ );
74
+ }
75
+
76
+ // Case 2: 自我测量模式 (回退方案)
77
+ return (
78
+ <View style={styles.mixLineRow}>
79
+ {/* 这个 View (测量器) 会自动收缩以填充'颜色条'应占的空间 */}
80
+ <View style={{ flex: 1 }} onLayout={handleLayout}>
81
+ {renderColorLine()}
82
+ </View>
83
+ {/* 图标作为测量器的兄弟节点,Flexbox 会先为它分配空间 */}
84
+ {renderIcon()}
85
+ </View>
86
+ );
87
+ }
88
+
89
+ const styles = StyleSheet.create({
90
+ mixLineRow: { flexDirection: 'row', alignItems: 'center' },
91
+ mixLineIconView: {
92
+ width: cx(24),
93
+ height: cx(24),
94
+ justifyContent: 'center',
95
+ alignItems: 'center',
96
+ backgroundColor: '#aaa',
97
+ borderRadius: cx(8),
98
+ },
99
+ mixLineIcon: { width: cx(16), height: cx(16), tintColor: '#fff' },
100
+ });
101
+
102
+ export default withTheme(MixMoodColorsLine)
@@ -108,14 +108,6 @@ function getRGBWDefSceneList(): RemoteMoodInfo[] {
108
108
  t: 0,
109
109
  e: false,
110
110
  },
111
- {
112
- n: I18n.getLang('mesh_device_detail_lighting_color_mode'),
113
- i:
114
- '05464601000003e803e800000000464601007803e803e80000000046460100f003e803e800000000464601003d03e803e80000000046460100ae03e803e800000000464601011303e803e800000000',
115
- s: '',
116
- t: 0,
117
- e: false,
118
- },
119
111
  {
120
112
  n: I18n.getLang('mesh_device_detail_lighting_white_mode'),
121
113
  i: '0646460100000000000003e8000046460100000000000003e8019046460100000000000003e803e8',
@@ -156,13 +148,6 @@ function getRGBDefSceneList(): RemoteMoodInfo[] {
156
148
  s: '',
157
149
  t: 0,
158
150
  e: false,
159
- },
160
- {
161
- n: I18n.getLang('mesh_device_detail_lighting_color_mode'),
162
- i: '05464601000003e803e800000000464601007803e803e80000000046460100f003e803e800000000464601003d03e803e80000000046460100ae03e803e800000000464601011303e803e800000000',
163
- s: '',
164
- t: 0,
165
- e: false,
166
151
  },
167
152
  ...defColorSceneList,
168
153
  ];
@@ -198,14 +183,6 @@ function getOnlyRGBDefSceneList(): RemoteMoodInfo[] {
198
183
  t: 0,
199
184
  e: false,
200
185
  },
201
- {
202
- n: I18n.getLang('mesh_device_detail_lighting_color_mode'),
203
- i:
204
- '05464601000003e803e800000000464601007803e803e80000000046460100f003e803e800000000464601003d03e803e80000000046460100ae03e803e800000000464601011303e803e800000000',
205
- s: '',
206
- t: 0,
207
- e: false,
208
- },
209
186
  ...defColorSceneList,
210
187
  ];
211
188
  }
@@ -1200,12 +1177,6 @@ function getDefMixLightSceneList(): MixRemoteMoodInfo[] {
1200
1177
  image: '',
1201
1178
  value: '00030101020e0d0001f401f40000',
1202
1179
  },
1203
- {
1204
- name: I18n.getLang('mesh_device_detail_lighting_color_mode'),
1205
- image: '',
1206
- value:
1207
- '00040000010603464601000003e803e8464601007803e803e846460100f003e803e8464601003d03e803e846460100ae03e803e8464601011303e803e8',
1208
- },
1209
1180
  {
1210
1181
  name: I18n.getLang('mesh_device_detail_lighting_white_mode'),
1211
1182
  image: '',
@@ -1,15 +1,13 @@
1
- import Card from '@ledvance/base/src/components/Card';
2
- import MoodColorsLine from '@ledvance/base/src/components/MoodColorsLine';
3
- import Spacer from '@ledvance/base/src/components/Spacer';
4
1
  import ThemeType from '@ledvance/base/src/config/themeType';
5
2
  import I18n from '@ledvance/base/src/i18n';
6
3
  import res from '@ledvance/base/src/res';
7
4
  import { hsv2Hex, mapFloatToRange } from '@ledvance/base/src/utils';
8
5
  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';
6
+ import React, { useMemo } from 'react';
7
+ import { Image, Platform, StyleSheet, Text, TouchableOpacity, View, ViewProps, ViewStyle } from 'react-native';
8
+ import LinearGradient from 'react-native-linear-gradient';
11
9
  import { Utils } from 'tuya-panel-kit';
12
- import { MoodJumpGradientMode, MoodLampInfo, MoodUIInfo } from './Interface';
10
+ import { MoodLampInfo, MoodUIInfo } from './Interface';
13
11
 
14
12
  const cx = Utils.RatioUtils.convertX;
15
13
  const { withTheme } = Utils.ThemeUtils;
@@ -32,189 +30,195 @@ interface MoodItemProps extends ViewProps {
32
30
  onSwitch: (enable: boolean) => void;
33
31
  }
34
32
 
33
+ const getGradientColors = (lampInfo: MoodLampInfo, defaultColors: string[]): string[] => {
34
+ if (!lampInfo.enable || lampInfo.nodes.length === 0) {
35
+ return defaultColors;
36
+ }
37
+
38
+ const colors = lampInfo.nodes.map(n => {
39
+ const s = Math.round(mapFloatToRange(n.s / 100, 30, 70));
40
+ const v = Math.round(mapFloatToRange(n.v / 100, 80, 100));
41
+ return n.isColorNode ? hsv2Hex(n.h, s, v) : cctToColor(n.colorTemp.toFixed());
42
+ });
43
+
44
+ if (colors.length === 1) {
45
+ return [colors[0], colors[0]];
46
+ }
47
+
48
+ return colors;
49
+ };
50
+
35
51
  const MoodItem = (props: MoodItemProps) => {
36
- const { mood, isMix, deviceTypeOption, theme } = props;
52
+ const { mood, isMix, deviceTypeOption, theme, onPress, onSwitch, enable, style } = props;
53
+ const styles = getStyles(theme);
54
+
37
55
  const isDynamic = useMemo(() => {
38
56
  return mood.mainLamp.nodes?.length > 1 || mood.secondaryLamp.nodes?.length > 1;
39
57
  }, [mood.mainLamp.nodes, mood.secondaryLamp.nodes]);
40
58
 
41
- const gradientMode = useMemo(() => (
42
- deviceTypeOption?.isStringLight ? MoodJumpGradientMode.StringGradient : deviceTypeOption?.isStripLight ? MoodJumpGradientMode.StripGradient : MoodJumpGradientMode.SourceGradient
43
- ), [MoodJumpGradientMode, deviceTypeOption]);
59
+ const isDarkMode = theme?.type === 'dark';
60
+ const defaultGreyGradient = isDarkMode ? ['#444444', '#333333'] : ['#E5E5E5', '#DCDCDC'];
44
61
 
45
- const styles = useMemo(() => getStyles(theme), [theme]);
62
+ const mainLampColors = useMemo(
63
+ () => getGradientColors(mood.mainLamp, defaultGreyGradient),
64
+ [mood.mainLamp, defaultGreyGradient]
65
+ );
66
+ const secondaryLampColors = useMemo(
67
+ () => getGradientColors(mood.secondaryLamp, defaultGreyGradient),
68
+ [mood.secondaryLamp, defaultGreyGradient]
69
+ );
46
70
 
47
- return (
48
- <Card style={[styles.card, props.style]} onPress={props.onPress}>
49
- <View style={styles.contentContainer}>
50
- <View style={styles.row}>
51
- <Text style={styles.headText}>{mood.name}</Text>
52
- <TouchableOpacity style={styles.checkbox} onPress={() => props.onSwitch(!props.enable)}>
53
- <Image source={{ uri: res.ic_check }}
54
- style={[styles.checkboxImage, { tintColor: props.enable ? theme?.icon.primary : theme?.icon.disable }]}/>
55
- </TouchableOpacity>
56
- </View>
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)) && (
64
- <>
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
- />
71
- </>
72
- )}
73
- <Spacer height={cx(12)}/>
74
- <View style={styles.row}>
75
- <View style={styles.moodTypeLabel}>
76
- <Text style={styles.moodTypeLabelText}>
77
- {I18n.getLang(isDynamic ? 'mood_overview_field_chip_2' : 'mood_overview_field_chip_text')}
78
- </Text>
79
- </View>
71
+ const isMixLight = isMix || deviceTypeOption?.isMixLight;
72
+
73
+ const hasMainLampColors = mood.mainLamp.enable && mood.mainLamp.nodes.length > 0;
74
+ const hasSecondaryLampColors = mood.secondaryLamp.enable && mood.secondaryLamp.nodes.length > 0;
75
+
76
+ const renderContent = () => (
77
+ // 关键修改 1: contentContainer 使用 justifyContent: 'space-between'
78
+ <View style={styles.contentContainer}>
79
+ {/* 顶部内容 */}
80
+ <View style={styles.row}>
81
+ <Text style={styles.headText}>{mood.name}</Text>
82
+ {/* checkbox 的 TouchableOpacity 现在也应用了阴影样式 */}
83
+ <TouchableOpacity style={styles.checkbox} onPress={() => onSwitch(!enable)}>
84
+ <Image
85
+ source={{ uri: res.ic_check }}
86
+ style={[styles.checkboxImage, { tintColor: enable ? theme?.icon.primary : theme?.icon.disable }]}
87
+ />
88
+ </TouchableOpacity>
89
+ </View>
90
+
91
+ {/* 底部内容 (移除了 Spacer) */}
92
+ <View style={styles.row}>
93
+ <View style={styles.moodTypeLabel}>
94
+ <Text style={styles.moodTypeLabelText}>
95
+ {I18n.getLang(isDynamic ? 'mood_overview_field_chip_2' : 'mood_overview_field_chip_text')}
96
+ </Text>
80
97
  </View>
81
98
  </View>
82
- </Card>
99
+ </View>
83
100
  );
84
- };
85
101
 
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);
102
+ const renderBackground = () => {
103
+ if (!isMixLight) {
104
+ return <LinearGradient colors={mainLampColors} style={{ flex: 1 }} start={{ x: 0, y: 0.5 }} end={{ x: 1, y: 0.5 }} />;
100
105
  }
101
- };
102
106
 
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 && (
118
- <MoodColorsLine
119
- nodeStyle={{ borderColor: '#ccc', borderWidth: 1 }}
120
- width={finalWidth}
121
- type={type}
122
- colors={lightColors}
123
- />
124
- )
125
- );
107
+ if (hasMainLampColors && hasSecondaryLampColors) {
108
+ return (
109
+ <>
110
+ <LinearGradient colors={mainLampColors} style={{ flex: 1 }} start={{ x: 0, y: 0.5 }} end={{ x: 1, y: 0.5 }} />
111
+ <LinearGradient
112
+ colors={secondaryLampColors}
113
+ style={{ flex: 1 }}
114
+ start={{ x: 0, y: 0.5 }}
115
+ end={{ x: 1, y: 0.5 }}
116
+ />
117
+ </>
118
+ );
119
+ }
126
120
 
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
- );
121
+ if (hasMainLampColors) {
122
+ return <LinearGradient colors={mainLampColors} style={{ flex: 1 }} start={{ x: 0, y: 0.5 }} end={{ x: 1, y: 0.5 }} />;
123
+ }
138
124
 
139
- // Case 1: 父组件已经计算并传入了宽度 (最高效)
140
- if (propWidth) {
141
- return (
142
- <View style={styles.mixLineRow}>
143
- {renderColorLine()}
144
- {renderIcon()}
145
- </View>
146
- );
147
- }
125
+ if (hasSecondaryLampColors) {
126
+ return (
127
+ <LinearGradient colors={secondaryLampColors} style={{ flex: 1 }} start={{ x: 0, y: 0.5 }} end={{ x: 1, y: 0.5 }} />
128
+ );
129
+ }
130
+
131
+ return <LinearGradient colors={mainLampColors} style={{ flex: 1 }} start={{ x: 0, y: 0.5 }} end={{ x: 1, y: 0.5 }} />;
132
+ };
148
133
 
149
- // Case 2: 自我测量模式 (回退方案)
150
134
  return (
151
- <View style={styles.mixLineRow}>
152
- {/* 这个 View (测量器) 会自动收缩以填充'颜色条'应占的空间 */}
153
- <View style={{ flex: 1 }} onLayout={handleLayout}>
154
- {renderColorLine()}
155
- </View>
156
- {/* 图标作为测量器的兄弟节点,Flexbox 会先为它分配空间 */}
157
- {renderIcon()}
158
- </View>
135
+ <TouchableOpacity activeOpacity={0.8} onPress={onPress} style={[styles.container, style || { marginHorizontal: cx(24) }]}>
136
+ <View style={styles.backgroundWrapper}>{renderBackground()}</View>
137
+ <View style={StyleSheet.absoluteFill}>{renderContent()}</View>
138
+ </TouchableOpacity>
159
139
  );
160
- }
140
+ };
141
+
142
+ const getStyles = (theme?: ThemeType) => {
143
+ const isDarkMode = theme?.type === 'dark';
144
+ const primaryTextColor = isDarkMode ? '#FFFFFF' : '#000000';
145
+ const shadowColor = isDarkMode ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.7)';
146
+ const tagBackgroundColor = isDarkMode ? 'rgba(0, 0, 0, 0.3)' : 'rgba(255, 255, 255, 0.5)';
147
+ const tagTextColor = isDarkMode ? '#FFFFFF' : '#333333';
148
+
149
+ return StyleSheet.create({
150
+ container: {
151
+ // marginHorizontal: cx(24),
152
+ height: cx(135),
153
+ ...Platform.select({
154
+ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8 },
155
+ android: { elevation: 5 },
156
+ }),
157
+ },
158
+ backgroundWrapper: {
159
+ flex: 1,
160
+ borderRadius: cx(16),
161
+ overflow: 'hidden',
162
+ },
163
+ // 关键修改 1: 使用 justifyContent 将内容推向两端
164
+ contentContainer: {
165
+ flex: 1,
166
+ paddingHorizontal: cx(16),
167
+ paddingVertical: cx(12),
168
+ backgroundColor: 'transparent',
169
+ justifyContent: 'space-between', // 使顶部和底部内容分别贴近上下边缘
170
+ },
171
+ row: {
172
+ flexDirection: 'row',
173
+ alignItems: 'center',
174
+ },
175
+ headText: {
176
+ flex: 1,
177
+ fontSize: cx(16),
178
+ lineHeight: cx(20),
179
+ fontWeight: 'bold',
180
+ color: primaryTextColor,
181
+ textShadowColor: shadowColor,
182
+ textShadowOffset: { width: 0, height: 1 },
183
+ textShadowRadius: 3,
184
+ },
185
+ // 关键修改 2: 为 checkbox 添加阴影
186
+ checkbox: {
187
+ width: cx(45),
188
+ height: cx(45),
189
+ marginRight: cx(-10),
190
+ justifyContent: 'center',
191
+ alignItems: 'flex-end',
192
+ // 添加一个通用的深色阴影以提供对比度
193
+ ...Platform.select({
194
+ ios: {
195
+ shadowColor: 'rgba(0, 0, 0, 0.4)',
196
+ shadowOffset: { width: 0, height: 1 },
197
+ shadowRadius: 2,
198
+ shadowOpacity: 1,
199
+ },
200
+ android: {
201
+ elevation: 3,
202
+ },
203
+ }),
204
+ },
205
+ checkboxImage: {
206
+ width: cx(44),
207
+ height: cx(44),
208
+ },
209
+ moodTypeLabel: {
210
+ paddingHorizontal: cx(12.5),
211
+ paddingVertical: cx(4),
212
+ borderRadius: cx(8),
213
+ backgroundColor: tagBackgroundColor,
214
+ },
215
+ moodTypeLabelText: {
216
+ fontSize: cx(10),
217
+ lineHeight: cx(12),
218
+ color: tagTextColor,
219
+ fontWeight: '500',
220
+ },
221
+ });
222
+ };
161
223
 
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);
224
+ export default withTheme(MoodItem);
@@ -30,10 +30,19 @@ import {WorkMode} from '@ledvance/base/src/utils/interface';
30
30
  import ThemeType from '@ledvance/base/src/config/themeType'
31
31
  import {showDialog} from '@ledvance/base/src/utils/common';
32
32
 
33
- const cx = Utils.RatioUtils.convertX;
33
+ const { convertX: cx, width: screenWidth } = Utils.RatioUtils;
34
34
  const { withTheme } = Utils.ThemeUtils
35
35
 
36
36
  const MAX_MOOD_COUNT = 255;
37
+ // --- 动态计算项目宽度 ---
38
+ // 1. 定义网格的边距和列间距
39
+ const GRID_HORIZONTAL_PADDING = cx(24);
40
+ const GRID_GAP = cx(16);
41
+ const NUM_COLUMNS = 2;
42
+ // 2. 计算每个 MoodItem 的宽度
43
+ // (屏幕总宽度 - 两边的边距 - (列数 - 1) * 列间距) / 列数
44
+ const ITEM_WIDTH =
45
+ (screenWidth - GRID_HORIZONTAL_PADDING * 2 - (NUM_COLUMNS - 1) * GRID_GAP) / NUM_COLUMNS;
37
46
 
38
47
  const MoodPage = (props: {theme?: ThemeType}) => {
39
48
  const params = useParams<MoodPageParams>();
@@ -281,6 +290,13 @@ const MoodPage = (props: {theme?: ThemeType}) => {
281
290
  padding: cx(5),
282
291
  alignItems: 'flex-start',
283
292
  },
293
+ refresh: {
294
+ alignItems: 'flex-end',
295
+ paddingRight: cx(24)
296
+ },
297
+ columnWrapperStyle: {
298
+ justifyContent: 'space-between',
299
+ },
284
300
  })
285
301
 
286
302
  return (
@@ -315,7 +331,8 @@ const MoodPage = (props: {theme?: ThemeType}) => {
315
331
  }}
316
332
  />
317
333
  </View>}
318
- <TouchableOpacity style={{ alignItems: 'flex-end',paddingRight: cx(24) }}
334
+ <TouchableOpacity
335
+ style={styles.refresh}
319
336
  onPress={() => {
320
337
  showDialog({
321
338
  method: 'confirm',
@@ -344,9 +361,21 @@ const MoodPage = (props: {theme?: ThemeType}) => {
344
361
  )}
345
362
  <FlatList
346
363
  data={state.filterMoods}
364
+ // 关键属性 1: 设置列数
365
+ numColumns={NUM_COLUMNS}
366
+ // 关键属性 2: 为 FlatList 提供一个唯一的 key,当列数改变时强制刷新
367
+ key={NUM_COLUMNS}
368
+ // 关键属性 3: 设置整个列表容器的样式,主要是左右边距
369
+ contentContainerStyle={{
370
+ paddingHorizontal: GRID_HORIZONTAL_PADDING,
371
+ }}
372
+ // 关键属性 4: 设置行包装器的样式,用于在列之间创建间距
373
+ columnWrapperStyle={styles.columnWrapperStyle}
347
374
  renderItem={({ item }) => {
348
375
  return (
349
376
  <MoodItem
377
+ // 关键修改:通过 style prop 传入计算好的宽度
378
+ style={{ width: ITEM_WIDTH }}
350
379
  enable={getItemEnable(item)}
351
380
  isMix={!!(params.isMixLight || params.isCeilingLight)}
352
381
  deviceTypeOption={params}
@@ -7,7 +7,7 @@ import Spacer from '@ledvance/base/src/components/Spacer';
7
7
  import ThemeType from '@ledvance/base/src/config/themeType';
8
8
 
9
9
  import { MoodJumpGradientMode, MoodUIInfo } from './Interface';
10
- import { MixMoodColorsLine } from './MoodItem'; // 从 MoodItem.tsx 导入重构后的组件
10
+ import MixMoodColorsLine from './MixMoodColorsLine';
11
11
 
12
12
  const cx = Utils.RatioUtils.convertX;
13
13
  const { withTheme } = Utils.ThemeUtils;