This commit is contained in:
oneao committed 2026-03-02 17:29:51 +08:00
1 parent ab3eaed7ce
commit cf23acdabc
28 files changed
+1292 -486

No files matched your search

@@ -47,6 +47,7 @@ const AInput = forwardRef<React.ComponentRef<typeof InputField>, AInputProps>(
isFocused={isFocused}
className={className}
style={style}
>
{leftIcon && (
<InputSlot className="pl-3">
@@ -1,18 +1,19 @@
import type { ViewStyle } from 'react-native'
import { useState } from 'react'
import { Text } from 'react-native'
import { Text, TouchableOpacity, View } from 'react-native'
import {
ButtonSpinner,
} from '@/components/ui/button'
import { Heading } from '@/components/ui/heading'
import { CloseIcon, Icon } from '@/components/ui/icon'
import {
Modal,
ModalBackdrop,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
} from '@/components/ui/modal'
import AButton from '../ao/Button'
interface AModalProps {
isOpen?: boolean
@@ -23,7 +24,6 @@ interface AModalProps {
onConfirm?: () => Promise<void> | void
onCancel?: () => void
showBackdrop?: boolean
showCloseIcon?: boolean
className?: string
style?: ViewStyle
@@ -37,7 +37,6 @@ export default function AModal({
onClose,
onConfirm,
showBackdrop = true,
showCloseIcon = false,
className,
style,
@@ -76,42 +75,58 @@ export default function AModal({
style={{
...style,
width: '80%',
padding: 0,
}}
>
<ModalHeader>
{title && (typeof title === 'string' ? <Heading>{title}</Heading> : title)}
{
showCloseIcon && (
<ModalCloseButton>
<Icon as={CloseIcon} />
</ModalCloseButton>
)
}
</ModalHeader>
<ModalBody>
{body && (typeof body === 'string' ? <Text>{body}</Text> : body)}
</ModalBody>
{title && (
<ModalHeader>
{typeof body === 'string'
? (
<View className="items-center justify-center w-full py-2">
<Heading>{title}</Heading>
</View>
)
: (
title
)}
</ModalHeader>
)}
{body && (
<ModalBody>
{typeof body === 'string'
? (
<View className="items-center justify-center w-full">
<Text>{body}</Text>
</View>
)
: (
body
)}
</ModalBody>
)}
<ModalFooter>
{
footer || (
<>
<AButton
variant="outline"
action="secondary"
className="mr-3"
<View className="flex-row border-t border-gray-300">
<TouchableOpacity
className="flex-1 items-center justify-center py-3 border-r border-gray-300"
onPress={onClose}
size="sm"
>
取消
</AButton>
<AButton
<Text className="text-gray-500">取消</Text>
</TouchableOpacity>
<TouchableOpacity
className="flex-1 flex-row gap-2 items-center justify-center py-3"
onPress={handleConfirm}
size="sm"
loading={innerLoading}
>
确认
</AButton>
</>
{
innerLoading && (
<ButtonSpinner color="#333" />
)
}
<Text className="text-[#333]">确认</Text>
</TouchableOpacity>
</View>
)
}
</ModalFooter>
@@ -0,0 +1,127 @@
import { useEffect } from 'react'
import { Pressable, StyleSheet } from 'react-native'
import Animated, {
interpolateColor,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
interface SwitchProps {
value: any
onChange: (val: any) => void
trueValue?: any
falseValue?: any
width?: number
height?: number
activeColor?: string
inactiveColor?: string
duration?: number
disabled?: boolean // 新增 disabled
}
export default function Switch({
value,
onChange,
trueValue,
falseValue,
width = 52,
height = 24,
activeColor = '#333',
inactiveColor = '#e5e7eb',
duration = 200,
disabled = false, // 默认可用
}: SwitchProps) {
// 默认值 fallback
const trueVal = trueValue ?? true
const falseVal = falseValue ?? false
// 当前状态是否为激活
const isActive = Object.is(value, trueVal)
// 控制动画的 sharedValue
const progress = useSharedValue(isActive ? 1 : 0)
useEffect(() => {
progress.value = withTiming(isActive ? 1 : 0, { duration })
}, [isActive, duration])
const knobSize = height - 4
const translateX = width - knobSize - 4
// 动画轨道样式
const trackStyle = useAnimatedStyle(() => {
const baseColor = interpolateColor(
progress.value,
[0, 1],
[inactiveColor, activeColor],
)
return {
backgroundColor: baseColor,
opacity: disabled ? 0.7 : 1, // disabled 半透明
}
})
// 动画滑块样式
const knobStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: progress.value * translateX,
},
],
opacity: disabled ? 0.8 : 1, // knob 半透明
}
})
return (
<Pressable
onPress={() => {
if (disabled)
return
const nextValue = isActive ? falseVal : trueVal
onChange(nextValue)
}}
style={{ width, height }}
>
<Animated.View
style={[
styles.track,
{
width,
height,
borderRadius: height / 2,
},
trackStyle,
]}
>
<Animated.View
style={[
styles.knob,
{
width: knobSize,
height: knobSize,
borderRadius: knobSize / 2,
},
knobStyle,
]}
/>
</Animated.View>
</Pressable>
)
}
const styles = StyleSheet.create({
track: {
justifyContent: 'center',
padding: 2,
},
knob: {
backgroundColor: '#fff',
elevation: 2,
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 2,
shadowOffset: { width: 0, height: 1 },
},
})
@@ -15,6 +15,7 @@ import ListItem from './ListItem'
import LucideIcon from './LucideIcon'
import Modal from './Modal'
import SegmentedTabs from './SegmentedTabs'
import Switch from './Switch'
import Tab from './Tab'
export {
@@ -35,6 +36,7 @@ export {
ListItem as AListItem,
Modal as AModal,
SegmentedTabs as ASegmentedTabs,
Switch as ASwitch,
Tab as ATab,
LucideIcon,
}