feature:基本功能

This commit is contained in:
吕新雨
2026-01-29 19:09:36 +08:00
parent c1a433a469
commit 0dd84b1873
47 changed files with 2978 additions and 219 deletions

View File

@@ -0,0 +1,138 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
Easing,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
type Props = {
visible: boolean;
title?: string;
onClose: () => void;
children: React.ReactNode;
};
/**
* 通用底部上拉弹窗Sheet
* - 内容区背景色固定:#FAF3EC
* - 关闭方式:点 X本期不要求点遮罩关闭
*/
export default function SheetModal({ visible, title, onClose, children }: Props) {
const insets = useSafeAreaInsets();
const [mounted, setMounted] = useState(false);
const progress = useSharedValue(0); // 0: 关闭, 1: 打开
useEffect(() => {
if (visible) {
setMounted(true);
progress.value = withTiming(1, { duration: 260, easing: Easing.out(Easing.cubic) });
return;
}
if (!mounted) return;
progress.value = withTiming(
0,
{ duration: 220, easing: Easing.in(Easing.cubic) },
(finished) => {
if (finished) runOnJS(setMounted)(false);
}
);
}, [visible, mounted, progress]);
const overlayStyle = useAnimatedStyle(() => {
return { opacity: 0.4 * progress.value };
});
const sheetStyle = useAnimatedStyle(() => {
const translateY = (1 - progress.value) * 380;
return { transform: [{ translateY }] };
});
const containerPaddingBottom = useMemo(() => Math.max(insets.bottom, 12), [insets.bottom]);
// 注意Modal 的 visible 必须为 true 才会渲染,因此用 mounted 保持退场动画
return (
<Modal transparent visible={mounted} animationType="none" onRequestClose={onClose}>
<View style={styles.root}>
<Animated.View style={[styles.overlay, overlayStyle]} />
<Animated.View
style={[
styles.sheet,
sheetStyle,
{
paddingBottom: containerPaddingBottom,
},
]}
>
<View style={styles.header}>
<Text style={styles.title} numberOfLines={1}>
{title ?? ''}
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel="关闭"
onPress={onClose}
hitSlop={10}
style={styles.close}
>
<Text style={styles.closeText}>×</Text>
</Pressable>
</View>
<View style={styles.body}>{children}</View>
</Animated.View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
justifyContent: 'flex-end',
},
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#000',
},
sheet: {
backgroundColor: '#FAF3EC',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
paddingTop: 14,
paddingHorizontal: 16,
},
header: {
height: 44,
justifyContent: 'center',
alignItems: 'center',
},
title: {
color: '#5E2A28',
fontSize: 16,
fontWeight: '700',
},
close: {
position: 'absolute',
left: 4,
top: 0,
height: 44,
width: 44,
alignItems: 'center',
justifyContent: 'center',
},
closeText: {
color: '#5E2A28',
fontSize: 28,
lineHeight: 28,
fontWeight: '400',
},
body: {
paddingTop: 10,
},
});