持续更新实战中遇到的问题
# 更新记录
- 2020-04-02 : 兼容bug
- 2019-01-14 : 组件封装整理
- 2019-07-02 : 基础项目结构
# 运行
安卓下:
- 打开genymotion 添加新设备
- android Studio里面 Run=》Run app,在设备上安装app
- npm run dev "dev": "react-native run-android"
- npm run start "start": "node node_modules/react-native/local-cli/cli.js start",
快捷键:
操作 | mac | window |
---|---|---|
刷新 | cmd+r | r+r |
操作列表 | cmd+d | ctrl+m/f1 |
版本变化时用android Studio安装,npm run start,打开软件会变化
# RN/react自带的那些好用属性
# Dimensions
获取到当前设备到高宽
deviceWidth: Dimensions.get('window').width,
deviceHeight: Dimensions.get('window').height
2
# PlatForm
# Platform. os
判断平台
if (Platform.OS === "android") {
//Android平台需要运行的代码
} else {
//iOS平台需要运行的代码
}
2
3
4
5
# Platform. select()
根据平台选择相应配件,如颜色/组件等
var styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red',
},
android: {
backgroundColor: 'blue',
},
}),
},
});
2
3
4
5
6
7
8
9
10
11
12
13
var Component = Platform.select({
ios: () => require('ComponentIOS'),
android: () => require('ComponentAndroid'),
})(); <
Component / > ;
2
3
4
5
# Platform. Version
Platform. Version可以拿到该设备的版本信息,检测相应版本
# 适配小能手
# SafeAreaView
针对iPhone X设备齐刘海页面适配的组件,使用头部搜索组件时可使用,使用时只需要将SafeAreaView嵌套在最根级别的视图中即可。
# 判断iPhone X设备
import {
Dimensions,
Platform
} from 'react-native';
export let screenW = Dimensions.get('window').width;
export let screenH = Dimensions.get('window').height;
// iPhoneX
const X_WIDTH = 375;
const X_HEIGHT = 812;
/**
* 判断是否为iphoneX
* @returns {boolean}
*/
export function isIphoneX() {
return (
Platform.OS === 'ios' &&
((screenH === X_HEIGHT && screenW === X_WIDTH) ||
(screenH === X_WIDTH && screenW === X_HEIGHT))
)
}
/**
* 根据是否是iPhoneX返回不同的样式
* @param iphoneXStyle
* @param iosStyle
* @param androidStyle
* @returns {*}
*/
export function ifIphoneX(iphoneXStyle, iosStyle, androidStyle) {
if (isIphoneX()) {
return iphoneXStyle;
} else if (Platform.OS === 'ios') {
return iosStyle
} else {
if (androidStyle) return androidStyle;
return iosStyle
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# KeyboardAvoidingView
手机上弹出的键盘常常会挡住当前的视图。本组件可以自动根据键盘的位置,调整自身的 position 或底部的 padding,以避免被遮挡
名称 | 类型 | 说明 |
---|---|---|
keyboardVerticalOffset | number | 有时候应用离屏幕顶部还有一些距离(比如状态栏等等),利用此属性来补偿修正这段距离。 |
behavior | enum('height', 'position', 'padding') | 注意:Android 和 iOS 在此属性上表现并不一致。 Android 可能不指定此属性更好,而 iOS 可能相反。 |
contentContainerStyle | View. style | 如果设定 behavior 值为'position',则会生成一个 View 作为内容容器。此属性用于指定此内容容器的样式。 |
enabled | boolean | 是否启用KeyboardAvoidingView。 |
< KeyboardAvoidingView
style = {
{
width: '100%'
}
}
behavior = "padding"
keyboardVerticalOffset = {
100
} >
<
/KeyboardAvoidingView>
2
3
4
5
6
7
8
9
10
11
12
# 字体适配
// 字号修改
formatTextSize(value, maxLength, oriSize) {
if (value && value.toString().length > maxLengh) {
return oriSize - (oriSize / 8);
}
return oriSize;
},
2
3
4
5
6
7
< Text
style = {
[styles.centerPrice, {
fontSize: commonUtil.formatTextSize(centerNumber, 8, 24)
}]
} > {
centerNumber
} <
/Text>
2
3
4
5
6
7
8
9
# 国际化
国际化采用的是react-native-i18n插件,通过导出translate(key) {return I18n. t(key); }方法来使用。
![image. png](https://cdn.nlark.com/yuque/0/2020/png/543173/1587880795281-650e5be0-7f4d-4c42-9ad6-ced6bdfdce41.png#align=left&display=inline&height=86&margin=%5Bobject%20Object%5D&name=image. png&originHeight=172&originWidth=330&size=10268&status=done&style=none&width=165)
三个文件书写时一一对照该格式
{
"账号": "Account",
"密码": "Password"
}
2
3
4
/**
*
* 通用方法
* 统一语言、主题使用入口,方便后续支持多语言和换肤
*
*/
import defaultTheme from './themes/default/index';
import I18n, {
getLanguages
} from 'react-native-i18n';
import config from './config';
import moment from 'moment';
require('moment/locale/zh-cn');
I18n.defaultLocale = "zh-CN";
I18n.fallbacks = true;
I18n.translations = {
'zh-CN': require('./langs/zh-CN'),
'zh-Hans': require('./langs/zh-CN'),
'en': require('./langs/en'),
'en_US': require('./langs/en'),
'en_GB': require('./langs/en'),
'en_HK': require('./langs/en'),
'en_WW': require('./langs/en'),
'en_CA': require('./langs/en'),
'en_AU': require('./langs/en'),
'en_SG': require('./langs/en'),
'en_NZ': require('./langs/en'),
'en_ID': require('./langs/en'),
'en_PH': require('./langs/en'),
'en_TH': require('./langs/en'),
'en_MY': require('./langs/en'),
'zh-TW': require('./langs/zh-FT'),
'zh-HK': require('./langs/zh-FT'),
'zh-Hant': require('./langs/zh-FT'),
};
let _theme = defaultTheme;
module.exports = {
translate(key) {
return I18n.t(key);
},
translateWithDefault(key) {
let result = I18n.t(key);
if (result.indexOf('missing') > 0) {
return key;
} else {
return result;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 样式
样式采用驼峰命名的方式
# JS形式
图片和样式公共文件,通过js的形式 export导出,再import导入使用
![image. png](https://cdn.nlark.com/yuque/0/2020/png/543173/1587881420450-93bed886-fe3c-4c00-8415-929d7f01eb4c.png#align=left&display=inline&height=464&margin=%5Bobject%20Object%5D&name=image. png&originHeight=928&originWidth=502&size=52743&status=done&style=none&width=251)
style示例:
import {
Dimensions
} from 'react-native';
import color from '../../../color';
let {
width,
height
} = Dimensions.get('window');
let statusBarHeight = 24;
export default {
container: {
alignSelf: 'stretch',
flex: 1,
paddingLeft: 40,
paddingRight: 40,
backgroundColor: color.background,
width: width,
height: height - statusBarHeight
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
颜色变量:
export default {
background: '#FFFFFF',
primary: '#0E95FF',
//second: '#0E95FF',
success: '#00C853',
warning: '#FFAB00',
danger: '#F44337',
info: '#A3A3A3',
mainText: '#212121',
regularText: '#616161',
secondaryText: '#9E9E9E',
placeholderText: '#BDBDBD',
whiteText: '#FFFFFF',
line: '#EEEEEE',
bgLine: '#F5F5F5',
borderColorGray: '#E0E0E0',
opacityPrimary: '#ddf1ff',
placePrimary: '#efefef',
dialogLine: '#d9d9d9'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
style引入:
import common from './styles/common';
import LineChart from './styles/component/LineChart';
import home from './styles/pages/main/main';
import login from './styles/pages/login/login';
export default {
common: common,
component: {
LineChart: LineChart,
},
pages: {
main: {
main: home,
},
login: {
login: login,
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
统一引出:
import color from './color';
import style from './style';
import image from './image';
export default {
color: color,
style: style,
image: image
}
2
3
4
5
6
7
8
9
使用:
let theme = this.theme();
let styles = theme.style.pages.login.login;
2
# styleSheet
通过给定的对象进行常见一个 StyleSheet 样式
import {
StyleSheet
} from 'react-native'
export default StyleSheet.create({
content: {
flex: 1,
justifyContent: 'center'
}
})
2
3
4
5
6
7
8
9
10
<View
style={[styles.backdrop]}
/>
2
3
# hairlineWidth
该用来定义当前平台最细的宽度。该属性用来设置边框或者两个组件之间的分割线。
dropdown: {
position: 'absolute',
height: (33 + StyleSheet.hairlineWidth) * 5,
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'lightgray',
borderRadius: 2,
backgroundColor: 'white'
},
2
3
4
5
6
7
8
# adsoluteFill
...StyleSheet.absoluteFill
// 相当于下面的缩写
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0
2
3
4
5
6
7
# flatten
整合样式,后者覆盖前者相同属性
var styles = StyleSheet.create({
listItem: {
flex: 1,
fontSize: 16,
color: 'white',
},
selectedListItem: {
color: 'green',
},
});
console.log(StyleSheet.flatten([styles.listItem, styles.selectedListItem]));
2
3
4
5
6
7
8
9
10
11
12
# 优点
代码质量
- 从 render 函数中移除具体的样式内容,可以使代码更清晰易懂。
- 给样式命名也可以对 render 函数中的组件增加语义化的描述。
整体性能:
- 通过 StyleSheet,我们可以通过标志的样式 ID 来引用,而不是每次都要创建一个新的 Style 对象
- 该允许样式通过桥接在原生代码和 JavaScript 中传递一次,后面全部通过该 id 进行引用(不过现在该功能还没有实现)
# 图片
https://reactnative.cn/docs/image-style-props
# 引入
注意使用图片时需要给图片长宽,否则不显示
{
/*从项目中加载图片*/
} <
Image source = {
require('./img/icon.png')
}
style = {
styles.imageStyle
}
/>
{
/*从资源包中加载图片*/
} <
Image source = {
{
uri: 'bd_logo1'
}
}
style = {
styles.imageStyle
}
/>
{
/*从网络中加载图片*/
} <
Image source = {
{
url: 'https://www.baidu.com/img/flexible/logo/pc/result@2.png'
}
}
style = {
styles.imageStyle
}
/>
{
/*用图片设置背景*/
} <
ImageBackground source = {
{
uri: 'https://www.baidu.com/img/flexible/logo/pc/result@2.png'
}
}
style = {
styles.imageStyle
} >
<
Text style = {
{
marginTop: 40,
backgroundColor: 'transparent',
fontSize: 30
}
} > 我是文字 < /Text> < /
ImageBackground >
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 缩放模式
决定当组件尺寸和图片尺寸不成比例的时候如何调整图片的大小。默认值为 cover
。
cover
: 在保持图片宽高比的前提下缩放图片,直到宽度和高度都大于等于容器视图的尺寸(如果容器有 padding 内衬的话,则相应减去)。图片完全覆盖甚至超出容器,容器中不留任何空白。contain
: 在保持图片宽高比的前提下缩放图片,直到宽度和高度都小于等于容器视图的尺寸(如果容器有 padding 内衬的话,则相应减去)。图片完全被包裹在容器中,容器中可能留有空白。stretch
: 拉伸图片且不维持宽高比,直到宽高都刚好填满容器。repeat
: 重复平铺图片直到填满容器。图片会维持原始尺寸,但是当尺寸超过容器时会在保持宽高比的前提下缩放到能被容器包裹。center
: 居中不拉伸。
# 圆角
目前在iOS的图片组件上还不支持:borderTopLeftRadiusborderTopRightRadiusborderBottomLeftRadiusborderBottomRightRadius
# 在 Android 上支持 GIF 和 WebP 格式图片
默认情况下 Android 是不支持 GIF 和 WebP 格式的。你需要在 android/app/build.gradle
文件中根据需要手动添加以下模块:
dependencies {
// 如果你需要支持Android4.0(API level 14)之前的版本
implementation 'com.facebook.fresco:animated-base-support:1.3.0'
// 如果你需要支持GIF动图
implementation 'com.facebook.fresco:animated-gif:2.0.0'
// 如果你需要支持WebP格式,包括WebP动图
implementation 'com.facebook.fresco:animated-webp:2.1.0'
implementation 'com.facebook.fresco:webpsupport:2.0.0'
// 如果只需要支持WebP格式而不需要动图
implementation 'com.facebook.fresco:webpsupport:2.0.0'
}
2
3
4
5
6
7
8
9
10
11
# 文件配置项
# Config
js的变量
- 开发正式本地等环境
- api路径
- _notifies通知
- 可以通过设置头部底部高度STATUS_BAR_HEIGHT;NAV_BAR_HEIGHT
'use strict';
var React = require('react-native');
var {
Platform,
} = React;
const _environment = 'production';
const _versionCode = '1.2.5';
const _version = '1.2.5';
const _environments = {
//生产环境
production: {
DEBUG: true,
API_BASE_URL: 'https://qietuniu.com/',
IMAGE_BASE_URL: 'https://qietuniu.com/',
},
//开发环境
development: {
DEBUG: true,
API_BASE_URL: 'https://qietuniu.com/',
IMAGE_BASE_URL: 'https://qietuniu.com/',
},
//本地调试环境
local: {
DEBUG: true,
API_BASE_URL: '',
IMAGE_BASE_URL: '',
},
};
const _api = {
//登录
API_SIGNIN: '/myshopapi/Account/SignIn',
};
const _notifies = {
CURRENTSTORE_CHANGE: 'storeChangeNotify',
DATERANGE_CHANGE: 'dateRangeChangeNotify',
THEME_CHANGE: 'THEME_CHANGE',
SALE_PAGE_CHANGE: 'SALE_PAGE_CHANGE',
MEMBER_PAGE_CHANGE: 'MEMBER_PAGE_CHANGE',
HOME_CHANGE: 'HOME_CHANGE'
};
module.exports = {
ENVIRONMENT: _environments[_environment],
DEBUG: _environments[_environment].DEBUG,
API: _api,
VERSIONCODE: _versionCode,
VERSION: _version,
STATUS_BAR_HEIGHT: (Platform.OS === 'ios' ? 20 : Platform.Version <= 19 ? 0 : 25),
NAV_BAR_HEIGHT: (Platform.OS === 'ios' ? 44 : 56),
NOTIFIES: _notifies,
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# httpUtil
封装网络请求
/**
*
* 网络请求
*
*/
'use strict';
var React = require('react-native');
var {
AsyncStorage,
Alert,
Platform
} = React;
import moment from 'moment';
import config from './config';
import storageUtil from './storageUtil';
import commonUtil from './commonUtil';
import md5 from './../component/md5';
import {
StackActions,
NavigationActions
} from 'react-navigation'
var _networkError = false;
var _lastNetwork = new Date();
//Json序列化
const serializeJSON = function(data) {
return Object.keys(data).map(function(keyName) {
return encodeURIComponent(keyName) + '=' + encodeURIComponent(data[keyName])
}).join('&');
};
module.exports = {
async get(url, data, area) {
let _this = this;
let _time = moment().format('X');
let _apiToken = this.getApiToken(url, _time);
let _userToken = await storageUtil.getUserToken();
let _storeId = await storageUtil.getUserId();
let _area = await storageUtil.getArea();
let _baseUrl = config.ENVIRONMENT.API_BASE_URL;
if (_area) {
_baseUrl = _baseUrl.replace('{area}', _area);
} else {
//_baseUrl = _baseUrl.replace('{area}', 2);
if (area > 0) {
_baseUrl = _baseUrl.replace('{area}', area);
} else {
_baseUrl = _baseUrl.replace('area{area}-', '');
}
}
let _url = _baseUrl + url + '?' + serializeJSON(data);
return new Promise(function(resolve, reject) {
fetch(_url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Plat': Platform.OS,
'Version': config.VERSIONCODE,
'TIME': _time,
'APITOKEN': _apiToken,
'USERTOKEN': _userToken,
'STOREID': _storeId,
},
body: null
}).then((response) => {
commonUtil.log(url, response);
_this.processData(response, resolve, reject, _this);
}).catch((error) => {
commonUtil.log(url, error);
_this.processNetworkError(resolve, reject, _this);
}).done();
});
},
async post(url, data, area) {
let _this = this;
let _time = moment().format('X');
let _apiToken = this.getApiToken(url, _time);
let _userToken = await storageUtil.getUserToken();
let _storeId = await storageUtil.getUserId();
let _area = await storageUtil.getArea();
let _baseUrl = config.ENVIRONMENT.API_BASE_URL;
if (_area) {
_baseUrl = _baseUrl.replace('{area}', _area);
} else {
//_baseUrl = _baseUrl.replace('{area}', 2);
if (area > 0) {
_baseUrl = _baseUrl.replace('{area}', area);
} else {
_baseUrl = _baseUrl.replace('area{area}-', '');
}
}
let _url = _baseUrl + url;
//console.warn(_userToken);
return new Promise(function(resolve, reject) {
fetch(_url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Plat': Platform.OS,
'Version': config.VERSIONCODE,
'TIME': _time,
'APITOKEN': _apiToken,
'USERTOKEN': _userToken,
'STOREID': _storeId,
},
body: data ? JSON.stringify(data) : null
}).then((response) => {
commonUtil.log(url, response);
_this.processData(response, resolve, reject, _this);
}).catch((error) => {
commonUtil.log(url, error);
_this.processNetworkError(resolve, reject, _this);
}).done();
});
},
getApiToken(url, time) {
let action = url.split("/").pop().toUpperCase();
//console.warn(action);
return md5.hex_md5( `${action}_POSPAL_${time}` );
},
processNetworkError(resolve, reject, _this) {
// var now = new Date();
// if (now - _lastNetwork > 1000 * 5) {
// _networkError = true;
// _lastNetwork = new Date();
// //TODO: Alert
// }
reject(new Error(_this.getError('0001')));
},
processData(response, resolve, reject, _this) {
// commonUtil.log("response status", response.status);
_networkError = false;
_lastNetwork = new Date();
if (response.status !== 200) {
commonUtil.log(response);
reject(new Error(_this.getError('0002')));
} else {
response.json().then(function(responseData) {
commonUtil.log(responseData);
if (responseData == null) {
reject(new Error(_this.getError('0002')));
} else if (responseData.successed) {
resolve(responseData);
} else {
if (responseData.messageCode == 9012) {
//console.warn(responseData.messageCode);
let navigation = storageUtil.getNavigation();
if (navigation != null) {
//navigation.popToTop();
storageUtil.delStoreInfo().then(() => {
var resetAction = StackActions.reset({
index: 0,
actions: [NavigationActions.navigate({
routeName: 'login'
})],
});
navigation.dispatch(resetAction);
}).done();
}
} else {
reject(new Error(_this.getError(responseData.messageCode)));
}
}
});
}
},
getError(messageCode) {
return commonUtil.translate( `MSG_${messageCode}` );
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# commonUtil
公共方法
/**
*
* 通用方法
* 统一语言、主题使用入口,方便后续支持多语言和换肤
*
*/
import defaultTheme from './themes/default/index';
import I18n, {
getLanguages
} from 'react-native-i18n';
import config from './config';
import moment from 'moment';
require('moment/locale/zh-cn');
I18n.defaultLocale = "zh-CN";
I18n.fallbacks = true;
I18n.translations = {
'zh-CN': require('./langs/zh-CN'),
'zh-Hans': require('./langs/zh-CN'),
'en': require('./langs/en'),
'en_US': require('./langs/en'),
'en_GB': require('./langs/en'),
'en_HK': require('./langs/en'),
'en_WW': require('./langs/en'),
'en_CA': require('./langs/en'),
'en_AU': require('./langs/en'),
'en_SG': require('./langs/en'),
'en_NZ': require('./langs/en'),
'en_ID': require('./langs/en'),
'en_PH': require('./langs/en'),
'en_TH': require('./langs/en'),
'en_MY': require('./langs/en'),
'zh-TW': require('./langs/zh-FT'),
'zh-HK': require('./langs/zh-FT'),
'zh-Hant': require('./langs/zh-FT'),
};
let _theme = defaultTheme;
module.exports = {
translate(key) {
return I18n.t(key);
},
translateWithDefault(key) {
let result = I18n.t(key);
if (result.indexOf('missing') > 0) {
return key;
} else {
return result;
}
},
translatePayment(key) {
if (key) {
return key.replace('现金支付', I18n.t('现金支付')).replace('银联支付', I18n.t('银联支付')).replace('储值卡支付', I18n.t('储值卡支付'));
} else {
return '';
}
},
getTheme() {
return _theme;
},
// 时间类型转文字
formatDateRangeType(dateRange) {
if (dateRange != null) {
if (dateRange.type == 'today') {
return this.translate('今天');
} else if (dateRange.type == 'yesterday') {
return this.translate('昨天');
} else if (dateRange.type == 'thisweek') {
return this.translate('本周');
} else if (dateRange.type == 'thismonth') {
return this.translate('本月');
} else if (dateRange.type == 'last3day') {
return this.translate('最近3天');
} else if (dateRange.type == 'last7day') {
return this.translate('最近7天');
} else if (dateRange.type == 'lastweek') {
return this.translate('上周');
} else if (dateRange.type == 'lastmonth') {
return this.translate('上月');
} else if (dateRange.type == 'custom') {
return this.translate('自定义');
}
}
return '';
},
// 开始结束时间分隔转格式
formatDateRangeDateTime(dateRange, format, split) {
if (dateRange != null) {
return `${moment(dateRange.begin).format(format)}${split}${moment(dateRange.end).format(format)}` ;
}
return '';
},
// 是否是生日
isBirthDay(date) {
if (date && moment(date).format('MM-DD') == moment().format('MM-DD')) {
return true;
} else {
return false;
}
},
// 时间转格式
formatDate(date, format) {
return moment(date).format(format);
},
// 字符串时间转格式
formatStringDate(strDate, format) {
return moment(strDate).format(format);
},
// 百分比
formatPercent(value) {
if (value) {
value = value * 100;
return `${value.toFixed(2)}%`
} else {
return '0.00%'
}
},
// 精度转,默认2精度
formatDecimal(value, digits) {
if (digits == null) {
digits = 2;
}
if (!value) {
value = 0;
}
return value.toFixed(digits)
},
// 字号修改
formatTextSize(value, maxLengh, oriSize) {
if (value && value.toString().length > maxLengh) {
return oriSize - (oriSize / 8);
}
return oriSize;
},
// 日志
log(a, b) {
if (config.DEBUG) {
if (b) {
console.log(a, b);
} else {
console.log(a);
}
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# cacheUtil
数据缓存使用的是AsyncStorage,它是一个简单的、异步的、持久化的 Key-Value 存储系统,它对于 App 来说是全局性的。有一个极大的缺点就是:只可以存储字符串。
/**
*
* 数据缓存
* 缓存一些常用请求数据如:商品分类、商品标签、会员等级...等等
*
*/
'use strict';
import config from './config';
import httpUtil from './httpUtil';
var _caches = [];
module.exports = {
async loadDataWithCache(url, params, cacheKey) {
return new Promise(function(resolve, reject) {
_caches.map((cache) => {
if (cache.url == url) {
cache.items.map((item) => {
//TODO: compare time
if (item.key == cacheKey) {
resolve(item.value);
}
});
}
});
let _items = [];
httpUtil.post(url, params).then((response) => {
_items.push({
key: cacheKey,
value: response.result,
time: new Date()
})
if (_items.length == 1) {
_caches.push({
url: url,
items: _items
});
}
resolve(response.result);
}).catch((error) => {
reject(error);
}).done();
});
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# importUtil
/**
*
* 引用管理
* 1. 统一引用入口,方便在界面中引用
* 2. 方便扩展组件
*
*/
import React from 'react';
import {
Dimensions,
StyleSheet
} from 'react-native';
export {
React,
Dimensions,
StyleSheet
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# storageUtil
/**
*
* 存储管理
*
*/
'use strict';
var React = require('react-native');
var {
AsyncStorage,
Alert
} = React;
import RCTDeviceEventEmitter from 'RCTDeviceEventEmitter';
import config from './config';
var moment = require('moment');
module.exports = {
setNavigation(navigation) {
this._navigation = navigation;
},
getNavigation() {
return this._navigation;
},
async setPermission(storeInfo, type) {
this._permission = false
if (storeInfo) {
if ((!type) || storeInfo.cashierAuths.indexOf('1376879633495704861') != -1) {
this._permission = true
}
}
AsyncStorage.setItem("PERMISSION", JSON.stringify(this._permission)).done();
},
async getPermission() {
if (this._permission) {
return this._permission;
} else {
let permission = await AsyncStorage.getItem("PERMISSION");
if (permission) {
this._permission = JSON.parse(permission);
return this._permission;
} else {
let signinOptions = await AsyncStorage.getItem("SIGNIN_OPTIONS");
if (signinOptions == null || signinOptions.cashierNum == null) {
return true
} else {
return false;
}
}
}
},
/**
* 添加搜索历史
* @param {*} searchHistoryType 搜索类型
* @param {*} key 搜索条件
*/
async addSearchHistory(searchHistoryType, key) {
if (this._searchHistory) {
if (this._searchHistory[searchHistoryType]) {
//console.warn('1');
var index = this._searchHistory[searchHistoryType].indexOf(key);
if (index > -1) {
this._searchHistory[searchHistoryType].splice(index, 1);
}
this._searchHistory[searchHistoryType].unshift(key);
if (this._searchHistory[searchHistoryType].length > 10) {
//console.warn('3');
this._searchHistory[searchHistoryType].pop();
}
} else {
//console.warn('2');
this._searchHistory[searchHistoryType] = [key];
}
} else {
//console.warn('4');
this._searchHistory = {};
this._searchHistory[searchHistoryType] = [key];
}
AsyncStorage.setItem("SEARCH_HISTORYS", JSON.stringify(this._searchHistory)).done();
return this._searchHistory[searchHistoryType] || [];
},
/**
* 查询搜索历史
* @param {*} searchHistoryType 搜索类型
*/
async getSearchHistory(searchHistoryType) {
if (!this._searchHistory) {
let searchHistory = await AsyncStorage.getItem("SEARCH_HISTORYS");
if (searchHistory) {
this._searchHistory = JSON.parse(searchHistory);
} else {
this._searchHistory = {};
}
}
return this._searchHistory[searchHistoryType] || [];
},
/**
* 清空搜索历史
* @param {*} searchHistoryType 搜索类型
*/
async clearSearchHistory(searchHistoryType) {
if (!this._searchHistory) {
let searchHistory = await AsyncStorage.getItem("SEARCH_HISTORYS");
if (searchHistory) {
this._searchHistory = JSON.parse(searchHistory);
} else {
this._searchHistory = {};
}
}
this._searchHistory[searchHistoryType] = [];
AsyncStorage.setItem("SEARCH_HISTORY", JSON.stringify(this._searchHistory)).done();
return this._searchHistory[searchHistoryType] || [];
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# prop-types
验证数据类型。
- 必填:isRequired
name: PropTypes.string.isRequired
- 多重嵌套:arrayOf/objectOf;如果要检测数组中的元素,或者对象中的属性
array: PropTypes.arrayOf(PropTypes.number)
- 检测不同对象的不同属性的不同数据类型:shape
todoList: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
text: PropTypes.string
}))
2
3
4
- 多类型:oneOfType
number: PropTypes.oneOfType(
[PropTypes.string, PropTypes.number]
)
2
3
- 多值:oneOf
number: PropTypes.oneOfType(
[12, 24, 35]
)
2
3
- optionalArray: PropTypes. array,
- optionalBool: PropTypes. bool,
- optionalFunc: PropTypes. func,
- optionalNumber: PropTypes. number,
- optionalObject: PropTypes. object,
- optionalString: PropTypes. string,
- optionalSymbol: PropTypes. symbol,
# 消息传递
RCTDeviceEventEmitter建立了一个全局的类进行接受与发送。 使用:
import RCTDeviceEventEmitter from 'RCTDeviceEventEmitter';
监听:componentDidMount中监听,componentWillUnmount中移除
componentDidMount() {
this.listener = RCTDeviceEventEmitter.addListener('通知名称', (value) => {
// 接受到通知后的处理
});
}
componentWillUnmount() {
// 移除 一定要写
this.listener.remove();
}
2
3
4
5
6
7
8
9
10
发送:
RCTDeviceEventEmitter.emit('通知名称', value);
# 基础组件封装
# BaseComponent公共方法
重写React的Component方法,建立一个基础组件,其他组件继承时可以使用里面的方法。对时间门店等切换进行实时监听,以及生命周期结束时统一销毁。
# componentWillReceiveProps生命周期
该方法当 props
发生变化时执行,初始化 render
时不执行,在这个回调函数里面,你可以根据属性的变化,通过调用 this.setState()
来更新你的组件状态,旧的属性还是可以通过 this.props
来获取, 这里调用更新状态是安全的,并不会触发额外的 render
调用**使用好处:**在这个生命周期中,可以在子组件的render函数执行前获取新的props,从而更新子组件自己的state。 可以将数据请求放在这里进行执行,需要传的参数则从componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。componentWillReceiveProps在初始化render的时候不会执行,它会在Component接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染
import React, {
Component
} from 'react';
import RCTDeviceEventEmitter from 'RCTDeviceEventEmitter';
import commonUtil from '../common/commonUtil';
import config from '../common/config';
export default class baseComponent extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
this.dateRangeListener = RCTDeviceEventEmitter.addListener(config.NOTIFIES.DATERANGE_CHANGE, (value) => {
this.onDateRangeChange();
});
this.storeListener = RCTDeviceEventEmitter.addListener(config.NOTIFIES.CURRENTSTORE_CHANGE, (value) => {
this.onStoreChange();
});
this.homeListener = RCTDeviceEventEmitter.addListener(config.NOTIFIES.HOME_CHANGE, (value) => {
this.onHomeChange();
});
}
componentWillReceiveProps(nextProps) {
if (nextProps.active != this.props.active) {
this.onActiveChange(nextProps.active);
}
if (nextProps.focused != this.props.focused) {
this.onFocusChange(nextProps.focused);
}
}
componentWillUnmount() {
this.setState = (state, callback) => {
return;
};
this.dateRangeListener && this.dateRangeListener.remove();
this.storeListener && this.storeListener.remove();
this.homeListener && this.homeListener.remove();
}
translate(key) {
return commonUtil.translate(key);
}
theme() {
return commonUtil.getTheme();
}
onActiveChange(active) {
}
onFocusChange(focused) {
}
onStoreChange() {
}
onDateRangeChange() {
}
onHomeChange() {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# radioButton
import React from 'react'
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Animated,
Image
} from 'react-native'
import PropTypes from 'prop-types'
// import commonUtil from '../common/commonUtil';
import defaultTheme from '../common/themes/default/index'
// import commonUtil from '../common/commonUtil';
const propTypes = {
options: PropTypes.array.isRequired,
testOptionEqual: PropTypes.func,
renderOption: PropTypes.func,
renderContainer: PropTypes.func,
onSelection: PropTypes.func
}
export default class RadioButtons extends React.Component {
constructor() {
super()
this.state = {
selectedOption: null,
selectedIndex: null
}
}
copySelectedOptionFromProps({ selectedOption, selectedIndex }) {
this.setState({
selectedOption,
selectedIndex
})
}
componentWillMount() {
this.copySelectedOptionFromProps(this.props)
}
componentWillReceiveProps(newProps) {
this.copySelectedOptionFromProps(newProps)
}
selectOption(selectedOption, selectedIndex) {
this.setState({
selectedOption,
selectedIndex
})
this.props.onSelection(selectedOption, selectedIndex)
}
render() {
const { selectedOption, selectedIndex } = this.state
const children = this.props.options.map(
function(option, index) {
const isSelected =
selectedIndex === index ||
this.props.testOptionEqual(selectedOption, option)
const onSelection = this.selectOption.bind(this, option, index)
return this.props.renderOption(option, isSelected, onSelection, index)
}.bind(this)
)
return this.props.renderContainer(children)
}
static getTextOptionRender(normalStyle, selectedStyle) {
let image = defaultTheme.image
let commonstyles = defaultTheme.style.common
normalStyle = normalStyle?normalStyle:commonstyles.radioTextNormal
selectedStyle = selectedStyle?selectedStyle:commonstyles.radioTextSelect
return function renderOption(option, selected, onSelect, index) {
const textStyle = selected ? [normalStyle, selectedStyle] : normalStyle
var style
if (index > 0) {
style = [commonstyles.radioItem,commonstyles.radioItem_line]
} else {
style = commonstyles.radioItem
}
return (
<TouchableOpacity onPress={onSelect} key={index}>
<View style={style}>
<Text style={textStyle}>{option}</Text>
{selected && <Image source={image.sort_select_icon} />}
</View>
</TouchableOpacity>
)
}
}
static getViewContainerRenderer(style) {
return function renderContainer(options) {
return <View style={style}>{options}</View>
}
}
}
RadioButtons.renderHorizontalContainer = RadioButtons.getViewContainerRenderer({
flexDirection: 'row'
})
RadioButtons.renderVerticalContainer = RadioButtons.getViewContainerRenderer({
flexDirection: 'column'
})
RadioButtons.defaultProps = {
testOptionEqual(a, b) {
return a === b
},
renderOption: RadioButtons.getTextOptionRender(),
renderContainer: RadioButtons.renderVerticalContainer,
onSelection(option) {}
}
RadioButtons.propTypes = propTypes
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# checkbox
import React, {
Component
} from 'react';
import {
StyleSheet,
View,
Image,
Text,
TouchableHighlight,
ViewPropTypes as RNViewPropTypes,
} from 'react-native';
import PropTypes from 'prop-types';
const ViewPropTypes = RNViewPropTypes || View.propTypes;
export default class CheckBox extends Component {
constructor(props) {
super(props);
}
static propTypes = {
...ViewPropTypes,
leftText: PropTypes.string,
leftTextView: PropTypes.element,
rightText: PropTypes.string,
leftTextStyle: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
rightTextView: PropTypes.element,
rightTextStyle: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
checkedImage: PropTypes.element,
unCheckedImage: PropTypes.element,
onClick: PropTypes.func.isRequired,
isChecked: PropTypes.bool.isRequired,
isIndeterminate: PropTypes.bool.isRequired,
checkBoxColor: PropTypes.string,
checkedCheckBoxColor: PropTypes.string,
uncheckedCheckBoxColor: PropTypes.string,
disabled: PropTypes.bool,
}
static defaultProps = {
isChecked: false,
isIndeterminate: false,
leftTextStyle: {},
rightTextStyle: {}
}
onClick() {
this.props.onClick();
}
_renderLeft() {
if (this.props.leftTextView) return this.props.leftTextView;
if (!this.props.leftText) return null;
return ( <
Text style = {
[styles.leftText, this.props.leftTextStyle]
} > {
this.props.leftText
} < /Text>
);
}
_renderRight() {
if (this.props.rightTextView) return this.props.rightTextView;
if (!this.props.rightText) return null;
return ( <
Text style = {
[styles.rightText, this.props.rightTextStyle]
} > {
this.props.rightText
} < /Text>
);
}
_renderImage() {
if (this.props.isIndeterminate) {
return this.props.indeterminateImage ? this.props.indeterminateImage : this.genCheckedImage();
}
if (this.props.isChecked) {
return this.props.checkedImage ? this.props.checkedImage : this.genCheckedImage();
} else {
return this.props.unCheckedImage ? this.props.unCheckedImage : this.genCheckedImage();
}
}
_getCheckedCheckBoxColor() {
return this.props.checkedCheckBoxColor ? this.props.checkedCheckBoxColor : this.props.checkBoxColor
}
_getUncheckedCheckBoxColor() {
return this.props.uncheckedCheckBoxColor ? this.props.uncheckedCheckBoxColor : this.props.checkBoxColor
}
_getTintColor() {
return this.props.isChecked ? this._getCheckedCheckBoxColor() : this._getUncheckedCheckBoxColor()
}
genCheckedImage() {
let source;
if (this.props.isIndeterminate) {
source = require('../common/themes/default/images/tuan-logo-icon.png');
} else {
source = this.props.isChecked ? require('../common/themes/default/images/multi-select-hl.png') : require('../common/themes/default/images/multi-select-nl.png');
}
return ( <
Image source = {
source
}
style = {
{
tintColor: this._getTintColor()
}
}
/>
);
}
render() {
return ( <
TouchableHighlight style = {
this.props.style
}
onPress = {
() => this.onClick()
}
underlayColor = 'transparent'
disabled = {
this.props.disabled
} >
<
View style = {
styles.container
} > {
this._renderLeft()
} {
this._renderImage()
} {
this._renderRight()
} <
/View> < /
TouchableHighlight >
);
}
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center'
},
leftText: {
flex: 1,
},
rightText: {
flex: 1,
marginLeft: 10
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# TextInput
import React, {
Component
} from 'react';
import {
View,
TextInput
} from 'react-native';
import commonUtil from '../common/commonUtil';
export default class baseTextInput extends Component {
constructor(props) {
super(props);
this.state = {
focus: false
};
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
}
onFocus() {
if (this.props.onFocus) {
this.props.onFocus();
} else {
this.setState({
focus: true
});
}
}
onBlur() {
if (this.props.onBlur) {
this.props.onBlur();
} else {
this.setState({
focus: false
});
}
}
render() {
var theme = commonUtil.getTheme();
let styles = theme.style.component.baseTextInput;
return ( <
TextInput {
...this.props
}
style = {
[styles.input, this.state.focus ? styles.focus : {}, this.props.style]
}
keyboardType = {
this.props.keyboardType ? this.props.keyboardType : "default"
}
autoCorrect = {
this.props.autoCorrect ? this.props.autoCorrect : false
}
autoCapitalize = {
this.props.autoCapitalize ? this.props.autoCapitalize : "none"
}
clearButtonMode = {
this.props.clearButtonMode ? this.props.clearButtonMode : "while-editing"
}
underlineColorAndroid = 'transparent'
onFocus = {
this.onFocus
}
onBlur = {
this.onBlur
}
/>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# switch
'use strict'
import React, { Component } from 'react';
import { View, Animated, Dimensions, Text } from 'react-native'
import { PropTypes } from 'prop-types';
import commonUtil from '../common/commonUtil';
export const DURATION = {
LENGTH_LONG: 2400,
LENGTH_SHORT: 500,
FOREVER: 0,
};
const { height, width } = Dimensions.get('window');
export default class Toast extends Component {
constructor(props) {
super(props);
this.state = {
isShow: false,
text: '',
opacityValue: new Animated.Value(this.props.opacity),
}
}
static propTypes = {
//style: PropTypes.style,
position: PropTypes.oneOf([
'top',
'center',
'bottom',
]),
//textStyle: PropTypes.style,
positionValue: PropTypes.number,
fadeInDuration: PropTypes.number,
fadeOutDuration: PropTypes.number,
opacity: PropTypes.number
}
static defaultProps = {
position: 'center',
//textStyle: {},
positionValue: 120,
fadeInDuration: 600,
fadeOutDuration: 600,
opacity: 1
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
}
show(text, duration) {
this.duration = typeof duration === 'number' ? duration : DURATION.LENGTH_LONG;
this.setState({ isShow: true, text: text, });
Animated.timing(
this.state.opacityValue,
{
toValue: this.props.opacity,
duration: this.props.fadeInDuration,
}
).start(() => {
this.isShow = true;
if (duration !== DURATION.FOREVER) this.close();
});
}
close(duration) {
let delay = typeof duration === 'undefined' ? this.duration : duration;
if (delay === DURATION.FOREVER) delay = this.props.defaultCloseDelay || 250;
if (!this.isShow && !this.state.isShow) return;
this.timer && clearTimeout(this.timer);
this.timer = setTimeout(() => {
Animated.timing(
this.state.opacityValue,
{
toValue: 0.0,
duration: this.props.fadeOutDuration,
}
).start(() => {
this.setState({
isShow: false,
});
this.isShow = false;
});
}, delay);
}
render() {
let theme = commonUtil.getTheme();
let styles = theme.style.component.Toast;
let positionStyle = {}
switch (this.props.position) {
case 'top':
positionStyle = { justifyContent: 'flex-start', paddingTop: 30 };
break;
case 'bottom':
positionStyle = { justifyContent: 'flex-start', paddingBottom: 30 };
break;
}
let view = this.state.isShow ?
<View style={[styles.container, positionStyle]} pointerEvents="none">
<Animated.View style={[styles.content, { opacity: this.state.opacityValue }]}>
<Text style={styles.text}>
{this.state.text}
</Text>
</Animated.View>
</View> : null;
return view;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# Container
import React, {
Component
} from 'react';
import {
View,
Text,
Platform
} from 'react-native';
import {
PropTypes
} from 'prop-types';
import commonUtil from '../common/commonUtil';
import Header from './Header';
import LoadingView from './LoadingView';
export default class Container extends Component {
constructor(props) {
super(props);
}
static propTypes = {
headerProps: PropTypes.object,
loadingProps: PropTypes.object,
};
static defaultProps = {
headerProps: {
mode: "simple",
backable: false,
hideDate: false,
adType: null
},
loadingProps: {
loading: false,
loadingText: "",
loadingColor: '',
loadingSize: 'small', // 'normal',
overlayColor: '',
cancelable: false,
loadingPaddingTop: 0,
loadingPaddingBottom: 0,
isEmpty: false,
emptyText: '',
loadingShowContent: false,
},
};
render() {
let theme = commonUtil.getTheme();
let styles = theme.style.component.Container;
return ( <
View style = {
styles.container
} > {
Platform.OS == 'ios' && < View style = {
{
alignSelf: 'stretch',
height: 20
}
}
/>
}
<
Header style = {
styles.header
} {
...this.props.headerProps
}
/> {
this.props.children
} <
/View>
);
}
}
{
/* <LoadingView style={styles.loadingView} {...this.props.loadingProps} >
{this.props.children}
</LoadingView> */
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# Toast
'use strict'
import React, { Component } from 'react';
import { View, Animated, Dimensions, Text } from 'react-native'
import { PropTypes } from 'prop-types';
import commonUtil from '../common/commonUtil';
export const DURATION = {
LENGTH_LONG: 2400,
LENGTH_SHORT: 500,
FOREVER: 0,
};
const { height, width } = Dimensions.get('window');
export default class Toast extends Component {
constructor(props) {
super(props);
this.state = {
isShow: false,
text: '',
opacityValue: new Animated.Value(this.props.opacity),
}
}
static propTypes = {
//style: PropTypes.style,
position: PropTypes.oneOf([
'top',
'center',
'bottom',
]),
//textStyle: PropTypes.style,
positionValue: PropTypes.number,
fadeInDuration: PropTypes.number,
fadeOutDuration: PropTypes.number,
opacity: PropTypes.number
}
static defaultProps = {
position: 'center',
//textStyle: {},
positionValue: 120,
fadeInDuration: 600,
fadeOutDuration: 600,
opacity: 1
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
}
show(text, duration) {
this.duration = typeof duration === 'number' ? duration : DURATION.LENGTH_LONG;
this.setState({ isShow: true, text: text, });
Animated.timing(
this.state.opacityValue,
{
toValue: this.props.opacity,
duration: this.props.fadeInDuration,
}
).start(() => {
this.isShow = true;
if (duration !== DURATION.FOREVER) this.close();
});
}
close(duration) {
let delay = typeof duration === 'undefined' ? this.duration : duration;
if (delay === DURATION.FOREVER) delay = this.props.defaultCloseDelay || 250;
if (!this.isShow && !this.state.isShow) return;
this.timer && clearTimeout(this.timer);
this.timer = setTimeout(() => {
Animated.timing(
this.state.opacityValue,
{
toValue: 0.0,
duration: this.props.fadeOutDuration,
}
).start(() => {
this.setState({
isShow: false,
});
this.isShow = false;
});
}, delay);
}
render() {
let theme = commonUtil.getTheme();
let styles = theme.style.component.Toast;
let positionStyle = {}
switch (this.props.position) {
case 'top':
positionStyle = { justifyContent: 'flex-start', paddingTop: 30 };
break;
case 'bottom':
positionStyle = { justifyContent: 'flex-start', paddingBottom: 30 };
break;
}
let view = this.state.isShow ?
<View style={[styles.container, positionStyle]} pointerEvents="none">
<Animated.View style={[styles.content, { opacity: this.state.opacityValue }]}>
<Text style={styles.text}>
{this.state.text}
</Text>
</Animated.View>
</View> : null;
return view;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# 渐变色
import LinearGradient from 'react-native-linear-gradient';
<LinearGradient
colors={['#0E95FF', '#5CC0FF']}
style={[styles.submit_btn]}
>
<Text style={styles.submit_btnTxt}>
{this.translate('提交')}
</Text>
</LinearGradient>
2
3
4
5
6
7
8
9
# 加载中容器
'use strict';
import React from 'react';
import { StyleSheet, View, Text, Modal, ActivityIndicator, TouchableOpacity } from 'react-native';
import { PropTypes } from 'prop-types';
import commonUtil from '../common/commonUtil';
const SIZES = ['small', 'large'];
const LOADINGSTYLES = ['inside', 'over'];
export default class LoadingView extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: this.props.loading,
loadingText: this.props.loadingText,
isEmpty: this.props.isEmpty,
loadingShowContent: this.props.loadingShowContent
};
}
static propTypes = {
loading: PropTypes.bool,
loadingText: PropTypes.string,
loadingColor: PropTypes.string,
loadingSize: PropTypes.oneOf(SIZES),
overlayColor: PropTypes.string,
cancelable: PropTypes.bool,
loadingPaddingTop: PropTypes.number,
loadingPaddingBottom: PropTypes.number,
isEmpty: PropTypes.bool,
emptyText: PropTypes.string,
loadingShowContent: PropTypes.bool,
relative: PropTypes.bool,
loadingStyle: PropTypes.oneOf(LOADINGSTYLES),
};
static defaultProps = {
loading: false,
loadingText: "",
loadingColor: '',
loadingSize: 'small', // 'normal',
overlayColor: '',
cancelable: false,
loadingPaddingTop: 0,
loadingPaddingBottom: 0,
isEmpty: false,
emptyText: '',
loadingShowContent: false,
relative: false,
loadingStyle: 'over'
};
close() {
this.setState({ loading: false });
}
componentWillReceiveProps(nextProps) {
const { loading, isEmpty, textContent } = nextProps;
this.setState({ loading, isEmpty, textContent });
}
_renderLoading(styles) {
if (this.state.loading) {
if (this.props.loadingStyle == 'over') {
return (
<TouchableOpacity onPress={() => {
this.props.cancelable
? this.setState({ loading: false })
: null
}} style={[
this.props.relative ? styles.background_relative : styles.background,
]}>
<View style={[styles.activityIndicatorContainer, this.props.overlayColor != '' && { backgroundColor: this.props.overlayColor }]}>
<ActivityIndicator
color={this.props.loadingColor == '' ? styles.loadingColor : this.props.loadingColor}
size={this.props.loadingSize}
/>
<Text style={styles.loadingText}>
{commonUtil.translate('加载中')}
</Text>
</View>
</TouchableOpacity>
);
}
else {
return (
<TouchableOpacity onPress={() => {
this.props.cancelable
? this.setState({ loading: false })
: null
}} style={[styles.background_relative]}>
<View style={[styles.activityIndicatorContainer, { backgroundColor: 'transparent' }]}>
<ActivityIndicator
color={styles.insideLoadingColor}
size={this.props.loadingSize}
/>
</View>
</TouchableOpacity>
);
}
}
else {
return null;
}
}
_renderContent(styles) {
let { loading, isEmpty, loadingShowContent } = this.state;
if (loading == false) {
if (isEmpty == true) {
return (
<View style={styles.emptyTextContainer}>
<Text style={styles.emptyText}>
{this.props.emptyText == '' ? commonUtil.translate('暂无数据') : this.props.emptyText}
</Text>
</View>
);
} else {
return this.props.children;
}
} else if (loadingShowContent) {
return this.props.children;
}
}
render() {
let theme = commonUtil.getTheme();
let styles = theme.style.component.LoadingView;
return (
<View
// {...this.props}
style={[styles.container, this.props.style && this.props.style]}>
{this._renderContent(styles)}
{this._renderLoading(styles)}
</View>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# 复杂组件封装
# 时间插件
# 安卓
时间需要格式化处理,时间是异步的,使用async/await和try/catch来同步。日期使用DatePickerAndroid, 时间使用TimePickerAndroid。
open(options): 打开一个标准的Android时间选择器的对话框。
options对象的key值如下:
- hour (0-23) - 要显示的小时,默认为当前时间。
- minute (0-59) - 要显示的分钟,默认为当前时间。
- is24Hour (boolean) - 如果设为true,则选择器会使用24小时制。如果设为false,则会额外显示AM/PM的选项。如果不设定,则采取当前地区的默认设置。
在用户选好时间后返回一个Promise,回调参数为一个对象,其中包含有action, hour (0-23), minute (0-59)。如果用户取消了对话框,Promise仍然会执行,返回的action为TimePickerAndroid. dismissedAction,其他几项参数则为undefined。所以请在使用其他值之前务必先检查action的值。
- dismissedAction: 取消对话框
不同的手机系统上显示的new Date()是不同的,有的显示的是:2016/4/15,有的是04/15/16,所以将时间用toLocalString()方法转成上面的字符串的时候,得到的结果不同。这时就需要将得到的时间拆分,然后重组。
import React, {
Component
} from 'react'
import {
DatePickerAndroid,
TimePickerAndroid
} from 'react-native'
import PropTypes from 'prop-types'
import moment from 'moment'
export default class CustomDatePickerAndroid extends Component {
static propTypes = {
date: PropTypes.instanceOf(Date),
mode: PropTypes.oneOf(['date', 'time', 'datetime']),
onCancel: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
is24Hour: PropTypes.bool,
isVisible: PropTypes.bool,
minimumDate: PropTypes.instanceOf(Date),
maximumDate: PropTypes.instanceOf(Date)
}
static defaultProps = {
date: new Date(),
mode: 'date',
is24Hour: true,
isVisible: false
}
componentWillReceiveProps(nextProps) {
const {
date
} = nextProps;
this.setState({
date
});
}
componentDidUpdate = (prevProps) => {
if (!prevProps.isVisible && this.props.isVisible) {
if (this.props.mode === 'date' || this.props.mode === 'datetime') {
this._showDatePickerAndroid()
} else {
this._showTimePickerAndroid()
}
}
}
_showDatePickerAndroid = async () => {
try {
const {
action,
year,
month,
day
} = await DatePickerAndroid.open({
date: this.props.date,
minDate: this.props.minimumDate,
maxDate: this.props.maximumDate
})
if (action !== DatePickerAndroid.dismissedAction) {
const date = moment({
year,
month,
day
}).toDate()
if (this.props.mode === 'datetime') {
// Prepopulate and show time picker
const timeOptions = !this.props.date ? {} : {
hour: this.props.date.getHours(),
minute: this.props.date.getMinutes()
};
TimePickerAndroid.open(timeOptions).then(({
action,
minute,
hour
}) => {
if (action === TimePickerAndroid.timeSetAction) {
let selectedDate = new Date(year, month, day, hour, minute);
this.props.onConfirm(selectedDate);
}
});
} else {
this.props.onConfirm(date)
}
} else {
this.props.onCancel()
}
} catch ({
code,
message
}) {
console.warn('Cannot open date picker', message)
}
}
_showTimePickerAndroid = async () => {
try {
const {
action,
hour,
minute
} = await TimePickerAndroid.open({
hour: moment(this.props.date).hour(),
minute: moment(this.props.date).minute(),
is24Hour: this.props.is24Hour
})
if (action !== TimePickerAndroid.dismissedAction) {
const date = moment({
hour,
minute
}).toDate()
this.props.onConfirm(date)
} else {
this.props.onCancel()
}
} catch ({
code,
message
}) {
console.warn('Cannot open time picker', message)
}
}
render() {
return null
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# IOS
DatePickerIOS
- date:选中的日期。
- maximumDate:最大日期。选择的日期大于此日期则回到此日期。
- minimumDate:最小日期。选择的日期小于此日期则回到此日期。
- mode:(‘date’, ‘time’, ‘datetime’) 选择器模式。
- minuteInterval:最小间隔。(1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30)
- onDateChange:日期变化大回调函数.
- timeZoneOffsetInMinutes:时区差,单位是分钟。默认是手机当前时区。
import React, {
Component
} from 'react'
import {
DatePickerIOS,
Text,
TouchableOpacity,
View,
ViewPropTypes
} from 'react-native'
import AnimatedModal from './animated-modal/index'
import PropTypes from 'prop-types'
import styles from './index.style'
export default class CustomDatePickerIOS extends Component {
static propTypes = {
cancelTextIOS: PropTypes.string,
confirmTextIOS: PropTypes.string,
customCancelButtonIOS: PropTypes.node,
customConfirmButtonIOS: PropTypes.node,
customTitleContainerIOS: PropTypes.node,
datePickerContainerStyleIOS: ViewPropTypes.style,
date: PropTypes.instanceOf(Date),
mode: PropTypes.oneOf(['date', 'time', 'datetime']),
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
titleIOS: PropTypes.string,
isVisible: PropTypes.bool
}
static defaultProps = {
cancelTextIOS: '取消',
confirmTextIOS: '确认',
date: new Date(),
mode: 'date',
titleIOS: '选择日期',
isVisible: false
}
state = {
date: this.props.date
}
_handleConfirm = () => this.props.onConfirm(this.state.date)
_handleDateChange = (date) => this.setState({
date
})
componentWillReceiveProps(nextProps) {
const {
date
} = nextProps;
this.setState({
date
});
}
render() {
const {
onCancel,
isVisible,
mode,
titleIOS,
confirmTextIOS,
cancelTextIOS,
customCancelButtonIOS,
customConfirmButtonIOS,
customTitleContainerIOS,
datePickerContainerStyleIOS,
date,
...otherProps
} = this.props
const titleContainer = ( <
View style = {
styles.titleContainer
} >
<
Text style = {
styles.title
} > {
titleIOS
} < /Text> < /
View >
)
const confirmButton = ( <
View style = {
styles.confirmButton
} >
<
Text style = {
styles.confirmText
} > {
confirmTextIOS
} < /Text> < /
View >
)
const cancelButton = ( <
View style = {
styles.cancelButton
} >
<
Text style = {
styles.cancelText
} > {
cancelTextIOS
} < /Text> < /
View >
)
return ( <
AnimatedModal isVisible = {
isVisible
}
style = {
styles.contentContainer
} >
<
View style = {
[styles.datepickerContainer, datePickerContainerStyleIOS]
} > {
customTitleContainerIOS || titleContainer
} <
DatePickerIOS date = {
this.state.date
}
mode = {
mode
}
onDateChange = {
this._handleDateChange
} {
...otherProps
}
/> <
TouchableOpacity onPress = {
this._handleConfirm
} > {
customConfirmButtonIOS || confirmButton
} <
/TouchableOpacity> < /
View > <
TouchableOpacity style = {
styles.cancelButton
}
onPress = {
onCancel
} > {
customCancelButtonIOS || cancelButton
} <
/TouchableOpacity> < /
AnimatedModal >
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 遮罩弹框
采用了react-native-animatable插件来增加动效
/* eslint-disable no-return-assign, no-unused-vars */
import React, {
Component
} from 'react'
import {
Dimensions,
Modal
} from 'react-native'
import PropTypes from 'prop-types'
import {
View
} from './../react-native-animatable/index'
import styles from './index.style.js'
export class AnimatedModal extends Component {
static propTypes = {
animationIn: PropTypes.string,
animationInTiming: PropTypes.number,
animationOut: PropTypes.string,
animationOutTiming: PropTypes.number,
backdropColor: PropTypes.string,
backdropOpacity: PropTypes.number,
backdropTransitionInTiming: PropTypes.number,
backdropTransitionOutTiming: PropTypes.number,
children: PropTypes.node.isRequired,
isVisible: PropTypes.bool.isRequired,
onModalShow: PropTypes.func,
onModalHide: PropTypes.func,
style: PropTypes.any
}
static defaultProps = {
animationIn: 'slideInUp',
animationInTiming: 300,
animationOut: 'slideOutDown',
animationOutTiming: 300,
backdropColor: 'black',
backdropOpacity: 0.70,
backdropTransitionInTiming: 300,
backdropTransitionOutTiming: 300,
onModalShow: () => null,
onModalHide: () => null,
isVisible: false
}
state = {
isVisible: false,
deviceWidth: Dimensions.get('window').width,
deviceHeight: Dimensions.get('window').height
}
componentWillReceiveProps(nextProps) {
if (!this.state.isVisible && nextProps.isVisible) {
this.setState({
isVisible: true
})
}
}
componentDidUpdate(prevProps, prevState) {
// On modal open request slide the view up and fade in the backdrop
if (this.state.isVisible && !prevState.isVisible) {
this._open()
// On modal close request slide the view down and fade out the backdrop
} else if (!this.props.isVisible && prevProps.isVisible) {
this._close()
}
}
_open = () => {
this.backdropRef.transitionTo({
opacity: this.props.backdropOpacity
}, this.props.backdropTransitionInTiming)
this.contentRef[this.props.animationIn](this.props.animationInTiming)
.then(() => {
this.props.onModalShow()
})
}
_close = async () => {
this.backdropRef.transitionTo({
opacity: 0
}, this.props.backdropTransitionOutTiming)
this.contentRef[this.props.animationOut](this.props.animationOutTiming)
.then(() => {
this.setState({
isVisible: false
})
this.props.onModalHide()
})
}
_handleLayout = (event) => {
const deviceWidth = Dimensions.get('window').width
const deviceHeight = Dimensions.get('window').height
if (deviceWidth !== this.state.deviceWidth || deviceHeight !== this.state.deviceHeight) {
this.setState({
deviceWidth,
deviceHeight
})
}
}
render() {
const {
animationIn,
animationInTiming,
animationOut,
animationOutTiming,
backdropColor,
backdropOpacity,
backdropTransitionInTiming,
backdropTransitionOutTiming,
children,
isVisible,
onModalShow,
onModalHide,
style,
...otherProps
} = this.props
const {
deviceWidth,
deviceHeight
} = this.state
return ( <
Modal transparent = {
true
}
animationType = {
'none'
}
visible = {
this.state.isVisible
}
onRequestClose = {
() => null
} {
...otherProps
} >
<
View onLayout = {
this._handleLayout
}
ref = {
(ref) => this.backdropRef = ref
}
style = {
[
styles.backdrop,
{
backgroundColor: backdropColor,
width: deviceWidth,
height: deviceHeight
}
]
}
/> <
View ref = {
(ref) => this.contentRef = ref
}
style = {
[{
margin: deviceWidth * 0.05
}, styles.content, style]
} {
...otherProps
} > {
children
} <
/View> < /
Modal >
)
}
}
export default AnimatedModal
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# 下拉刷新容器
RefreshControl可以进行刷新,这一组件可以用在 ScrollView 或 FlatList 内部,为其添加下拉刷新的功能。当 ScrollView 处于竖直方向的起点位置(scrollY: 0),此时下拉会触发一个onRefresh事件。
'use strict'
import React, { Component } from 'react';
import {
View,
StyleSheet,
ScrollView,
RefreshControl,
ActivityIndicator,
Dimensions,
Platform
} from 'react-native';
import { PropTypes } from 'prop-types';
import commonUtil from '../common/commonUtil';
import LoadingView from './LoadingView';
let { width, height } = Dimensions.get('window');
module.exports = class PullRefreshScrollView extends Component {
constructor(props) {
super(props);
this.state = {
height: 0,
contentHeight: 0
}
}
static propTypes = {
onRefresh: PropTypes.func,
refreshing: PropTypes.bool,
onLoadingMore: PropTypes.func,
loadingmore: PropTypes.bool,
loadingcompleted: PropTypes.bool,
loading: PropTypes.bool,
isEmpty: PropTypes.bool,
};
static defaultProps = {
refreshing: false,
loadingmore: false,
loadingcompleted: true,
loading: false,
isEmpty: false,
};
handleScroll(event) {
//console.log('EVN###',event.nativeEvent);
let { loadingmore, refreshing } = this.props;
if (refreshing == false) {
let nativeEvent = event.nativeEvent;
if (nativeEvent.contentOffset.y > nativeEvent.contentSize.height - nativeEvent.layoutMeasurement.height + (Platform.OS === 'android' ? -5 : 5) &&
loadingmore == false) {
this.props.onLoadingMore();
}
}
}
renderLoadingMore(styles) {
let { onLoadingMore, loadingmore } = this.props;
if (onLoadingMore != null && loadingmore == true) {
return (
<View style={styles.activityIndicatorContainer}>
<ActivityIndicator color={styles.loadingColor} size='large' style={{ flex: 1 }} />
</View>
);
}
}
scrollToEnd() {
if (this.state.contentHeight > (height - 240)) {
this.refs.scrollview.scrollToEnd({ animated: false });
}
}
render() {
let { onLoadingMore, loadingcompleted } = this.props;
let theme = commonUtil.getTheme();
let styles = theme.style.component.PullRefreshScrollView;
return (
<ScrollView
ref='scrollview'
{...this.props}
refreshControl={<RefreshControl
refreshing={this.props.refreshing}
onRefresh={this.props.onRefresh}
colors={[styles.loadingColor, styles.loadingColor + '88', styles.loadingColor + '22']}
tintColor={styles.loadingColor + '88'}
title=''
/>}
scrollEventThrottle={300}
onScroll={(onLoadingMore != null && loadingcompleted == false) ? this.handleScroll.bind(this) : null}
onContentSizeChange={(w, h) => this.setState({ contentHeight: h })}
style={[styles.container, this.props.style]}
>
<LoadingView
loading={this.props.loading}
isEmpty={this.props.isEmpty}
style={styles.loadingView}
relative={true}>
{this.props.children}
</LoadingView>
{this.renderLoadingMore(styles)}
{
Platform.OS === 'android'
?
(<View style={{ width: width, height: 90 }} />)
:
(null)
}
</ScrollView>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# 静态下拉框
import React, { Component } from 'react'
import { View, Text, Image, Dimensions, TextInput } from 'react-native'
import PropTypes from 'prop-types'
import ModalDropdown from 'react-native-modal-dropdown'
import commonUtil from '../common/commonUtil'
const { width, height } = Dimensions.get('window')
export default class Dropdown extends Component {
constructor(props) {
super(props)
this.state = {
idx: this.props.defaultIndex,
isShow:false
}
}
static propTypes = {
disabled: PropTypes.bool,
defaultIndex: PropTypes.number,
defaultValue: PropTypes.string,
options: PropTypes.array,
onSelect: PropTypes.func,
buttonTextStyle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
]),
buttonArrowStyle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
])
}
static defaultProps = {
disabled: false,
defaultIndex: -1,
defaultValue: '',
options: [],
buttonTextStyle: {},
buttonArrowStyle: {}
}
componentWillReceiveProps(nextProps) {
const { defaultIndex } = nextProps
if (nextProps.defaultIndex != this.props.defaultIndex) {
this.setState({ idx: defaultIndex })
}
}
renderRow(rowData, rowID, isSelected, styles, image) {
if (rowData.key == commonUtil.translate('过期预警')) {
return (
<View style={styles.dropdown_row_container}>
<View style={styles.dropdown_inputContainer}>
<TextInput
style={styles.dropdown_input}
underlineColorAndroid='transparent'
defaultValue='30'
/>
<Text
style={[
styles.dropdown_inputTxt,
isSelected && styles.dropdown_text_highlight
]}
>
{commonUtil.translate('天内过期')}
</Text>
</View>
{isSelected && (
<Image
style={[styles.dropdown_icon, styles.dropdown_inputImg]}
source={image.sort_select_icon}
/>
)}
</View>
)
}
return (
<View style={styles.dropdown_row_container}>
<Text
style={[
styles.dropdown_text,
isSelected && styles.dropdown_text_highlight
]}
>
{rowData.key}
</Text>
{isSelected && (
<Image style={styles.dropdown_icon} source={image.sort_select_icon} />
)}
</View>
)
}
renderSeparator(sectionID, rowID, adjacentRowHighlighted) {
return <View />
}
adjustFrame(style) {
style.left = 0
style.right = 0
style.top += 10
style.height = height - style.top
return style
}
onSelect(idx, rowData) {
this.setState({ idx: idx })
if (this.props.onSelect) {
this.props.onSelect(parseInt(idx), rowData)
}
}
render() {
let theme = commonUtil.getTheme()
let styles = theme.style.component.Dropdown
let image = theme.image
let btnText = this.props.defaultValue
if (this.state.idx >= 0) {
btnText = this.props.options[this.state.idx].key
}
return (
<ModalDropdown
options={this.props.options}
defaultIndex={this.props.defaultIndex}
dropdownStyle={styles.dropdown_style}
renderRow={(rowData, rowID, isSelected) => {
return this.renderRow(rowData, rowID, isSelected, styles, image)
}}
renderSeparator={this.renderSeparator.bind(this)}
adjustFrame={this.adjustFrame.bind(this)}
onSelect={this.onSelect.bind(this)}
onDropdownWillShow={()=>{
this.setState({
isShow:true
})
}}
onDropdownWillHide={()=>{
this.setState({
isShow:false
})
}}
>
<View style={styles.button_text_row}>
<Text
style={[styles.button_text_default, this.props.buttonTextStyle]}
>
{btnText}
</Text>
<Image
style={[styles.button_arrow_icon, this.props.buttonArrowStyle,this.state.isShow?styles.toggle_rotate:'']}
source={image.menu_arrow_icon}
/>
</View>
</ModalDropdown>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# 动态下拉列表
react-native-modal-dropdown组件不适用,将其重写从index变成id
# CustomModalDropdown. js:
/*
* @Author: ct
* @Date: 2019-09-02 14:48:02
* @Last Modified by: ct
* @Last Modified time: 2020-05-09 11:05:14
*/
'use strict'
import React, { Component } from 'react'
import {
StyleSheet,
Dimensions,
View,
Text,
ListView,
TouchableWithoutFeedback,
TouchableNativeFeedback,
TouchableOpacity,
TouchableHighlight,
Modal,
ActivityIndicator,
TextInput,
Image,
FlatList
} from 'react-native'
import PropTypes from 'prop-types'
import commonUtil from '../common/commonUtil'
const TOUCHABLE_ELEMENTS = [
'TouchableHighlight',
'TouchableOpacity',
'TouchableWithoutFeedback',
'TouchableNativeFeedback'
]
export default class ModalDropdown extends Component {
static propTypes = {
disabled: PropTypes.bool,
scrollEnabled: PropTypes.bool,
isSearch: PropTypes.bool,
defaultIndex: PropTypes.number,
defaultId: PropTypes.number,
defaultValue: PropTypes.string,
options: PropTypes.array,
accessible: PropTypes.bool,
animated: PropTypes.bool,
showsVerticalScrollIndicator: PropTypes.bool,
keyboardShouldPersistTaps: PropTypes.string,
style: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
]),
textStyle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
]),
dropdownStyle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
]),
dropdownTextStyle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
]),
dropdownTextHighlightStyle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
]),
adjustFrame: PropTypes.func,
renderRow: PropTypes.func,
renderSeparator: PropTypes.func,
renderButtonText: PropTypes.func,
onDropdownWillShow: PropTypes.func,
onDropdownWillHide: PropTypes.func,
onSelect: PropTypes.func
}
// defaultId 要获取总部的id
static defaultProps = {
isSearch: false,
disabled: false,
scrollEnabled: true,
defaultIndex: -1,
defaultId: 0,
defaultValue: 'Please select...',
options: null,
animated: true,
showsVerticalScrollIndicator: true,
keyboardShouldPersistTaps: 'never'
}
constructor(props) {
super(props)
this._button = null
this._buttonFrame = null
this._nextValue = null
this._nextIndex = null
this.state = {
accessible: !!props.accessible,
loading: !props.options,
showDropdown: false,
buttonText: props.defaultValue,
selectedIndex: props.defaultId,
keyword: '',
dataSource: this.props.options
}
console.log('this.props.options', this.props.options)
}
componentWillReceiveProps(nextProps) {
let { buttonText, selectedIndex } = this.state
const { defaultId, defaultValue, options } = nextProps
buttonText = this._nextValue == null ? buttonText : this._nextValue
selectedIndex = this._nextIndex == null ? selectedIndex : this._nextIndex
if (selectedIndex < 0) {
selectedIndex = defaultId
buttonText = defaultValue
}
this._nextValue = null
this._nextIndex = null
this.setState({
loading: !options,
buttonText,
selectedIndex
})
}
render() {
return (
<View {...this.props}>
{this._renderButton()}
{this._renderModal()}
</View>
)
}
_updatePosition(callback) {
if (this._button && this._button.measure) {
this._button.measure((fx, fy, width, height, px, py) => {
this._buttonFrame = { x: px, y: py, w: width, h: height }
callback && callback()
})
}
}
show() {
this._updatePosition(() => {
let selectIdx = 0
this.props.options.map((item, index) => {
if (item.id === this.props.defaultId) {
selectIdx = index
return
}
})
this.setState({
showDropdown: true
})
if (this._flatList && this._flatList.scrollToIndex) {
setTimeout(() => {
this._flatList.scrollToIndex({
animated: true,
index: selectIdx,
viewPosition: 0
})
}, 100)
}
})
}
hide() {
this.setState({
showDropdown: false,
keyword: ''
})
}
getOption(id) {
const { options } = this.props
let item = {}
options.map(k => {
if (k.id == id) {
item = k
}
})
return item
}
select(id) {
const { defaultValue, options, defaultId, renderButtonText } = this.props
let value = defaultValue
if (id == null || !options) {
id = defaultId
}
if (id > 0) {
value = renderButtonText
? renderButtonText(this.getOption(id))
: this.getOption(id).toString()
}
this._nextValue = value
this._nextIndex = id
this.setState({
buttonText: value,
selectedIndex: id
})
}
_renderButton() {
const { disabled, accessible, children, textStyle } = this.props
const { buttonText } = this.state
return (
<TouchableOpacity
ref={button => (this._button = button)}
disabled={disabled}
accessible={accessible}
onPress={this._onButtonPress}
>
{children || (
<View style={styles.button}>
<Text style={[styles.buttonText, textStyle]} numberOfLines={1}>
{buttonText}
</Text>
</View>
)}
</TouchableOpacity>
)
}
_onButtonPress = () => {
const { onDropdownWillShow } = this.props
if (!onDropdownWillShow || onDropdownWillShow() !== false) {
this.show()
}
}
_renderModal() {
const { animated, accessible, dropdownStyle } = this.props
const { showDropdown, loading } = this.state
if (showDropdown && this._buttonFrame) {
const frameStyle = this._calcPosition()
const animationType = animated ? 'fade' : 'none'
return (
<Modal
animationType={animationType}
visible={true}
transparent={true}
onRequestClose={this._onRequestClose}
supportedOrientations={[
'portrait',
'portrait-upside-down',
'landscape',
'landscape-left',
'landscape-right'
]}
>
<TouchableWithoutFeedback
accessible={accessible}
disabled={!showDropdown}
onPress={this._onModalPress}
>
<View style={styles.modal}>
<View
style={[
styles.dropdown,
dropdownStyle,
frameStyle,
loading ? { justifyContent: 'center' } : {}
]}
>
{loading ? this._renderLoading() : this._renderDropdown()}
</View>
</View>
</TouchableWithoutFeedback>
</Modal>
)
}
}
_calcPosition() {
const { dropdownStyle, style, adjustFrame } = this.props
const dimensions = Dimensions.get('window')
const windowWidth = dimensions.width
const windowHeight = dimensions.height
const dropdownHeight =
(dropdownStyle && StyleSheet.flatten(dropdownStyle).height) ||
StyleSheet.flatten(styles.dropdown).height
const bottomSpace = windowHeight - this._buttonFrame.y - this._buttonFrame.h
const rightSpace = windowWidth - this._buttonFrame.x
const showInBottom =
bottomSpace >= dropdownHeight || bottomSpace >= this._buttonFrame.y
const showInLeft = rightSpace >= this._buttonFrame.x
const positionStyle = {
height: dropdownHeight,
top: showInBottom
? this._buttonFrame.y + this._buttonFrame.h
: Math.max(0, this._buttonFrame.y - dropdownHeight)
}
if (showInLeft) {
positionStyle.left = this._buttonFrame.x
} else {
const dropdownWidth =
(dropdownStyle && StyleSheet.flatten(dropdownStyle).width) ||
(style && StyleSheet.flatten(style).width) ||
-1
if (dropdownWidth !== -1) {
positionStyle.width = dropdownWidth
}
positionStyle.right = rightSpace - this._buttonFrame.w
}
return adjustFrame ? adjustFrame(positionStyle) : positionStyle
}
_onRequestClose = () => {
const { onDropdownWillHide } = this.props
if (!onDropdownWillHide || onDropdownWillHide() !== false) {
this.hide()
}
}
_onModalPress = () => {
const { onDropdownWillHide } = this.props
if (!onDropdownWillHide || onDropdownWillHide() !== false) {
this.hide()
}
}
_renderLoading() {
return <ActivityIndicator size='small' />
}
_renderDropdown() {
const {
scrollEnabled,
renderSeparator,
showsVerticalScrollIndicator,
keyboardShouldPersistTaps,
isSearch,
options
} = this.props
return (
<View
style={{
backgroundColor: '#fff'
}}
>
{this.props.options.length > 15 && (
<View style={styles.search_wrapper}>
<Image
style={styles.date_icon}
source={require('../common/themes/default/images/search-icon.png')}
/>
<TextInput
style={styles.text_input}
underlineColorAndroid='transparent'
placeholder='请输入关键字'
value={this.state.keyword}
onChangeText={text => {
this.setState({ keyword: text })
this._dataSource(text)
}}
/>
</View>
)}
<FlatList
ref={flatList => (this._flatList = flatList)}
data={this.state.dataSource}
extraData={this.state}
renderItem={this._renderItem}
scrollEnabled={scrollEnabled}
automaticallyAdjustContentInsets={false}
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
keyExtractor={ (item,index)=>item.key }
getItemLayout={(data, index) => {
return { length: 50, offset: 50 * index, index }
}}
/>
</View>
)
}
_dataSource(keyword) {
const { options } = this.props
let cacheList = []
options.map(item => {
if (item.key.indexOf(keyword) > -1) {
cacheList.push(item)
}
})
this.setState({
dataSource: cacheList
})
}
_onLayout = e => {
let { x, y, width, height } = e.nativeEvent.layout
console.log(height)
}
_renderItem = ({ item, index, separators }) => {
let theme = commonUtil.getTheme()
let styles = theme.style.component.Dropdown
let image = theme.image
const {
dropdownTextStyle,
dropdownTextHighlightStyle,
accessible
} = this.props
const { selectedIndex } = this.state
const key = `row_${item.value.userId}`
const highlighted = item.value.userId == selectedIndex
const row = (
<View
onLayout={this._onLayout}
style={[
styles.dropdown_row_container,
{ height: 50 }
]}
>
<Text
style={[
styles.dropdown_text,
highlighted && styles.dropdown_text_highlight
]}
>
{item.key}
</Text>
{highlighted && (
<Image style={styles.dropdown_icon} source={image.sort_select_icon} />
)}
</View>
)
const preservedProps = {
key,
accessible,
onPress: () => this._onRowPress(item)
}
if (TOUCHABLE_ELEMENTS.find(name => name == row.type.displayName)) {
const props = { ...row.props }
props.key = preservedProps.key
props.onPress = preservedProps.onPress
const { children } = row.props
switch (row.type.displayName) {
case 'TouchableHighlight': {
return <TouchableHighlight {...props}>{children}</TouchableHighlight>
}
case 'TouchableOpacity': {
return <TouchableOpacity {...props}>{children}</TouchableOpacity>
}
case 'TouchableWithoutFeedback': {
return (
<TouchableWithoutFeedback {...props}>
{children}
</TouchableWithoutFeedback>
)
}
case 'TouchableNativeFeedback': {
return (
<TouchableNativeFeedback {...props}>
{children}
</TouchableNativeFeedback>
)
}
default:
break
}
}
return <TouchableHighlight {...preservedProps}>{row}</TouchableHighlight>
}
_onRowPress(item) {
const { onSelect, renderButtonText, onDropdownWillHide } = this.props
if (!onSelect || onSelect(item.value.userId, item) !== false) {
// highlightRow(sectionID, rowID)
const value =
(renderButtonText && renderButtonText(item.value.userId)) ||
item.value.userId.toString()
this._nextValue = value
this._nextIndex = item.value.userId
this.setState({
buttonText: value,
selectedIndex: item.value.userId,
keyword: ''
})
this._dataSource('')
}
if (!onDropdownWillHide || onDropdownWillHide() !== false) {
this.setState({
showDropdown: false
})
}
}
}
const styles = StyleSheet.create({
button: {
justifyContent: 'center'
},
buttonText: {
fontSize: 12
},
modal: {
flexGrow: 1
},
dropdown: {
position: 'absolute',
height: (33 + StyleSheet.hairlineWidth) * 5,
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'lightgray',
borderRadius: 2,
backgroundColor: 'white'
},
loading: {
alignSelf: 'center'
},
list: {
//flexGrow: 1,
},
rowText: {
paddingHorizontal: 6,
paddingVertical: 10,
fontSize: 11,
color: 'gray',
backgroundColor: 'white',
textAlignVertical: 'center'
},
highlightedRowText: {
color: 'black'
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: 'lightgray'
},
search_wrapper: {
flexDirection: 'row',
backgroundColor: '#f6f6f6',
justifyContent: 'center',
alignContent: 'center',
alignItems: 'center',
paddingHorizontal: 15,
borderRadius: 20,
marginVertical: 10,
marginHorizontal: 10
},
text_input: {
flex: 1,
backgroundColor: '#f6f6f6',
color: '#212121',
height: 40,
padding: 10,
fontSize: 16
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
# CustomDropdown. js
import React, { Component } from 'react'
import { View, Text, Image, Dimensions, Platform,TextInput } from 'react-native'
import PropTypes from 'prop-types'
import CustomModalDropdown from './CustomModalDropdown'
import commonUtil from '../common/commonUtil'
const { width, height } = Dimensions.get('window')
export default class Dropdown extends Component {
constructor(props) {
super(props)
this.state = {
idx: this.props.defaultIndex,
isShow: true
}
}
static propTypes = {
disabled: PropTypes.bool,
defaultIndex: PropTypes.number,
defaultId: PropTypes.number,
defaultValue: PropTypes.string,
options: PropTypes.array,
onSelect: PropTypes.func,
buttonTextStyle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
]),
buttonArrowStyle: PropTypes.oneOfType([
PropTypes.number,
PropTypes.object,
PropTypes.array
]),
isSearch: PropTypes.bool
}
static defaultProps = {
disabled: false,
defaultIndex: -1,
defaultId: -1,
defaultValue: '',
options: [],
buttonTextStyle: {},
buttonArrowStyle: {},
isSearch: false
}
componentWillReceiveProps(nextProps) {
const { defaultIndex } = nextProps
if (nextProps.defaultIndex != this.props.defaultIndex) {
this.setState({ idx: defaultIndex })
}
}
renderRow(rowData, rowID, isSelected, styles, image) {
return (
<View style={styles.dropdown_row_container}>
<Text
style={[
styles.dropdown_text,
isSelected && styles.dropdown_text_highlight
]}
>
{rowData.key}
</Text>
{isSelected && (
<Image style={styles.dropdown_icon} source={image.sort_select_icon} />
)}
</View>
)
}
renderSeparator(sectionID, rowID, adjacentRowHighlighted) {
return <View />
}
adjustFrame(style) {
style.left = 0
style.right = 0
style.top += 10
let searchHeight = 0
// let searchHeight = (Platform.OS == 'ios' ? 35 : 0)
// Platform.OS == 'ios' ? 35 : 60
style.height = this.props.options.length > 15 ? height - style.top- searchHeight : height - style.top
style.paddingBottom = (Platform.OS == 'ios' ? 60 : 80)
return style
}
onSelect(idx, rowData) {
this.setState({ idx: idx })
if (this.props.onSelect) {
this.props.onSelect(parseInt(idx), rowData)
}
}
onDropdownWillShow(){
}
render() {
let theme = commonUtil.getTheme()
let styles = theme.style.component.Dropdown
let image = theme.image
let btnText = this.props.defaultValue
// if (this.state.idx >= 0) {
// btnText = this.props.options[this.state.idx].key
// }
if (this.state.idx != this.props.defaultId) {
this.props.options.map(item => {
if (item.id == this.state.idx) {
btnText = item.key
}
})
}
return (
<CustomModalDropdown
options={this.props.options}
defaultIndex={this.props.defaultIndex}
defaultId={this.props.defaultId}
dropdownStyle={styles.dropdown_style}
adjustFrame={this.adjustFrame.bind(this)}
onSelect={this.onSelect.bind(this)}
onDropdownWillShow={() => {
this.setState({
isShow: true
})
}}
onDropdownWillHide={() => {
this.setState({
isShow: false
})
}}
>
<View style={styles.button_text_row}>
<Text
style={[styles.button_text_default, this.props.buttonTextStyle]}
>
{btnText}
</Text>
<Image
style={[
styles.button_arrow_icon,
this.props.buttonArrowStyle,
this.state.isShow ? styles.toggle_rotate : ''
]}
source={image.menu_arrow_icon}
/>
</View>
</CustomModalDropdown>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# 数据图表封装
# 线状图
import React, { Component } from 'react';
import { ScrollView, PanResponder, Dimensions } from 'react-native';
import Svg, { Path, Text, Circle, Line, Rect } from 'react-native-svg';
import { PropTypes } from 'prop-types';
import commonUtil from '../common/commonUtil';
var d3 = require('d3');
export default class LineChart extends Component {
constructor(props) {
super(props);
this.state = {
selectedIndex: this.props.data && this.props.data.length > 0 ? this.props.data.length - 1 : null,
chartWidth: this.props.data.length > 7 ? (this.props.width + (this.props.data.length - 7) * 50) : (this.props.width || 300)
};
}
static propTypes = {
data: PropTypes.array,
width: PropTypes.number,
height: PropTypes.number,
top: PropTypes.number,
selectedIndex: PropTypes.number,
onSelectIndexChange: PropTypes.func,
};
static defaultProps = {
data: [],
width: Dimensions.get('window').width,
height: 200,
top: 0,
};
componentWillReceiveProps(nextProps) {
if (nextProps.data != this.props.data) {
this.setState({ chartWidth: nextProps.data.length > 7 ? (this.props.width + (nextProps.data.length - 7) * 50) : this.props.width })
}
}
// 背景线
bgline(data, styles) {
let lines = [];
let dY = (this.props.height - styles.xLabelHeight) / 4;
for (let i = 0; i < 5; i++) {
lines.push(
<Line
key={ `L_${i}` }
x1={0}
y1={i * dY}
x2={this.state.chartWidth}
y2={i * dY}
stroke={styles.bgLine}
strokeWidth={1}
/>
);
}
return lines;
}
// 区域
areas(data, styles) {
var returnValue = d => d.value;
var x = d3.scaleLinear().domain([0, data.length - 1]).range([styles.paddingX, this.state.chartWidth - styles.paddingX]);
let maxY = d3.max(data, returnValue);
maxY = maxY || 1;
var y = d3.scaleLinear().domain([maxY, d3.min(data, returnValue)]).range([styles.paddingY - 15, this.props.height - (styles.paddingY)]);
let y1 = this.props.height - styles.xLabelHeight;
var area = d3.area().x(function (d, i) {
return x(i);
}).y(function (d) {
return y(d.value);
}).y1(function (d) { return y1 });
return (
<Path
fill={styles.primaryColor}
fillOpacity={0.1}
d={area(data)}
/>
);
}
// 描绘线
paths(data, styles) {
var returnValue = d => d.value;
var x = d3.scaleLinear().domain([0, data.length - 1]).range([styles.paddingX, this.state.chartWidth - styles.paddingX]);
let maxY = d3.max(data, returnValue);
maxY = maxY || 1;
var y = d3.scaleLinear().domain([maxY, d3.min(data, returnValue)]).range([styles.paddingY - 15, this.props.height - (styles.paddingY)]);
var line = d3.line().x(function (d, i) {
return x(i);
}).y(function (d) {
return y(d.value);
});
return (
<Path
fill="none"
stroke={styles.primaryColor}
strokeWidth={2}
strokeMiterlimit={10}
d={line(data)}
/>
);
}
// 描绘点
points(data, styles) {
var returnValue = d => d.value;
var x = d3.scaleLinear().domain([0, this.props.data.length - 1]).range([styles.paddingX, this.state.chartWidth - styles.paddingX]);
var y = d3.scaleLinear().domain([d3.max(data, returnValue), d3.min(data, returnValue)]).range([styles.paddingY - 15, this.props.height - (styles.paddingY)]);
var paths = [];
data.map((point, _index) => {
// if (point.value === 0) {
// return;
// }
var cx = x(_index) + '';
var cy = y(point.value) + '';
if (this.state.selectedIndex == _index) {
paths.push(
<Line
key={-1}
x1={cx}
y1={0}
x2={cx}
y2={this.props.height - styles.xLabelHeight}
stroke={styles.primaryColor}
strokeOpacity={'0.2'}
strokeWidth={1}
/>
);
paths.push(
<Circle
key={_index}
cx={cx}
cy={cy}
r={4}
fill={'#FFF'}
strokeWidth={2}
stroke={styles.primaryColor}
/>
);
paths.push(
<Rect
key={-4}
x={cx - 30}
y={cy - 33}
rx={11}
ry={11}
width={65}
height={22}
fill={styles.primaryColor}
/>
);
paths.push(
<Text
key={-3}
fill={'#FFF'}
strokeWidth={0}
fontSize={11}
fontWeight={'normal'}
x={cx }
y={cy - 18}
textAnchor={'middle'}
>
{point.displayValue}
</Text>
);
}
else {
paths.push(
<Circle
key={_index + 'T'}
cx={cx}
cy={cy}
r={21}
fill={'#ffffff01'}
onPress={() => {
this.setState({ selectedIndex: _index });
if (this.props.onSelectIndexChange) {
this.props.onSelectIndexChange(_index);
}
}}
/>
);
}
});
return paths;
}
// 描绘标签文字
labels(data, styles) {
try {
var x = d3.scaleLinear().domain([0, this.props.data.length - 1]).range([styles.paddingX, this.state.chartWidth - styles.paddingX]);
var paths = [];
data.map((point, _index) => {
var cx = x(_index);
paths.push(
<Text
key={'LT' + _index} // 上下两个_index相同 会报wranning
fill={this.state.selectedIndex == _index ? styles.xLabelActiveColor : styles.xLabelColor}
strokeWidth={0}
fontSize="12"
fontWeight={this.state.selectedIndex == _index ? "bold" : "normal"}
x={cx}
y={this.props.height - 15}
textAnchor="middle"
onPress={() => {
this.setState({ selectedIndex: _index });
if (this.props.onSelectIndexChange) {
this.props.onSelectIndexChange(_index);
}
}}
>
{point.xLabel}
</Text>
);
paths.push(
<Text
key={'lt' + _index}
fill={this.state.selectedIndex == _index ? styles.xLabelActiveColor : styles.xLabelColor}
strokeWidth={0}
fontSize="8"
fontWeight={this.state.selectedIndex == _index ? "bold" : "normal"}
x={cx}
y={this.props.height - 3}
textAnchor="middle"
onPress={() => {
this.setState({ selectedIndex: _index });
if (this.props.onSelectIndexChange) {
this.props.onSelectIndexChange(_index);
}
}}
>
{point.xLabel2}
</Text>
);
});
return paths;
} catch (error) {
}
}
render() {
let theme = commonUtil.getTheme();
let styles = theme.style.component.LineChart;
let { chartWidth, selectedIndex } = this.state;
let { data, width } = this.props;
let scorllX = 0;
if (data.length > 7) {
if (selectedIndex) {
scorllX = (selectedIndex + 1 - 7) * 50;
} else {
scorllX = chartWidth - width - 15;
}
}
return (
<ScrollView
style={[this.props.style, { width: this.props.width }]}
ref="chartView"
automaticallyAdjustContentInsets={false}
horizontal={true}
showsHorizontalScrollIndicator={false}
onContentSizeChange={(e) => {
this.refs.chartView.scrollTo({ animated: false, x: scorllX, y: 0 });
}}>
{(data != null && data.length > 0) ? (
<Svg
width={chartWidth}
height={this.props.height}
forceUpdate="0"
style={{
width: chartWidth,
height: this.props.height,
}}
>
{this.bgline(data, styles)}
{this.areas(data, styles)}
{this.paths(data, styles)}
{this.points(data, styles)}
{this.labels(data, styles)}
</Svg>
) : null}
</ScrollView>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# 饼状图+柱形图
victory插件
import {
React,
View,
Text,
Component,
ImageBackground,
LinearGradient,
commonUtil
} from '../common/importUtil';
import {
VictoryBar,
VictoryChart,
VictoryTheme,
VictoryArea,
VictoryPie,
VictoryGroup,
VictoryScatter,
VictoryLabel,
VictoryLine,
VictoryAxis,
VictoryPolarAxis,
VictoryLegend,
VictoryZoomContainer,
VictoryBrushContainer,
} from "victory-native";
import { Dimensions } from 'react-native';
let {
width,
height
} = Dimensions.get('window');
import { PropTypes } from 'prop-types';
import { Svg, Text as SvgText, TSpan } from 'react-native-svg';
import victoryChart from 'victory-native/lib/components/victory-chart';
// 饼图
class PieChart extends Component {
constructor(props) {
super(props);
this.state = {
};
}
static = {
data: PropTypes.array,
centerNumber: PropTypes.number,
centerTxt: PropTypes.string,
colors: PropTypes.array,
renderListItem: PropTypes.func,
formatCenterNumber: PropTypes.func,
formatPercent: PropTypes.func,
formatNum: PropTypes.func,
formatName: PropTypes.func,
pieSize: PropTypes.number,
innerRadius: PropTypes.number,
hidePercent: PropTypes.bool,
spaceHeight:PropTypes.number,
}
static defaultProps = {
data: [],
centerNumber: 0,
colors: ["#0E95FF", "#FF9B39", "#24CB5E", "#FF6585", "#8B7BFF", "#02D2D8", "#607084", "#78C812", "#24CB5E", "#FF9B39", "#0E95FF", "#C34064", "#AA8064"],
centerTxt: '',
renderListItem: null,
formatCenterNumber: null,
formatPercent: null,
formatNum: null,
formatName: null,
pieSize: 200,
innerRadius: 64,
hidePercent: false,
spaceHeight:0
}
render() {
let styles = this._styles()
let txtList = []
let centerNumber = 0
let data = []
if (this.props.data) {
data = this.props.data.map(p => {
if (!p.ignore) {
centerNumber = centerNumber + p.num;
}
return p.num;
})
}
if (centerNumber == 0) {
data = [1]
}
if (this.props.centerNumber) {
centerNumber = this.props.centerNumber
} else if (this.props.formatCenterNumber) {
centerNumber = this.props.formatCenterNumber(centerNumber)
} else {
centerNumber = commonUtil.formatDecimal(centerNumber, 2)
}
let colors = this.props.colors
if (this.props.data) {
for (let i = 0; i < this.props.data.length; i++) {
let item = this.props.data[i];
if (this.props.renderListItem) {
txtList.push(
<View style={[styles.item,{height:20}]} key={i}>
<View style={[{ backgroundColor: `${colors[i % colors.length]}` }, styles.item_hd]}></View>
{this.props.renderListItem(item)}
</View>
);
}
else {
txtList.push(
<View style={[styles.item,{height:20}]} key={i}>
<View style={[{ backgroundColor: `${colors[i % colors.length]}` }, styles.item_hd]}></View>
<Text style={styles.item_name}>
{this.props.formatName ? this.props.formatName(item.name) : item.name}
</Text>
{
!this.props.hidePercent &&
<Text style={styles.item_bd}>
{this.props.formatPercent ? this.props.formatPercent(item.per) : item.per}
</Text>
}
<Text style={styles.item_ft}>
{this.props.formatNum ? this.props.formatNum(item.num) : item.num}
</Text>
</View>
);
}
}
}
let pieSize = this.props.pieSize || 200;
let innerRadius = this.props.innerRadius || 64;
return (
<View style={[styles.ViewContian, this.props.style]}>
<View style={styles.picLeft}>
<Svg width={pieSize} height={pieSize}>
<VictoryPie
colorScale={colors}
standalone={false}
width={pieSize} height={pieSize} padding={10}
innerRadius={innerRadius}
startAngle={-30}
endAngle={420}
labels={(d) => { }}
data={data}
/>
</Svg>
<View style={{
position: 'absolute',
top: 0,
left: 0,
width: pieSize,
height: pieSize,
alignItems: 'center',
justifyContent: 'center'
}}>
<Text
style={[styles.centerPrice, { fontSize: commonUtil.formatTextSize(centerNumber, 8, 24) }]}
>
{centerNumber}
</Text>
<Text
style={[styles.centerTxt]}
>
{this.props.centerTxt}
</Text>
</View>
</View>
<View style={[{flex: 1},styles.fullContainer]}>
<View style={[styles.items,{marginTop:this.props.spaceHeight}]}>
{txtList}
</View>
</View>
</View>
);
}
_styles() {
return {
ViewContian: {
// flexDirection: 'row',
width:width,
paddingLeft: 15,
paddingRight: 15,
alignItems: 'center',
},
picLeft: {
//marginRight: 10
},
centerPrice: {
color: '#212121',
fontSize: 24,
},
centerTxt: {
color: '#BDBDBD',
fontSize: 10,
},
fullContainer:{
width:width,
paddingHorizontal: 18,
},
items: {
flexDirection: 'column',
//justifyContent: 'center',
flex: 1,
},
item: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-around',
marginTop: 6,
marginBottom: 6,
},
item_hd: {
marginRight: 4,
width: 8,
height: 8,
borderRadius: 4,
},
item_name: {
fontSize: 12,
color: '#212121',
flex: 4
},
item_bd: {
fontSize: 12,
color: '#9E9E9E',
textAlign: 'right',
flex: 2,
},
item_ft: {
fontSize: 12,
color: '#212121',
textAlign: 'right',
flex: 3
},
}
}
}
// 柱状图
class BarChart extends Component {
constructor(props) {
super(props);
this.state = {};
}
theme() {
// let theme = this.theme();
return {
"area": {
"style": {
"data": {
"fill": "#212121"
},
},
},
//里面的虚线。
"axis": {
"style": {
"axis": {
"fill": "transparent",
"stroke": "transparent",
"strokeWidth": 1,
},
"grid": {
"fill": "none",
"stroke": "transparent",
"strokeDasharray": "6, 10",
"strokeLinecap": "round",
"strokeLinejoin": "round",
"pointerEvents": "painted"
},
// "ticks": {
// "fill": "transparent",
// "size": 5,
// "stroke": "#90A4AE",
// "strokeWidth": 0,
// "strokeLinecap": "round",
// "strokeLinejoin": "round"
// },
//xy的标题文字
"tickLabels": {
"fontFamily": "'Roboto', 'Helvetica Neue', Helvetica, sans-serif",
"fontSize": 12,
"letterSpacing": "normal",
"padding": 18,
"fill": "#9E9E9E",
"stroke": "transparent",
"strokeWidth": 1
}
},
},
//柱子顶部的字
"bar": {
"style": {
"data": {
"fill": "#455A64",
"padding": 8,
"strokeWidth": 0
},
"labels": {
"fontFamily": "'Roboto', 'Helvetica Neue', Helvetica, sans-serif",
"fontSize": 12,
"letterSpacing": "normal",
"padding": 8,
"fill": "#455A64",
"stroke": "transparent",
"strokeWidth": 0
}
},
"width": 350,
"height": 350,
"padding": 50
},
"chart": {
"width": width,
"height": 250,
"padding": 40,
"paddingTop": 0,
},
}
}
static = {
data: PropTypes.array,
}
static defaultProps = {
data: [], //[{x:'',y:'',yTopName:''}] yName 是y轴显示的名称。
}
render() {
//console.log('bar chart data ', this.props.data)
let maxNumber = 0
if (this.props.data) {
this.props.data.forEach(p => {
if (p.y > maxNumber) {
maxNumber = p.y
}
})
}
return (
<View >
<VictoryChart
theme={this.theme()}
domainPadding={{ x: [20, 0], y: [-10, 0] }}
// maxDomain={{ y: maxNumber + (maxNumber / 5) }}
>
<VictoryBar
alignment="middle"
data={this.props.data}
categories={{ x: this.props.data.map(p => p.x) }}
style={{
data: { fill: "#0E95FF" },
labels: { fill: "#0E95FF", fontSize: 10, }
}}
labels={(d) => d._y}
labelComponent={<VictoryLabel
text={(datum) => datum.yTopName}
textAnchor={'middle'}
/>}
/>
</VictoryChart>
</View>
);
}
}
export { PieChart, BarChart };
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# 上传图片
react-native-image-crop-picker可剪裁的图片上传
import {
React,
View,
Text,
Component,
TouchableOpacity,
commonUtil,
httpUtil,
Container,
ScrollView,
ToggleSwitch,
RadioButton,
Image,
config,
storageUtil,
InteractionManager,
ImageBackground,
LinearGradient,
Toast,
Alert,
KeyboardAvoidingView
} from '../../common/importUtil'
import { TextInput } from 'react-native'
import ImagePicker from 'react-native-image-crop-picker'
export default class feedback extends Component {
constructor(props) {
super(props)
this.state = {
isOn: true,
typeOptions: [this.translate('功能建议'), this.translate('体验建议'), this.translate('异常反馈'), this.translate('其他')],
typeSelect: this.translate('功能建议'),
avatarSource: [],
}
}
// 提交
submitData() {
// params.base64Images = this.state.avatarSource
}
// 图片
renderAddImageView() {
let theme = this.theme()
let styles = theme.style.pages.more.feedback
let commonstyles = theme.style.common
let image = theme.image
var pages = []
if (this.state.avatarSource.length > 0) {
let images = this.state.avatarSource
images.forEach((url,index) => {
pages.push(
<ImageBackground index={1} style={styles.feedback_image} key={index}>
<ImageBackground source={{ uri: url }} style={styles.uploadImage} />
<TouchableOpacity
style={styles.rightDelButton}
onPress={() => this.deleteLoadedImage(url)}
>
<Image style={styles.deleteBtn} source={image.dot_del_icon} />
</TouchableOpacity>
</ImageBackground>
)
})
}
//注意这里,如果图片数量小于5,那么我们需要显示可以继续添加。
if (this.state.avatarSource.length < 5) {
pages.push(
<ImageBackground style={styles.feedback_image} key={-1}>
<TouchableOpacity onPress={this.addOnClicked.bind(this)}>
<Image
style={{ width: 56, height: 56 }}
source={image.add_pic_box}
/>
</TouchableOpacity>
</ImageBackground>
)
}
return pages
}
addOnClicked() {
ImagePicker.openPicker({
width: 400,
height: 400,
cropping: true,
includeBase64: true,
includeExif: true
}).then(image => {
let images = this.state.avatarSource
images.push( `data:${image.mime};base64,` + image.data)
this.setState({
avatarSource: images
})
})
}
deleteLoadedImage(url) {
let imageUrls = this.state.avatarSource
//从set中删除掉url
for (let i in imageUrls) {
if (imageUrls[i] == url) {
imageUrls.splice(i, 1)
return
}
}
//重新刷新视图
this.setState({ avatarSource: imageUrls })
}
render() {
let theme = this.theme()
let styles = theme.style.pages.more.feedback
let commonstyles = theme.style.common
let image = theme.image
return (
<Container
headerProps={{
mode: 'simple',
backable: true,
hideDate: true,
navigation: this.props.navigation
}}
>
<ScrollView style={commonstyles.greyBg}>
<View style={[commonstyles.greyBg, { height: 12 }]} />
<View style={styles.feed_container}>
<KeyboardAvoidingView
style={{width:'100%'}}
behavior="padding"
keyboardVerticalOffset={100}
>
<View style={styles.image_container}>
{this.renderAddImageView()}
</View>
<TouchableOpacity
style={styles.submit_container}
onPress={() => {
this.submitData()
}}
>
<LinearGradient
colors={['#0E95FF', '#5CC0FF']}
style={[styles.submit_btn]}
>
<Text style={styles.submit_btnTxt}>
{this.translate('提交')}
</Text>
</LinearGradient>
</TouchableOpacity>
</KeyboardAvoidingView>
</View>
</ScrollView>
<Toast ref='toast' />
</Container>
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 友盟
var { NativeModules } = require('react-native');
module.exports = NativeModules.UMPushModule;
2
var { NativeModules } = require('react-native');
module.exports = NativeModules.UMAnalyticsModule;
2
# 代码报错
# setState批量延迟问题
componentWillUnmount() {
this.setState = (state, callback) => {
return;
};
}
2
3
4
5
# 背景图的Text自带白色背景
< ImageBackground
style = {
styles.imgWraper
}
source = {
require("./img/bg.png")
} >
<
Text style = {
styles.ntTitle
} > 欢迎~ < /Text> < /
ImageBackground >
2
3
4
5
6
7
8
9
10
11
12
给ImageBackground添加一个透明背景。ImageBackground的source属性必填否则部分机型会闪退。
imgWraper: {
height: 115,
width: "100%",
backgroundColor: "transparent"
}
2
3
4
5
6
# 编译报错
# library not found for xx
- 查找 targets => build phases => link binary with Libraries,删除该引用
![image. png](https://cdn.nlark.com/yuque/0/2020/png/543173/1599032919211-15a06823-35da-46be-84d2-1a26e321e400.png#align=left&display=inline&height=670&margin=%5Bobject%20Object%5D&name=image. png&originHeight=1340&originWidth=2346&size=1439721&status=done&style=none&width=1173)
- 清空报错,重新编译
![image. png](https://cdn.nlark.com/yuque/0/2020/png/543173/1599033051936-a5890cea-77dd-489d-a532-44d568e03f7f.png#align=left&display=inline&height=344&margin=%5Bobject%20Object%5D&name=image. png&originHeight=688&originWidth=406&size=314278&status=done&style=none&width=203)
# 报错链接
- https://www.jianshu.com/p/38aeb9cd97b3
- https://www.jianshu.com/p/79aa379282fa
- https://www.cnblogs.com/pengsi/p/7397166.html
- https://www.cnblogs.com/wood-life/p/10577812.html
- https://www.jianshu.com/p/b0cce9e5c952
# 插件链接
- 截屏组件(react-native-view-shot)
- 文档查看组件 (react-native-doc-viewer)
- 圆形进度条组件(react-native-circular-progress)
- 弹出框组件 (react-native-popup-dialog)
- 表格组件 (react-native-data-table)
- 图片组件 (react-native-image-viewer)
- 智能提示输入框组件 (react-native-autocomplete-input)
- 图片选择裁剪组件 (react-native-image-crop-picker)
- 二维码组件 (react-native-qrcode)
- 悬浮按钮组件 (react-native-action-button)
- 日期时间选择组件 (react-native-datepicker)