u
This commit is contained in:
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,
|
||||
}
|
||||
Reference in new issue
Block a user