React Native 自制对话框组件 SuperModal

204 阅读4分钟

在项目中需要编写多个对话框,为了方便复用,自己编写了一个SuperModal组件,并实现了通过使用一个SuperModal组件切换不同对话框的功能,减少了重复的代码量。

组件代码:

import {
  View, Text, TouchableOpacity, TouchableWithoutFeedback,
  GestureResponderEvent, Modal, StyleSheet,
} from "react-native";
import * as React from "react";

const SuperModal = ({ isModalVisible, closeModal, handleConfirm=null, title, content, confirmText="确认", customButton = null, current= "Main", next=null, }:
{ isModalVisible:boolean, closeModal:() => void, handleConfirm:(() => boolean)|null, title:string, content:any, confirmText:string, customButton: {textLeft:string,textRight:string,onPressLeft:(event: GestureResponderEvent) => void,onPressRight:(event: GestureResponderEvent) => void }|null, current: string, next:any, }) => {

  return <Modal
    transparent={true}
    visible={isModalVisible}
    onRequestClose={closeModal}
  >
    <TouchableWithoutFeedback onPress={closeModal}><View style={styles.overlay} >
      <TouchableWithoutFeedback onPress={()=>{}}><View style={styles.modalContainer}>
        { current==='Main'?
          <>
              <Text style={styles.modalTitle}>{text}</Text>
              {content}
              <View style={styles.buttonsContainer}>
                {customButton ?
                  <>
                    <TouchableOpacity onPress={customButton.onPressLeft}
                                      style={[styles.menuButton, { borderRightWidth: 0.5 }]}>
                      <Text style={{ textAlign: "center" }}>{customButton.textLeft}</Text>
                    </TouchableOpacity>
                    <TouchableOpacity onPress={customButton.onPressRight}
                                      style={[styles.menuButton, { borderLeftWidth: 0.5 }]}>
                      <Text style={{ textAlign: "center" }}>{customButton.textRight}</Text>
                    </TouchableOpacity>
                  </>
                  :
                  <>
                    <TouchableOpacity onPress={closeModal} style={[styles.menuButton, { borderRightWidth: 0.5 }]}>
                      <Text style={{ textAlign: "center" }}>取消</Text>
                    </TouchableOpacity>
                    <TouchableOpacity onPress={() => {
                      handleConfirm ? handleConfirm() && closeModal() : closeModal()
                    }} style={[styles.menuButton, { borderLeftWidth: 0.5 }]}>
                      <Text style={{ textAlign: "center" }}>{confirmText}</Text>
                    </TouchableOpacity>
                  </>
                }
              </View>
            </>
          :
          <>{next}</>
        }
        </View></TouchableWithoutFeedback>
    </View></TouchableWithoutFeedback>
  </Modal>
}

export default SuperModal;

const styles = StyleSheet.create({
  modalTitle: {
    textAlign: 'center',
    fontSize: 16,
    marginBottom: 10,
  },
  overlay: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    zIndex: 200,
  },
  modalContainer: {
    width: '80%',
    paddingTop: 20,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ECECEC',
    borderRadius: 34,
    elevation: 1,
  },
  inputContainer: {
    height: 50,
    lineHeight: 25,
    paddingLeft: 15,
    flexDirection: 'row',
    alignItems: 'center',
    width: '85%',
    backgroundColor: '#F7F6F6',
    borderRadius: 28,
    overflow: 'hidden',
  },
  input_new: {
    flexGrow: 1,
    overflow: 'hidden',
  },
  buttonsContainer: {
    width: "100%",
    justifyContent: 'space-between',
    flexDirection: 'row',
    borderTopWidth: 0.5,
    borderColor: 'rgba(0,0,0,0.3)'
  },
  menuButton: {
    flex: 1,
    padding: 20,
    borderRadius: 5,
    alignItems: "center",
    borderColor: 'rgba(0,0,0,0.3)'
  },
});

