Commit 991859dd authored by XieZhiXiong's avatar XieZhiXiong

feat: 搬运部分基础组件

parent 45ea58e7
......@@ -14,6 +14,7 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"dependencies": {
"@linkseeks/god-mobile": "^1.0.4",
"@react-native-async-storage/async-storage": "^1.17.11",
"@react-navigation/bottom-tabs": "^6.4.0",
"@react-navigation/native": "^6.0.13",
......@@ -26,7 +27,8 @@
"react-native": "0.70.6",
"react-native-safe-area-context": "^4.4.1",
"react-native-screens": "^3.18.2",
"react-native-storage": "^1.0.1"
"react-native-storage": "^1.0.1",
"expo-blur": "~8.2.0"
},
"devDependencies": {
"@babel/core": "^7.12.9",
......@@ -40,19 +42,19 @@
"@types/react-test-renderer": "^18.0.0",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"axios": "^0.21.0",
"babel-jest": "^26.6.3",
"chalk": "^4.1.0",
"clone": "^2.1.2",
"dotenv": "^8.2.0",
"eslint": "^7.32.0",
"fs-extra": "^9.0.1",
"jest": "^26.6.3",
"json2ts": "^0.0.7",
"metro-react-native-babel-preset": "0.72.3",
"react-test-renderer": "18.1.0",
"typescript": "^4.8.3",
"axios": "^0.21.0",
"clone": "^2.1.2",
"chalk": "^4.1.0",
"ora": "^5.1.0",
"fs-extra": "^9.0.1",
"dotenv": "^8.2.0",
"json2ts": "^0.0.7"
"react-test-renderer": "18.1.0",
"typescript": "^4.8.3"
},
"jest": {
"preset": "react-native",
......
/*
* @Author: XieZhiXiong
* @Date: 2021-01-04 16:16:21
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-04-22 14:38:39
* @Description: 上拉菜单
*/
import React, { useRef, useEffect, useState } from 'react';
import {
View,
Text,
TouchableOpacity,
Animated,
ViewStyle,
LayoutChangeEvent,
ScrollView,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import useAppStyle from '../../hooks/useAppStyle';
import themeLayout from '../../constants/theme/layout';
import Overlay from '../Overlay';
import styles from './styles';
function isTextNode(node: React.ReactNode) {
return typeof node !== 'object';
}
export interface ActionsItem {
/**
* 标题
*/
name: string,
/**
* 值
*/
value?: number,
/**
* 描述
*/
describe?: string,
/**
* 选项文字颜色
*/
color?: string,
/**
* 是否为加载状态
*/
loading?: boolean,
/**
* 是否为禁用状态
*/
disabled?: boolean,
}
interface ActionSheetProps {
/**
* 是否可见
*/
visible: boolean;
/**
* 菜单选项
*/
actions?: ActionsItem[],
/**
* 标题
*/
title?: React.ReactNode,
/**
* 是否展示 标题上部的 线条
*/
hasLine?: boolean,
/**
* 取消按钮文字
*/
cancelText?: React.ReactNode,
/**
* 是否显示圆角
*/
round?: boolean,
/**
* actions 内容区块高度
*/
actionsContentHeight?: number,
/**
* 是否在点击选项后关闭
*/
closeOnClickAction?: boolean,
/**
* 点击遮罩是否关闭菜单
*/
closeOnClickOverlay?: boolean,
/**
* 内容区域自定义样式
*/
contentStyle?: ViewStyle,
/**
* 遮罩层样式
*/
maskStyle?: ViewStyle,
/**
* 关闭事件
*/
onClose?: () => void;
/**
* 选中菜单事件
*/
onSelect?: (item: ActionsItem) => void;
/**
* 取消事件
*/
onCancel?: () => void;
/**
* 点击遮罩层时触发
*/
onClickOverlay?: () => void;
/**
* zIndex,默认 100,比 GlobalHeader 高一点点
*/
zIndex?: number,
/**
* 是否使用Modal
*/
useModal?: boolean,
children?: React.ReactNode,
}
const ActionSheet: React.FC<ActionSheetProps> = (props: ActionSheetProps) => {
const {
visible,
actions,
title,
hasLine,
cancelText,
round,
actionsContentHeight,
closeOnClickAction,
closeOnClickOverlay,
contentStyle,
maskStyle,
onClose,
onSelect,
onCancel,
onClickOverlay,
zIndex,
useModal,
children,
} = props;
const [contentHeight, setContentHeight] = useState(0);
const myStyle = useAppStyle(styles);
const slideToTop = useRef(new Animated.Value(0)).current;
const safeInset = useSafeAreaInsets();
useEffect(() => {
if (visible) {
Animated.timing(slideToTop, {
toValue: 1,
duration: 300,
useNativeDriver: false,
}).start();
} else {
Animated.timing(slideToTop, {
toValue: 0,
duration: 300,
useNativeDriver: false,
}).start();
}
}, [visible]);
const handleOverlayClick = () => {
if (closeOnClickOverlay && onClose) {
onClose();
}
if (onClickOverlay) {
onClickOverlay();
}
};
const handleCancel = () => {
if (onClose) {
onClose();
}
if (onCancel) {
onCancel();
}
};
const handleSelect = (item: ActionsItem) => {
if (closeOnClickAction && onClose) {
onClose();
}
if (onSelect) {
onSelect(item);
}
};
const getContentHeight = (e: LayoutChangeEvent) => {
const { height } = e.nativeEvent.layout;
// 64 头部高度,56 为底部取消区域的高度
let sum = height + 64;
if (cancelText) {
sum += 56;
}
setContentHeight(sum);
};
const slideValue = slideToTop.interpolate({
inputRange: [0, 1],
outputRange: [contentHeight, 0],
});
const renderTitle = () => (
isTextNode(title) ? (
<Text style={myStyle['actionSheet-ship-head-title']}>
{title}
</Text>
) : (
title
)
);
return (
<Overlay
visible={visible}
onClick={handleOverlayClick}
position="bottom"
zIndex={zIndex}
style={maskStyle}
useModal={useModal}
>
<Animated.View
style={[
{
...myStyle['actionSheet-ship'],
borderTopRightRadius: round ? 16 : 0,
borderTopLeftRadius: round ? 16 : 0,
paddingBottom: safeInset.bottom,
},
{
transform: [
{
translateY: slideValue,
},
],
},
]}
>
{(title || hasLine) ? (
<View
style={[
myStyle['actionSheet-ship-head'],
{
paddingTop: hasLine ? themeLayout['padding-xl'] : themeLayout['padding-m'],
},
]}
>
{renderTitle()}
{hasLine ? (
<TouchableOpacity
style={myStyle['actionSheet-ship-head-close']}
activeOpacity={1}
onPress={handleCancel}
>
<View style={myStyle['actionSheet-ship-head-close-line']} />
</TouchableOpacity>
) : null}
</View>
) : null}
{!children && actions && (
<ScrollView
style={[
myStyle['actionSheet-list'],
actionsContentHeight !== undefined && {
height: actionsContentHeight,
},
]}
onLayout={getContentHeight}
>
{actions.map((item, index) => (
<TouchableOpacity
style={{
...myStyle['actionSheet-list-item'],
}}
activeOpacity={0.8}
onPress={() => handleSelect(item)}
// eslint-disable-next-line react/no-array-index-key
key={index}
>
<Text style={myStyle['actionSheet-list-item-name']}>
{item.name}
</Text>
{item.describe && (
<Text style={myStyle['actionSheet-list-item-describe']}>
{item.describe}
</Text>
)}
</TouchableOpacity>
))}
</ScrollView>
)}
{children && (
<View
style={{
...myStyle['actionSheet-ship-content'],
...contentStyle,
}}
onLayout={getContentHeight}
>
{children}
</View>
)}
{!!cancelText && (
<View style={myStyle['actionSheet-gap']} />
)}
{!!cancelText && (
<TouchableOpacity
activeOpacity={0.8}
onPress={handleCancel}
>
<Text style={myStyle['actionSheet-cancel']}>
{cancelText}
</Text>
</TouchableOpacity>
)}
</Animated.View>
</Overlay>
);
};
ActionSheet.defaultProps = {
title: null,
hasLine: false,
actions: [],
cancelText: '',
round: true,
actionsContentHeight: undefined,
closeOnClickAction: true,
closeOnClickOverlay: true,
contentStyle: {},
maskStyle: {},
onClose: undefined,
onSelect: undefined,
onCancel: undefined,
onClickOverlay: undefined,
zIndex: 100,
children: null,
useModal: false,
};
export default ActionSheet;
/*
* @Author: XieZhiXiong
* @Date: 2021-01-04 16:16:25
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-04-21 20:23:17
* @Description:
*/
import { StyleSheet } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
actionSheet: {
position: 'relative',
},
'actionSheet-ship': {
width: '100%',
backgroundColor: '#FFFFFF',
overflow: 'hidden',
},
'actionSheet-ship-head': {
alignItems: 'center',
paddingTop: themeLayout['padding-m'],
paddingBottom: themeLayout['padding-m'],
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
backgroundColor: '#FFFFFF',
},
'actionSheet-ship-head-title': {
paddingHorizontal: themeLayout['padding-m'],
fontSize: 16,
fontWeight: '500',
textAlign: 'center',
color: theme.fonts.black1,
},
'actionSheet-ship-head-close': {
position: 'absolute',
top: 0,
left: '50%',
padding: 16,
transform: [{
translateX: -36,
}],
},
'actionSheet-ship-head-close-line': {
width: 40,
height: 4,
backgroundColor: '#D8DDE6',
borderRadius: 100,
},
'actionSheet-ship-content': {
padding: themeLayout['padding-m'],
backgroundColor: '#FFFFFF',
},
'actionSheet-list': {
},
'actionSheet-list-item': {
paddingVertical: themeLayout['padding-l'] - 3,
paddingHorizontal: themeLayout['padding-m'],
backgroundColor: '#FFFFFF',
},
'actionSheet-list-item-name': {
fontSize: 14,
color: theme.fonts.black1,
textAlign: 'center',
},
'actionSheet-list-item-describe': {
marginTop: themeLayout['padding-xs'],
fontSize: 12,
color: theme.fonts.black3,
textAlign: 'center',
},
'actionSheet-gap': {
width: '100%',
height: 4,
backgroundColor: theme.colors.border,
},
'actionSheet-cancel': {
paddingHorizontal: themeLayout['padding-s'],
paddingVertical: themeLayout['padding-m'],
fontSize: 16,
color: theme.fonts.black1,
textAlign: 'center',
backgroundColor: '#FFFFFF',
},
});
/*
* @Author: XieZhiXiong
* @Date: 2021-03-03 10:33:37
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-08-18 16:14:01
* @Description: CellItem 组件
*/
import React from 'react';
import { View, Text, TouchableOpacity, ViewStyle, TextStyle } from 'react-native';
import { Icons } from '@linkseeks/god-mobile';
import { useTheme } from '@react-navigation/native';
import useAppStyle from '../../hooks/useAppStyle';
import { ThemeStyle } from '../../constants/theme';
import styles from './styles';
export interface CellItemProps {
/**
* 左侧标题
*/
title?: React.ReactNode,
/**
* 左侧标题 numberOfLines
*/
titleNumberOfLines?: number,
/**
* icon 名称
*/
icon?: string,
/**
* 自定义渲染 icon
*/
customIcon?: React.ReactNode,
/**
* icon大小,默认 22,自定义渲染 icon 该属性无效
*/
iconSize?: number,
/**
* 右侧内容
*/
value?: React.ReactNode,
/**
* 标题下方的描述信息
*/
label?: React.ReactNode,
/**
* 是否展示右侧箭头
*/
hasArrow?: boolean,
/**
* 是否开启点击反馈
*/
clickable?: boolean,
/**
* 点击触发事件,需要开启 clickable,否则无效
*/
onPress?: () => void,
/**
* 是否展示边框
*/
border?: boolean,
/**
* 是否对调 title 与 value 的字体样式
*/
transposition?: boolean,
/**
* 自定义头部样式
*/
customHeadStyle?: ViewStyle,
/**
* 自定义标题样式
*/
customTitleStyle?: TextStyle,
}
function isTextNode(node: React.ReactNode) {
return typeof node === 'string' || typeof node === 'number';
}
const CellItem: React.FC<CellItemProps> = (props: CellItemProps) => {
const {
title,
titleNumberOfLines = undefined,
icon,
iconSize = 22,
customIcon,
value,
label,
hasArrow,
clickable,
onPress,
border,
transposition,
customHeadStyle,
customTitleStyle,
} = props;
const myStyle = useAppStyle(styles);
const appTheme = useTheme() as ThemeStyle;
const handlePress = () => {
if (clickable && onPress) {
onPress();
}
};
const renderTitle = () => (
isTextNode(title) ? (
<Text
style={!transposition ? [myStyle['list-item-title'], customTitleStyle] : myStyle['list-item-value']}
numberOfLines={titleNumberOfLines}
>
{title}
</Text>
) : (
title
)
);
const renderValue = () => (
isTextNode(value) ? (
<Text style={!transposition ? [myStyle['list-item-value'], myStyle['list-item-value__right']] : [myStyle['list-item-title'], customTitleStyle, myStyle['list-item-value__right']]}>{value}</Text>
) : (
value
)
);
const renderLabel = () => (
isTextNode(label) ? (
<Text style={myStyle['list-item-label']}>{label}</Text>
) : (
label
)
);
return (
<TouchableOpacity
style={[
myStyle['list-item'],
border && myStyle['list-item__border'],
]}
activeOpacity={clickable ? 0.8 : 1}
onPress={handlePress}
>
<View
style={[
myStyle['list-item-head'],
customHeadStyle,
]}
>
{(icon || customIcon) ? (
<View style={myStyle['list-item-icon']}>
{!customIcon ? (
<Icons name={icon} size={iconSize} color={appTheme.fonts.black1} />
) : (
customIcon
)}
</View>
) : null}
<View style={myStyle['list-item-titleWrap']}>
{renderTitle()}
</View>
{renderValue()}
{hasArrow ? (
<View style={myStyle['list-item-arrow']}>
<Icons name="right" size={14} color="#C0C4CC" />
</View>
) : null}
</View>
{label && (
<View style={myStyle['list-item-labelWrap']}>
{renderLabel()}
</View>
)}
</TouchableOpacity>
);
};
CellItem.defaultProps = {
title: null,
titleNumberOfLines: undefined,
icon: '',
iconSize: 22,
customIcon: null,
value: null,
label: null,
hasArrow: false,
clickable: false,
onPress: undefined,
border: undefined,
transposition: false,
customHeadStyle: {},
customTitleStyle: {},
};
CellItem.displayName = 'CellItem';
export default CellItem;
/* eslint-disable no-nested-ternary */
/*
* @Author: XieZhiXiong
* @Date: 2021-03-03 10:28:43
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-03-22 17:56:52
* @Description: Cell 组件
*/
import React from 'react';
import { View, ViewStyle } from 'react-native';
import useAppStyle from '../../hooks/useAppStyle';
import ListItem, { CellItemProps } from './Item';
import styles from './styles';
interface CellProps {
/**
* 是否展示边框
*/
border?: boolean,
/**
* 是否对调 title 与 value 的字体样式
*/
transposition?: boolean,
/**
* 自定义外部样式
*/
customStyle?: ViewStyle,
children?: React.ReactNode,
}
const Cell = (props: CellProps) => {
const {
border,
transposition,
customStyle,
children,
} = props;
const myStyle = useAppStyle(styles);
const childLen = React.Children.count(children);
const childNodes = React.Children.map(children, (child: any, index: number) => {
if (child) {
const childProps: CellItemProps = child.props || {};
if (child.type.displayName === 'CellItem') {
return React.cloneElement(child, {
...childProps,
border: childProps.border !== undefined ? childProps.border : index !== childLen - 1 ? border : false,
transposition,
});
}
}
return child;
});
return (
<View style={[myStyle.list, customStyle]}>
{childNodes}
</View>
);
};
Cell.defaultProps = {
border: true,
transposition: false,
customStyle: {},
children: null,
};
Cell.displayName = 'Cell';
Cell.Item = ListItem;
export default Cell;
/*
* @Author: XieZhiXiong
* @Date: 2021-03-03 10:28:48
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-11-10 16:06:31
* @Description:
*/
import { StyleSheet } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
list: {
paddingHorizontal: themeLayout['padding-s'],
backgroundColor: '#FFFFFF',
},
'list-item': {
},
'list-item-head': {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: themeLayout['padding-m'] - 1,
backgroundColor: '#FFFFFF',
},
'list-item-icon': {
alignSelf: 'flex-start',
marginLeft: themeLayout['padding-xxs'],
marginRight: themeLayout['padding-xs'],
},
'list-item-titleWrap': {
flex: 1,
},
'list-item-title': {
fontSize: 14,
fontWeight: '400',
color: theme.fonts.black1,
},
'list-item-value': {
maxWidth: '60%',
fontSize: 14,
fontWeight: '400',
color: theme.fonts.black3,
},
'list-item-value__right': {
textAlign: 'right',
},
'list-item-labelWrap': {
paddingBottom: themeLayout['padding-s'],
},
'list-item-label': {
fontSize: 14,
fontWeight: '400',
color: theme.fonts.black2,
},
'list-item-arrow': {
paddingVertical: themeLayout['padding-xxs'] - 1,
marginLeft: themeLayout['padding-xs'],
},
'list-item__border': {
borderBottomColor: theme.colors.border,
borderBottomWidth: 0.5,
},
});
/*
* @Author: XieZhiXiong
* @Date: 2020-12-24 15:45:47
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-08-30 17:48:43
* @Description: 描述列表项
*/
import React from 'react';
import { View, Text, ViewStyle, TextStyle } from 'react-native';
import useAppStyle from '../../hooks/useAppStyle';
import styles from './styles';
interface DescriptionsItemProps {
/**
* label 文本
*/
label: string,
/**
* Item 宽度
*/
width?: string | number,
/**
* label 宽度
*/
labelWidth?: string | number,
/**
* 冒号
*/
colon?: string,
/**
* Text组件 numberOfLines 属性,只对 child 为文本的时候才生效
*/
numberOfLines?: number,
/**
* 自定义外部样式
*/
customStyle?: ViewStyle,
/**
* 自定义label样式
*/
customLabelStyle?: TextStyle,
/**
* 自定义content样式
*/
customContentStyle?: TextStyle,
/**
* 自定义content外部容器样式
*/
customContentWrapStyle?: ViewStyle,
children?: React.ReactNode,
}
const DescriptionsItem: React.FC<DescriptionsItemProps> = (props: DescriptionsItemProps) => {
const {
label,
colon,
width,
labelWidth,
numberOfLines,
customStyle,
customLabelStyle,
customContentStyle,
customContentWrapStyle,
children,
} = props;
const myStyle = useAppStyle(styles);
// 这里包括一层,方便控制样式,如果传入的是非 string,则需要在外边自己编写样式
const contentNode = typeof children !== 'object' ? (
<Text
style={[myStyle['descriptions-item-content'], customContentStyle]}
numberOfLines={numberOfLines}
>
{children}
</Text>
) : children;
return (
<View
style={{
...myStyle['descriptions-item'],
width,
flexBasis: width,
...(customStyle || {}),
}}
>
<View
style={{
...myStyle['descriptions-item-left'],
width: labelWidth,
}}
>
<Text style={[myStyle['descriptions-item-label'], customLabelStyle]}>
{label}
</Text>
<Text style={[myStyle['descriptions-item-colon'], customLabelStyle]}>{colon}</Text>
</View>
<View style={[myStyle['descriptions-item-right'], customContentWrapStyle]}>
{contentNode}
</View>
</View>
);
};
DescriptionsItem.defaultProps = {
colon: ':',
width: undefined,
labelWidth: undefined,
numberOfLines: undefined,
customStyle: {},
customLabelStyle: {},
customContentStyle: {},
customContentWrapStyle: {},
children: null,
};
DescriptionsItem.displayName = 'DescriptionsItem';
export default DescriptionsItem;
/*
* @Author: XieZhiXiong
* @Date: 2020-12-23 17:36:23
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-08-30 16:26:08
* @Description: 描述列表
*/
import React from 'react';
import { View, Text, ViewStyle } from 'react-native';
import DescriptionsItem from './Item';
import useAppStyle from '../../hooks/useAppStyle';
import styles from './styles';
interface DescriptionsProps {
/**
* 列数,默认 两列
*/
column?: number,
/**
* 冒号
*/
colon?: string,
/**
* 标题
*/
title?: React.ReactNode,
/**
* Item label 宽度
*/
labelWidth?: string | number,
/**
* 自定义内容样式
*/
customContentStyle?: ViewStyle,
children?: React.ReactNode,
}
const Descriptions = (props: DescriptionsProps) => {
const {
column,
colon,
title,
labelWidth,
customContentStyle,
children,
} = props;
const itemWidth = `${(100 / (column as number)).toFixed(3)}%`;
const myStyle = useAppStyle(styles);
const childNodes = React.Children.map(children, (child: any) => {
if (child) {
const childProps = child.props || {};
if (child.type.displayName === 'DescriptionsItem') {
return React.cloneElement(child, {
...childProps,
colon: childProps.colon && childProps.colon !== ':' ? childProps.colon : colon,
labelWidth: childProps.labelWidth && childProps.labelWidth !== 'auto' ? childProps.labelWidth : labelWidth,
width: itemWidth,
});
}
}
return child;
});
return (
<View style={myStyle.descriptions}>
{!!title && (
<Text style={myStyle['descriptions-title']}>
{title}
</Text>
)}
<View style={[myStyle['descriptions-view'], customContentStyle]}>
{childNodes}
</View>
</View>
);
};
Descriptions.defaultProps = {
column: 2,
colon: ':',
title: '',
labelWidth: 'auto',
customContentStyle: undefined,
children: null,
};
Descriptions.Item = DescriptionsItem;
export default Descriptions;
/*
* @Author: XieZhiXiong
* @Date: 2020-12-23 17:36:27
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-08-31 18:04:12
* @Description:
*/
import { StyleSheet } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
descriptions: {
flexDirection: 'column',
},
'descriptions-title': {
marginBottom: themeLayout['margin-xs'],
fontSize: 14,
color: theme.colors.text,
},
'descriptions-view': {
flexDirection: 'row',
flexWrap: 'wrap',
},
'descriptions-item': {
flexDirection: 'row',
marginBottom: themeLayout['margin-xs'],
alignItems: 'flex-start',
},
'descriptions-item-left': {
flexDirection: 'row',
flexShrink: 0,
alignItems: 'center',
},
'descriptions-item-right': {
flex: 1,
},
'descriptions-item-label': {
color: theme.fonts.black2,
fontSize: 12,
fontWeight: '400',
},
'descriptions-item-colon': {
marginLeft: 2,
marginRight: themeLayout['margin-xxs'] + 2,
color: theme.fonts.black2,
fontSize: 10,
fontWeight: '400',
},
'descriptions-item-content': {
color: theme.fonts.black1,
fontSize: 12,
fontWeight: '400',
},
});
import React from 'react';
import { View, Text, ViewStyle, TouchableOpacity } from 'react-native';
import { Icons } from '@linkseeks/god-mobile';
import useAppStyle from '../../hooks/useAppStyle';
import styles from './styles';
interface GridItemProps {
/**
* 边框样式
*/
borderStyle?: 'solid' | 'dotted' | 'dashed',
/**
* Icon: https://oblador.github.io/react-native-vector-icons/
*/
icon?: string,
/**
* Icon 大小
*/
iconSize?: number,
/**
* 自定义渲染 Icon
*/
customRenderIcon?: React.ReactNode,
/**
* 标题
*/
title?: React.ReactNode,
/**
* Item 宽度
*/
width?: string | number,
/**
* 是否显示左边框
*/
borderLeft?: boolean,
/**
* 是否显示上边框
*/
borderTop?: boolean,
/**
* 外部容器自定义样式
*/
style?: ViewStyle,
/**
* 内部自定义样式
*/
contentStyle?: ViewStyle,
/**
* 点击事件
*/
onClick?: () => void | undefined,
children?: React.ReactNode,
}
const GridItem: React.FC<GridItemProps> = (props: GridItemProps) => {
const {
borderStyle,
icon,
iconSize,
customRenderIcon,
title,
width,
borderLeft,
borderTop,
style,
contentStyle,
onClick,
children,
} = props;
const myStyle = useAppStyle(styles);
// 这里包括一层,方便控制样式,如果传入的是非 string,则需要在外边自己编写样式
const contentNode = typeof children === 'string' ? (
<Text
style={myStyle['grid-item-title']}
>
{children}
</Text>
) : children;
const isHasChildren = !!children;
const handlePress = () => {
if (onClick) {
onClick();
}
};
return (
<View
style={{
...myStyle['grid-item'],
width,
...style,
}}
>
<TouchableOpacity
onPress={handlePress}
activeOpacity={0.8}
style={{ flex: 1 }}
>
<View
style={{
...myStyle['grid-item-wrap'],
borderStyle,
borderTopWidth: borderTop ? 1 : 0,
...contentStyle,
flex: 1,
}}
>
{!isHasChildren && (
<View style={myStyle['grid-item-icon']}>
{!customRenderIcon ? (
<Icons name={icon} size={iconSize} />
) : customRenderIcon}
</View>
)}
{!isHasChildren && (
<Text style={myStyle['grid-item-title']}>
{title}
</Text>
)}
{contentNode}
{borderLeft && (
<View style={myStyle['grid-item-leftBorder']} />
)}
</View>
</TouchableOpacity>
</View>
);
};
GridItem.defaultProps = {
borderStyle: 'solid',
icon: 'smileo',
iconSize: 28,
customRenderIcon: null,
title: '标题',
width: '100%',
borderLeft: true,
borderTop: false,
style: {},
contentStyle: {},
onClick: undefined,
children: null,
};
GridItem.displayName = 'GridItem';
export default GridItem;
/*
* @Author: XieZhiXiong
* @Date: 2020-12-28 20:18:24
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-09-03 16:01:06
* @Description: Grid 宫格组件
*/
import React from 'react';
import { View } from 'react-native';
import useAppStyle from '../../hooks/useAppStyle';
import GridItem from './Item';
import styles from './styles';
interface GridProps {
/**
* 列数
*/
column?: number;
/**
* 格子之间的间距
*/
gutter?: number;
/**
* 是否显示边框
*/
border?: boolean;
/**
* 边框样式
*/
borderStyle?: 'solid' | 'dotted' | 'dashed',
/**
* 格子内容排列的方向,可选值为 horizontal / vertical
*/
direction?: 'vertical' | 'horizontal';
children?: React.ReactNode,
}
const Grid = (props: GridProps) => {
const {
column,
gutter,
border,
borderStyle,
direction,
children,
} = props;
const childrenCount = React.Children.count(children);
const itemWidth = direction === 'vertical' ? `${(100 / (column as number)).toFixed(3)}%` : `${(100 / childrenCount).toFixed(3)}%`;
const isHasBorder = border && !gutter;
const gutterHalf = gutter ? Number.parseInt(`${(gutter as number) / 2}`, 10) : 0;
const myStyle = useAppStyle(styles);
const childNodes = React.Children.map(children, (child: any, index: number) => {
if (child) {
const childProps = child.props || {};
if (child.type.displayName === 'GridItem') {
return React.cloneElement(child, {
...childProps,
width: itemWidth,
// 当前索引取余不等于 1 或者 布局方向是 水平 方向,
// 且索引值 不等于 0 的时候设置 左边框
borderLeft: (isHasBorder && (index + 1) % (column as number) !== 1) || (isHasBorder && direction === 'horizontal' && index !== 0),
// 布局方向是 垂直 方向 且 索引值加 1 大于
// column列数 的时候设置 上边框
// borderTop: isHasBorder && direction === 'vertical' && (index + 1) > (column as number),
style: {
padding: gutter ? gutterHalf : 0,
...child.props.style,
},
borderStyle,
});
}
}
return child;
});
return (
<View
style={{
...myStyle.grid,
flexWrap: direction === 'vertical' ? 'wrap' : 'nowrap',
margin: gutter ? -gutterHalf : 0,
}}
>
{childNodes}
</View>
);
};
Grid.defaultProps = {
column: 3,
gutter: 0,
border: true,
borderStyle: 'solid',
direction: 'vertical',
children: null,
};
Grid.Item = GridItem;
export default Grid;
/*
* @Author: XieZhiXiong
* @Date: 2020-12-29 17:15:14
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-09-03 16:03:39
* @Description:
*/
import { StyleSheet } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
grid: {
flexDirection: 'row',
},
'grid-item': {
},
'grid-item-wrap': {
paddingVertical: themeLayout['padding-s'],
paddingHorizontal: themeLayout['padding-s'],
borderTopWidth: 1,
borderColor: theme.colors.border,
backgroundColor: '#FFFFFF',
position: 'relative',
},
'grid-item-leftBorder': {
position: 'absolute',
width: 1,
top: themeLayout['padding-s'],
bottom: themeLayout['padding-s'],
left: 0,
backgroundColor: '#F4F5F7',
transform: [{
scaleX: 0.5,
}],
},
'grid-item-icon': {
alignItems: 'center',
justifyContent: 'center',
marginBottom: themeLayout['padding-xs'],
},
'grid-item-title': {
width: '100%',
fontSize: 12,
color: theme.colors.text,
textAlign: 'center',
},
});
/*
* @Author: XieZhiXiong
* @Date: 2021-04-25 14:34:23
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-04-25 14:34:25
* @Description: 菊花
*/
import React, { useEffect, useRef } from 'react';
import { Animated, Easing, ViewStyle } from 'react-native';
import { Icons } from '@linkseeks/god-mobile';
import useAppStyle from '../../hooks/useAppStyle';
import styles from './styles';
interface IProps {
/**
* 颜色,默认 #C0C4CC
*/
color?: string,
/**
* 加载图标大小,默认 24
*/
size?: number,
/**
* 自定义样式
*/
customStyle?: ViewStyle,
}
const Spin: React.FC<IProps> = (props: IProps) => {
const {
color,
size = 14,
customStyle,
} = props;
const myStyle = useAppStyle(styles);
const rotate = useRef(new Animated.Value(0)).current;
const spin = () => {
Animated.loop(
Animated.timing(rotate, {
toValue: 1,
duration: 800,
useNativeDriver: false,
easing: Easing.ease,
}),
).start();
};
useEffect(() => {
spin();
return () => {
rotate.stopAnimation();
}
}, []);
const spinValue = rotate.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
return (
<Animated.View
style={[
myStyle['loading-icon'],
customStyle,
{
transform: [
{
rotate: spinValue,
},
],
},
]}
>
<Icons name="loading2" size={size} color={color} />
</Animated.View>
);
};
Spin.defaultProps = {
color: '#C0C4CC',
size: 14,
customStyle: {},
};
export default Spin;
import React, { PureComponent } from 'react';
import { Text, View, ActivityIndicator, Modal } from 'react-native';
// import { t } from 'react-i18next'
import Overlay from '../Overlay'
// import useLocale from '../../hooks/useLocale'
interface Props {
}
interface State {
show: boolean
}
let _this: any = null
class FullScreenLoading extends PureComponent<Props, State> {
// eslint-disable-next-line react/sort-comp
constructor(props: Props) {
super(props);
_this = this
this.state = {
show: false,
};
}
static show = () => {
if (_this) {
console.log(_this, "_this")
_this.setState({ show: true })
}
};
static hide = () => {
if (_this) {
_this.setState({ show: false })
}
};
render() {
const { show } = this.state
// const { t } = useLocale('components')
console.log(show, "show")
if (show) {
return (
<Overlay
visible
position="center"
// style={{
// flex: 1,
// }}
>
<View style={{
width: 100,
height: 100,
backgroundColor: "rgba(0,0,0,0.6)",
opacity: 1,
justifyContent: "center",
alignItems: "center",
borderRadius: 7,
}}
>
<ActivityIndicator size="large" color="#FFF" />
<Text style={{ marginLeft: 10, color: "#FFF" }} />
</View>
</Overlay>
);
}
return <View />
}
}
export default FullScreenLoading;
/*
* @Author: XieZhiXiong
* @Date: 2021-01-21 18:53:56
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-04-25 14:42:20
* @Description: 加载组件
*/
import React from 'react';
import {
StyleSheet,
ViewStyle,
} from 'react-native';
import {
View,
Text,
} from '@linkseeks/god-mobile';
import useAppStyle from '../../hooks/useAppStyle';
import Spin from './Spin';
import styles from './styles';
interface LoadingProps {
/**
* 是否加载中
*/
loading: boolean,
/**
* 颜色,默认 #C0C4CC
*/
color?: string,
/**
* 加载图标大小,默认 24
*/
size?: number,
/**
* 加载文本,默认 加载中...
*/
text?: string,
/**
* 是否没有更多
*/
noMore?: boolean,
/**
* 没有更多文本,默认 已经到底啦~
*/
noMoreText?: string,
/**
* 文字大小,默认 12
*/
textSize?: number,
/**
* 是否垂直排列图标和文字内容
*/
vertical?: boolean,
/**
* 自定义外部样式
*/
customStyle?: ViewStyle,
}
const Loading: React.FC<LoadingProps> = (props: LoadingProps) => {
const {
loading,
color = '#C0C4CC',
size = 14,
text,
noMore,
noMoreText,
textSize = 12,
vertical,
customStyle,
} = props;
const myStyle = useAppStyle(styles);
if (loading && text) {
return (
<View
style={StyleSheet.flatten([
myStyle.loading,
vertical && myStyle.loading__vertical,
customStyle,
])}
>
<Spin size={size} color={color} />
<Text
style={StyleSheet.flatten([
myStyle['loading-text'],
vertical && myStyle['loading-text__vertical'],
{
fontSize: textSize,
color,
},
])}
>
{text}
</Text>
</View>
);
}
if (!loading && noMore && noMoreText) {
return (
<View
style={StyleSheet.flatten([
myStyle.loading,
vertical && myStyle.loading__vertical,
customStyle,
])}
>
<Text
style={StyleSheet.flatten([
myStyle['loading-text'],
vertical && myStyle['loading-text__vertical'],
{
fontSize: textSize,
color,
margin: 0,
},
])}
>
{noMoreText}
</Text>
</View>
);
}
return null;
};
Loading.defaultProps = {
color: '#C0C4CC',
size: 14,
text: '正在加载...',
noMore: false,
noMoreText: '没有更多啦~',
textSize: 12,
vertical: false,
customStyle: {},
};
export default Loading;
/*
* @Author: XieZhiXiong
* @Date: 2021-01-21 18:54:00
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-01-21 18:54:01
* @Description:
*/
import { StyleSheet } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
loading: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: themeLayout['padding-xs'],
},
loading__vertical: {
flexDirection: 'column',
},
'loading-icon': {},
'loading-text': {
color: theme.fonts.black1,
marginLeft: themeLayout['margin-xs'],
},
'loading-text__vertical': {
marginLeft: 0,
marginTop: themeLayout['margin-xs'],
},
});
/*
* @Author: XieZhiXiong
* @Date: 2020-12-30 15:35:14
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-03-04 15:47:16
* @Description: 圆润的卡片
*/
import React from 'react';
import { View, Text, ViewStyle, TextStyle, TouchableOpacity } from 'react-native';
import useAppStyle from '../../hooks/useAppStyle';
import styles from './styles';
interface MellowCardProps {
/**
* 外部标题
*/
outerTitle?: React.ReactNode,
/**
* 标题
*/
title?: React.ReactNode,
/**
* 标题右侧部分
*/
extra?: React.ReactNode,
/**
* 自定义外部标题样式
*/
outerTitleStyle?: TextStyle,
/**
* 自定义样式
*/
style?: ViewStyle,
/**
* head 自定义样式
*/
headStyle?: ViewStyle,
/**
* body 自定义样式
*/
bodyStyle?: ViewStyle,
/**
* 标题左侧边框
*/
ribbon?: boolean;
children?: React.ReactNode,
/** onHeaderClick */
onHeaderClick?: () => void
}
const MellowCard: React.FC<MellowCardProps> = (props: MellowCardProps) => {
const {
outerTitle,
title,
extra,
outerTitleStyle,
style,
headStyle,
bodyStyle,
ribbon,
children,
onHeaderClick
} = props;
const myStyle = useAppStyle(styles);
// 这里包括一层,方便控制样式,如果传入的是非 string,则需要在外边自己编写样式
const contentNode = typeof children !== 'object' ? (
<Text>
{children}
</Text>
) : children;
const titleNode = typeof title !== 'object' ? (
<Text style={myStyle['card-head-title']}>
{title}
</Text>
) : title;
const outerTitleNode = typeof outerTitle !== 'object' ? (
<Text
style={[
myStyle.outerTitle,
outerTitleStyle,
]}
>
{outerTitle}
</Text>
) : outerTitle;
const handleClickHeader = () => {
onHeaderClick?.()
}
return (
<>
{outerTitle ? (
<View style={myStyle.outerTitleWrap}>
{outerTitleNode}
</View>
) : null}
<View
style={{
...myStyle.card,
...style,
}}
>
{(title || extra) && (
<TouchableOpacity
onPress={handleClickHeader}
style={{
...myStyle['card-head'],
...headStyle,
}}
activeOpacity={0.9}
>
<View style={myStyle['card-head-titleWrap']}>
{ribbon && (
<View style={myStyle['card-head-ribbon']} />
)}
{titleNode}
</View>
<View style={myStyle['card-head-extra']}>
{extra}
</View>
</TouchableOpacity>
)}
<View
style={{
...myStyle['card-body'],
...bodyStyle,
}}
>
{contentNode}
</View>
</View>
</>
);
};
MellowCard.defaultProps = {
outerTitle: null,
title: null,
extra: null,
outerTitleStyle: {},
style: {},
headStyle: {},
bodyStyle: {},
ribbon: false,
children: null,
};
export default MellowCard;
/*
* @Author: XieZhiXiong
* @Date: 2020-12-30 15:35:25
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-10-15 14:53:41
* @Description:
*/
import { StyleSheet } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
card: {
borderRadius: 8,
overflow: 'hidden',
backgroundColor: '#FFFFFF',
width: '100%',
},
outerTitleWrap: {
padding: themeLayout['padding-s'],
},
outerTitle: {
fontSize: 12,
fontWeight: '400',
color: theme.fonts.black2,
},
'card-head': {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: themeLayout['padding-s'],
borderBottomWidth: 0.5,
borderBottomColor: theme.colors.border,
},
'card-head-titleWrap': {
flex: 1,
position: 'relative',
},
'card-head-title': {
fontSize: 14,
fontWeight: '600',
color: theme.colors.text,
lineHeight: 14 * 1.1,
},
'card-head-extra': {
},
'card-head-ribbon': {
position: 'absolute',
top: 2,
left: -12,
width: 4,
height: 14,
borderTopRightRadius: 100,
borderBottomRightRadius: 100,
backgroundColor: theme.colors.primary,
},
'card-body': {
padding: themeLayout['padding-s'],
},
});
/* eslint-disable no-undef */
/*
* @Author: XieZhiXiong
* @Date: 2021-03-02 17:21:04
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-09-03 10:17:10
* @Description: 通知栏组件
*/
import React, { useEffect, useState, useRef } from 'react';
import {
View,
Text,
TouchableOpacity,
ScrollView,
LayoutChangeEvent,
} from 'react-native';
import { Icons } from '@linkseeks/god-mobile';
import themeColors from '../../constants/theme/colors';
import useAppStyle from '../../hooks/useAppStyle';
import styles from './styles';
interface NoticeBarProps {
/**
* 主题,可选值 default、danger,默认 default
*/
theme?: 'default' | 'danger',
/**
* 自定义 icon
*/
icon?: string,
/**
* 是否显示关闭按钮
*/
closable?: boolean,
/**
* 是否显示箭头
*/
hasArrow?: boolean,
/**
* 是否显示左侧图标
*/
hasIcon?: boolean,
/**
* 开启滚动功能时前后停留的时间(单位:ms),默认 2000
*/
delay?: number,
/**
* 点击触发事件
*/
onClick?: () => void,
/**
* 是否开启滚动播放,内容长度溢出时默认开启
*/
scrollable?: boolean,
/**
* 是否开启文本换行,只在禁用滚动时生效
*/
wrapable?: boolean,
children?: React.ReactNode,
}
// 第一个颜色是填充色,第二个颜色是边框颜色
const PRESETS_COLOR_MAP: {[key: string]: any} = {
default: [themeColors.orange[5], themeColors.orange[1]],
danger: ['red', themeColors.red[1]],
};
// 占位宽度
const GAP_WIDTH = 40;
const NoticeBar: React.FC<NoticeBarProps> = (props: NoticeBarProps) => {
const {
theme = 'default',
icon = 'sound',
closable = 14,
hasArrow,
hasIcon = true,
delay = 2000,
onClick,
scrollable,
wrapable,
children,
} = props;
const [closed, setClosed] = useState(false);
const [scrollViewWidth, setScrollViewWidth] = useState(0);
const [shipWidth, setShipWidth] = useState(0);
const myStyle = useAppStyle(styles);
const timer = useRef<NodeJS.Timeout | null>(null);
const delayer = useRef<NodeJS.Timeout | null>(null);
const scrollRef = useRef<any>();
const handleClick = () => {
if (onClick) {
onClick();
}
};
const handleClose = () => {
if (timer.current) {
clearInterval(timer.current);
}
if (delayer.current) {
clearTimeout(delayer.current);
}
setClosed(true);
};
const move = () => {
if (!scrollable) {
return;
}
let left = 0;
const del = shipWidth - scrollViewWidth;
delayer.current = setTimeout(() => {
timer.current = setInterval(() => {
left += 1;
// 大于可滚动的距离就复位0
if (left >= del) {
clearInterval(timer.current as NodeJS.Timeout);
move();
}
const offset = Math.min(del, Math.max(0, left));
if (scrollRef.current) {
scrollRef.current.scrollTo({ x: offset, y: 0, duration: 0, animated: false })
}
}, 21.932);
}, delay);
};
useEffect(() => {
if (shipWidth - GAP_WIDTH > scrollViewWidth) {
move();
}
return () => {
if (timer.current) {
clearInterval(timer.current);
}
if (delayer.current) {
clearTimeout(delayer.current);
}
};
}, [scrollViewWidth, shipWidth]);
const getScrollViewWidth = (e: LayoutChangeEvent) => {
const { width } = e.nativeEvent.layout;
if (width) {
setScrollViewWidth(width);
}
};
const getShipWidth = (e: LayoutChangeEvent) => {
const { width } = e.nativeEvent.layout;
if (width) {
setShipWidth(width);
}
};
// 这里包括一层,方便控制样式,如果传入的是非 string,则需要在外边自己编写样式
const contentNode = typeof children === 'string' || typeof children === 'number' ? (
<Text
style={[
myStyle['boticeBar-text'],
{
color: PRESETS_COLOR_MAP[theme][0],
},
]}
numberOfLines={!scrollable && !wrapable ? 1 : undefined}
>
{children}
</Text>
) : children;
const renderContent = () => {
if (scrollable) {
return (
<ScrollView
style={[
myStyle['boticeBar-scroll'],
]}
ref={scrollRef}
// scrollEnabled={false}
showsHorizontalScrollIndicator={false}
onLayout={getScrollViewWidth}
horizontal
>
<View
style={[
myStyle['boticeBar-ship'],
]}
onLayout={getShipWidth}
>
{contentNode}
{/* 多一点点爱 */}
<View style={myStyle['boticeBar-gap']} />
</View>
</ScrollView>
);
}
return (
<View
style={[
myStyle['boticeBar-ship'],
]}
>
{contentNode}
</View>
);
};
if (!closed) {
return (
<TouchableOpacity
style={[
myStyle.boticeBar,
{
backgroundColor: PRESETS_COLOR_MAP[theme][1],
},
]}
activeOpacity={1}
onPress={handleClick}
>
<View
style={[
myStyle.boticeBar,
hasIcon && myStyle.boticeBar__hasIcon,
]}
>
{hasIcon ? (
<View style={myStyle['boticeBar-icon']}>
<Icons
size={16}
name={icon || 'sound'}
color={PRESETS_COLOR_MAP[theme][0]}
/>
</View>
) : null}
</View>
<View
style={[
myStyle['boticeBar-content'],
]}
>
{renderContent()}
</View>
{hasArrow ? (
<TouchableOpacity
style={[
myStyle['boticeBar-action'],
]}
>
<Icons
size={16}
// eslint-disable-next-line no-nested-ternary
name="right"
color={PRESETS_COLOR_MAP[theme][0]}
/>
</TouchableOpacity>
) : null}
{closable ? (
<TouchableOpacity
style={[
myStyle['boticeBar-action'],
]}
activeOpacity={0.8}
onPress={handleClose}
>
<Icons
size={16}
// eslint-disable-next-line no-nested-ternary
name="close"
color={PRESETS_COLOR_MAP[theme][0]}
/>
</TouchableOpacity>
) : null}
</TouchableOpacity>
);
}
return null;
};
NoticeBar.defaultProps = {
theme: 'default',
icon: 'sound',
closable: false,
hasArrow: false,
hasIcon: true,
onClick: undefined,
delay: 2000,
scrollable: true,
wrapable: false,
children: null,
};
export default NoticeBar;
/*
* @Author: XieZhiXiong
* @Date: 2021-03-02 17:21:09
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-09-08 16:00:07
* @Description:
*/
import { StyleSheet } from 'react-native';
import themeLayout from '../../constants/theme/layout';
export default () => StyleSheet.create({
boticeBar: {
flexDirection: 'row',
paddingVertical: themeLayout['padding-xxs'],
paddingHorizontal: themeLayout['padding-s'] / 2,
minHeight: 16,
},
boticeBar__hasIcon: {
paddingHorizontal: themeLayout['padding-s'],
},
'boticeBar-icon': {
position: 'relative',
top: -1,
},
'boticeBar-content': {
flex: 1,
position: 'relative',
flexDirection: 'row',
alignItems: 'center',
},
'boticeBar-scroll': {
position: 'absolute',
width: '100%',
height: '100%',
},
'boticeBar-ship': {
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'center',
},
'boticeBar-text': {
lineHeight: 16,
fontSize: 12,
fontWeight: '400',
},
'boticeBar-action': {
marginLeft: themeLayout['padding-xs'],
padding: 2,
},
'boticeBar-gap': {
width: 40,
height: 1,
},
});
/*
* @Author: XieZhiXiong
* @Date: 2021-01-04 14:47:43
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-08-27 11:23:03
* @Description: 遮罩层
*/
import { BlurView } from 'expo-blur';
import React, { useRef, useEffect, useState } from 'react';
import {
View,
ViewStyle,
TouchableOpacity,
Animated,
StyleSheet,
Modal,
LayoutChangeEvent,
} from 'react-native';
import styles from './styles';
interface WrapperProps {
/**
* 是否可见
*/
visible: boolean,
/**
* zIndex,默认 100,比 GlobalHeader 高一点点
*/
zIndex: number,
/**
* 是否使用 modal
*/
useModal: boolean,
/**
* 安卓关闭 Modal 事件
*/
onRequestClose: () => void,
/**
* 自定义 style
*/
style: ViewStyle,
children: React.ReactNode,
}
interface OverlayProps {
/**
* 是否可见
*/
visible: boolean,
/**
* 内容定位,可选值为 top center bottom,如果设置为 center 是水平跟垂直都是居中的
*/
position?: 'top' | 'center' | 'bottom',
/**
* 点击事件
*/
onClick?: () => void,
/**
* 自定义 style
*/
style?: ViewStyle,
/**
* 遮罩层透明度 默认0.7 范围0 - 1
*/
opacity?: number;
/**
* zIndex,默认 100,比 GlobalHeader 高一点点
*/
zIndex?: number,
/**
* 是否使用 modal
*/
useModal?: boolean,
/**
* 动画时长, 默认 300ms
*/
duration?: number,
children?: React.ReactNode,
/**
* 背景模糊-模糊半径
*/
blurAmount?: number,
/**
* 自定义bottomStyle
*/
bottomStyle?: ViewStyle
}
const POSITION_MAP = {
top: {
top: 0,
right: 0,
left: 0,
},
center: {
top: '50%',
left: '50%',
},
bottom: {
bottom: 0,
right: 0,
left: 0,
},
};
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 1000 / 30));
const Wrapper: React.FC<WrapperProps> = (props: WrapperProps) => {
const {
visible,
zIndex,
useModal,
onRequestClose,
style,
children,
} = props;
const handleRequestClose = () => {
if (onRequestClose) {
onRequestClose();
}
};
if (useModal) {
return (
<Modal
visible={visible}
onRequestClose={handleRequestClose}
transparent
>
{children}
</Modal>
)
}
return (
<View
pointerEvents="box-none"
style={[
{
...styles.overlay,
height: visible ? '100%' : 0,
zIndex,
...style,
},
]}
>
{children}
</View>
)
}
const Overlay: React.FC<OverlayProps> = (props: OverlayProps) => {
const {
visible,
position,
onClick,
style = {},
opacity,
zIndex = 100,
children,
useModal = false,
duration,
blurAmount = 0,
bottomStyle = 0
} = props;
const [display, setDisplay] = useState(false);
const [contentHeight, setContentHeight] = useState(0);
const [contentWidth, setContentWidth] = useState(0);
const fadeAnim = useRef(new Animated.Value(0)).current;
const onTransitionEnd = () => {
if (!visible && display) {
setDisplay(false);
}
};
const enter = () => {
Promise.resolve()
.then(nextTick)
.then(() => {
setDisplay(true);
})
.then(nextTick)
.then(() => {
Animated.timing(fadeAnim, {
toValue: 1,
duration,
useNativeDriver: false,
}).start();
})
.catch(() => {});
};
const leave = () => {
Promise.resolve()
.then(nextTick)
.then(() => {
Animated.timing(fadeAnim, {
toValue: 0,
duration,
useNativeDriver: false,
}).start();
setTimeout(() => onTransitionEnd(), duration);
})
.catch(() => {});
};
useEffect(() => {
visible ? enter() : leave();
}, [visible]);
const hanldePress = () => {
if (onClick) {
onClick();
}
}
// 这里给个空标签
const childNode = children || <View />;
const positionStyle = position ? POSITION_MAP[position] : {};
const getContentHeight = (e: LayoutChangeEvent) => {
const { height, width } = e.nativeEvent.layout;
setContentHeight(height);
setContentWidth(width);
};
return (
<Wrapper
visible={display}
zIndex={zIndex}
useModal={useModal}
onRequestClose={hanldePress}
style={style}
>
{!!blurAmount && (
<BlurView
style={[styles['overlay-blur']]}
blurType="dark"
blurAmount={blurAmount}
/>
)}
<Animated.View
style={StyleSheet.flatten([
styles['overlay-mask'],
{
backgroundColor: `rgba(0, 0, 0, ${opacity})`,
opacity: fadeAnim,
},
])}
>
<TouchableOpacity
style={{
flex: 1,
}}
activeOpacity={1}
onPress={hanldePress}
/>
</Animated.View>
<View
style={StyleSheet.flatten([
styles['overlay-ship'],
positionStyle,
bottomStyle,
position === 'center' && {
transform: [
{
translateX: -Math.ceil(contentWidth / 2),
},
{
translateY: -Math.ceil(contentHeight / 2),
},
],
},
])}
onLayout={getContentHeight}
>
<View
style={styles['overlay-content']}
>
{childNode}
</View>
</View>
</Wrapper>
)
};
Overlay.defaultProps = {
onClick: undefined,
position: 'bottom',
style: {},
opacity: 0.7,
zIndex: 100,
children: null,
useModal: false,
duration: 300,
blurAmount: 0,
};
export default Overlay;
/*
* @Author: XieZhiXiong
* @Date: 2021-01-04 14:47:49
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-04-29 11:22:57
* @Description:
*/
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
overlay: {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
overflow: 'hidden',
zIndex: 9
},
'overlay-mask': {
flex: 1,
},
'overlay-ship': {
position: 'absolute',
},
'overlay-content': {
position: 'relative',
},
'overlay-blur': {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
},
});
/*
* @Author: XieZhiXiong
* @Date: 2021-01-14 17:26:29
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-09-16 14:24:15
* @Description: 弹出层
*/
import React, { useRef, useEffect, useState } from 'react';
import {
View,
Text,
TouchableOpacity,
Animated,
ViewStyle,
LayoutChangeEvent,
StyleSheet,
TextStyle,
} from 'react-native';
import { Icons } from '@linkseeks/god-mobile';
import Overlay from '../Overlay';
import useAppStyle from '../../hooks/useAppStyle';
import styles from './styles';
interface PopupProps {
/**
* 是否可见
*/
visible: boolean;
/**
* 弹出位置,可选值为 top bottom right left
* 目前只支持 bottom
*/
// position?: 'top' | 'bottom' | 'right' | 'left';
/**
* 是否显示圆角
*/
round?: boolean,
/**
* 点击遮罩是否关闭菜单
*/
closeOnClickOverlay?: boolean,
/**
* 内容区域自定义样式
*/
contentStyle?: ViewStyle,
/**
* 是否显示关闭图标
*/
closeable?: boolean,
/**
* 关闭图标名称或图片链接
* Icon: https://oblador.github.io/react-native-vector-icons/
*/
closeIcon?: string,
/**
* 关闭图标定位位置,可选值为 'top-left' | 'top-right'
*/
closeIconPosition?: string,
/**
* 关闭事件
*/
onClose?: () => void;
/**
* 点击遮罩层时触发
*/
onClickOverlay?: () => void;
/**
* zIndex,默认 100,比 GlobalHeader 高一点点
*/
zIndex?: number,
/**
* 是否使用Modal
*/
useModal?: boolean,
/**
* 标题
*/
title?: string,
/**
* 自定义标题样式
*/
customTitleStyle?: TextStyle,
/**
* 关闭之后触发事件
*/
onAfterClose?: () => void,
children?: React.ReactNode,
/**
* 自定义bottomStyle
*/
bottomStyle?: ViewStyle
}
const CLOSE_ICON_POSITION_STYLE_MAP = {
'top-left': {
top: 0,
left: 0,
},
'top-right': {
top: 0,
right: 0,
},
};
const Popup: React.FC<PopupProps> = (props: PopupProps) => {
const {
visible,
// position,
round,
closeOnClickOverlay,
contentStyle,
closeable,
closeIcon,
closeIconPosition,
onClose,
onClickOverlay,
zIndex,
useModal,
title,
customTitleStyle,
onAfterClose,
children,
bottomStyle,
} = props;
const [contentHeight, setContentHeight] = useState(0);
const myStyle = useAppStyle(styles);
const slideToTop = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (visible) {
Animated.timing(slideToTop, {
toValue: 1,
duration: 300,
useNativeDriver: false,
}).start();
} else {
Animated.timing(slideToTop, {
toValue: 0,
duration: 300,
useNativeDriver: false,
}).start(({ finished }) => {
if (finished) {
onAfterClose?.();
}
});
}
}, [visible]);
const getContentHeight = (e: LayoutChangeEvent) => {
const { height } = e.nativeEvent.layout;
setContentHeight(height);
};
const handleOverlayClick = () => {
if (closeOnClickOverlay && onClose) {
onClose();
}
if (onClickOverlay) {
onClickOverlay();
}
};
const handleCancel = () => {
if (onClose) {
onClose();
}
};
const slideValue = slideToTop.interpolate({
inputRange: [0, 1],
outputRange: [contentHeight, 0],
});
return (
<Overlay
visible={visible}
onClick={handleOverlayClick}
position="bottom"
zIndex={zIndex}
useModal={useModal}
bottomStyle={bottomStyle}
>
<Animated.View
style={[
{
...myStyle['popup-ship'],
borderTopRightRadius: round ? 16 : 0,
borderTopLeftRadius: round ? 16 : 0,
},
{
transform: [
{
translateY: slideValue,
},
],
},
]}
>
{!!closeable && (
<TouchableOpacity
style={StyleSheet.flatten([
myStyle['popup-ship-close'],
CLOSE_ICON_POSITION_STYLE_MAP[(closeIconPosition as ('top-left' | 'top-right'))],
{
zIndex,
},
])}
activeOpacity={0.8}
onPress={handleCancel}
>
{closeIcon === 'close' ? (
<View style={myStyle['popup-ship-close-icon']}>
<Icons name={closeIcon} size={14} color="rgb(227, 228, 229)" />
</View>
) : (
<Icons name={closeIcon} size={24} color="rgb(227, 228, 229)" />
)}
</TouchableOpacity>
)}
{title ? (
<Text style={[myStyle['popup-title'], customTitleStyle]}>
{title}
</Text>
) : null}
{children && (
<View
style={{
...myStyle['popup-ship-content'],
...contentStyle,
}}
onLayout={getContentHeight}
>
{children}
</View>
)}
</Animated.View>
</Overlay>
);
};
Popup.defaultProps = {
// position: 'bottom',
round: true,
closeOnClickOverlay: true,
contentStyle: {},
onClose: undefined,
onClickOverlay: undefined,
closeable: true,
closeIcon: 'closecircle',
closeIconPosition: 'top-right',
zIndex: 100,
useModal: false,
title: undefined,
customTitleStyle: {},
children: null,
};
export default Popup;
/*
* @Author: XieZhiXiong
* @Date: 2021-01-14 17:26:35
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-04-28 19:59:34
* @Description:
*/
import { StyleSheet } from 'react-native';
import themeLayout from '../../constants/theme/layout';
import { ThemeStyle } from '../../constants/theme';
export default (theme: ThemeStyle) => StyleSheet.create({
popup: {
position: 'relative',
},
'popup-ship': {
width: '100%',
backgroundColor: '#FFFFFF',
overflow: 'hidden',
position: 'relative',
},
'popup-ship-close': {
position: 'absolute',
top: 0,
right: 0,
padding: themeLayout['padding-s'],
zIndex: 2,
},
'popup-ship-close-icon': {
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 24,
backgroundColor: '#F4F5F7',
},
'popup-ship-content': {
minHeight: 48,
backgroundColor: '#FFFFFF',
},
'popup-title': {
paddingVertical: themeLayout['padding-m'],
paddingHorizontal: themeLayout['padding-m'] * 2,
fontSize: 16,
fontWeight: '500',
textAlign: 'center',
borderBottomColor: theme.colors.border,
borderBottomWidth: 1,
},
});
/*
* @Author: XieZhiXiong
* @Date: 2021-03-10 17:28:06
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-03-19 10:23:34
* @Description: 按钮单选框
*/
import React, { useContext, useEffect } from 'react';
import { Text, TouchableOpacity } from 'react-native';
import useAppStyle from '../../hooks/useAppStyle';
import { RadioContext } from './Group';
import styles from './styles';
interface RadioProps {
/**
* 根据 value 进行比较,判断是否选中
*/
value: any,
/**
* 大小,默认 middle
*/
size?: 'middle' | 'small',
/**
* 是否禁用
*/
disabled?: boolean,
/**
* 主题,可选值 'dark' 'light',默认 'dark',只对 只对按钮样式生效
*/
theme?: 'dark' | 'light',
children?: React.ReactNode,
}
const RadioButton = (props: RadioProps) => {
const {
value,
size = 'middle',
disabled,
theme = 'dark',
children,
} = props;
const myStyle: { [key: string]: any } = useAppStyle(styles);
const radioContext = useContext(RadioContext);
const finalDisabled = radioContext.disabled || disabled;
const finalSize = radioContext.buttonSize || size;
const finalTheme = radioContext.theme || theme;
const handleClick = () => {
if (finalDisabled) {
return;
}
if (radioContext.toggleChange) {
radioContext.toggleChange(value);
}
};
const check = value === radioContext.value;
const contentNode = typeof children !== 'object' ? (
<Text
style={[
myStyle[`radio-button-label__${finalTheme}`],
check && myStyle[`radio-button-label__${finalTheme}&&active`],
finalDisabled && myStyle['radio-label__disabled'],
]}
>
{children}
</Text>
) : children;
return (
<TouchableOpacity
activeOpacity={1}
onPress={handleClick}
style={[
myStyle[`radio-button`],
myStyle[`radio-button__${finalTheme}`],
myStyle[`radio-button__${finalSize}`],
check && myStyle[`radio-button__${finalTheme}&&active`],
finalDisabled && myStyle['radio-button__disabled'],
check && finalDisabled && myStyle['radio-button__check&&disabled'],
]}
>
{!!children && (
contentNode
)}
</TouchableOpacity>
);
};
RadioButton.defaultProps = {
size: 'middle',
disabled: false,
theme: 'dark',
children: null,
};
RadioButton.displayName = 'Radio.Button';
export default RadioButton;
/*
* @Author: XieZhiXiong
* @Date: 2021-03-03 13:51:08
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-09-15 16:04:37
* @Description: 单选框组
*/
import React from 'react';
import { View, ViewStyle, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
'radio-button-group': {
flexDirection: 'row',
},
'radio-button-group__dark': {
padding: 2,
backgroundColor: '#F7F8FA',
borderRadius: 8,
overflow: 'hidden',
},
'radio-button-group__light': {
borderRadius: 8,
overflow: 'hidden',
},
});
interface RadioData {
/**
* 当前选中的值
*/
value: '',
/**
* 选中改变触发事件
*/
toggleChange: ((value: any) => void) | undefined,
/**
* 是否禁用
*/
disabled: boolean,
/**
* checkbox 大小,默认 22
*/
size: number,
/**
* 'middle' | 'small' | (string & {}),
*/
buttonSize: 'middle' | 'small' | (string & {}),
/**
* 主题,可选值 'dark' 'light',默认 'dark',只对 只对按钮样式生效
*/
theme?: 'dark' | 'light',
}
export const RadioContext = React.createContext<RadioData>({
value: '',
toggleChange: undefined,
disabled: false,
size: 22,
buttonSize: 'middle',
theme: 'dark',
});
interface RadioGroupProps {
/**
* 当前选中值
*/
value?: any;
/**
* 默认选中值当前选中值
*/
defaultValue?: any;
/**
* 选项变化时的回调函数
*/
onChange?: (value: any) => void;
/**
* 禁选所有子单选器
*/
disabled?: boolean,
/**
* 单选框大小,默认22,只对 单选样式生效
*/
size?: number,
/**
* 按钮大小,可选值 'middle' 'small',只对 只对按钮样式生效
*/
buttonSize?: 'middle' | 'small' | (string & {}),
/**
* 主题,可选值 'dark' 'light',默认 'dark',只对 只对按钮样式生效
*/
theme?: 'dark' | 'light',
/**
* 自定义外部样式
*/
customStyle?: ViewStyle,
/**
* 子元素类型,默认 radio
*/
type?: 'radio' | 'radio.button',
children?: React.ReactNode,
}
interface RadioGroupState {
value: any,
toggleChange: (value: any) => void,
}
class RadioGroup extends React.Component<RadioGroupProps, RadioGroupState> {
static getDerivedStateFromProps(nextProps: RadioGroupProps) {
const { value } = nextProps;
if ('value' in nextProps) {
return {
value,
};
}
return null;
}
constructor(props: RadioGroupProps) {
super(props);
this.state = {
value: props.defaultValue,
toggleChange: this.toggleChange,
};
}
toggleChange = (next: any) => {
const { value } = this.state;
const { onChange } = this.props;
if (next === value) {
return;
}
if (!('value' in this.props)) {
this.setState({
value: next,
});
}
if (onChange) {
onChange(next);
}
};
render() {
const {
disabled,
size,
buttonSize,
customStyle,
theme = 'dark',
type,
children,
} = this.props;
return (
<View
style={[
styles['radio-button-group'],
type === 'radio.button' && [
styles[`radio-button-group__${theme}`],
],
customStyle,
]}
>
<RadioContext.Provider
value={{
...this.state,
disabled: !!disabled,
size: size as number,
buttonSize: buttonSize as string,
theme,
}}
>
{children}
</RadioContext.Provider>
</View>
);
}
}
export default RadioGroup;
/*
* @Author: XieZhiXiong
* @Date: 2021-03-03 13:46:38
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-04-09 10:31:00
* @Description: 单选框
*/
import React, { useContext } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Icons } from '@linkseeks/god-mobile';
import { useTheme } from '@react-navigation/native';
import { ThemeStyle } from '../../constants/theme';
import useAppStyle from '../../hooks/useAppStyle';
import RadioGroup, { RadioContext } from './Group';
import RadioButton from './Button';
import styles from './styles';
interface RadioProps {
/**
* 根据 value 进行比较,判断是否选中
*/
value: any,
/**
* 选中颜色,默认主题色
*/
// eslint-disable-next-line react/require-default-props
color?: string
/**
* 大小,默认22
*/
size?: number,
/**
* 是否禁用
*/
disabled?: boolean,
/**
* 是否选中的,一般用于单独使用时,默认 false
*/
checked?: boolean,
children?: React.ReactNode,
}
const Radio = (props: RadioProps) => {
const {
value,
color,
size,
disabled,
checked,
children,
} = props;
const myStyle = useAppStyle(styles);
const appTheme = useTheme() as ThemeStyle;
const radioContext = useContext(RadioContext);
const finalColor = color || appTheme.colors.primary;
const finalDisabled = radioContext.disabled || disabled;
const finalSize = radioContext.size || size;
const handleClick = () => {
if (finalDisabled) {
return;
}
if (radioContext.toggleChange) {
radioContext.toggleChange(value);
}
};
const contentNode = typeof children !== 'object' ? (
<Text
style={[
myStyle['radio-label'],
finalDisabled && myStyle['radio-label__disabled'],
]}
>
{children}
</Text>
) : children;
const isCheck = checked || value === radioContext.value;
return (
<TouchableOpacity
style={myStyle.radio}
activeOpacity={1}
onPress={handleClick}
>
<View
style={StyleSheet.flatten([
myStyle['radio-icon'],
{
width: finalSize,
height: finalSize,
},
isCheck ? {
...myStyle['radio-icon__check'],
backgroundColor: finalColor,
borderColor: finalColor,
} : null,
finalDisabled ? myStyle['radio-icon__disabled'] : null,
])}
>
<Icons
name="check"
size={finalSize - 4}
color={!finalDisabled ? '#FFFFFF' : '#c8c9cc'}
style={{
opacity: isCheck ? 1 : 0,
}}
/>
</View>
{!!children && (
contentNode
)}
</TouchableOpacity>
);
};
Radio.defaultProps = {
size: 22,
disabled: false,
children: null,
};
Radio.displayName = 'Radio';
Radio.Group = RadioGroup;
Radio.Button = RadioButton;
export default Radio;
/*
* @Author: XieZhiXiong
* @Date: 2021-03-03 13:46:43
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-09-15 14:50:20
* @Description:
*/
import { StyleSheet } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
radio: {
flexDirection: 'row',
alignItems: 'center',
},
'radio-icon': {
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: '#EBECF0',
borderRadius: 9999,
},
'radio-icon__check': {
borderColor: theme.colors.primary,
backgroundColor: theme.colors.primary,
},
'radio-icon__disabled': {
borderColor: '#c8c9cc',
backgroundColor: '#ebedf0',
},
'radio-label': {
marginLeft: themeLayout['margin-xs'],
fontSize: 12,
color: theme.fonts.black1,
},
'radio-label__disabled': {
color: '#c8c9cc',
},
'radio-button': {
borderRadius: 8,
overflow: 'hidden',
marginVertical: 1,
marginHorizontal: themeLayout['margin-xxs'] / 2,
},
'radio-button-label': {
fontSize: 12,
fontWeight: '600',
color: theme.fonts.black2,
},
'radio-button__middle': {
minWidth: 64,
paddingHorizontal: themeLayout['padding-m'],
paddingVertical: themeLayout['padding-xs'],
},
'radio-button__small': {
paddingHorizontal: themeLayout['padding-s'],
paddingVertical: themeLayout['padding-xxs'],
},
'radio-button__disabled': {
backgroundColor: '#f5f5f5',
},
'radio-button__check&&disabled': {
backgroundColor: '#e6e6e6',
},
'radio-button__dark': {
backgroundColor: '#F7F8FA',
},
'radio-button-label__dark': {
color: theme.fonts.black2,
},
'radio-button__dark&&active': {
backgroundColor: '#EDEEEF',
borderRadius: 8,
},
'radio-button-label__dark&&active': {
color: theme.fonts.black1,
},
'radio-button__light': {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
},
'radio-button-label__light': {
color: 'rgba(255, 255, 255, 0.6)',
},
'radio-button__light&&active': {
backgroundColor: '#FFFFFF',
borderRadius: 4,
},
'radio-button-label__light&&active': {
color: theme.colors.primary,
},
});
/* eslint-disable react/require-default-props */
/*
* @Author: XieZhiXiong
* @Date: 2021-01-11 11:11:20
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-05-14 18:36:03
* @Description: 搜索组件
*/
import React, { useRef, useImperativeHandle } from 'react';
import {
StyleSheet,
TextInput,
TouchableOpacity,
} from 'react-native';
import { View, Text, Icons } from '@linkseeks/god-mobile';
import { useTheme } from '@react-navigation/native';
import { ThemeStyle } from '../../constants/theme';
import useAppStyle from '../../hooks/useAppStyle';
import themeLayout from '../../constants/theme/layout';
import styles from './styles';
export interface SearchProps {
/**
* 搜索框左侧文本
*/
label?: string,
/**
* 形状,可选值为 shape | round
*/
shape?: 'shape' | 'round';
/**
* 当前输入的值
*/
value?: string,
/**
* 是否在搜索框右侧显示取消按钮
*/
showAction?: boolean;
/**
* 搜索框背景色
*/
background?: string;
/**
* 取消按钮文字
*/
actionText?: string;
/**
* 是否自动获取焦点
*/
focus?: boolean;
/**
* 是否可编辑的
*/
editable?: boolean;
/**
* 是否启用清除控件
*/
clearable?: boolean;
/**
* 输入框为空时占位符
*/
placeholder?: string;
/**
* 输入框左侧图标名称
*/
leftIcon?: string;
/**
* 自定义左侧图标
*/
customLeftIcon?: React.ReactNode;
/**
* 自定义搜索框右侧按钮
*/
customAction?: React.ReactNode;
/**
* 输入内容变化时触发
*/
onChange?: (value: string) => void;
/**
* 取消搜索搜索时触发,同时会触发 onChange 传递 字符串
*/
onCancel?: (value: string) => void;
/**
* 确定搜索时触发
*/
onSearch?: (value: string) => void;
/**
* 点击清空控件时触发,同时会触发 onChange 传递 字符串
*/
onClear?: (value: string) => void;
/**
* 点击整个 Search 框触发
*/
onClick?: () => void;
/**
* Search Input 失焦触发事件
*/
onBlur?: () => void;
/**
* 是否在清空搜索框之后调用 onSearch 事件,默然为 true
*/
searchOnClearAction?: boolean,
/**
* 引用
*/
ref?: any,
}
const Search: React.FC<SearchProps> = React.forwardRef((props: SearchProps, ref: any) => {
const {
label,
shape,
value = '',
showAction,
background,
actionText,
focus,
editable,
clearable,
placeholder,
leftIcon,
customLeftIcon,
customAction,
searchOnClearAction,
onChange,
onSearch,
onCancel,
onClear,
onClick,
onBlur,
} = props;
const myStyle = useAppStyle(styles);
const appTheme = useTheme() as ThemeStyle;
const inputRef = useRef<null | TextInput>(null);
const triggerChange = (text: string) => {
if (onChange) {
onChange(text);
}
};
const handleChange = (text: string) => {
triggerChange(text);
};
const handleClear = () => {
triggerChange('');
if (onClear) {
onClear('');
}
if (searchOnClearAction && onSearch) {
onSearch('');
}
};
const handleCancel = () => {
triggerChange('');
if (onCancel) {
onCancel('');
}
};
const handleSearchSubmit = () => {
if (onSearch) {
onSearch(value);
}
};
const handleClick = () => {
if (onClick) {
onClick();
}
};
const handleBlur = () => {
if (onBlur) {
onBlur();
}
};
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current!.focus();
},
isFocused: () => inputRef.current!.isFocused(),
blur: () => {
inputRef.current!.blur();
},
}), [inputRef.current]);
return (
<TouchableOpacity
style={StyleSheet.flatten([
myStyle.search,
showAction && myStyle['search-show-action'],
{
backgroundColor: background,
},
])}
activeOpacity={1}
onPress={handleClick}
>
<View
style={StyleSheet.flatten([
myStyle['search-content'],
shape === 'round' ? myStyle['search-content__round'] : null,
])}
>
{!!label && (
<Text style={myStyle['search-label']}>{label}</Text>
)}
<View style={[myStyle['search-control']]}>
{!customLeftIcon ? (
<Icons
type="feather"
name={leftIcon}
size={18}
color="#C0C4CC"
style={myStyle['search-control-left-icon']}
/>
) : (
customLeftIcon
)}
<View
style={myStyle['search-field-wrap']}
>
<TextInput
ref={inputRef}
value={value}
onChangeText={handleChange}
placeholder={placeholder}
placeholderTextColor={appTheme.fonts.black3}
returnKeyType="search"
returnKeyLabel="搜索"
autoFocus={focus}
editable={editable}
onSubmitEditing={handleSearchSubmit}
onBlur={handleBlur}
style={StyleSheet.flatten([
myStyle['search-field'],
{
paddingRight: !clearable ? themeLayout['padding-xs'] : themeLayout['padding-xs'] + 18,
},
])}
/>
{/* 为了解决 ios 点击 Input 不会触发父级点击事件的问题 */}
{!editable ? (
<View
style={myStyle['search-field-placeholder']}
/>
) : null}
</View>
{clearable && value.length > 0 && (
<TouchableOpacity
activeOpacity={0.8}
style={myStyle['search-control-right-icon']}
onPress={handleClear}
>
<Icons
name="closecircle"
size={18}
color="#C0C4CC"
/>
</TouchableOpacity>
)}
</View>
</View>
{!!showAction && (
!customAction ? (
<TouchableOpacity
activeOpacity={0.8}
onPress={handleCancel}
>
<Text style={myStyle['search-action']}>
{actionText}
</Text>
</TouchableOpacity>
) : (
customAction
)
)}
</TouchableOpacity>
);
});
Search.defaultProps = {
label: '',
shape: 'round',
showAction: false,
background: '#FFFFFF',
actionText: '取消',
focus: false,
editable: true,
clearable: false,
placeholder: '请输入搜索关键词',
leftIcon: 'search',
customLeftIcon: null,
customAction: null,
searchOnClearAction: true,
onSearch: undefined,
onCancel: undefined,
onClear: undefined,
onClick: undefined,
onBlur: undefined,
};
export default Search;
/*
* @Author: XieZhiXiong
* @Date: 2021-01-11 11:11:25
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-09-10 11:48:03
* @Description:
*/
import { StyleSheet, Platform } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
search: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 6,
paddingHorizontal: themeLayout['padding-s'],
},
'search-show-action': {
paddingRight: 0,
},
'search-content': {
flex: 1,
paddingLeft: themeLayout['padding-s'] - 2,
backgroundColor: '#F7F8FA',
borderRadius: 2,
},
'search-content__round': {
borderRadius: 8,
},
'search-label': {
paddingHorizontal: 5,
color: theme.fonts.black1,
lineHeight: 34,
fontSize: 14,
},
'search-control': {
flex: 1,
alignItems: 'center',
paddingVertical: Platform.OS === 'ios' ? 8 : 2,
paddingRight: themeLayout['padding-xs'],
position: 'relative',
},
'search-control-left-icon': {
marginRight: themeLayout['margin-xs'],
},
'search-control-right-icon': {
padding: themeLayout['padding-xs'],
position: 'absolute',
top: -1,
right: 0,
},
'search-field-wrap': {
flex: 1,
position: 'relative',
},
'search-field': {
flex: 1,
padding: 0,
},
'search-field-placeholder': {
paddingVertical: 2,
flex: 1,
fontSize: 14,
color: theme.fonts.black3,
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
},
'search-action': {
paddingHorizontal: 8,
color: theme.fonts.black1,
lineHeight: 34,
fontSize: 14,
},
});
/*
* @Author: XieZhiXiong
* @Date: 2021-01-12 18:13:19
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-10-15 15:22:29
* @Description: 穿梭机
*/
import React from 'react';
import { TextStyle, TouchableOpacity } from 'react-native';
import { Text, Icons } from '@linkseeks/god-mobile';
import useAppStyle from '../../hooks/useAppStyle';
import styles from './styles';
interface ShuttleProps {
/**
* 描述
*/
describe: string,
/**
* 点击跳转触发
*/
onJump: () => void,
/**
* 自定义描述样式
*/
customDescribeStyle?: TextStyle,
}
const Shuttle: React.FC<ShuttleProps> = (props: ShuttleProps) => {
const { describe, onJump, customDescribeStyle } = props;
const myStyle = useAppStyle(styles);
const handleJump = () => {
if (onJump) {
onJump();
}
};
return (
<TouchableOpacity
activeOpacity={0.8}
style={myStyle.shuttle}
onPress={handleJump}
>
<Text style={[myStyle['shuttle-describe'], customDescribeStyle]}>
{describe}
</Text>
<Icons
name="right"
size={12}
color="#C0C4CC"
style={customDescribeStyle}
/>
</TouchableOpacity>
);
};
export default Shuttle;
/*
* @Author: XieZhiXiong
* @Date: 2021-01-12 18:13:24
* @LastEditors: XieZhiXiong
* @LastEditTime: 2021-03-31 20:27:46
* @Description:
*/
import { StyleSheet } from 'react-native';
import { ThemeStyle } from '../../constants/theme';
import themeLayout from '../../constants/theme/layout';
export default (theme: ThemeStyle) => StyleSheet.create({
shuttle: {
alignItems: 'center',
flexDirection: 'row',
},
'shuttle-describe': {
marginRight: themeLayout['margin-xxs'],
fontSize: 12,
color: theme.fonts.black3,
},
});
import { useTheme } from '@react-navigation/native'
import { ThemeStyle } from '../constants/theme'
// 样式文件中所使用的函数
type componentStyleFn<T> = (theme: ThemeStyle) => T
const useAppStyle = <T>(componentStyle: componentStyleFn<T>): T => {
const appTheme = useTheme() as ThemeStyle
const assignStyle = componentStyle(appTheme)
return assignStyle
}
export default useAppStyle
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment