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 { 16 fontSizeConst, 17 fontWeightConst, 18 iconSizeConst, 19} from '@/constants/uiConst'; 20import FastImage from './fastImage'; 21import {ImageStyle} from 'react-native-fast-image'; 22import Icon, {IIconName} from '@/components/base/icon.tsx'; 23 24interface IListItemProps { 25 // 是否有左右边距 26 withHorizontalPadding?: 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 withHorizontalPadding, 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: withHorizontalPadding ? leftPadding : 0, 65 paddingRight: withHorizontalPadding ? 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: IIconName; 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 color?: string; 139} 140 141function ListItemIcon(props: IListItemIconProps) { 142 const { 143 icon, 144 iconSize = iconSizeConst.normal, 145 position = 'left', 146 fixedWidth, 147 width, 148 containerStyle, 149 contentStyle, 150 onPress, 151 color, 152 } = props; 153 154 const colors = useColors(); 155 156 const defaultStyle: StyleProp<ViewStyle> = { 157 marginRight: position === 'left' ? defaultPadding : 0, 158 marginLeft: position === 'right' ? defaultPadding : 0, 159 width: fixedWidth ? width ?? defaultActionWidth : undefined, 160 flexBasis: fixedWidth ? width ?? defaultActionWidth : undefined, 161 }; 162 163 const innerContent = ( 164 <View style={[styles.actionBase, defaultStyle, containerStyle]}> 165 <Icon 166 name={icon} 167 size={iconSize} 168 style={contentStyle} 169 color={color || colors.text} 170 /> 171 </View> 172 ); 173 174 return onPress ? ( 175 <TouchableOpacity onPress={onPress}>{innerContent}</TouchableOpacity> 176 ) : ( 177 innerContent 178 ); 179} 180 181interface IListItemImageProps { 182 uri?: string; 183 fallbackImg?: number; 184 imageSize?: number; 185 width?: number; 186 position?: 'left' | 'right'; 187 fixedWidth?: boolean; 188 containerStyle?: StyleProp<ViewStyle>; 189 contentStyle?: StyleProp<ImageStyle>; 190 maskIcon?: IIconName | null; 191} 192 193function ListItemImage(props: IListItemImageProps) { 194 const { 195 uri, 196 fallbackImg, 197 position = 'left', 198 fixedWidth, 199 width, 200 containerStyle, 201 contentStyle, 202 maskIcon, 203 } = props; 204 205 const defaultStyle: StyleProp<ViewStyle> = { 206 marginRight: position === 'left' ? defaultPadding : 0, 207 marginLeft: position === 'right' ? defaultPadding : 0, 208 width: fixedWidth ? width ?? defaultActionWidth : undefined, 209 flexBasis: fixedWidth ? width ?? defaultActionWidth : undefined, 210 }; 211 212 return ( 213 <View style={[styles.actionBase, defaultStyle, containerStyle]}> 214 <FastImage 215 style={[styles.leftImage, contentStyle]} 216 uri={uri} 217 emptySrc={fallbackImg} 218 /> 219 {maskIcon ? ( 220 <View style={[styles.leftImage, styles.imageMask]}> 221 <Icon 222 name={maskIcon} 223 size={iconSizeConst.normal} 224 color="red" 225 /> 226 </View> 227 ) : null} 228 </View> 229 ); 230} 231 232interface IContentProps { 233 title?: ReactNode; 234 children?: ReactNode; 235 description?: ReactNode; 236 containerStyle?: StyleProp<ViewStyle>; 237} 238 239function Content(props: IContentProps) { 240 const { 241 children, 242 title = children, 243 description = null, 244 containerStyle, 245 } = props; 246 247 let realTitle; 248 let realDescription; 249 250 if (typeof title === 'string' || typeof title === 'number') { 251 realTitle = <ThemeText numberOfLines={1}>{title}</ThemeText>; 252 } else { 253 realTitle = title; 254 } 255 256 if (typeof description === 'string' || typeof description === 'number') { 257 realDescription = ( 258 <ThemeText 259 numberOfLines={1} 260 fontSize="description" 261 fontColor="textSecondary" 262 style={styles.contentDesc}> 263 {description} 264 </ThemeText> 265 ); 266 } else { 267 realDescription = description; 268 } 269 270 return ( 271 <View style={[styles.itemContentContainer, containerStyle]}> 272 {realTitle} 273 {realDescription} 274 </View> 275 ); 276} 277 278export function ListItemHeader(props: {children?: ReactNode}) { 279 const {children} = props; 280 return ( 281 <ListItem 282 withHorizontalPadding 283 heightType="smallest" 284 style={styles.listItemHeader}> 285 {typeof children === 'string' ? ( 286 <ThemeText 287 fontSize="subTitle" 288 fontColor="textSecondary" 289 fontWeight="bold"> 290 {children} 291 </ThemeText> 292 ) : ( 293 children 294 )} 295 </ListItem> 296 ); 297} 298 299const styles = StyleSheet.create({ 300 /** listitem */ 301 container: { 302 width: '100%', 303 flexDirection: 'row', 304 alignItems: 'center', 305 }, 306 /** left */ 307 actionBase: { 308 height: '100%', 309 flexShrink: 0, 310 flexGrow: 0, 311 flexBasis: 0, 312 flexDirection: 'row', 313 justifyContent: 'center', 314 alignItems: 'center', 315 }, 316 317 leftImage: { 318 width: rpx(80), 319 height: rpx(80), 320 borderRadius: rpx(16), 321 }, 322 imageMask: { 323 position: 'absolute', 324 alignItems: 'center', 325 justifyContent: 'center', 326 backgroundColor: '#00000022', 327 }, 328 itemContentContainer: { 329 flex: 1, 330 height: '100%', 331 justifyContent: 'center', 332 }, 333 contentDesc: { 334 marginTop: rpx(16), 335 }, 336 337 listItemHeader: { 338 marginTop: rpx(20), 339 }, 340}); 341 342ListItem.Size = Size; 343ListItem.ListItemIcon = ListItemIcon; 344ListItem.ListItemImage = ListItemImage; 345ListItem.ListItemText = ListItemText; 346ListItem.Content = Content; 347 348export default ListItem; 349