u
This commit is contained in:
1 parent
61e29b5f76
commit
157c84b0b4
15 files changed
+598
-320
No files matched your search
@@ -1,49 +1,22 @@
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import React, { ReactNode } from "react";
|
||||
import { View, Text, Pressable, StyleSheet } from "react-native";
|
||||
import {
|
||||
ChevronLeft,
|
||||
SlidersHorizontal,
|
||||
CirclePower,
|
||||
House,
|
||||
LayoutGrid,
|
||||
} from "lucide-react-native";
|
||||
import { Shadow } from "react-native-shadow-2";
|
||||
import { View } from "react-native";
|
||||
|
||||
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
type LayoutProps = {
|
||||
import Tabbar from "./tabbar";
|
||||
import { TabbarItem } from "./tabbar";
|
||||
|
||||
import Navbar from "./navbar";
|
||||
|
||||
interface LayoutProps {
|
||||
tabs: TabbarItem[];
|
||||
children: ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
const AnimatedView = Animated.View;
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
export default function Layout({ tabs, children }: LayoutProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// 返回按钮
|
||||
const scale = useSharedValue(1);
|
||||
const style = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
// 右侧按钮动画
|
||||
const leftScale = useSharedValue(1);
|
||||
const rightScale = useSharedValue(1);
|
||||
|
||||
const leftStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: leftScale.value }],
|
||||
opacity: leftScale.value === 1 ? 1 : 0.7,
|
||||
}));
|
||||
|
||||
const rightStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: rightScale.value }],
|
||||
opacity: rightScale.value === 1 ? 1 : 0.7,
|
||||
}));
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white">
|
||||
{/* 内容区域 */}
|
||||
<View
|
||||
className="flex-1"
|
||||
style={{
|
||||
@@ -54,167 +27,14 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{children}
|
||||
</View>
|
||||
|
||||
{/* Header */}
|
||||
<View
|
||||
pointerEvents="box-none"
|
||||
className="absolute top-0 left-0 right-0"
|
||||
style={{ paddingTop: insets.top }}
|
||||
>
|
||||
<View className="h-12 flex-row items-center mx-3">
|
||||
{/* 返回按钮 */}
|
||||
<Pressable
|
||||
onPressIn={() => {
|
||||
scale.value = withSpring(0.8);
|
||||
}}
|
||||
onPressOut={() => {
|
||||
scale.value = withSpring(1);
|
||||
}}
|
||||
>
|
||||
<AnimatedView style={style}>
|
||||
<Shadow
|
||||
distance={32}
|
||||
startColor="rgba(0,0,0,0.03)"
|
||||
endColor="rgba(255,255,255,0)"
|
||||
offset={[0, 0]}
|
||||
paintInside={false}
|
||||
safeRender
|
||||
style={styles.iconButtonShadow}
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
</Shadow>
|
||||
</AnimatedView>
|
||||
</Pressable>
|
||||
<Navbar title="123" style={{ paddingTop: insets.top }} />
|
||||
|
||||
{/* 标题 */}
|
||||
<View className="flex-1 px-4">
|
||||
<Text className="text-xl font-semibold tracking-tight text-black/90">记账</Text>
|
||||
</View>
|
||||
|
||||
{/* 右侧按钮组 */}
|
||||
<Shadow
|
||||
distance={32}
|
||||
startColor="rgba(0,0,0,0.03)"
|
||||
endColor="rgba(255,255,255,0)"
|
||||
offset={[0, 0]}
|
||||
paintInside={false}
|
||||
safeRender
|
||||
style={styles.groupShadow}
|
||||
>
|
||||
<Pressable
|
||||
onPressIn={() => {
|
||||
leftScale.value = withSpring(0.8);
|
||||
}}
|
||||
onPressOut={() => {
|
||||
leftScale.value = withSpring(1);
|
||||
}}
|
||||
>
|
||||
<AnimatedView style={leftStyle} className="w-14 items-center justify-center">
|
||||
<SlidersHorizontal size={20} />
|
||||
</AnimatedView>
|
||||
</Pressable>
|
||||
|
||||
<View className="w-[1px] h-4 bg-black/10" />
|
||||
|
||||
{/* 右按钮 */}
|
||||
<Pressable
|
||||
onPressIn={() => {
|
||||
rightScale.value = withSpring(0.8);
|
||||
}}
|
||||
onPressOut={() => {
|
||||
rightScale.value = withSpring(1);
|
||||
}}
|
||||
>
|
||||
<AnimatedView style={rightStyle} className="w-14 items-center justify-center">
|
||||
<CirclePower size={20} />
|
||||
</AnimatedView>
|
||||
</Pressable>
|
||||
</Shadow>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* tabbar */}
|
||||
<View
|
||||
className="absolute left-0 right-0 bottom-0"
|
||||
style={{ paddingBottom: insets.bottom + 6 }}
|
||||
style={{ paddingBottom: insets.bottom + 8 }}
|
||||
>
|
||||
<View className="m-3">
|
||||
<Shadow
|
||||
distance={32}
|
||||
startColor="rgba(0,0,0,0.03)"
|
||||
endColor="rgba(255,255,255,0)"
|
||||
offset={[0, 0]}
|
||||
paintInside={false}
|
||||
safeRender
|
||||
style={styles.tabbarShadown}
|
||||
>
|
||||
<View className="w-full h-16 flex-row items-center justify-between">
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<House size={24} color="#666" />
|
||||
<Text className="text-[10px] text-[#666] mt-1">首页</Text>
|
||||
</View>
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<House size={24} color="#666" />
|
||||
<Text className="text-[10px] text-[#666] mt-1">分类</Text>
|
||||
</View>
|
||||
<View className="flex-1 items-center justify-center" style={{ position: "relative" }}>
|
||||
<Pressable>
|
||||
<View
|
||||
style={{
|
||||
width: 68,
|
||||
height: 68,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#5B8CFF",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 8,
|
||||
borderColor: "white",
|
||||
}}
|
||||
>
|
||||
<LayoutGrid size={24} color="white" />
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<House size={24} color="#666" />
|
||||
<Text className="text-[10px] text-[#666] mt-1">统计</Text>
|
||||
</View>
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<House size={24} color="#666" />
|
||||
<Text className="text-[10px] text-[#666] mt-1">我的</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Shadow>
|
||||
</View>
|
||||
<Tabbar tabs={tabs} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
iconButtonShadow: {
|
||||
borderRadius: 999,
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 0.5,
|
||||
borderColor: "rgba(255,255,255,0.6)",
|
||||
},
|
||||
groupShadow: {
|
||||
borderRadius: 999,
|
||||
height: 36,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 0.5,
|
||||
borderColor: "rgba(255,255,255,0.6)",
|
||||
},
|
||||
tabbarShadown: {
|
||||
borderRadius: 999,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 0.5,
|
||||
borderColor: "rgba(255,255,255,0.6)",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleProp, ViewStyle, Pressable, StyleSheet } from "react-native";
|
||||
import { cn } from "@/utils/cn";
|
||||
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from "react-native-reanimated";
|
||||
import { Shadow } from "react-native-shadow-2";
|
||||
import { ChevronLeft, SlidersHorizontal, CirclePower } from "lucide-react-native";
|
||||
|
||||
interface NavbarProps {
|
||||
title?: string;
|
||||
|
||||
showBack?: boolean;
|
||||
onBack?: () => void;
|
||||
|
||||
className?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const AnimatedView = Animated.View;
|
||||
|
||||
export default function Navbar({ title, showBack = true, onBack, className, style }: NavbarProps) {
|
||||
// 返回按钮
|
||||
const scale = useSharedValue(1);
|
||||
const backAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
// 右侧按钮动画
|
||||
const leftScale = useSharedValue(1);
|
||||
const rightScale = useSharedValue(1);
|
||||
|
||||
const leftStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: leftScale.value }],
|
||||
opacity: leftScale.value === 1 ? 1 : 0.7,
|
||||
}));
|
||||
|
||||
const rightStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: rightScale.value }],
|
||||
opacity: rightScale.value === 1 ? 1 : 0.7,
|
||||
}));
|
||||
|
||||
return (
|
||||
// <View
|
||||
// pointerEvents="box-none"
|
||||
// className={cn("absolute top-0 left-0 right-0", className)}
|
||||
// style={[style]}
|
||||
// >
|
||||
<View
|
||||
className={cn("flex-row gap-3 items-center justify-between mx-3", className)}
|
||||
style={[
|
||||
{
|
||||
...style,
|
||||
height: 48
|
||||
},
|
||||
]}
|
||||
>
|
||||
{showBack && (
|
||||
<Pressable
|
||||
onPress={onBack}
|
||||
onPressIn={() => {
|
||||
scale.value = withSpring(0.8);
|
||||
}}
|
||||
onPressOut={() => {
|
||||
scale.value = withSpring(1);
|
||||
}}
|
||||
>
|
||||
<AnimatedView style={backAnimatedStyle}>
|
||||
<Shadow
|
||||
distance={20}
|
||||
startColor="rgba(0,0,0,0.035)"
|
||||
endColor="rgba(255,255,255,0)"
|
||||
offset={[0, 0]}
|
||||
paintInside={false}
|
||||
safeRender
|
||||
style={styles.iconButtonShadow}
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
</Shadow>
|
||||
</AnimatedView>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{title && (
|
||||
<View className="flex-1">
|
||||
<Text className="text-xl font-semibold tracking-tight text-black/90">{title}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Shadow
|
||||
distance={20}
|
||||
startColor="rgba(0,0,0,0.035)"
|
||||
endColor="rgba(255,255,255,0)"
|
||||
offset={[0, 0]}
|
||||
paintInside={false}
|
||||
safeRender
|
||||
style={styles.groupShadow}
|
||||
>
|
||||
<Pressable
|
||||
onPressIn={() => {
|
||||
leftScale.value = withSpring(0.8);
|
||||
}}
|
||||
onPressOut={() => {
|
||||
leftScale.value = withSpring(1);
|
||||
}}
|
||||
>
|
||||
<AnimatedView style={leftStyle} className="w-14 items-center justify-center">
|
||||
<SlidersHorizontal size={20} />
|
||||
</AnimatedView>
|
||||
</Pressable>
|
||||
|
||||
<View className="w-[1px] h-4 bg-black/10" />
|
||||
|
||||
{/* 右按钮 */}
|
||||
<Pressable
|
||||
onPressIn={() => {
|
||||
rightScale.value = withSpring(0.8);
|
||||
}}
|
||||
onPressOut={() => {
|
||||
rightScale.value = withSpring(1);
|
||||
}}
|
||||
>
|
||||
<AnimatedView style={rightStyle} className="w-14 items-center justify-center">
|
||||
<CirclePower size={20} />
|
||||
</AnimatedView>
|
||||
</Pressable>
|
||||
</Shadow>
|
||||
</View>
|
||||
// </View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
iconButtonShadow: {
|
||||
borderRadius: 999,
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 0.5,
|
||||
borderColor: "rgba(255,255,255,0.6)",
|
||||
},
|
||||
groupShadow: {
|
||||
borderRadius: 999,
|
||||
height: 36,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 0.5,
|
||||
borderColor: "rgba(255,255,255,0.6)",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { View, StyleProp, ViewStyle, Pressable, StyleSheet } from "react-native";
|
||||
import { LucideIcon } from "lucide-react-native";
|
||||
import { cn } from "@/utils/cn";
|
||||
import { Shadow } from "react-native-shadow-2";
|
||||
import { useState } from "react";
|
||||
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from "react-native-reanimated";
|
||||
|
||||
export interface TabbarItem {
|
||||
id: string;
|
||||
icon: LucideIcon;
|
||||
title?: string;
|
||||
isCenter?: boolean;
|
||||
}
|
||||
|
||||
export interface TabbarProps {
|
||||
tabs: TabbarItem[];
|
||||
value?: string;
|
||||
onChange?: (id: string) => void;
|
||||
|
||||
className?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export default function Tabbar({ tabs, value, onChange, className, style }: TabbarProps) {
|
||||
const [innerValue, setInnerValue] = useState(tabs[0]?.id);
|
||||
|
||||
const activeTab = value ?? innerValue;
|
||||
|
||||
const handlePress = (id: string) => {
|
||||
if (value === undefined) {
|
||||
setInnerValue(id);
|
||||
}
|
||||
onChange?.(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className={cn("mx-3", className)} style={style}>
|
||||
<Shadow
|
||||
distance={20}
|
||||
startColor="rgba(0,0,0,0.035)"
|
||||
endColor="rgba(255,255,255,0)"
|
||||
style={styles.tabbarShadow}
|
||||
>
|
||||
<View className="w-full flex-row items-center justify-between">
|
||||
{tabs.map((tab) =>
|
||||
tab.isCenter ? (
|
||||
<CenterButton key={tab.id} tab={tab} onPress={() => handlePress(tab.id)} />
|
||||
) : (
|
||||
<TabItem
|
||||
key={tab.id}
|
||||
tab={tab}
|
||||
isActive={activeTab === tab.id}
|
||||
onPress={() => handlePress(tab.id)}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</View>
|
||||
</Shadow>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function TabItem({
|
||||
tab,
|
||||
isActive,
|
||||
onPress,
|
||||
}: {
|
||||
tab: TabbarItem;
|
||||
isActive: boolean;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const Icon = tab.icon;
|
||||
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
const scaleStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onPressIn={() => {
|
||||
scale.value = withTiming(0.9, { duration: 80 });
|
||||
}}
|
||||
onPressOut={() => {
|
||||
scale.value = withTiming(1, { duration: 120 });
|
||||
}}
|
||||
className="flex-1 items-center justify-center"
|
||||
>
|
||||
<Animated.View style={scaleStyle}>
|
||||
<Icon size={24} color={isActive ? "#5FAF7B" : "#666"} />
|
||||
{tab.title && (
|
||||
<Animated.Text
|
||||
style={[
|
||||
{
|
||||
fontSize: 11,
|
||||
marginTop: 4,
|
||||
fontWeight: isActive ? "600" : "400",
|
||||
color: isActive ? "#5FAF7B" : "#666",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{tab.title}
|
||||
</Animated.Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function CenterButton({ tab, onPress }: { tab: TabbarItem; onPress?: () => void }) {
|
||||
const Icon = tab.icon;
|
||||
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<Pressable
|
||||
className="absolute"
|
||||
onPress={onPress}
|
||||
onPressIn={() => {
|
||||
scale.value = withTiming(0.9, { duration: 80 });
|
||||
}}
|
||||
onPressOut={() => {
|
||||
scale.value = withTiming(1, { duration: 120 });
|
||||
}}
|
||||
>
|
||||
<Animated.View style={animatedStyle}>
|
||||
{/* <Shadow
|
||||
distance={12}
|
||||
startColor="rgba(140,140,140,0.03)"
|
||||
endColor="rgba(255,255,255,0)"
|
||||
style={styles.centerButtonShadown}
|
||||
> */}
|
||||
<View
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "#22C55E",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 8,
|
||||
borderColor: "white",
|
||||
top: -12,
|
||||
}}
|
||||
>
|
||||
<Icon size={30} color="#fff" />
|
||||
</View>
|
||||
{/* </Shadow> */}
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centerButtonShadown: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 999,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(0,0,0,0.03)",
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
tabbarShadow: {
|
||||
borderRadius: 999,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 0.5,
|
||||
borderColor: "rgba(255,255,255,0.6)",
|
||||
height: 56,
|
||||
marginBottom: 12,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user