1import React, {ReactNode} from 'react'; 2import { 3 StyleProp, 4 StyleSheet, 5 TextProps, 6 TextStyle, 7 TouchableHighlight, 8 TouchableOpacity, 9 View, 10 ViewStyle, 11} from 'react-native'; 12import rpx from '@/utils/rpx'; 13import useColors from '@/hooks/useColors'; 14import ThemeText from './themeText'; 15import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 16import { 17 fontSizeConst, 18 fontWeightConst, 19 iconSizeConst, 20} from '@/constants/uiConst'; 21import FastImage from './fastImage'; 22import {ImageStyle} from 'react-native-fast-image'; 23 24interface IListItemProps { 25 // 是否有左右边距 26 withHorizonalPadding?: boolean; 27 // 左边距 28 leftPadding?: number; 29 // 右边距 30 rightPadding?: number; 31 // height: 32 style?: StyleProp<ViewStyle>; 33 // 高度类型 34 heightType?: 'big' | 'small' | 'smallest' | 'normal' | 'none'; 35 children?: ReactNode; 36 onPress?: () => void; 37 onLongPress?: () => void; 38} 39 40const defaultPadding = rpx(24); 41const defaultActionWidth = rpx(80); 42 43const Size = { 44 big: rpx(120), 45 normal: rpx(108), 46 small: rpx(96), 47 smallest: rpx(72), 48 none: undefined, 49}; 50 51function ListItem(props: IListItemProps) { 52 const { 53 withHorizonalPadding, 54 leftPadding = defaultPadding, 55 rightPadding = defaultPadding, 56 style, 57 heightType = 'normal', 58 children, 59 onPress, 60 onLongPress, 61 } = props; 62 63 const defaultStyle: StyleProp<ViewStyle> = { 64 paddingLeft: withHorizonalPadding ? leftPadding : 0, 65 paddingRight: withHorizonalPadding ? rightPadding : 0, 66 height: Size[heightType], 67 }; 68 69 const colors = useColors(); 70 71 return ( 72 <TouchableHighlight 73 style={styles.container} 74 underlayColor={colors.listActive} 75 onPress={onPress} 76 onLongPress={onLongPress}> 77 <View style={[styles.container, defaultStyle, style]}> 78 {children} 79 </View> 80 </TouchableHighlight> 81 ); 82} 83 84interface IListItemTextProps { 85 children?: number | string; 86 fontSize?: keyof typeof fontSizeConst; 87 fontWeight?: keyof typeof fontWeightConst; 88 width?: number; 89 position?: 'left' | 'right' | 'none'; 90 fixedWidth?: boolean; 91 containerStyle?: StyleProp<ViewStyle>; 92 contentStyle?: StyleProp<TextStyle>; 93 contentProps?: TextProps; 94} 95 96function ListItemText(props: IListItemTextProps) { 97 const { 98 children, 99 fontSize, 100 fontWeight, 101 position = 'left', 102 fixedWidth, 103 width, 104 containerStyle, 105 contentStyle, 106 contentProps = {}, 107 } = props; 108 109 const defaultStyle: StyleProp<ViewStyle> = { 110 marginRight: position === 'left' ? defaultPadding : 0, 111 marginLeft: position === 'right' ? defaultPadding : 0, 112 width: fixedWidth ? width ?? defaultActionWidth : undefined, 113 flexBasis: fixedWidth ? width ?? defaultActionWidth : undefined, 114 }; 115 116 return ( 117 <View style={[styles.actionBase, defaultStyle, containerStyle]}> 118 <ThemeText 119 fontSize={fontSize} 120 style={contentStyle} 121 fontWeight={fontWeight} 122 {...contentProps}> 123 {children} 124 </ThemeText> 125 </View> 126 ); 127} 128 129interface IListItemIconProps { 130 icon: string; 131 iconSize?: number; 132 width?: number; 133 position?: 'left' | 'right' | 'none'; 134 fixedWidth?: boolean; 135 containerStyle?: StyleProp<ViewStyle>; 136 contentStyle?: StyleProp<TextStyle>; 137 onPress?: () => void; 138} 139 140function ListItemIcon(props: IListItemIconProps) { 141 const { 142 icon, 143 iconSize = iconSizeConst.normal, 144 position = 'left', 145 fixedWidth, 146 width, 147 containerStyle, 148 contentStyle, 149 onPress, 150 } = props; 151 152 const colors = useColors(); 153 154 const defaultStyle: StyleProp<ViewStyle> = { 155 marginRight: position === 'left' ? defaultPadding : 0, 156 marginLeft: position === 'right' ? defaultPadding : 0, 157 width: fixedWidth ? width ?? defaultActionWidth : undefined, 158 flexBasis: fixedWidth ? width ?? defaultActionWidth : undefined, 159 }; 160 161 const innerContent = ( 162 <View style={[styles.actionBase, defaultStyle, containerStyle]}> 163 <Icon 164 name={icon} 165 size={iconSize} 166 style={contentStyle} 167 color={colors.text} 168 /> 169 </View> 170 ); 171 172 return onPress ? ( 173 <TouchableOpacity onPress={onPress}>{innerContent}</TouchableOpacity> 174 ) : ( 175 innerContent 176 ); 177} 178 179interface IListItemImageProps { 180 uri?: string; 181 fallbackImg?: number; 182 imageSize?: number; 183 width?: number; 184 position?: 'left' | 'right'; 185 fixedWidth?: boolean; 186 containerStyle?: StyleProp<ViewStyle>; 187 contentStyle?: StyleProp<ImageStyle>; 188 maskIcon?: string | null; 189} 190 191function ListItemImage(props: IListItemImageProps) { 192 const { 193 uri, 194 fallbackImg, 195 position = 'left', 196 fixedWidth, 197 width, 198 containerStyle, 199 contentStyle, 200 maskIcon, 201 } = props; 202 203 const defaultStyle: StyleProp<ViewStyle> = { 204 marginRight: position === 'left' ? defaultPadding : 0, 205 marginLeft: position === 'right' ? defaultPadding : 0, 206 width: fixedWidth ? width ?? defaultActionWidth : undefined, 207 flexBasis: fixedWidth ? width ?? defaultActionWidth : undefined, 208 }; 209 210 return ( 211 <View style={[styles.actionBase, defaultStyle, containerStyle]}> 212 <FastImage 213 style={[styles.leftImage, contentStyle]} 214 uri={uri} 215 emptySrc={fallbackImg} 216 /> 217 {maskIcon ? ( 218 <View style={[styles.leftImage, styles.imageMask]}> 219 <Icon 220 name={maskIcon} 221 size={iconSizeConst.normal} 222 color="red" 223 /> 224 </View> 225 ) : null} 226 </View> 227 ); 228} 229 230interface IContentProps { 231 title?: ReactNode; 232 children?: ReactNode; 233 description?: ReactNode; 234 containerStyle?: StyleProp<ViewStyle>; 235} 236 237function Content(props: IContentProps) { 238 const { 239 children, 240 title = children, 241 description = null, 242 containerStyle, 243 } = props; 244 245 let realTitle; 246 let realDescription; 247 248 if (typeof title === 'string' || typeof title === 'number') { 249 realTitle = <ThemeText numberOfLines={1}>{title}</ThemeText>; 250 } else { 251 realTitle = title; 252 } 253 254 if (typeof description === 'string' || typeof description === 'number') { 255 realDescription = ( 256 <ThemeText 257 numberOfLines={1} 258 fontSize="description" 259 fontColor="textSecondary" 260 style={styles.contentDesc}> 261 {description} 262 </ThemeText> 263 ); 264 } else { 265 realDescription = description; 266 } 267 268 return ( 269 <View style={[styles.itemContentContainer, containerStyle]}> 270 {realTitle} 271 {realDescription} 272 </View> 273 ); 274} 275 276export function ListItemHeader(props: {children?: ReactNode}) { 277 const {children} = props; 278 return ( 279 <ListItem 280 withHorizonalPadding 281 heightType="smallest" 282 style={styles.listItemHeader}> 283 {typeof children === 'string' ? ( 284 <ThemeText 285 fontSize="subTitle" 286 fontColor="textSecondary" 287 fontWeight="bold"> 288 {children} 289 </ThemeText> 290 ) : ( 291 children 292 )} 293 </ListItem> 294 ); 295} 296 297const styles = StyleSheet.create({ 298 /** listitem */ 299 container: { 300 width: '100%', 301 flexDirection: 'row', 302 alignItems: 'center', 303 }, 304 /** left */ 305 actionBase: { 306 height: '100%', 307 flexShrink: 0, 308 flexGrow: 0, 309 flexBasis: 0, 310 flexDirection: 'row', 311 justifyContent: 'center', 312 alignItems: 'center', 313 }, 314 315 leftImage: { 316 width: rpx(80), 317 height: rpx(80), 318 borderRadius: rpx(16), 319 }, 320 imageMask: { 321 position: 'absolute', 322 alignItems: 'center', 323 justifyContent: 'center', 324 backgroundColor: '#00000022', 325 }, 326 itemContentContainer: { 327 flex: 1, 328 height: '100%', 329 justifyContent: 'center', 330 }, 331 contentDesc: { 332 marginTop: rpx(16), 333 }, 334 335 listItemHeader: { 336 marginTop: rpx(20), 337 }, 338}); 339 340ListItem.Size = Size; 341ListItem.ListItemIcon = ListItemIcon; 342ListItem.ListItemImage = ListItemImage; 343ListItem.ListItemText = ListItemText; 344ListItem.Content = Content; 345 346export default ListItem; 347