import { useState, useRef, useEffect } from 'react' import { View, Text, StyleSheet, Pressable, Modal, FlatList, TouchableWithoutFeedback, LayoutChangeEvent, Platform, useWindowDimensions, } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { DownArrowIcon } from '@/components/icon' export interface DropdownOption { label: string value: T disabled?: boolean } export interface DropdownProps { /** * 选项列表 */ options: DropdownOption[] /** * 当前选中的值 */ value?: T /** * 选择回调 */ onSelect?: (value: T, option: DropdownOption) => void /** * 占位符文本 */ placeholder?: string /** * 是否禁用 */ disabled?: boolean /** * 自定义触发按钮渲染 */ renderTrigger?: (selectedOption: DropdownOption | undefined, isOpen: boolean, toggle: () => void) => React.ReactNode /** * 下拉列表的最大高度 */ maxHeight?: number /** * 下拉菜单距离触发按钮的垂直偏移(默认4px) */ offsetTop?: number /** * 自定义样式 */ style?: any /** * 触发按钮样式 */ triggerStyle?: any /** * 下拉列表样式 */ dropdownStyle?: any /** * 选项项样式 */ optionStyle?: any /** * 选项文本样式 */ optionTextStyle?: any /** * 自定义选项渲染函数 */ renderOption?: (option: DropdownOption, isSelected: boolean) => React.ReactNode } export default function Dropdown({ options, value, onSelect, placeholder = '请选择', disabled = false, renderTrigger, maxHeight = 200, offsetTop = 4, style, triggerStyle, dropdownStyle, optionStyle, optionTextStyle, renderOption, }: DropdownProps) { const [isOpen, setIsOpen] = useState(false) const [triggerLayout, setTriggerLayout] = useState({ x: 0, y: 0, width: 0, height: 0 }) const triggerRef = useRef(null) const layoutRef = useRef({ width: 0, height: 0 }) const { width: screenWidth } = useWindowDimensions() const insets = useSafeAreaInsets() // 获取当前选中的选项 const selectedOption = options.find(option => option.value === value) // 测量触发按钮的位置(跨平台兼容) const measureTrigger = () => { if (!triggerRef.current) return if (Platform.OS === 'android') { // Android 上使用 measure 方法,它返回相对于父组件的坐标 triggerRef.current.measure((_x, _y, width, height, pageX, pageY) => { const finalWidth = width || layoutRef.current.width || 200 const finalHeight = height || layoutRef.current.height || 40 // pageY 在 Android 上已经包含了状态栏高度,所以直接使用 setTriggerLayout({ x: pageX, y: pageY, width: finalWidth, height: finalHeight }) }) } else { // iOS 和 Web 使用 measureInWindow triggerRef.current.measureInWindow((x, y, width, height) => { const finalWidth = width || layoutRef.current.width || 200 const finalHeight = height || layoutRef.current.height || 40 if (Platform.OS === 'web' && (width === 0 || height === 0)) { // Web 平台使用 measure 作为后备 triggerRef.current?.measure((_x, _y, w, h, pageX, pageY) => { setTriggerLayout({ x: pageX || x, y: pageY || y, width: w || finalWidth, height: h || finalHeight }) }) } else { setTriggerLayout({ x, y, width: finalWidth, height: finalHeight }) } }) } } // 测量触发按钮的位置 const handleTriggerLayout = (event: LayoutChangeEvent) => { const { width, height } = event.nativeEvent.layout layoutRef.current = { width, height } // 延迟测量以确保布局已完成 setTimeout(() => { measureTrigger() }, 0) } // 处理选项选择 const handleSelect = (option: DropdownOption) => { if (option.disabled) return onSelect?.(option.value, option) setIsOpen(false) } // 切换下拉菜单显示状态 const toggleDropdown = () => { if (disabled) return // 在打开时重新测量位置 if (!isOpen) { // 使用 setTimeout 确保在打开前完成测量 // Android 上需要稍微延迟以确保测量准确 const delay = Platform.OS === 'android' ? 50 : 0 setTimeout(() => { measureTrigger() }, delay) } setIsOpen(!isOpen) } // 当打开时,重新测量位置(处理滚动等情况) useEffect(() => { if (isOpen) { // Android 上需要稍微延迟以确保测量准确 const delay = Platform.OS === 'android' ? 50 : 0 setTimeout(() => { measureTrigger() }, delay) } }, [isOpen]) // 关闭下拉菜单 const closeDropdown = () => { setIsOpen(false) } // 默认触发按钮渲染 const defaultTrigger = ( {selectedOption ? selectedOption.label : placeholder} ) return ( {renderTrigger ? renderTrigger(selectedOption, isOpen, toggleDropdown) : defaultTrigger} {(() => { const dropdownWidth = (dropdownStyle as any)?.minWidth || (dropdownStyle as any)?.width || Math.max(triggerLayout.width, 200) const hasRightAlignment = (dropdownStyle as any)?.right !== undefined const rightValue = (dropdownStyle as any)?.right const calculatedLeft = hasRightAlignment ? screenWidth - (rightValue || 0) - dropdownWidth : triggerLayout.x // 从 dropdownStyle 中移除 right,避免样式冲突 const { right, ...restDropdownStyle } = dropdownStyle || {} return ( `option-${index}-${String(item.value)}`} renderItem={({ item }) => { const isSelected = item.value === value if (renderOption) { return ( handleSelect(item)} disabled={item.disabled} > {renderOption(item, isSelected)} ) } return ( handleSelect(item)} disabled={item.disabled} > {item.label} ) }} showsVerticalScrollIndicator={false} /> ) })()} ) } const styles = StyleSheet.create({ container: { position: 'relative', }, trigger: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, backgroundColor: '#272A30', minHeight: 40, }, triggerDisabled: { opacity: 0.5, }, triggerText: { color: '#F5F5F5', fontSize: 14, fontWeight: '500', flex: 1, }, triggerTextPlaceholder: { color: '#ABABAB', }, arrowIcon: { marginLeft: 8, transform: [{ rotate: '0deg' }], }, arrowIconRotated: { transform: [{ rotate: '180deg' }], }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.3)', }, dropdown: { position: 'absolute', backgroundColor: '#272A30', borderRadius: 12, overflow: 'hidden', ...Platform.select({ ios: { shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.3, shadowRadius: 8, }, android: { elevation: 8, }, web: { shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.3, shadowRadius: 8, boxShadow: '0 4px 8px rgba(0, 0, 0, 0.3)', }, }), }, option: { paddingHorizontal: 16, paddingVertical: 12, }, optionSelected: { backgroundColor: '#3A3D42', }, optionDisabled: { opacity: 0.5, }, optionText: { color: '#F5F5F5', fontSize: 14, fontWeight: '400', }, optionTextSelected: { color: '#FFCF00', fontWeight: '500', }, optionTextDisabled: { color: '#ABABAB', }, })