Commit 8fb450c1 by peii

添加typescript支持

parent 34bf3598
Showing with 1727 additions and 3 deletions
......@@ -5,8 +5,15 @@
* @format
*/
module.exports = {
const { getDefaultConfig } = require('metro-config')
module.exports = (async () => {
const {
resolver: { sourceExts },
} = await getDefaultConfig()
return {
transformer: {
babelTransformerPath: require.resolve('react-native-stylus-transformer'),
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
......@@ -14,4 +21,8 @@ module.exports = {
},
}),
},
};
resolver: {
sourceExts: [...sourceExts, 'styl'],
},
}
})()
......@@ -45,7 +45,19 @@
"jetifier": "^1.6.6",
"metro-react-native-babel-preset": "^0.54.1",
"react-native-gesture-handler": "1.0.5",
"react-test-renderer": "16.8.3"
"react-test-renderer": "16.8.3",
"@babel/plugin-transform-runtime": "^7.13.10",
"@types/jest": "^26.0.22",
"@types/ramda": "^0.27.39",
"@types/react": "^17.0.3",
"@types/react-native": "^0.64.2",
"@types/react-test-renderer": "^17.0.1",
"babel-plugin-module-resolver": "^4.1.0",
"react-native-postcss-transformer": "^1.2.4",
"react-native-stylus-transformer": "^1.2.0",
"reflect-metadata": "^0.1.13",
"stylus": "^0.54.8",
"typescript": "^4.2.4"
},
"jest": {
"preset": "react-native"
......
@import './variable.styl'
.container
flex 1
.row
flex-direction row
.center
align-items center
.middle
justify-content center
.icon
resizeMode cover
width 100%
height 100%
hToR($color, opacity = 1)
return rgba(red($color), green($color), blue($color), opacity)
.bg-gray
background-color home_background_color
.animated
animation-duration 1s
animation-fill-mode both
@keyframes slide-up
from
// -webkit-transform translate3d(0, 100%, 0)
// transform translate3d(0, 100%, 0)
translateY 100%
visibility visible
to
// -webkit-transform translate3d(0, 0, 0)
// transform translate3d(0, 0, 0)
translateY 0
/*
* @FilePath: /BoneHouse_Hospital_APP/src/assets/styles/base.ts
* @Author: peii
* @Date: 2021-04-24 22:45:15
* @Vision: 1.0
* @Description:
*
* @Revision:
*
*/
import {
Dimensions
} from 'react-native'
const { width, height } = Dimensions.get('window');
export const getHeight = () => {
return height
};
export const getWidth = () => {
return width
};
// UI 默认图是414*896
const uiWidthPx = 414;
const uiHeightPx = 896;
export function pxSize(uiElementPx) {
return uiElementPx * getWidth() / uiWidthPx;
}
export function pxHeight(uiElementPx) {
return uiElementPx * getHeight() / uiHeightPx;
}
// 背景色
export const primary_color = "#20BEB8"; // 主色
export const foundation_color = "#ffffff"; // 底色
export const promary_shadow_color = "#3CA2FF"; // 按钮阴影色
export const home_background_color = "#F7F7F7"; // 背景色
export const btn_sub_color = "#20BEB8"; // 按钮色
export const dis_sub_color = "#BBBBBB"; // 禁用按钮色
// 字体色
export const primary_text_color = "#000000"; // 主字颜色
export const title_text_color = "#ffffff"; // 标题颜色
export const placehold_text_color = "#919191"; // input placeholder颜色
export const first_text_color = '#333333'; // 一级字体
export const second_text_color = "#666666"; // 次级字体
export const third_text_color = "#999999"; // 三级字体
export const point_color = "#ff0000"; // * 颜色
export const text_default_color = "#01B2B9"; // 默认颜色
export const text_audit_color = "#FF0000"; // 拒绝颜色
export const text_return_color = "#007EFF"; // 归还颜色
export const text_other_color = "#F4B61B"; // 其他颜色
export const list_tit_color = 'rgba(0, 0, 0, 0.87)'; // 列表标题颜色
export const list_str_color = "#0CB4E8"; // 列表加粗颜色
export const list_one_color = "#1B40B5"; // 列表一级颜色
export const list_thr_color = "#3B4C82"; // 列表其他颜色
export const list_one_light_color = "#3c64e2"; // 列表一级较浅颜色
// 字号
export const first_text_size = 20; // 一级字号
export const second_text_size = 16; // 二级字号
export const third_text_size = 12; // 三级字号
// 字体样式
export const font_family_semibold = "PingFangSC-Semibold";
export const font_family_medium = "PingFangSC-Medium";
export const font_family_regular = "PingFangSC-Regular";
export const font_family_light = "PingFangSC-Light";
export const header_height = 58
export const icon_style = {
resizeMode: 'cover',
width: '100%',
height: '100%'
}
export const safe_view = {
flex: 1
}
// 背景色
primary_color = #20BEB8// 主色
foundation_color = #ffffff // 底色
promary_shadow_color = #20BEB8// 按钮阴影色
home_background_color = #F7F7F7 // 背景色
btn_sub_color = #20BEB8 // 按钮色
dis_sub_color = #BBBBBB // 禁用按钮色
input_background_color = #efefef // 输入框底色
btn_color = #ffffff // 按钮字体颜色
// 字体色
primary_text_color = #000000 // 主字颜色
title_text_color = #ffffff // 标题颜色
placehold_text_color = #919191 // input placeholder颜色
first_text_color = #333333 // 一级字体
second_text_color = #666666 // 次级字体
third_text_color = #999999 // 三级字体
point_color = #ff0000 // * 颜色
text_default_color = #01B2B9 // 默认颜色
text_audit_color = #FF0000 // 拒绝颜色
text_return_color = #007EFF // 归还颜色
text_other_color = #F4B61B // 其他颜色
list_tit_color = rgba(0, 0, 0, 0.87) // 列表标题颜色
list_str_color = #0CB4E8 // 列表加粗颜色
list_one_color = #1B40B5 // 列表一级颜色
list_thr_color = #3B4C82 // 列表其他颜色
list_one_light_color = #3c64e2 // 列表一级较浅颜色
// 字号
first_text_size = 20px // 一级字号
second_text_size = 16px // 二级字号
third_text_size = 12px // 三级字号
// 字体样式
font_family_semibold = 'PingFangSC-Semibold'
font_family_medium = 'PingFangSC-Medium'
font_family_regular = 'PingFangSC-Regular'
font_family_light = 'PingFangSC-Light'
header_height = 66.5px
import React, { Component, PropTypes } from 'react'
import { Dimensions, Platform, PixelRatio, StatusBar, View, ScrollView } from 'react-native'
let props = {}
/**
* UI兼容类,把最外层的的组件使用这个类包起来,后续开发可以直接使用设计稿去开发
*/
export default class Resolution {
static get(useFixWidth = true) {
return useFixWidth ? { ...props.fw } : { ...props.fh }
}
static setDesignSize(dWidth = 414, dHeight = 896, dim = 'window'): void {
let designSize = { width: dWidth, height: dHeight }
// let navHeight = Platform.OS === 'android' ? StatusBar.currentHeight : 64
let navHeight = 0
let pxRatio = PixelRatio.get()
let { width, height } = Dimensions.get(dim)
if (dim != 'screen') height -= navHeight
let w = PixelRatio.getPixelSizeForLayoutSize(width)
let h = PixelRatio.getPixelSizeForLayoutSize(height)
let fwDesignScale = designSize.width / w
let fwWidth = designSize.width
let fwHeight = h * fwDesignScale
let fwScale = 1 / pxRatio / fwDesignScale
let fhDesignScale = designSize.height / h
let fhWidth = w * fhDesignScale
let fhHeight = designSize.height
let fhScale = 1 / pxRatio / fhDesignScale
props.fw = { width: fwWidth, height: fwHeight, scale: fwScale, navHeight }
props.fh = { width: fhWidth, height: fhHeight, scale: fhScale, navHeight }
}
static FixWidthView(p) {
let { width, height, scale, navHeight } = props.fw
return (
<View
{...p}
style={{
marginTop: navHeight,
width: width,
height: height,
backgroundColor: 'transparent',
transform: [
{ translateX: -width * 0.5 },
{ translateY: -height * 0.5 },
{ scale: scale },
{ translateX: width * 0.5 },
{ translateY: height * 0.5 },
],
}}
>
{p.children}
</View>
)
}
static FixHeightView(p) {
let { width, height, scale, navHeight } = props.fh
return (
<View
{...p}
style={{
marginTop: navHeight,
// width: width,
height: height,
backgroundColor: 'transparent',
transform: [
{ translateX: -width * 0.5 },
{ translateY: -height * 0.5 },
{ scale: scale },
{ translateX: width * 0.5 },
{ translateY: height * 0.5 },
],
}}
>
{p.children}
</View>
)
}
}
Resolution.setDesignSize()
@import '../../assets/styles/base.styl'
@import '../../assets/styles/variable.styl'
.checkbox
&-inner
@extend .row
margin-right 10px
&__box
width 25px
height @width
border-radius 13.5px
border 1px solid #ccc
&-img
width 25px
height 25px
&-active
border-color transparent
import React, { Component } from 'react'
import { View, Text, TouchableWithoutFeedback, Image } from 'react-native'
import * as R from 'ramda'
import { isBlank, g, isNotBlank } from '../../utils/utils'
import styles from './checkbox.styl'
type IProps = {
checked: boolean
label?: string
onChange: Function
}
export default ({ checked, label, onChange, style }: Iprops) => {
return (
<TouchableWithoutFeedback
style={g(styles, 'checkbox')}
onPress={() => {
onChange && onChange(!checked)
}}
>
<View style={[g(styles, 'checkbox-inner'), style]}>
<View
style={g(styles, { 'checkbox-inner__box': true, 'checkbox-inner__box-active': checked })}
>
{!!checked && (
<Image
source={require('../../assets/images/ic_checked_blue.png')}
style={g(styles, 'checkbox-inner__box-img')}
/>
)}
</View>
{isNotBlank(label) && <Text>{label}</Text>}
</View>
</TouchableWithoutFeedback>
)
}
@import '../../assets/styles/base.styl'
@import '../../assets/styles/variable.styl'
.empty
flex 1
@extend .middle
@extend .center
padding-top 30px
&-icon
width 80px
height 75px
&-text
font-size 20px
color second_text_color
margin-top 20px
font-family font_family_regular
/*
* @FilePath: /BoneHouse_Hospital_APP/src/components/empty/index.tsx
* @Author: peii
* @Date: 2021-05-30 17:04:50
* @Vision: 1.0
* @Description: 加载
*
* @Revision:
*
*/
import React, { Component } from 'react'
import { View, Text, Image } from 'react-native'
import { g } from '../../utils/utils'
import styles from './index.styl'
export default ({ text = '暂无数据', style }) => {
return (
<View style={[g(styles, 'empty'), style]}>
<Image style={[g(styles, 'empty-icon')]} source={require('../../assets/images/mail_empty.png')} />
<Text style={[g(styles, 'empty-text')]}>{text}</Text>
</View>
)
}
import React, { Component } from 'react'
import { View, Text, Image, TouchableOpacity, TextInput } from 'react-native'
import { IFormField } from 'bonehouse'
import { g, isRequired } from '../../utils/utils'
import styles from './index.styl'
export const Title = props => {
const { item } = props
const required = isRequired(item)
return (
<Text style={g(styles, 'form-label')}>
{!!required && <Text style={g(styles, 'red', 'pr5')}>*</Text>}
<Text>{item.label}</Text>
</Text>
)
}
@import '../../assets/styles/base.styl'
@import '../../assets/styles/variable.styl'
.modal
&-hd
height 50px
padding 15px 20px
&__btns
flex-direction row
justify-content space-between
&-btn
color btn_sub_color
font-size second_text_size
&-bd
height 100%
align-items center
/*
* @FilePath: /BoneHouse_Hospital_APP/src/components/form/date-modal.tsx
* @Author: peii
* @Date: 2021-04-27 21:53:02
* @Vision: 1.0
* @Description: 选择组件的弹出框
*
*
*/
// @ts-nocheck
import React, { useEffect, useRef, useState, createRef } from 'react'
import { View, Text, TouchableOpacity, ScrollView, Animated } from 'react-native'
import DatePicker from 'react-native-date-picker'
import { IFormField } from 'bonehouse'
import * as R from 'ramda'
import dayjs from 'dayjs'
import { isBlank } from '../../utils/utils'
import { BottomModal } from '../modals/base/bottom'
import { FieldType } from '../../enums'
import { g } from '../../utils/utils'
import styles from './date-modal.styl'
type IModalProps = {
value: string | number
mask?: boolean
title?: string
visible: boolean
onChange?: Function
onClose: Function
item?: IFormField
}
/**
* @description: 选择器弹窗
* @param {IModalProps} props
* @return {*}
*/
export function DateModal(props: IModalProps) {
const { item, value, mask, onChange, visible, onClose } = props
const [contentHeight, setContentHeight] = useState(300)
const [dateValue, setDateValue] = useState(value)
const [action, setAction] = useState(0)
return (
<BottomModal contentHeight={contentHeight} visible={visible} onClose={onClose} action={action}>
<View style={g(styles, ['modal-hd', 'modal-hd__btns'])}>
<TouchableOpacity
activeOpacity={0.8}
onPress={() => {
// animated(bottom, -contentHeight, opacity, 0, onClose)
// onClose()
setAction(action + 1)
}}
>
<Text style={g(styles, 'modal-hd__btns-btn')}>取消</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.8}
onPress={() => {
const date = dateValue || value
onChange(dayjs(date).format('YYYY-MM-DD HH:mm'))
setAction(action + 1)
}}
>
<Text style={g(styles, 'modal-hd__btns-btn')}>确定</Text>
</TouchableOpacity>
</View>
<View style={g(styles, 'modal-bd')}>
<DatePicker
date={dayjs(dateValue).toDate() || dayjs().toDate()}
mode={item.dateMode || 'date'}
locale="zh"
minuteInterval={item.minuteInterval || 30}
onDateChange={date => {
setDateValue(date)
}}
/>
</View>
</BottomModal>
)
}
/*
* @FilePath: /BoneHouse_Hospital_APP/src/components/form/date.tsx
* @Author: peii
* @Date: 2021-04-25 23:36:10
* @Vision: 1.0
* @Description: 选择器组件
*
* @Revision:
*
*/
import React, { Component } from 'react'
import { View, Text, Image, TouchableOpacity } from 'react-native'
import { IFormField, IOption } from 'bonehouse'
import * as R from 'ramda'
import { isBlank, show, isNotBlank, g } from '../../utils/utils'
import { FieldType } from '../../enums'
import { Title } from './common'
import { DateModal } from './date-modal'
import styles from './index.styl'
type IProps = {
item: IFormField
value: any
onChange: Function
data: any
fields: any[]
modalCallback: Function
}
/**
* @description: 获取显示字符
* @param {*} item
* @param {*} val
* @return {*}
*/
const getText = (item, val) => {
if (!val) return null
return R.compose(R.find(R.propEq('value', val)), R.propOr([], 'options'))(item)
}
export default class DatePicker extends Component<IProps> {
state = {
visible: false,
}
constructor(props) {
super(props)
this.onPressHandler = this.onPressHandler.bind(this)
this.onChange = this.onChange.bind(this)
}
shouldComponentUpdate(nextProps: IProps) {
return true
}
componentWillReceiveProps(nextProps, nextState) {
const { item, value } = nextProps
const { visible } = this.state
}
/**
* @description: 点击显示行
*/
onPressHandler() {
const { item, data, fields, value } = this.props
if (item.disabled) return
// 需要先选择依赖的字段
for (const ref of item.refrence || []) {
if (isBlank(data[ref])) {
const field = R.find(R.propEq('field', ref))(fields)
if (isNotBlank(field)) {
return show(`请先选择${field.label}`)
}
}
}
this.setState({ visible: true }, () => {
const modal = (
<DateModal
item={item}
value={value}
visible={true}
mask={true}
onChange={this.onChange}
onClose={() => {
this.setState({ visible: false }, () => {
this.props.modalCallback()
})
}}
/>
)
this.props.modalCallback(modal)
})
}
/**
* @description: 点击弹出选中值
*/
onChange(val: any): void {
const { item, value } = this.props
if (val === value) return
this.props && this.props.onChange(item.field, val, item.callback)
}
render() {
const { visible } = this.state
const { item, value } = this.props
return (
<>
<TouchableOpacity
style={g(styles, 'form-item')}
activeOpacity={item.disabled ? 1 : 0.8}
onPress={this.onPressHandler}
>
<Title item={item} />
<View style={g(styles, 'form-input')}>
{/* 值 */}
{isBlank(value) ? (
<Text style={g(styles, 'form-input__placeholder')}>{item.placeholder || '请选择'}</Text>
) : (
<Text numberOfLines={2} style={g(styles, 'form-input__text')}>
{value}
</Text>
)}
{/* 箭头 */}
{item.arrow !== false && (
<Image source={require('../../assets/images/arr_rig.png')} style={g(styles, 'form-input__arrow')} />
)}
</View>
</TouchableOpacity>
</>
)
}
}
@import '../../assets/styles/base.styl'
@import '../../assets/styles/variable.styl'
.image
margin-bottom 20px
&-title
color rgba(182, 182, 182, 100)
font-size 17px
font-family font_family_regular
margin-bottom 10px
&-bd
width 378px
height 172px
background-color rgba(255, 255, 255, 1)
padding 16px
&__inner
width 378px
flex-direction row
flex-wrap wrap
padding-bottom 16px
&-box
width 56px
height @width
border-radius 6px
background-color rgba(255, 255, 255, 1)
border 1px solid rgba(221, 221, 221, 1)
margin-bottom 15px
margin-right 15px
@extend .center
@extend .middle
&__remove
position absolute
right -10px
top -10px
&-icon
width 20px
height @width
&__touch
width 54px
height 54px
&-plus
font-size 50px
line-height 54px
color rgba(221, 221, 221, 1)
font-weight 200
&-img
width 54px
height 54px
border-radius 6px
import React, { Component } from 'react'
import {
View,
Text,
Image,
TouchableOpacity,
ScrollView,
Alert,
Linking,
Platform,
} from 'react-native'
import { IFormField, IOption } from 'bonehouse'
import * as R from 'ramda'
import ImagePicker from 'react-native-image-picker'
import Toast, { Positions } from 'react-native-root-toast'
import { isBlank, show, isRequired, isNotBlank, g } from '../../utils/utils'
import styles from './image.styl'
// import Container from '../../inversify'
// import { TYPES } from '../../inversify/types'
// import Service from '../../services/service'
// const service: Service = Container.get(TYPES.Service)
type IProps = {
item: IFormField
value: string[]
onChange: Function
}
export default class ImageForm extends React.Component<IProps> {
state = {
options: {
title: '选择图片',
cancelButtonTitle: '取消',
takePhotoButtonTitle: '拍照',
chooseFromLibraryButtonTitle: '相册',
cameraType: 'back',
mediaType: 'photo',
videoQuality: 'high',
durationLimit: 10,
maxWidth: 720,
maxHeight: 1280,
aspectX: 2,
aspectY: 1,
quality: 1,
angle: 0,
allowsEditing: false,
noData: false,
storageOptions: {
skipBackup: true,
path: 'Wisdom', // 存储本地地址
},
},
urls: [],
}
constructor(props) {
super(props)
this.onPickImageHandler = this.onPickImageHandler.bind(this)
}
/**
* @description: 点击添加图标
* @param {*}
* @return {*}
*/
onPickImageHandler() {
const { options } = this.state
ImagePicker.showImagePicker(options, async res => {
if (res.didCancel) return
if (res.error) {
const errMsgs = ['Camera permissions not granted', 'Photo library permissions not granted']
const errTips = [
'APP需要使用相机,请打开相机权限允许APP使用',
'APP需要使用相册,请打开相册权限允许APP使用',
]
for (const [idx, msg] of Object.entries(errMsgs)) {
if (R.indexOf(res.error, msg) > -1) {
Alert.alert('提示信息', errTips[idx], [
{
text: '设置',
onPress: () => {
Linking.openURL('app-settings:').catch(err => console.log('error', err))
},
},
{
text: '取消',
},
])
return
}
}
return show(res.error)
}
let source = res.uri
if (Platform.OS === 'ios') {
source = res.uri.replace('file://', '')
!res.fileName && (res.fileName = new Date().getTime() + '.heic')
}
const formData = new FormData()
let file = { uri: source, type: 'multipart/form-data', name: res.fileName }
formData.append('file', file)
try {
const loading = Toast.show('上传中', { duration: 60000, position: Toast.positions.CENTER })
// const result = await service.uploadImg(formData)
Toast.hide(loading)
let value = this.props.value || []
value = R.append(result.data.url, value)
this.props.onChange(this.props.item.field, value)
let { urls } = this.state
urls = R.append(source, urls)
this.setState({ urls })
} catch (error) {
Toast.hide(loading)
}
})
}
/**
* @description: 删除单项
* @param {*} idx
* @return {*}
*/
removeHandler(idx) {
let { value, onChange, item } = this.props
let { urls } = this.state
urls = R.remove(idx, 1, urls)
value = R.remove(idx, 1, value)
this.setState({ urls })
onChange && onChange(item.field, value)
}
render() {
const { item, value } = this.props
const { urls } = this.state
return (
<View style={g(styles, 'image')}>
<Text style={g(styles, 'image-title')}>{item.label}</Text>
<ScrollView style={g(styles, 'image-bd')}>
<View style={g(styles, 'image-bd__inner')}>
{isNotBlank(urls) &&
urls.map((url, idx) => {
return (
<View style={g(styles, 'image-box')} key={url}>
<TouchableOpacity style={g(styles, 'image-box__touch')}>
<Image source={{ uri: url }} style={g(styles, 'image-img')} />
</TouchableOpacity>
<TouchableOpacity
style={g(styles, 'image-box__remove')}
onPress={() => {
this.removeHandler(idx)
}}
>
<Image
source={require('../../assets/images/close_err_icon.png')}
style={g(styles, 'image-box__remove-icon')}
/>
</TouchableOpacity>
</View>
)
})}
<TouchableOpacity
style={g(styles, 'image-box')}
activeOpacity={0.7}
onPress={() => {
this.onPickImageHandler()
}}
>
<Text style={g(styles, 'image-plus')}>+</Text>
</TouchableOpacity>
</View>
</ScrollView>
</View>
)
}
}
@import '../../assets/styles/base.styl'
@import '../../assets/styles/variable.styl'
.text
font-size second_text_size
font-family font_family_regular
.container
flex 1
.form
flex 1
&-inner
padding 18px
width 100%
height 100%
&-item
width 378px
height 58px
background-color #fff
padding 0 18px
flex-direction row
justify-content space-between
align-items center
margin-bottom 18px
&-multi
height 160px
padding 18px
flex-direction column
align-items flex-start
&-label
@extend .text
color primary_text_color
width 120px
&-input
flex-direction row
align-items center
justify-content flex-end
flex 1
&__placeholder
@extend .text
color placehold_text_color
&__text
@extend .text
color second_text_color
line-height 18px
text-align right
&__arrow
width 8px
height 14px
margin-left 9px
&-text-input
@extend .text
text-align right
color second_text_color
&__multi
text-align left
&-radio
flex 1
@extend .row
justify-content flex-end
&__item
@extend .row
align-items center
padding-left 20px
&-box
margin-right 8px
width 20px
height @width
border-color #ccc
border-width 1px
border-radius 10px
&-image
width 18px
height @width
&-text
@extend .text
color second_text_color
.red
color #f00
.submit-btn
width 343px
height 52px
border-radius 9px
background-color primary_color
justify-content center
align-items center
align-self center
margin-bottom 30px
&__inner
@extend .row
&__disabled
background-color #BBBBBB
&__text
color rgba(255, 255, 255, 100)
font-size 17px
font-family font_family_semibold
&__disabled
color rgba(255, 255, 255, 0.5)
.ml10
margin-left 10px
/*
* @FilePath: /BoneHouse_Hospital_APP/src/components/form/index.tsx
* @Author: peii
* @Date: 2021-04-25 22:40:31
* @Vision: 1.0
* @Description: 基础表单组件
*
* @Revision:
*
*/
// @ts-nocheck
import React, { Component } from 'react'
import {
View,
Text,
ScrollView,
SafeAreaView,
TouchableOpacity,
ActivityIndicator,
KeyboardAvoidingView,
} from 'react-native'
import { IFormField } from 'bonehouse'
import * as R from 'ramda'
import { FieldType } from '../../enums'
import Select from './select'
import Radio from './radio'
import Input from './input'
import DatePicker from './date'
import ImageForm from './image'
import { g, isBlank, show, debounce } from '../../utils/utils'
import styles from './index.styl'
type IProps = {
fields: IFromField[]
data: { [key: string]: any }
}
const formComponents = {
[FieldType.SELECT]: Select,
[FieldType.TEXT]: Input,
[FieldType.DATE]: DatePicker,
[FieldType.IMAGE]: ImageForm,
[FieldType.RADIO]: Radio,
}
export default class Form extends Component<IProps> {
state = {
value: '',
scrollable: true,
modal: null,
canSubmit: false,
submiting: false,
}
constructor(props) {
super(props)
this.onChange = this.onChange.bind(this)
}
componentDidMount() {
this.checkCanSubmit = debounce(this.checkCanSubmit.bind(this))
this.submitHandler = debounce(this.submitHandler.bind(this), 50)
}
/**
* @description: 修改回调
* @param {*} key
* @param {*} value
* @param {*} callback
* @return {*}
*/
async onChange(key: string, value: any, callback?: Function) {
try {
this.props.onChange && (await this.props.onChange(key, value))
// callback && callback()
this.checkCanSubmit()
} catch (error) {
console.log(error)
}
}
/**
* @description: 是否可以提交
*/
checkCanSubmit() {
const { fields, data } = this.props
for (const item of fields) {
if (isBlank(item.rules)) continue
// 校验是否可以提交
for (const rule of item.rules) {
if (rule.required && isBlank(data[item.field])) {
return this.setState({ canSubmit: false })
}
if (rule.pattern && R.complement(R.test)(rule.pattern, data[item.field])) {
return this.setState({ canSubmit: false })
}
}
this.setState({ canSubmit: true })
}
}
/**
* modal框开关回调
* @param modalOpen 是否有modal框打开
*/
modalCallback(modal?: Component) {
this.setState({ modal })
}
/**
* @description: 提交信息
*/
async submitHandler() {
this.setState({ submiting: true })
this.props.submitHandler && (await this.props.submitHandler())
this.setState({ submiting: false })
}
render() {
const { fields = [], data = {} } = this.props
const { scrollable, modal, canSubmit, submiting } = this.state
return (
<View style={g(styles, 'container')}>
<ScrollView style={g(styles, 'form')}>
<KeyboardAvoidingView style={g(styles, 'form-inner')} behavior="padding" enabled>
{fields.map(item => {
let FormComponent = formComponents[item.type] || Select
if (item.type === FieldType.CUSTOM) {
FormComponent = item.component
}
return (
<FormComponent
key={item.field}
item={item}
value={data[item.field]}
data={data}
fields={fields}
onChange={this.onChange}
modalCallback={this.modalCallback.bind(this)}
/>
)
})}
<TouchableOpacity
style={g(styles, {
'submit-btn': true,
'submit-btn__disabled': !canSubmit || submiting,
})}
disabled={!canSubmit || submiting}
activeOpacity={0.8}
onPress={this.submitHandler}
>
{submiting ? (
<View style={g(styles, 'submit-btn__inner')}>
<ActivityIndicator style={g(styles, 'submit-btn__loading')} color="rgba(255,255,255,0.5)" />
<Text style={g(styles, 'submit-btn__text', 'submit-btn__text__disabled', 'ml10')}>提交中</Text>
</View>
) : (
<Text
style={g(styles, {
'submit-btn__text': true,
'submit-btn__text__disabled': !canSubmit,
})}
>
提交
</Text>
)}
</TouchableOpacity>
</KeyboardAvoidingView>
</ScrollView>
{/* form内层的弹出窗 */}
{modal}
</View>
)
}
}
/*
* @FilePath: /BoneHouse_Business_APP/src/components/form/input.tsx
* @Author: peii
* @Date: 2021-05-06 13:05:38
* @Vision: 1.0
* @Description: 文本输入组件
*
*/
import React, { Component } from 'react'
import { View, Text, Image, TouchableOpacity, TextInput } from 'react-native'
import { IFormField } from 'bonehouse'
import * as R from 'ramda'
import { isBlank, show, isRequired, debounce } from '../../utils/utils'
import { placehold_text_color } from '../../assets/styles/base'
import { FieldType } from '../../enums'
import { g } from '../../utils/utils'
import { Title } from './common'
import styles from './index.styl'
type IProps = {
item: IFormField
onChange: Function
}
export default class Input extends Component<IProps> {
constructor(props) {
super(props)
this.onChangeText = debounce(this.onChangeText.bind(this), 500)
}
onChangeText(text: string) {
const { item, onChange } = this.props
onChange && onChange(item.field, text)
}
render() {
const { item, value } = this.props
return (
<View style={g(styles, { 'form-item': true, 'form-item-multi': item.multiline })}>
<Title item={item} />
<TextInput
style={g(styles, {
'form-input': true,
'form-text-input': true,
'form-text-input__multi': item.multiline,
})}
defaultValue={value}
autoCapitalize="none"
placeholder={item.placeholder || '请输入'}
onChangeText={this.onChangeText}
multiline={item.multiline}
numberOfLines={4}
maxLength={140}
placeholderTextColor={placehold_text_color}
underlineColorAndroid="transparent"
/>
</View>
)
}
}
/*
* @FilePath: /BoneHouse_Hospital_APP/src/components/form/radio.tsx
* @Author: peii
* @Date: 2021-06-06 16:24:05
* @Vision: 1.0
* @Description: Radio组件
*
* @Revision:
*
*/
import React, { Component } from 'react'
import { View, Text, Image, TouchableOpacity } from 'react-native'
import { IFormField, IOption } from 'bonehouse'
import * as R from 'ramda'
import { isBlank, show, isNotBlank, g } from '../../utils/utils'
import { FieldType } from '../../enums'
import { Title } from './common'
import styles from './index.styl'
type IProps = {
item: IFormField
data: any
value: any
}
export default class RadioButton extends Component<IProps> {
shouldComponentUpdate(nextProps: IProps): boolean {
if (nextProps.value !== this.props.value) {
return true
}
return false
}
render() {
const { item, value, onChange } = this.props
return (
<View style={g(styles, 'form-item')}>
<Title item={item} />
<View style={g(styles, 'form-radio')}>
{item.options &&
item.options.map(option => {
return (
<TouchableOpacity
style={g(styles, 'form-radio__item')}
key={option.value}
activeOpacity={0.8}
onPress={() => {
if (value === option.value) return
onChange(item.field, option.value)
}}
>
<View style={g(styles, 'form-radio__item-box')}>
{value === option.value && (
<Image
style={g(styles, 'form-radio__item-image')}
source={require('../../assets/images/ic_checked_blue.png')}
></Image>
)}
</View>
<Text style={g(styles, 'form-radio__item-text')}>{option.label}</Text>
</TouchableOpacity>
)
})}
</View>
</View>
)
}
}
@import '../../assets/styles/base.styl'
@import '../../assets/styles/variable.styl'
.option-text
width 100%
text-align center
color third_text_color
font-size second_text_size
.select
width 100%
height 100%
padding 30px 0 40px
&-bd
width 100%
height 100%
&-option
height 35px
justify-content center
&__text
@extend .option-text
&-active
@extend .option-text
color first_text_color
.loading
flex-direction row
height 100%
justify-content center
align-items center
&-text
font-size second_text_size
color second_text_color
margin-left 5px
.modal-header
@extend .row
width 414px
height 32px
justify-content space-between
align-items center
\ No newline at end of file
/*
* @FilePath: /BoneHouse_Hospital_APP/src/components/form/select-modal.tsx
* @Author: peii
* @Date: 2021-04-27 21:53:02
* @Vision: 1.0
* @Description: 选择组件的弹出框
*
* @Revision:
* 1.1 添加多选
*/
// @ts-nocheck
import React, { useEffect, useRef, useState, createRef } from 'react'
import { View, Text, TouchableOpacity, ActivityIndicator, FlatList } from 'react-native'
import { Provider, Modal } from '@ant-design/react-native'
import { IOption } from 'bonehouse'
import * as R from 'ramda'
import { isBlank } from '../../utils/utils'
import { BottomModal } from '../modals/base/bottom'
import { FieldType } from '../../enums'
import { g } from '../../utils/utils'
import styles from './select-modal.styl'
type IModalProps = {
data: IOption[]
value: string | number
mask?: boolean
loading: boolean
title?: string
visible: boolean
onChange?: Function
onClose: Function
}
let scrollRef = null
/**
* @description: 选择器弹窗
* @param {IModalProps} props
* @return {*}
*/
export function SelectModal(props: IModalProps) {
const { data = [], value, mask, onChange, visible, title, onClose, loading, headerHeight } = props
const [contentHeight, setContentHeight] = useState(300)
const [action, setAction] = useState(0)
useEffect(() => {
setContentHeight(35 * data.length + 70)
const idx = R.findIndex(R.propEq('value', value))(data)
if (idx > 1) {
scrollRef && scrollRef.scrollToIndex({ animated: true, index: idx })
}
}, [data.length])
const Item = ({ item }) => {
return (
<TouchableOpacity
activeOpacity={0.8}
style={g(styles, 'select-option')}
onPress={() => {
onChange && onChange(item.value)
setAction(action + 1)
}}
>
<Text
numberOfLines={1}
style={g(styles, {
'select-option__text': true,
'select-option__text-active': !!value && R.includes(item.value, value),
})}
>
{item.label}
</Text>
</TouchableOpacity>
)
}
return (
<BottomModal
contentHeight={contentHeight}
visible={visible}
onClose={onClose}
action={action}
headerHeight={headerHeight}
>
{/* <View style={g(styles, 'modal-header')}>
<TouchableOpacity style={g(styles, 'modal-header__btn')}>
<Text style={g(styles, 'modal-header__btn-text')}>取消</Text>
</TouchableOpacity>
<TouchableOpacity style={g(styles, 'modal-header__btn')}>
<Text style={g(styles, 'modal-header__btn-text')}>确定</Text>
</TouchableOpacity>
</View> */}
{!!loading ? (
<View style={g(styles, 'loading')}>
<ActivityIndicator size="small" />
<Text style={g(styles, 'loading-text')}>加载中</Text>
</View>
) : (
<View style={g(styles, 'select')}>
<FlatList
style={g(styles, 'select-bd')}
renderItem={Item}
data={data}
keyExtractor={item => item.value}
ref={ref => (scrollRef = ref)}
getItemLayout={(item, index) => {
return { length: item.length, offset: 35 * index, index }
}}
/>
</View>
)}
</BottomModal>
)
}
/*
* @FilePath: /BoneHouse_Hospital_APP/src/components/form/select.tsx
* @Author: peii
* @Date: 2021-04-25 23:36:10
* @Vision: 1.0
* @Description: 选择器组件
*
*/
// @ts-nocheck
import React, { Component } from 'react'
import { View, Text, Image, TouchableOpacity } from 'react-native'
import { IFormField, IOption } from 'bonehouse'
import * as R from 'ramda'
import { isBlank, show, isRequired, isNotBlank, g } from '../../utils/utils'
import { FieldType, SelectMode } from '../../enums'
import { Title } from './common'
import { SelectModal } from './select-modal'
import styles from './index.styl'
type IProps = {
item: IFormField
value: any
onChange: Function
data: any
fields: any[]
modalCallback: Function
}
/**
* @description: 获取显示字符
* @param {*} item
* @param {*} val
* @return {*}
*/
const getText = (item: IFormField, val) => {
if (!val) return null
if (item.multiline) {
let values = R.ifElse(isBlank, R.always([]), R.split(','))(val)
return R.compose(
R.join(','),
R.pluck('label'),
R.filter(R.pipe(R.prop('value'), R.includes(R.__, values))),
R.propOr([], 'options'),
)(item)
} else {
return R.compose(R.prop('label'), R.find(R.propEq('value', val)), R.propOr([], 'options'))(item)
}
}
export default class Select extends Component<IProps> {
state = {
visible: false,
loading: false,
}
constructor(props) {
super(props)
this.onPressHandler = this.onPressHandler.bind(this)
this.onChange = this.onChange.bind(this)
}
componentWillReceiveProps(nextProps, nextState) {
const { item, value } = nextProps
const { visible, loading } = this.state
// 当前只有一个选项时,赋值第一个选项作为当前选项值
if (item && item.options && item.options.length === 1 && item.options[0].value !== value) {
this.onChange(item.options[0].value)
}
// 换了options,表示依赖的项没有了,把当前设置为空
const val = getText(item, value)
if (isNotBlank(value) && isBlank(val) && R.complement(R.equals)(this.props.data, nextProps.data)) {
this.onChange(null)
}
// 因为是回调中设置的ReactElement,所以只有手动更新
// item的loading与当前组件的loading有且只有一个为true且visible为true的时候,手动更新Modal的状态
if ((item.loading || loading) && item.loading !== loading) {
this.setState({ loading: item.loading }, () => {
if (loading && visible) {
const modal = (
<SelectModal
data={item.options}
loading={item.loading}
value={value}
visible={true}
mask={true}
onChange={this.onChange}
onClose={() => {
this.setState({ visible: false }, () => {
this.props.modalCallback()
})
}}
/>
)
this.props.modalCallback(modal)
}
})
}
}
/**
* @description: 点击显示行
*/
onPressHandler() {
const { item, data, fields, value } = this.props
if (item.disabled) return
// 需要先选择依赖的字段
for (const ref of item.refrence || []) {
if (isBlank(data[ref])) {
const field = R.find(R.propEq('field', ref))(fields)
if (isNotBlank(field)) {
return show(`请先选择${field.label}`)
}
}
}
// 跳转到其他页面的选择
if (item.selectMode === SelectMode.PAGE) {
return item.customHandler && item.customHandler(item.field)
}
if (!item.loading && isBlank(item.options)) return show(`没有可选择的${item.label}`)
this.setState({ visible: true }, () => {
// 弹出窗,要放在最外层, 所以放在这里作为参数返回给父组件
const modal = (
<SelectModal
data={item.options}
loading={item.loading}
value={value}
visible={true}
mask={true}
onChange={this.onChange}
onClose={() => {
this.setState({ visible: false }, () => {
this.props.modalCallback()
})
}}
/>
)
this.props.modalCallback(modal)
})
}
/**
* @description: 点击弹出选中值
*/
onChange(val: any): void {
const { item, value } = this.props
// 多选
if (item.multiline) {
let values = R.ifElse(isBlank, R.always([]), R.split(','))(value)
if (R.includes(val, values)) {
values = R.compose(R.remove(R.__, 1, values), R.findIndex(R.equals(val)))(values)
} else {
values = R.append(val, values)
}
this.props && this.props.onChange(item.field, R.join(',', values))
}
// 单选
else {
if (val === value) return
this.props && this.props.onChange(item.field, val, item.callback)
}
}
render() {
const { item, value } = this.props
const text = getText(item, value)
return (
<>
<TouchableOpacity
style={g(styles, 'form-item')}
activeOpacity={item.disabled ? 1 : 0.8}
onPress={this.onPressHandler}
>
<Title item={item} />
<View style={g(styles, 'form-input')}>
{/* 值 */}
{isBlank(value) ? (
<Text style={g(styles, 'form-input__placeholder')}>{item.placeholder || '请选择'}</Text>
) : (
<Text numberOfLines={2} style={g(styles, 'form-input__text')}>
{text}
</Text>
)}
{/* 箭头 */}
{item.arrow !== false && (
<Image source={require('../../assets/images/arr_rig.png')} style={g(styles, 'form-input__arrow')} />
)}
</View>
</TouchableOpacity>
</>
)
}
}
/*
* @FilePath: /BoneHouse_Business_APP/src/components/form/textarea.tsx
* @Author: peii
* @Date: 2021-05-06 13:05:38
* @Vision: 1.0
* @Description: 多行文本输入组件
*
*/
import React, { Component } from 'react'
import { View, Text, Image, TouchableOpacity, } from 'react-native'
import { IFormField } from 'bonehouse'
import * as R from 'ramda'
import { isBlank, show, isRequired, debounce } from '../../utils/utils'
import { FieldType } from '../../enums'
import { g } from '../../utils/utils'
import { SelectModal } from './select-modal'
import styles from './index.styl'
type IProps = {
item: IFormField
onChange: Function
}
export default class Input extends Component<IProps> {
constructor(props) {
super(props)
this.onChangeText = debounce(this.onChangeText.bind(this), 500)
}
onChangeText(text: string) {
const { item, onChange } = this.props
onChange && onChange(item.field, text)
}
render() {
const { item, value } = this.props
const required = isRequired(item)
return (
<View style={g(styles, 'form-item')}>
<Text style={g(styles, 'form-label')}>
{!!required && <Text style={g(styles, 'red', 'pr5')}>*</Text>}
<Text>{item.label}</Text>
</Text>
<TextInput
style={g(styles, 'form-input', 'form-text-input')}
autoCapitalize="none"
placeholder={item.placeholder || '请输入'}
onChangeText={this.onChangeText}
/>
</View>
)
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment