This commit is contained in:
oneao committed 2025-11-20 17:30:28 +08:00
1 parent 63b168affc
commit 069b4506e4
29 files changed
+1077 -1116

No files matched your search

@@ -0,0 +1,20 @@
import React from 'react'
import IconFont from '@/components/iconfont'
interface CategoryIconProps {
name?: string
size?: number
color?: string
type?: 0 | 1
}
/**
* 分类图标组件
*/
export default function MoneyCategoryIcon({
name,
size = 24,
color,
}: CategoryIconProps) {
return <IconFont name={name as any} size={size} color={color} />
}
Whitespace-only changes.
@@ -51,7 +51,6 @@ const Button: React.FC<CustomButtonProps> = ({
return <span className="mr-2">{leftIcon}</span>
}
// 只剩下 LucideIconProps & { icon }
const { icon, ...props } = leftIcon as LucideIconProps & { icon: keyof typeof icons }
return <LucideIcon icon={icon} className="mr-2" {...props} />
}
@@ -0,0 +1,105 @@
import type { icons } from 'lucide-react-native'
import type { ReactElement, ReactNode } from 'react'
import type { ViewStyle } from 'react-native'
import { Children, cloneElement, isValidElement } from 'react'
import { Text, TouchableOpacity, View } from 'react-native'
import LucideIcon from './LucideIcon'
/* ================= Cell ================= */
export interface CellProps {
left?: ReactNode
right?: ReactNode
title?: string
leftIcon?: keyof typeof icons
rightIcon?: keyof typeof icons
rightText?: string
isLink?: boolean
className?: string
style?: ViewStyle
showDivider?: boolean // 由 CellGroup 注入
onPress?: () => void
}
export function Cell({
left,
right,
title,
leftIcon,
rightIcon = 'ChevronRight',
rightText,
isLink = false,
className,
style,
showDivider = false,
onPress,
}: CellProps) {
return (
<View>
<TouchableOpacity
activeOpacity={isLink ? 0.5 : 1}
className={`flex flex-row items-center justify-between px-4 py-3 ${className}`}
style={style}
onPress={onPress}
>
{left || (
<View className="flex flex-row items-center space-x-2">
{leftIcon && <LucideIcon icon={leftIcon} size={18} />}
{title && <Text className="text-base text-gray-900 ml-2">{title}</Text>}
</View>
)}
{right || (
<View className="flex flex-row items-center ">
{rightText && <Text className="text-gray-500 text-base mr-2">{rightText}</Text>}
{isLink && rightIcon && (
<LucideIcon icon={rightIcon} size={16} color="#9ca3af" />
)}
</View>
)}
</TouchableOpacity>
{showDivider && (
<View
className="bg-gray-100"
style={{ marginLeft: 16, marginRight: 16, height: 1 }}
/>
)}
</View>
)
}
/* ================= CellGroup ================= */
export interface CellGroupProps {
className?: string
style?: ViewStyle
children?: ReactNode
showDivider?: boolean // 整组是否显示下划线
}
export function CellGroup({
className,
style,
children,
showDivider = true,
}: CellGroupProps) {
const elements = Children.toArray(children)
return (
<View className={`bg-white ${className}`} style={style}>
{elements.map((child, index) => {
if (!isValidElement<CellProps>(child))
return child
const isLast = index === elements.length - 1
return cloneElement<CellProps>(child as ReactElement<CellProps>, {
showDivider: showDivider && !isLast,
})
})}
</View>
)
}
@@ -0,0 +1,94 @@
import type { ViewStyle } from 'react-native'
import { Text } from 'react-native'
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
title?: string | React.ReactNode
body?: string | React.ReactNode
footer?: React.ReactNode
onClose?: () => void
onConfirm?: () => void
onCancel?: () => void
showBackdrop?: boolean
showCloseIcon?: boolean
className?: string
style?: ViewStyle
}
export default function AModal({
isOpen,
title,
body,
footer,
onClose,
onConfirm,
showBackdrop = true,
showCloseIcon = false,
className,
style,
}: AModalProps) {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
>
{ showBackdrop && <ModalBackdrop />}
<ModalContent
className={className}
style={{
...style,
width: '80%',
}}
>
<ModalHeader>
{title && (typeof title === 'string' ? <Text>{title}</Text> : title)}
{
showCloseIcon && (
<ModalCloseButton>
<Icon as={CloseIcon} />
</ModalCloseButton>
)
}
</ModalHeader>
<ModalBody>
{body && (typeof body === 'string' ? <Text>{body}</Text> : body)}
</ModalBody>
<ModalFooter>
{
footer || (
<>
<AButton
variant="outline"
action="secondary"
className="mr-3"
onPress={onClose}
>
取消
</AButton>
<AButton
onPress={onConfirm}
>
确认
</AButton>
</>
)
}
</ModalFooter>
</ModalContent>
</Modal>
)
}
@@ -1,18 +1,23 @@
import Button from './Button'
import { Cell, CellGroup } from './Cell'
import Checkbox from './Checkbox'
import DatePicker from './DatePicker'
import Form, { FormRef } from './Form'
import FormItem from './FormItem'
import Input from './Input'
import LucideIcon from './LucideIcon'
import Modal from './Modal'
export {
Button as AButton,
Cell as ACell,
CellGroup as ACellGroup,
Checkbox as ACheckbox,
DatePicker as ADatePicker,
Form as AForm,
FormItem as AFormItem,
FormRef as AFormRef,
Input as AInput,
Modal as AModal,
LucideIcon,
}
@@ -1,5 +1,5 @@
import type { ViewStyle } from 'react-native'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { Animated, Text, TouchableOpacity, View } from 'react-native'
import PagerView from 'react-native-pager-view'
@@ -31,16 +31,20 @@ export default function PagerTabs({
className,
style,
}: PagerTabsProps) {
const pagerRef = React.useRef<PagerView>(null)
const [index, setIndex] = React.useState(selectedIndex ?? 0)
const [tabWidth, setTabWidth] = React.useState(0)
const scrollX = React.useRef(new Animated.Value(selectedIndex ?? 0)).current
const pagerRef = useRef<PagerView>(null)
const [index, setIndex] = useState(selectedIndex ?? 0)
const [tabWidth, setTabWidth] = useState(0)
const scrollX = useRef(new Animated.Value(selectedIndex ?? 0)).current
// 外部 selectedIndex 更新时同步内部 index
React.useEffect(() => {
useEffect(() => {
if (typeof selectedIndex === 'number' && selectedIndex !== index) {
setIndex(selectedIndex)
pagerRef.current?.setPage(selectedIndex)
requestAnimationFrame(() => {
if (pagerRef.current) {
pagerRef.current.setPage(selectedIndex)
}
})
}
}, [selectedIndex])
@@ -0,0 +1,276 @@
'use client'
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'
import type {
MotionComponentProps,
} from '@legendapp/motion'
import type { ViewStyle } from 'react-native'
import { createModal } from '@gluestack-ui/core/modal/creator'
import { tva, useStyleContext, withStyleContext } from '@gluestack-ui/utils/nativewind-utils'
import {
AnimatePresence,
createMotionAnimatedComponent,
Motion,
} from '@legendapp/motion'
import { cssInterop } from 'nativewind'
import React from 'react'
import { Pressable, ScrollView, View } from 'react-native'
type IAnimatedPressableProps = React.ComponentProps<typeof Pressable>
& MotionComponentProps<typeof Pressable, ViewStyle, unknown, unknown, unknown>
const AnimatedPressable = createMotionAnimatedComponent(
Pressable,
) as React.ComponentType<IAnimatedPressableProps>
const SCOPE = 'MODAL'
type IMotionViewProps = React.ComponentProps<typeof View>
& MotionComponentProps<typeof View, ViewStyle, unknown, unknown, unknown>
const MotionView = Motion.View as React.ComponentType<IMotionViewProps>
const UIModal = createModal({
Root: withStyleContext(View, SCOPE),
Backdrop: AnimatedPressable,
Content: MotionView,
Body: ScrollView,
CloseButton: Pressable,
Footer: View,
Header: View,
AnimatePresence,
})
cssInterop(AnimatedPressable, { className: 'style' })
cssInterop(MotionView, { className: 'style' })
const modalStyle = tva({
base: 'group/modal w-full h-full justify-center items-center web:pointer-events-none',
variants: {
size: {
xs: '',
sm: '',
md: '',
lg: '',
full: '',
},
},
})
const modalBackdropStyle = tva({
base: 'absolute left-0 top-0 right-0 bottom-0 bg-background-dark web:cursor-default',
})
const modalContentStyle = tva({
base: 'bg-background-0 rounded-md overflow-hidden border border-outline-100 shadow-hard-2 p-6',
parentVariants: {
size: {
xs: 'w-[60%] max-w-[360px]',
sm: 'w-[70%] max-w-[420px]',
md: 'w-[80%] max-w-[510px]',
lg: 'w-[90%] max-w-[640px]',
full: 'w-full',
},
},
})
const modalBodyStyle = tva({
base: 'mt-2 mb-6',
})
const modalCloseButtonStyle = tva({
base: 'group/modal-close-button z-10 rounded data-[focus-visible=true]:web:bg-background-100 web:outline-0 cursor-pointer',
})
const modalHeaderStyle = tva({
base: 'justify-between items-center flex-row',
})
const modalFooterStyle = tva({
base: 'flex-row justify-end items-center gap-2',
})
type IModalProps = React.ComponentProps<typeof UIModal>
& VariantProps<typeof modalStyle> & { className?: string }
type IModalBackdropProps = React.ComponentProps<typeof UIModal.Backdrop>
& VariantProps<typeof modalBackdropStyle> & { className?: string }
type IModalContentProps = React.ComponentProps<typeof UIModal.Content>
& VariantProps<typeof modalContentStyle> & { className?: string }
type IModalHeaderProps = React.ComponentProps<typeof UIModal.Header>
& VariantProps<typeof modalHeaderStyle> & { className?: string }
type IModalBodyProps = React.ComponentProps<typeof UIModal.Body>
& VariantProps<typeof modalBodyStyle> & { className?: string }
type IModalFooterProps = React.ComponentProps<typeof UIModal.Footer>
& VariantProps<typeof modalFooterStyle> & { className?: string }
type IModalCloseButtonProps = React.ComponentProps<typeof UIModal.CloseButton>
& VariantProps<typeof modalCloseButtonStyle> & { className?: string }
const Modal = React.forwardRef<React.ComponentRef<typeof UIModal>, IModalProps>(
({ className, size = 'md', ...props }, ref) => (
<UIModal
ref={ref}
{...props}
pointerEvents="box-none"
className={modalStyle({ size, class: className })}
context={{ size }}
/>
),
)
const ModalBackdrop = React.forwardRef<
React.ComponentRef<typeof UIModal.Backdrop>,
IModalBackdropProps
>(({ className, ...props }, ref) => {
return (
<UIModal.Backdrop
ref={ref}
initial={{
opacity: 0,
}}
animate={{
opacity: 0.5,
}}
exit={{
opacity: 0,
}}
transition={{
type: 'spring',
damping: 18,
stiffness: 250,
opacity: {
type: 'timing',
duration: 250,
},
}}
{...props}
className={modalBackdropStyle({
class: className,
})}
/>
)
})
const ModalContent = React.forwardRef<
React.ComponentRef<typeof UIModal.Content>,
IModalContentProps
>(({ className, size, ...props }, ref) => {
const { size: parentSize } = useStyleContext(SCOPE)
return (
<UIModal.Content
ref={ref}
initial={{
opacity: 0,
scale: 0.9,
}}
animate={{
opacity: 1,
scale: 1,
}}
exit={{
opacity: 0,
}}
transition={{
type: 'spring',
damping: 18,
stiffness: 250,
opacity: {
type: 'timing',
duration: 250,
},
}}
{...props}
className={modalContentStyle({
parentVariants: {
size: parentSize,
},
size,
class: className,
})}
pointerEvents="auto"
/>
)
})
const ModalHeader = React.forwardRef<
React.ComponentRef<typeof UIModal.Header>,
IModalHeaderProps
>(({ className, ...props }, ref) => {
return (
<UIModal.Header
ref={ref}
{...props}
className={modalHeaderStyle({
class: className,
})}
/>
)
})
const ModalBody = React.forwardRef<
React.ComponentRef<typeof UIModal.Body>,
IModalBodyProps
>(({ className, ...props }, ref) => {
return (
<UIModal.Body
ref={ref}
{...props}
className={modalBodyStyle({
class: className,
})}
/>
)
})
const ModalFooter = React.forwardRef<
React.ComponentRef<typeof UIModal.Footer>,
IModalFooterProps
>(({ className, ...props }, ref) => {
return (
<UIModal.Footer
ref={ref}
{...props}
className={modalFooterStyle({
class: className,
})}
/>
)
})
const ModalCloseButton = React.forwardRef<
React.ComponentRef<typeof UIModal.CloseButton>,
IModalCloseButtonProps
>(({ className, ...props }, ref) => {
return (
<UIModal.CloseButton
ref={ref}
{...props}
className={modalCloseButtonStyle({
class: className,
})}
/>
)
})
Modal.displayName = 'Modal'
ModalBackdrop.displayName = 'ModalBackdrop'
ModalContent.displayName = 'ModalContent'
ModalHeader.displayName = 'ModalHeader'
ModalBody.displayName = 'ModalBody'
ModalFooter.displayName = 'ModalFooter'
ModalCloseButton.displayName = 'ModalCloseButton'
export {
Modal,
ModalBackdrop,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
}