最终效果
技术要点
用户协议 – 打开本地浏览器
点击后,直接打开本地浏览器浏览网页
// 最终需修改为 《用户协议》 的网址
Linking.openURL("https://www.baidu.com");
手机号输入框的 344 展示
onChangeText={(text: string) => {
setPhone(formatPhone(text));
}}
export function formatPhone(phone: string): string {
let trim: string = phone.replace(/\s+/g, "");
const result = [trim.slice(0, 3), trim.slice(3, 7), trim.slice(7, 11)]
.filter((item) => !!item)
.join(" ");
return result;
}
但在访问接口登录时,传参需清除空格
UserStore.requestLogin(replaceBlank(phone), pwd, (success: boolean) => {
export function replaceBlank(phone: string): string {
return phone ? phone.replace(/\s+/g, "") : "";
}
代码实现
app/_layout.tsx
<Stack>
{/* 登录页 */}
<Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
app/login.tsx
import UserStore from "@/stores/UserStore";
import { formatPhone, replaceBlank } from "@/utils/StringUtil";
import { Toast } from "@ant-design/react-native";
import AntDesign from "@expo/vector-icons/AntDesign";
import Entypo from "@expo/vector-icons/Entypo";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { useRouter } from "expo-router";
import { useState } from "react";
import {
Image,
KeyboardAvoidingView,
Linking,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import Animated from "react-native-reanimated";
export default function LoginScreen() {
const router = useRouter();
const [ifRead, setIfRead] = useState(false);
const [ifQuickLogin, setIfQuickLogin] = useState(false);
const [phone, setPhone] = useState<string>("");
const [pwd, setPwd] = useState<string>("");
const [showPassword, setShowPassword] = useState(false);
const canLogin = phone?.length === 13 && pwd?.length >= 6;
const onLoginPress = async () => {
if (!canLogin) {
return;
}
if (!ifRead) {
Toast.show({
content: "请先同意并阅读用户协议",
});
return;
}
UserStore.requestLogin(replaceBlank(phone), pwd, (success: boolean) => {
if (success) {
Toast.success("登录成功");
router.replace("/(tabs)");
} else {
Toast.fail("登录失败,账号/密码错误!");
}
});
};
const render_passwordLogin = () => {
const passwordLogin_styles = StyleSheet.create({
inputBox: {
borderColor: "#ccc",
borderBottomColor: "#ccc",
borderBottomWidth: 1,
flexDirection: "row",
alignItems: "center",
marginBottom: 14,
height: 55,
},
phonePrefix: {
fontSize: 20,
color: "#bbb",
marginRight: 8,
},
phoneInput: {
fontSize: 24,
paddingLeft: 14,
textAlignVertical: "bottom",
height: 75,
flex: 1,
},
passwordInput: {
fontSize: 20,
textAlignVertical: "bottom",
flex: 1,
height: 75,
},
loginButton: {
backgroundColor: "#ff2442",
},
loginButtonDisable: {
backgroundColor: "#DDDDDD",
},
moreBox: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 26,
},
tip: {
fontSize: 14,
color: "#bbb",
marginBottom: 20,
textAlign: "center",
},
toCodeLoginBtn: {
flexDirection: "row",
alignItems: "center",
},
toCodeLoginBtnTxt: {
marginLeft: 6,
},
moreBoxTxt: {
color: "#303080",
},
});
return (
<View>
<Text style={passwordLogin_styles.tip}>
未注册的手机号登录成功后将自动注册
</Text>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 30}
>
<View style={passwordLogin_styles.inputBox}>
<Text style={passwordLogin_styles.phonePrefix}>+86</Text>
<AntDesign name="caretdown" size={14} color="#bbb" />
<TextInput
style={passwordLogin_styles.phoneInput}
placeholder="请输入手机号"
placeholderTextColor="#bbb"
autoFocus={false}
keyboardType="number-pad"
maxLength={13}
value={phone}
onChangeText={(text: string) => {
setPhone(formatPhone(text));
}}
/>
</View>
<View style={passwordLogin_styles.inputBox}>
<TextInput
style={passwordLogin_styles.passwordInput}
placeholder="请输入密码"
placeholderTextColor="#bbb"
autoFocus={false}
maxLength={20}
secureTextEntry={!showPassword}
value={pwd}
onChangeText={(text: string) => {
setPwd(text);
}}
/>
<TouchableOpacity
activeOpacity={0.7}
onPress={() => {
setShowPassword(!showPassword);
}}
>
<Entypo
name={showPassword ? "eye" : "eye-with-line"}
size={28}
color="#bbb"
/>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
<View style={passwordLogin_styles.moreBox}>
<TouchableOpacity
style={passwordLogin_styles.toCodeLoginBtn}
onPress={() => {
Toast.show({
content: "待需要时完善",
});
}}
>
<FontAwesome name="exchange" size={12} color="#303080" />
<Text
style={[
passwordLogin_styles.toCodeLoginBtnTxt,
passwordLogin_styles.moreBoxTxt,
]}
>
验证码登录
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
Toast.show({
content: "待需要时完善",
});
}}
>
<Text style={passwordLogin_styles.moreBoxTxt}>忘记密码?</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={[
styles.btn,
canLogin
? passwordLogin_styles.loginButton
: passwordLogin_styles.loginButtonDisable,
]}
activeOpacity={canLogin ? 0.7 : 1}
onPress={onLoginPress}
>
<Text style={[styles.btnTxt]}>登录</Text>
</TouchableOpacity>
</View>
);
};
return (
<View style={styles.page}>
<Image
source={require("@/assets/images/icon.png")}
style={{ width: 200, height: 200, marginTop: 100 }}
resizeMode="cover"
/>
<Animated.View style={styles.LoginBox}>
{ifQuickLogin ? (
<>
<TouchableOpacity
style={[styles.btn, styles.oneKeyLoginButton]}
activeOpacity={0.7}
onPress={() => {
Toast.show({
content: "待需要时完善",
});
}}
>
<Text style={[styles.btnTxt, styles.oneKeyLoginTxt]}>
一键登录
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, styles.wxLoginButton]}
activeOpacity={0.7}
onPress={() => {
Toast.show({
content: "待需要时完善",
});
}}
>
<AntDesign name="wechat" size={24} color="white" />
<Text style={[styles.btnTxt]}>微信登录</Text>
</TouchableOpacity>
</>
) : (
render_passwordLogin()
)}
<TouchableOpacity
activeOpacity={0.7}
style={styles.moreLoginWayBox}
onPress={() => {
setIfQuickLogin(!ifQuickLogin);
}}
>
<Text style={[styles.moreLoginWayTxt]}>
{ifQuickLogin ? "其他登录方式" : "快捷登录"}
</Text>
<Entypo name="chevron-small-right" size={20} />
</TouchableOpacity>
<View style={styles.moreBox}>
<TouchableOpacity
onPress={() => {
setIfRead(!ifRead);
}}
>
{ifRead ? (
<AntDesign
style={styles.moreIcon}
name="checkcircle"
size={14}
color="#05c160"
/>
) : (
<Entypo
style={styles.moreIcon}
name="circle"
size={14}
color="gray"
/>
)}
</TouchableOpacity>
<Text style={styles.infoTxt}>我已阅读并同意</Text>
<TouchableOpacity
onPress={() => {
// 最终需修改为 《用户协议》 的网址
Linking.openURL("https://www.baidu.com");
}}
>
<Text style={styles.linkTxt}>《用户协议》</Text>
</TouchableOpacity>
<Text style={styles.infoTxt}>和</Text>
<TouchableOpacity
onPress={() => {
// 最终需修改为 《隐私政策》 的网址
Linking.openURL("https://www.baidu.com");
}}
>
<Text style={styles.linkTxt}>《隐私政策》</Text>
</TouchableOpacity>
</View>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
page: {
flex: 1,
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#e8e8e7",
padding: 40,
},
LoginBox: {
width: "100%",
padding: 20,
marginBottom: 100,
},
btn: {
width: "100%",
height: 56,
borderRadius: 28,
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
marginBottom: 20,
},
btnTxt: {
fontSize: 18,
color: "white",
marginBottom: 6,
marginLeft: 14,
},
oneKeyLoginButton: {
backgroundColor: "#ff2442",
},
oneKeyLoginTxt: {
marginLeft: 4,
},
wxLoginButton: {
backgroundColor: "#05c160",
},
moreBox: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
},
moreIcon: {
marginRight: 4,
marginTop: 2,
},
moreLoginWayBox: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
marginBottom: 60,
},
moreLoginWayTxt: {
fontSize: 16,
marginBottom: 4,
},
infoTxt: {
fontSize: 14,
color: "gray",
},
linkTxt: {
fontSize: 14,
color: "blue",
},
});
utils/StringUtil.ts
export function formatPhone(phone: string): string {
let trim: string = phone.replace(/\s+/g, "");
const result = [trim.slice(0, 3), trim.slice(3, 7), trim.slice(7, 11)]
.filter((item) => !!item)
.join(" ");
return result;
}
export function replaceBlank(phone: string): string {
return phone ? phone.replace(/\s+/g, "") : "";
}