参数说明:

  • isModalVisible:boolean, 父组件中用于控制对话框是否可见(弹出/关闭)的布尔值;

  • closeModal:() => void, 用于关闭对话框的函数,其中应包含将isModalVisible设为false的操作;

  • handleConfirm:(() => boolean)|null, 【可选】用于处理确认键点击的函数,返回布尔值决定是否关闭对话框(默认为null,效果为点击确认直接调用closeModal,一般使用customButton时才将其设为null);

  • title:string, 对话框的标题;

  • content:any, 对话框的内容(JSX);

  • confirmText:string, 【可选】右下角确认键的文字(默认为“确认”);

  • customButton: {

    • textLeft:string,
    • textRight:string,
    • onPressLeft:(event: GestureResponderEvent) => void,
    • onPressRight:(event: GestureResponderEvent) => void

    }|null, 【可选】自定义底部左右按钮的文字与功能(默认为“null”,即不自定义);

  • current: string, 【可选】用于管理对话框内多级页面,表示当前对话框所在页面(默认为“Main”,即默认页面);

  • next:any,【可选】对话框内次级页面的内容(JSX)(默认为“null”,即无次级页面);

使用示例:

  • 普通使用:
<SuperModal isModalVisible={isSizeModalVisible}
            closeModal={() => {setIsSizeModalVisible(false);}}
            handleConfirm={() => {
              //TODO: 处理确认逻辑
              return true
            }}
            title={'长宽比'}
            content={<View style={{flexDirection: 'row',width: '90%', marginBottom: 10, alignItems: 'center', justifyContent: 'space-between'}}>
              <TextInput style={styles.input}/>
              <Text>×</Text>
              <TextInput style={styles.input}/>
            </View>}
            confirmText={"确认修改"}
/>

image.png

  • 多重复用:
  type ModalType = 'None'|'EditPassword'|'EditNickname'|'EditPhone'|'ToggleEnterPrise'
  const [modalVisible, setModalVisible] = useState<ModalType>('None')
  const [currentModalScreen,setCurrentModalScreen]=useState('Main')

  let modalTitle=useMemo(()=>{
    switch (modalVisible){
      case 'EditPassword': return '修改密码'   
      case 'EditNickname': return '修改昵称'
      case 'EditPhone': return '修改手机'
      case 'ToggleEnterPrise': return '切换企业'
      default: return ''
    }
  },[modalVisible])
const modalItems= {
    'None': {
      content: [],
      confirm: null,
    },

    'EditPassword': {
      content: [
        {
          title: "旧密码",
          value: passwordOld,
          onChangeValue: onChangePasswordOld,
          secure: true,
          tip: <Image style={[styles.judge, { opacity: passwordOld ? 1 : 0 }]}
                      source={userSlice.password === passwordOld ? require("../../../../assets/correct.png") : require("../../../../assets/error.png")} />
        }, {
          title: "新密码",
          value: passwordNew,
          onChangeValue: onChangePasswordNew,
          secure: true,
          tip: <View style={[styles.judge]} />
        }, {
          title: "确认密码",
          value: passwordConfirm,
          onChangeValue: onChangePasswordConfirm,
          secure: true,
          tip: <Image style={[styles.judge, { opacity: passwordConfirm ? 1 : 0 }]}
                      source={passwordNew === passwordConfirm ? require("../../../../assets/correct.png") : require("../../../../assets/error.png")} />
        },],
      confirm: () => {
        if (userSlice.password !== passwordOld) {
          alert('密码错误!')
          return false;
        }
        if (passwordNew !== passwordConfirm) {
          alert('两次输入的密码不一致!!')
          return false;
        }
        setUserSlice({ ...userSlice, password: passwordNew });
        alert('密码修改成功!')
        return true;
      },
    },

    'EditNickname': {
      content: [{
        title: "新昵称",
        value: nicknameNew,
        onChangeValue: onChangeNicknameNew,
        secure: false,
        tip: null,
      }],
      confirm: () => {
        if (nicknameNew === '') {
          alert('昵称不能为空!');
          return false;
        }
        setUserSlice({ ...userSlice, username: nicknameNew });
        alert('昵称修改成功!')
        return true;
      },
    },

    'EditPhone': {
      content: [{
        title: "新手机",
        value: phoneNew,
        onChangeValue: onChangePhoneNew,
        secure: false,
        tip: null
      },{
      title: "验证码",
        value: captchaCode,
        onChangeValue: onChangeCaptchaCode,
        secure: false,
        tip:  <TouchableOpacity
          activeOpacity={0.5}
          onPress={sendCaptchaCode}
          disabled={isCounting}
          style={{width: 80, borderRadius: 20, borderWidth:1, borderColor: '#A5A5A5', height: 45, padding: 5, justifyContent: 'center', alignItems: 'center', marginLeft: 10}}
        ><Text style={{color: 'grey', fontSize: 12, fontFamily: 'Source Han Sans CN', fontWeight: '400',}}>
          {isCounting ? ` ${remainingTime} 秒` : '发送验证码'}
        </Text></TouchableOpacity>,
      },],
      confirm: () => {
        /*验证码验证*/
        if (0/*验证码错误*/) {
          alert('请输入正确的验证码!');
          return false;
        }
        setUserSlice({ ...userSlice, phone: phoneNew });
        alert('手机修改成功!')
        return true;
      },
    },

    'ToggleEnterPrise': {
      content: [{enterpriseName: '个人'},{enterpriseName: 'A企业'},{enterpriseName: 'B企业'},{enterpriseName: 'C企业'},],
      confirm: null,
      customButton: {
        textLeft: '申请加入',
        onPressLeft: ()=>{setCurrentModalScreen("Next")},
        textRight: '企业注册',
        onPressRight: ()=>{closeModal();navigation.navigate('EnterpriseRegister')},
      },
      next: <View style={{padding: 20, justifyContent: 'space-between', marginTop: -20 }}>
              <View style={{width: '100%', flexDirection:'row', alignItems: 'center', marginBottom: 20}}>
                <TouchableOpacity onPress={()=>{setCurrentModalScreen("Main")}}>
                  <Image source={require('../../../../assets/back.png')} style={{width: 30, height: 30, marginRight: 10}}/>
                </TouchableOpacity>
                <View style={{flexGrow: 1, height: 40, backgroundColor: 'rgba(255,255,255,0.5)',borderRadius: 28, paddingHorizontal: 10,paddingVertical:0, flexDirection: 'row', alignItems: 'center' }}>
                  <TextInput placeholder={"请输入企业名称"}
                             value={searchText}
                             onChangeText={setSearchText}
                             onBlur={handleSearch}
                             style={{flexGrow: 1}}
                  />
                  <TouchableOpacity onPress={handleSearch}>
                    <Image source={require('../../../../assets/search.png')} style={{width: 30, height: 30}}/>
                  </TouchableOpacity>
                </View>
              </View>
              <View style={{height: 250}}>
                <FlatList style={{ width: '100%', borderRadius: 28}}
                          fadingEdgeLength={50}
                          data={filteredData}
                          keyExtractor={(item,index) => index.toString()}
                          renderItem={({ item }) => (
                            <TouchableOpacity style={{alignSelf: 'center', width:"100%",flexDirection: 'row', justifyContent: 'space-evenly', backgroundColor: 'white', borderRadius: 28, marginVertical: 10, paddingVertical:10}}>
                              <Text>{item.name}</Text>
                              <Text>{item.ceo}</Text>
                            </TouchableOpacity>
                          )}
                />
              </View>
            </View>,
      current: currentModalScreen
    }
  }
