@ledvance/group-ui-biz-bundle 1.0.121 → 1.0.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/modules/biorhythm/BiorhythmActions.ts +7 -2
- package/src/modules/biorhythm/BiorhythmPage.tsx +51 -16
- package/src/modules/biorhythm/circular/RhythmsCircle.tsx +488 -0
- package/src/modules/biorhythm/circular/conical-gradient/Android.tsx +63 -0
- package/src/modules/biorhythm/circular/conical-gradient/Ios.tsx +26 -0
- package/src/modules/biorhythm/circular/conical-gradient/Normal.tsx +187 -0
- package/src/modules/biorhythm/circular/conical-gradient/index.android.tsx +164 -0
- package/src/modules/biorhythm/circular/conical-gradient/index.ios.tsx +124 -0
- package/src/modules/biorhythm/circular/conical-gradient/index.tsx +3 -0
- package/src/modules/biorhythm/circular/conical-gradient/index.web.tsx +94 -0
- package/src/modules/biorhythm/circular/conical-gradient/interface.ts +19 -0
- package/src/modules/biorhythm/circular/conical-gradient/utils.ts +25 -0
- package/src/modules/biorhythm/circular/interface.ts +114 -0
- package/src/modules/energyConsumption/component/EnergyModal.tsx +3 -0
- package/src/modules/lightMode/LightModePage.tsx +50 -144
- package/src/modules/mood/DynamicMoodEditorPage.tsx +1 -1
- package/src/modules/mood/StaticMoodEditorPage.tsx +1 -1
- package/src/modules/mood_new/DynamicMoodEditorPage.tsx +1 -1
- package/src/modules/mood_new/MixDynamicMoodEditor.tsx +1 -1
- package/src/modules/mood_new/MoodInfo.ts +1 -1
- package/src/modules/mood_new/StaticMoodEditorPage.tsx +1 -1
- package/src/modules/powerOnBehavior/LightBehaviorPage.tsx +208 -0
- package/src/modules/powerOnBehavior/PlugBehaviorPage.tsx +99 -0
- package/src/modules/powerOnBehavior/PowerOnBehaviorActions.ts +131 -0
- package/src/modules/powerOnBehavior/Router.ts +27 -0
- package/src/navigation/Routers.ts +3 -1
- package/src/modules/biorhythm/circular/ItemIcon.d.ts +0 -22
- package/src/modules/biorhythm/circular/ItemIcon.tsx +0 -173
- package/src/modules/biorhythm/circular/Progress.d.ts +0 -24
- package/src/modules/biorhythm/circular/Progress.tsx +0 -372
- package/src/modules/biorhythm/circular/TimeCircular.d.ts +0 -11
- package/src/modules/biorhythm/circular/TimeCircular.tsx +0 -64
package/package.json
CHANGED
|
@@ -23,12 +23,15 @@ export const useBiorhythm = (): [BiorhythmBean, (value: BiorhythmBean) => Promis
|
|
|
23
23
|
return obj2Dp(val)
|
|
24
24
|
})
|
|
25
25
|
const supportOldData = useCallback((oldData: BiorhythmBean) =>{
|
|
26
|
+
const repeatPeriod = [...oldData.repeatPeriod]
|
|
27
|
+
repeatPeriod.sort((a, b) => a.index - b.index)
|
|
26
28
|
return {
|
|
27
29
|
...oldData,
|
|
30
|
+
repeatPeriod: repeatPeriod,
|
|
28
31
|
planList: oldData.planList.map(p => {
|
|
29
32
|
// @ts-ignore 老数据中使用的是startTime
|
|
30
33
|
if (!p.hasOwnProperty('time') && p.startTime){
|
|
31
|
-
|
|
34
|
+
// @ts-ignore
|
|
32
35
|
const t = p.startTime.split(':')
|
|
33
36
|
return {
|
|
34
37
|
...p,
|
|
@@ -72,6 +75,7 @@ export function dp2Obj(dp: string): BiorhythmBean {
|
|
|
72
75
|
enabled: p === '1',
|
|
73
76
|
}
|
|
74
77
|
})
|
|
78
|
+
repeatPeriod.sort((a, b) => a.index - b.index)
|
|
75
79
|
dpCopy = dpCopy.slice(2)
|
|
76
80
|
// 节点个数 (每个节点长度18),最多8个节点
|
|
77
81
|
hex2Int(dpCopy.slice(0, 2))
|
|
@@ -203,8 +207,9 @@ function obj2Dp(obj: BiorhythmBean): string {
|
|
|
203
207
|
const versionHex = '00'
|
|
204
208
|
const enableHex = obj.enable ? '01' : '00'
|
|
205
209
|
const gradientHex = obj.gradient === BiorhythmGradientType.EntireGradient ? '00' : '0F'
|
|
210
|
+
const newRepeatPeriod = [obj.repeatPeriod[obj.repeatPeriod.length - 1], ...obj.repeatPeriod.slice(0, -1)]
|
|
206
211
|
const repeatPeriodHex = parseInt(
|
|
207
|
-
|
|
212
|
+
newRepeatPeriod
|
|
208
213
|
.map(p => (p.enabled ? '1' : '0'))
|
|
209
214
|
.reverse()
|
|
210
215
|
.join(''),
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo } from 'react'
|
|
2
2
|
import { FlatList, Image, Linking, ScrollView, Switch, Text, TouchableOpacity, View } from 'react-native'
|
|
3
3
|
import { useNavigation } from '@react-navigation/native'
|
|
4
|
-
import TimeCircular from './circular/TimeCircular'
|
|
5
4
|
import { useDebounceFn, useReactive, useUpdateEffect } from 'ahooks'
|
|
6
5
|
import {
|
|
7
6
|
BiorhythmBean,
|
|
@@ -23,7 +22,6 @@ import I18n from '@ledvance/base/src/i18n'
|
|
|
23
22
|
import res from '@ledvance/base/src/res'
|
|
24
23
|
import { ui_biz_routerKey } from '../../navigation/Routers'
|
|
25
24
|
import { cctToColor } from '@ledvance/base/src/utils/cctUtils'
|
|
26
|
-
import { setDataSource } from '@ledvance/base/src/components/weekSelect'
|
|
27
25
|
import { BiorhythmEditPageParams } from './BiorhythmDetailPage'
|
|
28
26
|
import { useBiorhythm } from './BiorhythmActions'
|
|
29
27
|
import { convertMinutesTo12HourFormat, showDialog as showCommonDialog, showDialog } from '@ledvance/base/src/utils/common'
|
|
@@ -34,6 +32,7 @@ import ApplyForDeviceList from '@ledvance/base/src/components/ApplyForDeviceList
|
|
|
34
32
|
import DeleteButton from '@ledvance/base/src/components/DeleteButton'
|
|
35
33
|
import InfoText from '@ledvance/base/src/components/InfoText'
|
|
36
34
|
import ThemeType from '@ledvance/base/src/config/themeType'
|
|
35
|
+
import RhythmsCircle from './circular/RhythmsCircle'
|
|
37
36
|
|
|
38
37
|
const cx = Utils.RatioUtils.convertX
|
|
39
38
|
const { withTheme } = Utils.ThemeUtils
|
|
@@ -73,6 +72,14 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
73
72
|
loading: false
|
|
74
73
|
})
|
|
75
74
|
|
|
75
|
+
const timeImg = useMemo(() => {
|
|
76
|
+
if (is24Hour) {
|
|
77
|
+
return devicesJudge ? res.ic_warning_amber_sun : res.ic_warning_amber_new
|
|
78
|
+
} else {
|
|
79
|
+
return devicesJudge ? res.ic_warning_amber_sun_12 : res.ic_warning_amber_new_12
|
|
80
|
+
}
|
|
81
|
+
}, [is24Hour, devicesJudge])
|
|
82
|
+
|
|
76
83
|
const showGradientTypeSelectModal = useCallback((show: boolean) => {
|
|
77
84
|
state.showGradientTypeSelectModal = show
|
|
78
85
|
}, [])
|
|
@@ -177,11 +184,7 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
177
184
|
}, [JSON.stringify(biorhythm)])
|
|
178
185
|
|
|
179
186
|
useEffect(() => {
|
|
180
|
-
const weeks
|
|
181
|
-
biorhythm.repeatPeriod.map(item => {
|
|
182
|
-
return item?.enabled ? 1 : 0
|
|
183
|
-
})).filter(item => item.enabled)
|
|
184
|
-
.map(item => item.title)
|
|
187
|
+
const weeks = biorhythm.repeatPeriod.filter(it => it.enabled).map(it => it.title)
|
|
185
188
|
|
|
186
189
|
if (weeks.length > 0) {
|
|
187
190
|
// / more than one
|
|
@@ -215,7 +218,7 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
215
218
|
fontSize: cx(14),
|
|
216
219
|
flexDirection: 'row',
|
|
217
220
|
}}>
|
|
218
|
-
<Text style={{color: props.theme?.global.fontColor }}>{text[0]}</Text>
|
|
221
|
+
<Text style={{ color: props.theme?.global.fontColor }}>{text[0]}</Text>
|
|
219
222
|
<Text onPress={openLink}
|
|
220
223
|
style={{
|
|
221
224
|
fontFamily: 'helvetica_neue_lt_std_roman',
|
|
@@ -223,7 +226,7 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
223
226
|
textDecorationLine: 'underline',
|
|
224
227
|
flexWrap: 'wrap',
|
|
225
228
|
}}>SUN@HOME</Text>
|
|
226
|
-
<Text style={{color: props.theme?.global.fontColor }}>{text[1]}</Text>
|
|
229
|
+
<Text style={{ color: props.theme?.global.fontColor }}>{text[1]}</Text>
|
|
227
230
|
</Text>
|
|
228
231
|
}
|
|
229
232
|
|
|
@@ -243,6 +246,18 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
243
246
|
return !!state.planList.filter(p => p.id !== plan.id).find(p => p.name === plan.name)
|
|
244
247
|
}, [state.planList])
|
|
245
248
|
|
|
249
|
+
const convertPlandata = useCallback(() => {
|
|
250
|
+
return state.planList.map(item => {
|
|
251
|
+
return {
|
|
252
|
+
...item,
|
|
253
|
+
index: item.id,
|
|
254
|
+
noActiveColor: '#474e5d',
|
|
255
|
+
activeColor: '#F7EB2A',
|
|
256
|
+
color: item?.brightness === 0 ? '#000' : !params.isSupportTemperature && cctToColor(1) || cctToColor(item.colorTemperature.toFixed(), item?.brightness)
|
|
257
|
+
}
|
|
258
|
+
}).filter(plan => plan.enable)
|
|
259
|
+
}, [JSON.stringify(state.planList), params.isSupportTemperature])
|
|
260
|
+
|
|
246
261
|
return (
|
|
247
262
|
<Page
|
|
248
263
|
backText={uaGroupInfo.name}
|
|
@@ -327,10 +342,10 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
327
342
|
})}
|
|
328
343
|
</View>
|
|
329
344
|
<View style={{ marginHorizontal: cx(24), marginTop: cx(20) }}>
|
|
330
|
-
<Text style={{color: props.theme?.global.fontColor}}>{state.weekString}</Text>
|
|
345
|
+
<Text style={{ color: props.theme?.global.fontColor }}>{state.weekString}</Text>
|
|
331
346
|
</View>
|
|
332
347
|
<View style={{ marginHorizontal: cx(24), marginTop: cx(16) }}>
|
|
333
|
-
<Text style={{color: props.theme?.global.fontColor}}>
|
|
348
|
+
<Text style={{ color: props.theme?.global.fontColor }}>
|
|
334
349
|
{I18n.getLang('bio_ryhthm_default_selectionfield_topic_text')}
|
|
335
350
|
</Text>
|
|
336
351
|
<TouchableOpacity
|
|
@@ -369,7 +384,7 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
369
384
|
</TouchableOpacity>
|
|
370
385
|
</View>
|
|
371
386
|
<View style={{ height: cx(20) }} />
|
|
372
|
-
<TimeCircular
|
|
387
|
+
{/* <TimeCircular
|
|
373
388
|
planEdit={true}
|
|
374
389
|
planList={state.planList}
|
|
375
390
|
onPanMoved={(id, time) => {
|
|
@@ -383,7 +398,27 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
383
398
|
}}
|
|
384
399
|
replaceStatus={devicesJudge}
|
|
385
400
|
gradient={state.gradient === BiorhythmGradientType.DirectGradient}
|
|
386
|
-
isSupportTemperature={!params.isSupportTemperature} />
|
|
401
|
+
isSupportTemperature={!params.isSupportTemperature} /> */}
|
|
402
|
+
|
|
403
|
+
<View style={{ alignItems: 'center', justifyContent: 'center' }}>
|
|
404
|
+
<RhythmsCircle
|
|
405
|
+
size={250}
|
|
406
|
+
ringWidth={40}
|
|
407
|
+
thumbSize={36}
|
|
408
|
+
timeImg={timeImg}
|
|
409
|
+
gradientMode={state.gradient === BiorhythmGradientType.EntireGradient}
|
|
410
|
+
data={convertPlandata()}
|
|
411
|
+
onRelease={(planList) => {
|
|
412
|
+
state.planList = state.planList.map(item => {
|
|
413
|
+
return {
|
|
414
|
+
...item,
|
|
415
|
+
time: planList.find(p => p.index === item.id)?.time
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
state.flag = Symbol()
|
|
419
|
+
}}
|
|
420
|
+
/>
|
|
421
|
+
</View>
|
|
387
422
|
<View
|
|
388
423
|
style={{
|
|
389
424
|
flexDirection: 'row',
|
|
@@ -394,7 +429,7 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
394
429
|
}}>
|
|
395
430
|
{state.planList.length === 8 && <View
|
|
396
431
|
style={{ marginVertical: cx(10), flexDirection: 'row', alignItems: 'center', width: width - cx(48) }}>
|
|
397
|
-
<Image style={{ width: cx(16), height: cx(16), tintColor: props.theme?.global.warning }} source={{uri: res.ic_warning_amber}} />
|
|
432
|
+
<Image style={{ width: cx(16), height: cx(16), tintColor: props.theme?.global.warning }} source={{ uri: res.ic_warning_amber }} />
|
|
398
433
|
<Text
|
|
399
434
|
style={{
|
|
400
435
|
flexWrap: 'wrap',
|
|
@@ -450,7 +485,7 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
450
485
|
}
|
|
451
486
|
navigation.navigate(ui_biz_routerKey.group_ui_biz_biological_detail, editPageParams)
|
|
452
487
|
}}>
|
|
453
|
-
<Image source={{uri: res.biorhythom_add}} style={{ height: cx(24), width: cx(24), tintColor: props.theme?.icon.primary }} />
|
|
488
|
+
<Image source={{ uri: res.biorhythom_add }} style={{ height: cx(24), width: cx(24), tintColor: props.theme?.icon.primary }} />
|
|
454
489
|
</TouchableOpacity>
|
|
455
490
|
</>
|
|
456
491
|
}
|
|
@@ -516,7 +551,7 @@ const BiorhythmPage = (props: { theme?: ThemeType }) => {
|
|
|
516
551
|
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'space-between', marginTop: cx(16) }}>
|
|
517
552
|
<View style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center' }}>
|
|
518
553
|
<Image
|
|
519
|
-
source={{uri: setImg(item?.iconId)} || type && { uri: item?.icon } || item?.icon}
|
|
554
|
+
source={{ uri: setImg(item?.iconId) } || type && { uri: item?.icon } || item?.icon}
|
|
520
555
|
style={{
|
|
521
556
|
width: cx(24),
|
|
522
557
|
height: cx(24),
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-types */
|
|
2
|
+
/* eslint-disable no-return-assign */
|
|
3
|
+
/* eslint-disable no-lonely-if */
|
|
4
|
+
import React, { useRef, useEffect, useState, useMemo } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
View,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Image,
|
|
9
|
+
PanResponder,
|
|
10
|
+
GestureResponderEvent,
|
|
11
|
+
PanResponderGestureState,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { usePersistFn } from 'ahooks';
|
|
14
|
+
import ConicalGradient from './conical-gradient';
|
|
15
|
+
import { IData, RhythmsCircleProps } from './interface';
|
|
16
|
+
import { Utils } from 'tuya-panel-kit';
|
|
17
|
+
const { convertX: cx } = Utils.RatioUtils
|
|
18
|
+
|
|
19
|
+
const totalTime = 24 * 60;
|
|
20
|
+
const fullDeg = Math.PI * 2;
|
|
21
|
+
|
|
22
|
+
// 获取图标的x,y坐标
|
|
23
|
+
const getPoint = (deg: number, radius: number, center: number) => {
|
|
24
|
+
return {
|
|
25
|
+
x: radius * Math.sin(deg) + center,
|
|
26
|
+
y: -radius * Math.cos(deg) + center,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const RhythmsCircle: React.FC<RhythmsCircleProps & { gradientMode?: boolean }> = props => {
|
|
31
|
+
const {
|
|
32
|
+
thumbSize,
|
|
33
|
+
thumbStyle,
|
|
34
|
+
disabled = false,
|
|
35
|
+
disabledOpacity = 1,
|
|
36
|
+
iconStyle,
|
|
37
|
+
timeStyle,
|
|
38
|
+
pickerStyle,
|
|
39
|
+
timeImg,
|
|
40
|
+
lastRingColor,
|
|
41
|
+
gradientMode = true,
|
|
42
|
+
} = props;
|
|
43
|
+
|
|
44
|
+
const dragEnabled = useRef(false);
|
|
45
|
+
|
|
46
|
+
const markRefs = useRef<any>([]).current;
|
|
47
|
+
|
|
48
|
+
const markImgRefs = useRef<any>([]).current;
|
|
49
|
+
|
|
50
|
+
const dragIndex = useRef(0);
|
|
51
|
+
|
|
52
|
+
const outerRadius = useRef(props.size / 2);
|
|
53
|
+
|
|
54
|
+
const size = useRef(props.size);
|
|
55
|
+
|
|
56
|
+
const ringWidth = useRef(props.ringWidth);
|
|
57
|
+
|
|
58
|
+
const innerSize = useRef(props.size - props.ringWidth * 2);
|
|
59
|
+
|
|
60
|
+
const innerRadius = useRef(innerSize.current / 2);
|
|
61
|
+
|
|
62
|
+
const ringRadius = useRef((innerRadius.current + outerRadius.current) / 2);
|
|
63
|
+
|
|
64
|
+
const ringRef = useRef<ConicalGradient>();
|
|
65
|
+
|
|
66
|
+
const initData = (v: IData[]) => {
|
|
67
|
+
return v.map(({ index, time, icon, color, noActiveColor, activeColor }) => ({
|
|
68
|
+
index,
|
|
69
|
+
time,
|
|
70
|
+
icon,
|
|
71
|
+
color,
|
|
72
|
+
noActiveColor,
|
|
73
|
+
activeColor,
|
|
74
|
+
deg: (time / totalTime) * fullDeg,
|
|
75
|
+
}));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const [allData, setAllData] = useState(() => initData(props.data || []));
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
size.current = props.size;
|
|
82
|
+
ringWidth.current = props.ringWidth;
|
|
83
|
+
innerSize.current = props.size - props.ringWidth * 2;
|
|
84
|
+
innerRadius.current = innerSize.current / 2;
|
|
85
|
+
outerRadius.current = props.size / 2;
|
|
86
|
+
ringRadius.current = (innerRadius.current + outerRadius.current) / 2;
|
|
87
|
+
}, [props.size, props.ringWidth]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
setAllData(() => initData(props.data || []));
|
|
91
|
+
}, [props.data]);
|
|
92
|
+
|
|
93
|
+
// 极坐标的渐变色 - 修改以支持两种模式
|
|
94
|
+
const getRingColors = (data: IData[]) => {
|
|
95
|
+
type ColorStop = { color: string; angle: number };
|
|
96
|
+
|
|
97
|
+
// 防止数据为空时崩溃
|
|
98
|
+
if (!data || !data.length) {
|
|
99
|
+
return [{ color: '#000', angle: 0 }, { color: '#000', angle: fullDeg }]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 角度规范化函数:确保角度在0到2π范围内
|
|
103
|
+
const normalizeAngle = (angle: number) => {
|
|
104
|
+
while (angle < 0) {
|
|
105
|
+
angle += fullDeg;
|
|
106
|
+
}
|
|
107
|
+
while (angle >= fullDeg) {
|
|
108
|
+
angle -= fullDeg;
|
|
109
|
+
}
|
|
110
|
+
return angle;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (gradientMode) {
|
|
114
|
+
// 渐变模式:原有逻辑
|
|
115
|
+
const colors: ColorStop[] = data.map(({ time, color }) => ({
|
|
116
|
+
color,
|
|
117
|
+
angle: normalizeAngle((time / totalTime) * fullDeg - Math.PI / 2),
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
// 在日落节点后多加一个颜色节点优化显示
|
|
121
|
+
colors.push({
|
|
122
|
+
color: lastRingColor || '#000',
|
|
123
|
+
angle: normalizeAngle(((data[0].time - 60) / totalTime) * fullDeg - Math.PI / 2),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
colors.sort((a, b) => {
|
|
127
|
+
return a.angle - b.angle;
|
|
128
|
+
});
|
|
129
|
+
return colors;
|
|
130
|
+
} else {
|
|
131
|
+
// 直接变化模式:创建阶梯式颜色变化,消除渐变效果
|
|
132
|
+
const sortedData = [...data].sort((a, b) => a.time - b.time);
|
|
133
|
+
const colors: ColorStop[] = [];
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < sortedData.length; i++) {
|
|
136
|
+
const currentItem = sortedData[i];
|
|
137
|
+
const nextItem = sortedData[(i + 1) % sortedData.length];
|
|
138
|
+
|
|
139
|
+
// 当前节点的开始角度
|
|
140
|
+
const currentAngle = normalizeAngle((currentItem.time / totalTime) * fullDeg - Math.PI / 2);
|
|
141
|
+
|
|
142
|
+
// 下一个节点的角度
|
|
143
|
+
let nextAngle = normalizeAngle((nextItem.time / totalTime) * fullDeg - Math.PI / 2);
|
|
144
|
+
|
|
145
|
+
// 处理跨越0点的情况
|
|
146
|
+
if (nextAngle <= currentAngle) {
|
|
147
|
+
nextAngle += fullDeg;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 在当前颜色区间内创建多个相同颜色的点来消除渐变
|
|
151
|
+
const segmentLength = nextAngle - currentAngle;
|
|
152
|
+
const numSegments = Math.max(10, Math.floor(segmentLength / (fullDeg / 100))); // 至少10个点
|
|
153
|
+
|
|
154
|
+
for (let j = 0; j < numSegments; j++) {
|
|
155
|
+
const angle = currentAngle + (segmentLength * j / numSegments);
|
|
156
|
+
colors.push({
|
|
157
|
+
color: currentItem.color,
|
|
158
|
+
angle: normalizeAngle(angle),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 在下一个节点前的最后一个点
|
|
163
|
+
if (i === sortedData.length - 1) {
|
|
164
|
+
// 最后一个段,延伸到第一个节点
|
|
165
|
+
colors.push({
|
|
166
|
+
color: currentItem.color,
|
|
167
|
+
angle: normalizeAngle(nextAngle - 0.001),
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
colors.push({
|
|
171
|
+
color: currentItem.color,
|
|
172
|
+
angle: normalizeAngle(nextAngle - 0.001),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return colors;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// 是否成为响应者
|
|
182
|
+
const handleSetPanResponder = usePersistFn((e: GestureResponderEvent) => {
|
|
183
|
+
// 如果传过来的disabled属性为true,则返回false阻止成为响应者
|
|
184
|
+
if (disabled) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
// 拿到触摸点相对于父元素的横纵坐标
|
|
188
|
+
const { locationX, locationY } = e.nativeEvent;
|
|
189
|
+
let selectIndex = -1;
|
|
190
|
+
const exist = allData.some(({ deg }, index) => {
|
|
191
|
+
const { x, y } = getPoint(deg, cx(ringRadius.current), cx(outerRadius.current));
|
|
192
|
+
const length = Math.sqrt((x - locationX) ** 2 + (y - locationY) ** 2);
|
|
193
|
+
if (length <= thumbSize / 2) {
|
|
194
|
+
selectIndex = index;
|
|
195
|
+
dragEnabled.current = true;
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
});
|
|
200
|
+
dragIndex.current = selectIndex;
|
|
201
|
+
if (exist) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const moveMark = usePersistFn(
|
|
208
|
+
({ dx, dy }: PanResponderGestureState, callback: Function | null, needUpdateState: boolean) => {
|
|
209
|
+
const dragInx = dragIndex.current;
|
|
210
|
+
const rinRad = cx(ringRadius.current);
|
|
211
|
+
const outRad = cx(outerRadius.current);
|
|
212
|
+
const mark = allData[dragInx];
|
|
213
|
+
const markRef = markRefs[dragInx];
|
|
214
|
+
if (!mark || !markRef) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const markImgRef = markImgRefs[dragInx];
|
|
219
|
+
markImgRef.setNativeProps({
|
|
220
|
+
style: {
|
|
221
|
+
tintColor: allData[dragInx].activeColor,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
const { deg } = mark;
|
|
225
|
+
const { x, y } = getPoint(deg, rinRad, outRad);
|
|
226
|
+
const newX = x + dx;
|
|
227
|
+
const newY = y + dy;
|
|
228
|
+
// const length = Math.sqrt((newX - outRad) ** 2 + (newY - outRad) ** 2);
|
|
229
|
+
let angle = Math.atan2(newX - outRad, -(newY - outRad));
|
|
230
|
+
if (angle < 0) {
|
|
231
|
+
angle += fullDeg;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 角度对应的时间
|
|
235
|
+
let time = Math.floor((angle * totalTime) / fullDeg);
|
|
236
|
+
// 是否是允许的时间范围
|
|
237
|
+
time = handleStepOver(time);
|
|
238
|
+
// 转回成角度,确保不出现跳动问题
|
|
239
|
+
angle = (time / totalTime) * fullDeg;
|
|
240
|
+
const point = getPoint(angle, rinRad, outRad);
|
|
241
|
+
markRef.setNativeProps({
|
|
242
|
+
style: {
|
|
243
|
+
top: point.y,
|
|
244
|
+
left: point.x,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const newData = allData.map((item, index) => {
|
|
249
|
+
if (index !== dragInx) {
|
|
250
|
+
return item;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
...item,
|
|
254
|
+
deg: angle,
|
|
255
|
+
time,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
callback && callback(newData, dragInx);
|
|
259
|
+
// onChange
|
|
260
|
+
props.onChange && props.onChange(newData);
|
|
261
|
+
|
|
262
|
+
ringRef?.current?.setColors(getRingColors(newData));
|
|
263
|
+
if (needUpdateState) {
|
|
264
|
+
setAllData(newData);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const handleStepOver = (time: number) => {
|
|
270
|
+
let newTime = time;
|
|
271
|
+
if (allData.length === 1) {
|
|
272
|
+
return newTime;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const dragInx = dragIndex.current;
|
|
276
|
+
const currentTime = allData[dragInx].time;
|
|
277
|
+
const MIN_DISTANCE = 15; // 定义最小距离为15分钟
|
|
278
|
+
|
|
279
|
+
// 如果几乎没有移动,直接返回原时间
|
|
280
|
+
if (Math.abs(time - currentTime) < 2) {
|
|
281
|
+
return currentTime;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 找出所有其他节点的时间
|
|
285
|
+
const otherTimes = allData
|
|
286
|
+
.filter((_, index) => index !== dragInx)
|
|
287
|
+
.map(item => item.time);
|
|
288
|
+
|
|
289
|
+
// 检查是否接近其他节点
|
|
290
|
+
for (let i = 0; i < otherTimes.length; i++) {
|
|
291
|
+
const otherTime = otherTimes[i];
|
|
292
|
+
|
|
293
|
+
// 计算距离(考虑圆形24小时的特性)
|
|
294
|
+
let distance = Math.abs(newTime - otherTime);
|
|
295
|
+
if (distance > totalTime / 2) {
|
|
296
|
+
distance = totalTime - distance; // 取较短的距离
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 如果距离小于MIN_DISTANCE,调整位置
|
|
300
|
+
if (distance < MIN_DISTANCE) {
|
|
301
|
+
// 确定调整方向
|
|
302
|
+
const direction = ((newTime - otherTime + totalTime) % totalTime) < totalTime / 2 ? 1 : -1;
|
|
303
|
+
|
|
304
|
+
// 计算新位置,保持15分钟距离
|
|
305
|
+
newTime = (otherTime + direction * MIN_DISTANCE + totalTime) % totalTime;
|
|
306
|
+
|
|
307
|
+
// 检查新位置是否与其他节点冲突
|
|
308
|
+
let hasConflict = false;
|
|
309
|
+
for (let j = 0; j < otherTimes.length; j++) {
|
|
310
|
+
if (j !== i) {
|
|
311
|
+
const checkTime = otherTimes[j];
|
|
312
|
+
let checkDistance = Math.abs(newTime - checkTime);
|
|
313
|
+
if (checkDistance > totalTime / 2) {
|
|
314
|
+
checkDistance = totalTime - checkDistance;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (checkDistance < MIN_DISTANCE) {
|
|
318
|
+
hasConflict = true;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 如果新位置仍有冲突,回到原位置
|
|
325
|
+
if (hasConflict) {
|
|
326
|
+
newTime = currentTime;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return newTime;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const onMove = usePersistFn((_: GestureResponderEvent, g: PanResponderGestureState) => {
|
|
337
|
+
moveMark(g, props.onMove || null, false);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const onRelease = usePersistFn((_: GestureResponderEvent, g: PanResponderGestureState) => {
|
|
341
|
+
dragEnabled.current = false;
|
|
342
|
+
moveMark(g, props.onRelease || null, true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const _panResponder = useMemo(
|
|
346
|
+
() =>
|
|
347
|
+
PanResponder.create({
|
|
348
|
+
onStartShouldSetPanResponder: handleSetPanResponder,
|
|
349
|
+
// 另一个组件成为新的响应者
|
|
350
|
+
onPanResponderTerminationRequest: () => !(!props.disabled && dragEnabled.current),
|
|
351
|
+
onPanResponderGrant: () => { },
|
|
352
|
+
onPanResponderMove: onMove,
|
|
353
|
+
onPanResponderRelease: onRelease,
|
|
354
|
+
}),
|
|
355
|
+
[props.disabled]
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const bgSize = innerSize.current - 12;
|
|
359
|
+
return (
|
|
360
|
+
<View
|
|
361
|
+
style={[
|
|
362
|
+
disabled && { opacity: disabledOpacity },
|
|
363
|
+
pickerStyle,
|
|
364
|
+
{ borderRadius: cx(outerRadius.current) },
|
|
365
|
+
]}
|
|
366
|
+
>
|
|
367
|
+
{/* timeStyle */}
|
|
368
|
+
<View
|
|
369
|
+
style={[
|
|
370
|
+
{
|
|
371
|
+
width: cx(size.current),
|
|
372
|
+
height: cx(size.current),
|
|
373
|
+
borderRadius: cx(size.current / 2),
|
|
374
|
+
position: 'relative',
|
|
375
|
+
},
|
|
376
|
+
timeStyle,
|
|
377
|
+
]}
|
|
378
|
+
>
|
|
379
|
+
{/* ConicalGradient */}
|
|
380
|
+
<ConicalGradient
|
|
381
|
+
ref={(ref: ConicalGradient) => (ringRef.current = ref)}
|
|
382
|
+
innerRadius={cx(innerRadius.current)}
|
|
383
|
+
outerRadius={cx(outerRadius.current)}
|
|
384
|
+
colors={getRingColors(allData)}
|
|
385
|
+
offsetAngle={0}
|
|
386
|
+
segmentAngle={Math.PI / 120}
|
|
387
|
+
/>
|
|
388
|
+
{/* timeImg */}
|
|
389
|
+
<Image
|
|
390
|
+
source={timeImg || {}}
|
|
391
|
+
style={{
|
|
392
|
+
position: 'absolute',
|
|
393
|
+
top: '50%',
|
|
394
|
+
left: '50%',
|
|
395
|
+
width: cx(bgSize),
|
|
396
|
+
height: cx(bgSize),
|
|
397
|
+
transform: [{ translateX: -cx(bgSize / 2) }, { translateY: -cx(bgSize / 2) }],
|
|
398
|
+
}}
|
|
399
|
+
resizeMode="contain"
|
|
400
|
+
/>
|
|
401
|
+
|
|
402
|
+
{/* Icon */}
|
|
403
|
+
<View style={[styles.thumbBox, { width: cx(size.current), height: cx(size.current) }]}>
|
|
404
|
+
{allData.map((item, index) => {
|
|
405
|
+
const point = getPoint(item.deg, cx(ringRadius.current), cx(outerRadius.current));
|
|
406
|
+
return (
|
|
407
|
+
<View
|
|
408
|
+
key={item.time}
|
|
409
|
+
ref={(ref: View) => (markRefs[index] = ref)}
|
|
410
|
+
style={[
|
|
411
|
+
styles.thumbStyle,
|
|
412
|
+
{
|
|
413
|
+
width: cx(thumbSize),
|
|
414
|
+
height: cx(thumbSize),
|
|
415
|
+
borderRadius: cx(thumbSize / 2),
|
|
416
|
+
transform: [{ translateX: -cx(thumbSize / 2) }, { translateY: -cx(thumbSize / 2) }],
|
|
417
|
+
left: point.x,
|
|
418
|
+
top: point.y,
|
|
419
|
+
justifyContent: 'center',
|
|
420
|
+
alignItems: 'center',
|
|
421
|
+
},
|
|
422
|
+
thumbStyle,
|
|
423
|
+
]}
|
|
424
|
+
>
|
|
425
|
+
<Image
|
|
426
|
+
source={{ uri: item.icon }}
|
|
427
|
+
ref={(ref: Image) => (markImgRefs[index] = ref)}
|
|
428
|
+
style={[
|
|
429
|
+
{
|
|
430
|
+
width: cx(thumbSize - 10),
|
|
431
|
+
height: cx(thumbSize - 10),
|
|
432
|
+
tintColor: item.noActiveColor,
|
|
433
|
+
},
|
|
434
|
+
iconStyle,
|
|
435
|
+
]}
|
|
436
|
+
resizeMode="contain"
|
|
437
|
+
/>
|
|
438
|
+
</View>
|
|
439
|
+
);
|
|
440
|
+
})}
|
|
441
|
+
</View>
|
|
442
|
+
</View>
|
|
443
|
+
{/* panResponder */}
|
|
444
|
+
<View
|
|
445
|
+
style={[styles.thumbBox, { width: cx(size.current), height: cx(size.current) }]}
|
|
446
|
+
pointerEvents="box-only"
|
|
447
|
+
{..._panResponder.panHandlers}
|
|
448
|
+
/>
|
|
449
|
+
</View>
|
|
450
|
+
);
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
RhythmsCircle.defaultProps = {
|
|
454
|
+
size: 250,
|
|
455
|
+
ringWidth: 40,
|
|
456
|
+
thumbSize: 36,
|
|
457
|
+
data: [],
|
|
458
|
+
disabled: false,
|
|
459
|
+
disabledOpacity: 1,
|
|
460
|
+
pickerStyle: {},
|
|
461
|
+
timeStyle: {
|
|
462
|
+
backgroundColor: 'transparent',
|
|
463
|
+
},
|
|
464
|
+
thumbStyle: {
|
|
465
|
+
backgroundColor: '#fff',
|
|
466
|
+
},
|
|
467
|
+
iconStyle: {},
|
|
468
|
+
lastRingColor: '#000',
|
|
469
|
+
gradientMode: true,
|
|
470
|
+
onMove() { },
|
|
471
|
+
onRelease() { },
|
|
472
|
+
onChange() { },
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export default RhythmsCircle;
|
|
476
|
+
|
|
477
|
+
const styles = StyleSheet.create({
|
|
478
|
+
thumbBox: {
|
|
479
|
+
left: 0,
|
|
480
|
+
position: 'absolute',
|
|
481
|
+
top: 0,
|
|
482
|
+
zIndex: 1,
|
|
483
|
+
},
|
|
484
|
+
thumbStyle: {
|
|
485
|
+
backgroundColor: '#fff',
|
|
486
|
+
position: 'absolute',
|
|
487
|
+
},
|
|
488
|
+
});
|