@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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/src/modules/biorhythm/BiorhythmActions.ts +7 -2
  3. package/src/modules/biorhythm/BiorhythmPage.tsx +51 -16
  4. package/src/modules/biorhythm/circular/RhythmsCircle.tsx +488 -0
  5. package/src/modules/biorhythm/circular/conical-gradient/Android.tsx +63 -0
  6. package/src/modules/biorhythm/circular/conical-gradient/Ios.tsx +26 -0
  7. package/src/modules/biorhythm/circular/conical-gradient/Normal.tsx +187 -0
  8. package/src/modules/biorhythm/circular/conical-gradient/index.android.tsx +164 -0
  9. package/src/modules/biorhythm/circular/conical-gradient/index.ios.tsx +124 -0
  10. package/src/modules/biorhythm/circular/conical-gradient/index.tsx +3 -0
  11. package/src/modules/biorhythm/circular/conical-gradient/index.web.tsx +94 -0
  12. package/src/modules/biorhythm/circular/conical-gradient/interface.ts +19 -0
  13. package/src/modules/biorhythm/circular/conical-gradient/utils.ts +25 -0
  14. package/src/modules/biorhythm/circular/interface.ts +114 -0
  15. package/src/modules/energyConsumption/component/EnergyModal.tsx +3 -0
  16. package/src/modules/lightMode/LightModePage.tsx +50 -144
  17. package/src/modules/mood/DynamicMoodEditorPage.tsx +1 -1
  18. package/src/modules/mood/StaticMoodEditorPage.tsx +1 -1
  19. package/src/modules/mood_new/DynamicMoodEditorPage.tsx +1 -1
  20. package/src/modules/mood_new/MixDynamicMoodEditor.tsx +1 -1
  21. package/src/modules/mood_new/MoodInfo.ts +1 -1
  22. package/src/modules/mood_new/StaticMoodEditorPage.tsx +1 -1
  23. package/src/modules/powerOnBehavior/LightBehaviorPage.tsx +208 -0
  24. package/src/modules/powerOnBehavior/PlugBehaviorPage.tsx +99 -0
  25. package/src/modules/powerOnBehavior/PowerOnBehaviorActions.ts +131 -0
  26. package/src/modules/powerOnBehavior/Router.ts +27 -0
  27. package/src/navigation/Routers.ts +3 -1
  28. package/src/modules/biorhythm/circular/ItemIcon.d.ts +0 -22
  29. package/src/modules/biorhythm/circular/ItemIcon.tsx +0 -173
  30. package/src/modules/biorhythm/circular/Progress.d.ts +0 -24
  31. package/src/modules/biorhythm/circular/Progress.tsx +0 -372
  32. package/src/modules/biorhythm/circular/TimeCircular.d.ts +0 -11
  33. package/src/modules/biorhythm/circular/TimeCircular.tsx +0 -64
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.121",
7
+ "version": "1.0.123",
8
8
  "scripts": {},
9
9
  "dependencies": {
10
10
  "@ledvance/base": "^1.x",
@@ -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
- // @ts-ignore
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
- obj.repeatPeriod
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: string[] = setDataSource(
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
+ });