feat(i18n): Add common translations and mood-related strings in English and Chinese fix(i18n): Update metabolism titles for consistency in health translations chore: Update Podfile.lock to include SDWebImage 5.21.4 and other dependency versions refactor(moodCheckins): Improve mood configuration retrieval with optional translation support refactor(sleepHealthKit): Replace useI18n with direct i18n import for sleep quality descriptions
297 lines
7.3 KiB
TypeScript
297 lines
7.3 KiB
TypeScript
import * as Haptics from 'expo-haptics';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import React from 'react';
|
|
import {
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from 'react-native';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Gesture,
|
|
GestureDetector,
|
|
} from 'react-native-gesture-handler';
|
|
import Animated, {
|
|
interpolate,
|
|
interpolateColor,
|
|
runOnJS,
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withSpring,
|
|
} from 'react-native-reanimated';
|
|
|
|
interface MoodIntensitySliderProps {
|
|
value: number;
|
|
onValueChange: (value: number) => void;
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
width?: number;
|
|
height?: number;
|
|
}
|
|
|
|
export default function MoodIntensitySlider({
|
|
value,
|
|
onValueChange,
|
|
min = 1,
|
|
max = 10,
|
|
step = 1,
|
|
width = 320,
|
|
height = 16, // 更粗的进度条
|
|
}: MoodIntensitySliderProps) {
|
|
const { t } = useTranslation();
|
|
const thumbSize = 32; // 合适的触摸区域
|
|
const translateX = useSharedValue(0);
|
|
const isDragging = useSharedValue(0);
|
|
const sliderWidth = width - thumbSize; // thumb中心移动的有效范围
|
|
|
|
// 计算初始位置
|
|
React.useEffect(() => {
|
|
const initialPosition = ((value - min) / (max - min)) * sliderWidth;
|
|
translateX.value = withSpring(initialPosition);
|
|
}, [value, min, max, sliderWidth, translateX]);
|
|
|
|
const triggerHaptics = () => {
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
};
|
|
|
|
const startX = useSharedValue(0);
|
|
const lastValue = useSharedValue(value);
|
|
|
|
const gestureHandler = Gesture.Pan()
|
|
.onBegin(() => {
|
|
startX.value = translateX.value;
|
|
lastValue.value = value;
|
|
isDragging.value = withSpring(1);
|
|
runOnJS(triggerHaptics)();
|
|
})
|
|
.onUpdate((event) => {
|
|
const newX = startX.value + event.translationX;
|
|
const clampedX = Math.max(0, Math.min(sliderWidth, newX));
|
|
translateX.value = clampedX;
|
|
|
|
// 计算当前值
|
|
const currentValue = Math.round((clampedX / sliderWidth) * (max - min) + min);
|
|
|
|
// 当值改变时触发震动和回调
|
|
if (currentValue !== lastValue.value) {
|
|
lastValue.value = currentValue;
|
|
runOnJS(triggerHaptics)();
|
|
runOnJS(onValueChange)(currentValue);
|
|
}
|
|
})
|
|
.onEnd(() => {
|
|
// 计算最终值并吸附到最近的步长
|
|
const currentValue = Math.round((translateX.value / sliderWidth) * (max - min) + min);
|
|
const snapPosition = ((currentValue - min) / (max - min)) * sliderWidth;
|
|
|
|
translateX.value = withSpring(snapPosition);
|
|
isDragging.value = withSpring(0);
|
|
runOnJS(triggerHaptics)();
|
|
runOnJS(onValueChange)(currentValue);
|
|
});
|
|
|
|
const thumbStyle = useAnimatedStyle(() => {
|
|
const positionScale = interpolate(
|
|
translateX.value,
|
|
[0, sliderWidth],
|
|
[1, 1.1]
|
|
);
|
|
|
|
const dragScale = interpolate(
|
|
isDragging.value,
|
|
[0, 1],
|
|
[1, 1.2]
|
|
);
|
|
|
|
const finalScale = positionScale * dragScale;
|
|
|
|
// 让thumb在滑动条中正确居中
|
|
const thumbPosition = translateX.value + thumbSize / 2;
|
|
|
|
return {
|
|
transform: [
|
|
{ translateX: thumbPosition },
|
|
{ scale: withSpring(finalScale) }
|
|
],
|
|
};
|
|
});
|
|
|
|
const thumbInnerStyle = useAnimatedStyle(() => {
|
|
const borderColor = interpolateColor(
|
|
isDragging.value,
|
|
[0, 1],
|
|
['#7a5af8', '#ff6b6b']
|
|
);
|
|
|
|
return {
|
|
borderColor: borderColor,
|
|
};
|
|
});
|
|
|
|
const progressStyle = useAnimatedStyle(() => {
|
|
const progressWidth = translateX.value + thumbSize / 2;
|
|
return {
|
|
width: progressWidth,
|
|
};
|
|
});
|
|
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={[styles.sliderContainer, { width: width }]}>
|
|
{/* 背景轨道 - 更粗的灰色轨道 */}
|
|
<View style={[styles.track, { height }]}>
|
|
<LinearGradient
|
|
colors={['#f3f4f6', '#e5e7eb']}
|
|
style={[styles.trackGradient, { height }]}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
/>
|
|
</View>
|
|
|
|
{/* 进度条 - 动态渐变颜色 */}
|
|
<Animated.View style={[styles.progress, { height }, progressStyle]}>
|
|
<LinearGradient
|
|
colors={['#22c55e', '#84cc16', '#eab308', '#f97316', '#ef4444']}
|
|
locations={[0, 0.25, 0.5, 0.75, 1]}
|
|
style={[styles.progressGradient, { height }]}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
/>
|
|
</Animated.View>
|
|
|
|
{/* 可拖拽的thumb */}
|
|
<GestureDetector gesture={gestureHandler}>
|
|
<Animated.View style={[styles.thumb, { width: thumbSize, height: thumbSize }, thumbStyle]}>
|
|
{/* <LinearGradient
|
|
colors={['#ffffff', '#f8fafc']}
|
|
style={styles.thumbGradient}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 0, y: 1 }}
|
|
/> */}
|
|
<Animated.View style={[styles.thumbInner, thumbInnerStyle]} />
|
|
</Animated.View>
|
|
</GestureDetector>
|
|
</View>
|
|
|
|
{/* 标签 */}
|
|
<View style={[styles.labelsContainer, { width: width }]}>
|
|
<Text style={styles.labelText}>{t('mood.edit.intensityLow')}</Text>
|
|
<Text style={styles.labelText}>{t('mood.edit.intensityHigh')}</Text>
|
|
</View>
|
|
|
|
{/* 刻度 */}
|
|
<View style={[styles.scaleContainer, { width: width }]}>
|
|
{Array.from({ length: max - min + 1 }, (_, i) => i + min).map((num) => (
|
|
<View key={num} style={styles.scaleItem}>
|
|
<View style={[styles.scaleMark, value === num && styles.scaleMarkActive]} />
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
alignItems: 'center',
|
|
paddingTop: 8,
|
|
paddingBottom: 4,
|
|
},
|
|
sliderContainer: {
|
|
height: 10,
|
|
justifyContent: 'center',
|
|
position: 'relative',
|
|
},
|
|
track: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
borderRadius: 6,
|
|
overflow: 'hidden',
|
|
},
|
|
trackGradient: {
|
|
flex: 1,
|
|
borderRadius: 6,
|
|
},
|
|
progress: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
borderRadius: 6,
|
|
overflow: 'hidden',
|
|
},
|
|
progressGradient: {
|
|
flex: 1,
|
|
borderRadius: 6,
|
|
},
|
|
thumb: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
overflow: 'hidden',
|
|
},
|
|
thumbGradient: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
thumbInner: {
|
|
width: 16,
|
|
height: 28,
|
|
borderRadius: 8,
|
|
backgroundColor: '#ffffff',
|
|
borderWidth: 2,
|
|
borderColor: '#7a5af8',
|
|
},
|
|
valueContainer: {
|
|
marginTop: 20,
|
|
marginBottom: 12,
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 10,
|
|
backgroundColor: '#7a5af8',
|
|
borderRadius: 16,
|
|
shadowColor: '#7a5af8',
|
|
shadowOffset: { width: 0, height: 3 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 6,
|
|
elevation: 6,
|
|
},
|
|
valueText: {
|
|
fontSize: 20,
|
|
fontWeight: '800',
|
|
color: '#ffffff',
|
|
textAlign: 'center',
|
|
minWidth: 28,
|
|
},
|
|
labelsContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginTop: 6,
|
|
},
|
|
labelText: {
|
|
fontSize: 12,
|
|
color: '#5d6676',
|
|
fontWeight: '500',
|
|
},
|
|
scaleContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginTop: 12,
|
|
},
|
|
scaleItem: {
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
},
|
|
scaleMark: {
|
|
width: 1.5,
|
|
height: 8,
|
|
backgroundColor: '#e5e7eb',
|
|
borderRadius: 0.75,
|
|
},
|
|
scaleMarkActive: {
|
|
backgroundColor: '#7a5af8',
|
|
width: 2,
|
|
height: 10,
|
|
},
|
|
});
|