<SuperModal isModalVisible={modalVisible!=='None'}
            title={modalTitle}
            closeModal={closeModal}
            handleConfirm={modalItems[modalVisible].confirm}
            customButton={modalItems[modalVisible].customButton]}
            current={modalItems[modalVisible].current}
            next={modalItems[modalVisible].next}
            content={<>
              {modalVisible!='None' && 
               (modalVisible!=='ToggleEnterPrise' ?
                modalItems[modalVisible].content.map((item,index)=>{
                  return <View style={{flexDirection:'row', alignItems: 'center',marginBottom: 20, width: '85%'}} key={index}>
                    <View style={styles.inputContainer}>
                      <Text>{item.title}:</Text>
                      <TextInput
                        secureTextEntry={item.secure}
                        style={styles.input_new}
                        onChangeText={(text)=>item.onChangeValue(text)}
                        value={item.value}
                      />
                    </View>
                    {item.tip}
                  </View>
                })
                :
                <ScrollView style={{width: '100%', height: 180}} fadingEdgeLength={20}>
                  {modalItems[modalVisible].content.map((item,index)=>{
                    return <TouchableOpacity style={{width: '60%', alignSelf: 'center', paddingVertical:10, marginVertical: 10, backgroundColor: 'white', borderRadius: 28, overflow: 'hidden'}}
                                             onPress={()=>{
                                               setEnterpriseName(item.enterpriseName);
                                               /*切换企业*/
                                               closeModal()
                                             }}>
                      <Text style={{textAlign: 'center'}}>{item.enterpriseName}</Text>
                    </TouchableOpacity>
                  })}
                </ScrollView>
                )}
            </>}
            confirmText={"确认修改"}
/>

image.pngimage.pngimage.png

image.pngimage.png

右图为点击“切换企业”对话框中的“申请加入”后的次级页面