feat:登录页面

This commit is contained in:
李伊帆
2026-09-09 14:08:40 +08:00
parent 4a0cd543af
commit f44e6ce1a1
40 changed files with 3031 additions and 11 deletions

View File

@@ -0,0 +1,89 @@
<script setup lang="ts">
import { onMounted } from 'vue'
const SPLASH_DURATION = 3000
onMounted(() => {
setTimeout(() => {
uni.redirectTo({
url: '/pages/Login/user-login/index',
})
}, SPLASH_DURATION)
})
</script>
<template>
<view class="splash">
<view class="splash__bg-circle" />
<view class="splash__content">
<image
class="splash__logo"
src="/static/icons/logo.png"
mode="aspectFit"
/>
<text class="splash__title">
虫子智造
</text>
<text class="splash__subtitle">
智造研发一体化平台
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.splash {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background: linear-gradient(180deg, $color-splash-gradient-start 0%, $color-splash-gradient-end 100%);
}
.splash__bg-circle {
position: absolute;
top: 50%;
right: -120rpx;
width: 520rpx;
height: 520rpx;
border-radius: 50%;
background: $color-splash-circle;
transform: translateY(-50%);
}
.splash__content {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 38vh;
}
.splash__logo {
width: 160rpx;
height: 160rpx;
}
.splash__title {
margin-top: 40rpx;
font-family: 'Alimama FangYuanTi VF-Bold', 'Alimama FangYuanTi VF', sans-serif;
font-size: 61.07rpx;
font-weight: 700;
font-variation-settings: 'wght' 700, 'BEVL' 100;
color: $color-text-primary;
letter-spacing: 2rpx;
}
.splash__subtitle {
margin-top: 16rpx;
font-family: 'PingFangSC-Regular', 'PingFang SC', sans-serif;
font-size: 22.9rpx;
font-weight: 400;
color: $color-text-secondary;
letter-spacing: 1rpx;
}
</style>

View File

@@ -0,0 +1,159 @@
<script setup lang="ts">
function handleBack() {
uni.navigateBack()
}
</script>
<template>
<view class="user-agreement-page">
<view class="user-agreement-page__navbar">
<view
class="user-agreement-page__back"
@tap="handleBack"
>
<image
class="user-agreement-page__back-icon"
src="/static/icons/icon-back.svg"
mode="aspectFit"
/>
</view>
<text class="user-agreement-page__navbar-title">
帮助中心
</text>
<view class="user-agreement-page__navbar-placeholder" />
</view>
<scroll-view
class="user-agreement-page__scroll"
scroll-y
>
<view class="user-agreement-page__content">
<text class="user-agreement-page__doc-title">
【平台名称】用户协议
</text>
<text class="user-agreement-page__date">
更新日期:【YYYY】年【MM】月【DD】日
</text>
<text class="user-agreement-page__date">
生效日期:【YYYY】年【MM】月【DD】日
</text>
<text class="user-agreement-page__section-title">
导言
</text>
<text class="user-agreement-page__paragraph">
欢迎您使用【平台名称】!本平台由【公司全称】(以下简称"我们")运营,为您提供硬件电路设计、物联网全案服务、软件设计等一站式研发服务。在您注册成为本平台用户前,请您务必审慎阅读、充分理解本协议各条款内容,特别是以加粗或下划线标识的免除或限制责任的条款、知识产权条款、法律适用和争议解决条款。当您按照注册页面提示填写信息、阅读并同意本协议且完成全部注册程序后,即表示您已充分阅读、理解并接受本协议的全部内容。
</text>
<text class="user-agreement-page__section-title">
第一条 定义
</text>
<text class="user-agreement-page__paragraph">
1.1 本平台:指由【公司全称】运营的网站、移动应用程序及相关服务,包括但不限于【平台名称】官方网站及客户端。
</text>
<text class="user-agreement-page__paragraph">
1.2 用户:指完成本平台注册程序,使用本平台服务的自然人、法人或其他组织。
</text>
<text class="user-agreement-page__paragraph">
1.3 企业认证:指用户向本平台提交营业执照等企业资质材料,经平台审核通过的企业身份认证流程。
</text>
<text class="user-agreement-page__paragraph">
1.4 项目:指用户在本平台发起的服务需求,包括但不限于硬件设计、软件开发、物联网方案等研发类任务。
</text>
<text class="user-agreement-page__paragraph">
1.5 电子合同:指用户通过本平台在线签署的电子协议,与纸质合同具有同等法律效力。
</text>
</view>
</scroll-view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.user-agreement-page {
display: flex;
flex-direction: column;
height: 100vh;
background: $color-bg-page;
}
.user-agreement-page__navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: calc(env(safe-area-inset-top) + 20rpx) 32rpx 20rpx;
background: $color-bg-page;
border-bottom: 1rpx solid #f0f0f0;
}
.user-agreement-page__back {
display: flex;
align-items: center;
justify-content: center;
width: 64rpx;
height: 64rpx;
}
.user-agreement-page__back-icon {
width: 40rpx;
height: 40rpx;
}
.user-agreement-page__navbar-title {
font-size: 34rpx;
font-weight: 600;
color: $color-text-primary;
}
.user-agreement-page__navbar-placeholder {
width: 64rpx;
height: 64rpx;
}
.user-agreement-page__scroll {
flex: 1;
height: 0;
}
.user-agreement-page__content {
padding: 32rpx 40rpx calc(40rpx + env(safe-area-inset-bottom));
}
.user-agreement-page__doc-title {
display: block;
margin-bottom: 24rpx;
font-size: 36rpx;
font-weight: 700;
line-height: 1.5;
color: $color-text-primary;
}
.user-agreement-page__date {
display: block;
margin-bottom: 8rpx;
font-size: 28rpx;
line-height: 1.6;
color: $color-text-primary;
}
.user-agreement-page__section-title {
display: block;
margin-top: 40rpx;
margin-bottom: 16rpx;
font-size: 30rpx;
font-weight: 600;
line-height: 1.5;
color: $color-text-primary;
}
.user-agreement-page__paragraph {
display: block;
margin-bottom: 16rpx;
font-size: 28rpx;
line-height: 1.8;
color: $color-text-primary;
text-align: justify;
}
</style>

View File

@@ -0,0 +1,111 @@
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const countdown = ref(0)
let timer: ReturnType<typeof setInterval> | null = null
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
function handleGetCode() {
if (countdown.value > 0)
return
uni.showToast({ title: '验证码已发送', icon: 'none' })
countdown.value = 60
timer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0 && timer) {
clearInterval(timer)
timer = null
}
}, 1000)
}
</script>
<template>
<view class="code-input">
<input
class="code-input__field"
type="number"
placeholder="请输入验证码"
placeholder-class="code-input__placeholder"
:value="modelValue"
@input="onInput"
>
<view
class="code-input__btn"
:class="{ 'code-input__btn--disabled': countdown > 0 }"
@tap="handleGetCode"
>
<text class="code-input__btn-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.code-input {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 16rpx 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.code-input__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.code-input__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
.code-input__btn {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
height: 64rpx;
padding: 0 24rpx;
background: $color-primary;
border-radius: 32rpx;
&--disabled {
opacity: 0.6;
}
}
.code-input__btn-text {
font-size: 24rpx;
font-weight: 500;
color: #ffffff;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
placeholder: string
inputType?: 'text' | 'number'
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
</script>
<template>
<view class="forgot-input">
<input
class="forgot-input__field"
:type="inputType || 'text'"
:placeholder="placeholder"
placeholder-class="forgot-input__placeholder"
:value="modelValue"
@input="onInput"
>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.forgot-input {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.forgot-input__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.forgot-input__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
</style>

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
placeholder: string
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const visible = ref(false)
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
function toggleVisible() {
visible.value = !visible.value
}
</script>
<template>
<view class="password-field">
<input
class="password-field__field"
:password="!visible"
:placeholder="placeholder"
placeholder-class="password-field__placeholder"
:value="modelValue"
@input="onInput"
>
<image
class="password-field__eye"
:src="visible ? '/static/icons/icon-eye-open.svg' : '/static/icons/icon-eye-close.svg'"
mode="aspectFit"
@tap="toggleVisible"
/>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.password-field {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.password-field__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.password-field__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
.password-field__eye {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
margin-left: 16rpx;
}
</style>

View File

@@ -0,0 +1,273 @@
<script setup lang="ts">
import { ref } from 'vue'
import { navigateToResultPage } from '@/utils/navigate-result'
import CodeInput from './components/CodeInput.vue'
import ForgotInput from './components/ForgotInput.vue'
import PasswordField from './components/PasswordField.vue'
const phone = ref('')
const code = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const agreed = ref(false)
function toggleAgreed() {
agreed.value = !agreed.value
}
function handleSubmit() {
if (!agreed.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
return
}
if (!phone.value || !code.value || !newPassword.value || !confirmPassword.value) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
if (newPassword.value !== confirmPassword.value) {
uni.showToast({ title: '两次密码输入不一致', icon: 'none' })
return
}
navigateToResultPage('resetPassword')
}
function handleLogin() {
uni.navigateBack()
}
function handleUserAgreement() {
uni.navigateTo({
url: '/pages/Login/user-agreement/index',
})
}
</script>
<template>
<view class="forgot-page">
<view class="forgot-page__header">
<text class="forgot-page__title">
找回密码
</text>
<text class="forgot-page__subtitle">
使用手机号重新设置您的密码
</text>
</view>
<view class="forgot-page__form">
<view class="forgot-page__field">
<ForgotInput
v-model="phone"
placeholder="请输入手机号"
input-type="number"
/>
</view>
<view class="forgot-page__field">
<CodeInput v-model="code" />
</view>
<view class="forgot-page__field">
<PasswordField
v-model="newPassword"
placeholder="请输入新密码"
/>
</view>
<view class="forgot-page__field">
<PasswordField
v-model="confirmPassword"
placeholder="请再次输入新密码"
/>
</view>
<view
class="forgot-page__agreement"
@tap="toggleAgreed"
>
<view
class="forgot-page__checkbox"
:class="{ 'forgot-page__checkbox--checked': agreed }"
>
<text
v-if="agreed"
class="forgot-page__checkmark"
>
✓
</text>
</view>
<view class="forgot-page__agreement-text">
<text class="forgot-page__agreement-normal">
我已阅读并同意
</text>
<text
class="forgot-page__agreement-link"
@tap.stop="handleUserAgreement"
>
用户协议
</text>
<text class="forgot-page__agreement-normal">
和
</text>
<text class="forgot-page__agreement-link">
隐私政策
</text>
<text class="forgot-page__agreement-normal">
以及
</text>
<text class="forgot-page__agreement-link">
定制条款
</text>
</view>
</view>
<button
class="forgot-page__submit"
@tap="handleSubmit"
>
确认修改
</button>
</view>
<view class="forgot-page__footer">
<text class="forgot-page__footer-normal">
已有账号?
</text>
<text
class="forgot-page__footer-link"
@tap="handleLogin"
>
立即登录
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.forgot-page {
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 0 64rpx;
background: $color-bg-page;
}
.forgot-page__header {
padding-top: 120rpx;
text-align: center;
}
.forgot-page__title {
display: block;
font-size: 56rpx;
font-weight: 700;
color: $color-text-primary;
letter-spacing: 2rpx;
}
.forgot-page__subtitle {
display: block;
margin-top: 16rpx;
font-size: 28rpx;
font-weight: 400;
color: $color-text-secondary;
}
.forgot-page__form {
width: 100%;
margin-top: 80rpx;
box-sizing: border-box;
}
.forgot-page__field {
width: 100%;
margin-top: 24rpx;
box-sizing: border-box;
}
.forgot-page__agreement {
display: flex;
align-items: flex-start;
margin-top: 32rpx;
}
.forgot-page__checkbox {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 32rpx;
height: 32rpx;
margin-top: 4rpx;
margin-right: 12rpx;
border: 2rpx solid #cccccc;
border-radius: 50%;
transition: all 0.2s;
&--checked {
background: $color-primary;
border-color: $color-primary;
}
}
.forgot-page__checkmark {
font-size: 20rpx;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.forgot-page__agreement-text {
flex: 1;
font-size: 24rpx;
line-height: 1.6;
}
.forgot-page__agreement-normal {
color: $color-text-secondary;
}
.forgot-page__agreement-link {
color: $color-text-link;
}
.forgot-page__submit {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
margin-top: 48rpx;
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
background: $color-primary;
border: none;
border-radius: 48rpx;
line-height: 96rpx;
&::after {
border: none;
}
}
.forgot-page__footer {
display: flex;
align-items: center;
justify-content: center;
margin-top: auto;
padding: 60rpx 0 calc(60rpx + env(safe-area-inset-bottom));
}
.forgot-page__footer-normal {
font-size: 28rpx;
color: $color-text-secondary;
}
.forgot-page__footer-link {
margin-left: 4rpx;
font-size: 28rpx;
color: $color-text-link;
}
</style>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
icon: string
placeholder: string
inputType?: 'text' | 'number'
password?: boolean
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
</script>
<template>
<view class="login-input">
<image
class="login-input__icon"
:src="icon"
mode="aspectFit"
/>
<input
class="login-input__field"
:type="inputType || 'text'"
:password="password"
:placeholder="placeholder"
placeholder-class="login-input__placeholder"
:value="modelValue"
@input="onInput"
>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.login-input {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.login-input__icon {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.login-input__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.login-input__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
</style>

View File

@@ -0,0 +1,364 @@
<script setup lang="ts">
import { ref } from 'vue'
import { navigateToResultPage } from '@/utils/navigate-result'
import LoginInput from './components/LoginInput.vue'
const phone = ref('')
const password = ref('')
const agreed = ref(false)
function toggleAgreed() {
agreed.value = !agreed.value
}
function handleLogin() {
if (!agreed.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
return
}
if (!phone.value || !password.value) {
uni.showToast({ title: '请输入手机号和密码', icon: 'none' })
return
}
// TODO: 接入登录接口后,在接口返回失败时调用
navigateToResultPage('loginFailed')
}
function handleForgotPassword() {
uni.navigateTo({
url: '/pages/Login/user-forgot-password/index',
})
}
function handleSmsLogin() {
uni.navigateTo({
url: '/pages/Login/user-sms-login/index',
})
}
function handleRegister() {
uni.navigateTo({
url: '/pages/Login/user-register/index',
})
}
function handleUserAgreement() {
uni.navigateTo({
url: '/pages/Login/user-agreement/index',
})
}
</script>
<template>
<view class="login-page">
<view class="login-page__header">
<text class="login-page__title">
欢迎回来
</text>
<text class="login-page__subtitle">
使用密码登录您的账号
</text>
</view>
<view class="login-page__form">
<LoginInput
v-model="phone"
icon="/static/icons/icon-phone.png"
placeholder="请输入手机号"
input-type="number"
/>
<view class="login-page__password-group">
<LoginInput
v-model="password"
icon="/static/icons/icon-shield-check.png"
placeholder="请输入密码"
password
/>
<text
class="login-page__forgot"
@tap="handleForgotPassword"
>
忘记密码?
</text>
</view>
<view
class="login-page__agreement"
@tap="toggleAgreed"
>
<view
class="login-page__checkbox"
:class="{ 'login-page__checkbox--checked': agreed }"
>
<text
v-if="agreed"
class="login-page__checkmark"
>
✓
</text>
</view>
<view class="login-page__agreement-text">
<text class="login-page__agreement-normal">
我已阅读并同意
</text>
<text
class="login-page__agreement-link"
@tap.stop="handleUserAgreement"
>
用户协议
</text>
<text class="login-page__agreement-normal">
和
</text>
<text class="login-page__agreement-link">
隐私政策
</text>
<text class="login-page__agreement-normal">
以及
</text>
<text class="login-page__agreement-link">
定制条款
</text>
</view>
</view>
<button
class="login-page__submit"
@tap="handleLogin"
>
登录
</button>
</view>
<view class="login-page__other">
<view class="login-page__divider">
<view class="login-page__divider-line" />
<text class="login-page__divider-text">
其他登录方式
</text>
<view class="login-page__divider-line" />
</view>
<view
class="login-page__sms"
@tap="handleSmsLogin"
>
<view class="login-page__sms-icon-wrap">
<image
class="login-page__sms-icon"
src="/static/icons/icon-sms-login.png"
mode="aspectFit"
/>
</view>
<text class="login-page__sms-text">
验证码登录
</text>
</view>
</view>
<view class="login-page__footer">
<text class="login-page__footer-normal">
没有账号?
</text>
<text
class="login-page__footer-link"
@tap="handleRegister"
>
立即注册
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.login-page {
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 0 64rpx;
background: $color-bg-page;
}
.login-page__header {
padding-top: 120rpx;
text-align: center;
}
.login-page__title {
display: block;
font-size: 56rpx;
font-weight: 700;
color: $color-text-primary;
letter-spacing: 2rpx;
}
.login-page__subtitle {
display: block;
margin-top: 16rpx;
font-size: 28rpx;
font-weight: 400;
color: $color-text-secondary;
}
.login-page__form {
width: 100%;
margin-top: 80rpx;
box-sizing: border-box;
}
.login-page__password-group {
margin-top: 24rpx;
}
.login-page__forgot {
display: block;
margin-top: 16rpx;
font-size: 26rpx;
color: $color-text-secondary;
}
.login-page__agreement {
display: flex;
align-items: flex-start;
margin-top: 32rpx;
}
.login-page__checkbox {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 32rpx;
height: 32rpx;
margin-top: 4rpx;
margin-right: 12rpx;
border: 2rpx solid #cccccc;
border-radius: 50%;
transition: all 0.2s;
&--checked {
background: $color-primary;
border-color: $color-primary;
}
}
.login-page__checkmark {
font-size: 20rpx;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.login-page__agreement-text {
flex: 1;
font-size: 24rpx;
line-height: 1.6;
}
.login-page__agreement-normal {
color: $color-text-secondary;
}
.login-page__agreement-link {
color: $color-text-link;
}
.login-page__submit {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
margin-top: 48rpx;
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
background: $color-primary;
border: none;
border-radius: 48rpx;
line-height: 96rpx;
&::after {
border: none;
}
}
.login-page__other {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.login-page__divider {
display: flex;
align-items: center;
width: 100%;
}
.login-page__divider-line {
flex: 1;
height: 1rpx;
background: #e8e8e8;
}
.login-page__divider-text {
padding: 0 24rpx;
font-size: 24rpx;
color: $color-text-secondary;
white-space: nowrap;
}
.login-page__sms {
display: flex;
flex-direction: column;
align-items: center;
width: fit-content;
margin-top: 48rpx;
}
.login-page__sms-icon-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 96rpx;
height: 96rpx;
background: #B6CDFF;
border-radius: 50%;
}
.login-page__sms-icon {
width: 48rpx;
height: 48rpx;
}
.login-page__sms-text {
margin-top: 16rpx;
font-size: 24rpx;
color: #999999;
}
.login-page__footer {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
}
.login-page__footer-normal {
font-size: 28rpx;
color: $color-text-secondary;
}
.login-page__footer-link {
margin-left: 4rpx;
font-size: 28rpx;
color: $color-text-link;
}
</style>

View File

@@ -0,0 +1,109 @@
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import {
RESULT_PAGE_CONFIG,
RESULT_REDIRECT_DELAY,
type ResultPageType,
} from '@/types/result-page'
const pageType = ref<ResultPageType>('register')
const countdown = ref(RESULT_REDIRECT_DELAY / 1000)
let timer: ReturnType<typeof setInterval> | null = null
const pageConfig = computed(() => RESULT_PAGE_CONFIG[pageType.value])
const title = computed(() => pageConfig.value.title)
const subtitle = computed(() => `${countdown.value}秒后自动跳转至${pageConfig.value.subtitleTarget}`)
onLoad((options) => {
const type = options?.type
if (type === 'resetPassword' || type === 'reset-password') {
pageType.value = 'resetPassword'
return
}
if (type === 'loginFailed') {
pageType.value = 'loginFailed'
}
})
onMounted(() => {
timer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0) {
if (timer) {
clearInterval(timer)
timer = null
}
uni.redirectTo({
url: pageConfig.value.redirectUrl,
})
}
}, 1000)
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
</script>
<template>
<view class="result-page">
<view class="result-page__content">
<image
class="result-page__icon"
:src="pageConfig.icon"
mode="aspectFit"
/>
<text class="result-page__title">
{{ title }}
</text>
<text class="result-page__subtitle">
{{ subtitle }}
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.result-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: $color-bg-page;
}
.result-page__content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 64rpx;
}
.result-page__icon {
width: 160rpx;
height: 160rpx;
}
.result-page__title {
margin-top: 48rpx;
font-size: 40rpx;
font-weight: 700;
color: $color-text-primary;
letter-spacing: 1rpx;
}
.result-page__subtitle {
margin-top: 24rpx;
font-size: 28rpx;
font-weight: 400;
color: $color-text-secondary;
}
</style>

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const countdown = ref(0)
let timer: ReturnType<typeof setInterval> | null = null
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
function handleGetCode() {
if (countdown.value > 0)
return
uni.showToast({ title: '验证码已发送', icon: 'none' })
countdown.value = 60
timer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0 && timer) {
clearInterval(timer)
timer = null
}
}, 1000)
}
</script>
<template>
<view class="code-input">
<image
class="code-input__icon"
src="/static/icons/icon-shield-check.png"
mode="aspectFit"
/>
<input
class="code-input__field"
type="number"
placeholder="请输入验证码"
placeholder-class="code-input__placeholder"
:value="modelValue"
@input="onInput"
>
<view
class="code-input__btn"
:class="{ 'code-input__btn--disabled': countdown > 0 }"
@tap="handleGetCode"
>
<text class="code-input__btn-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.code-input {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 16rpx 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.code-input__icon {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.code-input__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.code-input__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
.code-input__btn {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
height: 64rpx;
padding: 0 24rpx;
background: $color-primary;
border-radius: 32rpx;
&--disabled {
opacity: 0.6;
}
}
.code-input__btn-text {
font-size: 24rpx;
font-weight: 500;
color: #ffffff;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,94 @@
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
placeholder: string
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const visible = ref(false)
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
function toggleVisible() {
visible.value = !visible.value
}
</script>
<template>
<view class="password-field">
<image
class="password-field__icon"
src="/static/icons/icon-lock.png"
mode="aspectFit"
/>
<input
class="password-field__field"
:password="!visible"
:placeholder="placeholder"
placeholder-class="password-field__placeholder"
:value="modelValue"
@input="onInput"
>
<image
class="password-field__eye"
:src="visible ? '/static/icons/icon-eye-open.svg' : '/static/icons/icon-eye-close.svg'"
mode="aspectFit"
@tap="toggleVisible"
/>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.password-field {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.password-field__icon {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.password-field__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.password-field__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
.password-field__eye {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
margin-left: 16rpx;
}
</style>

View File

@@ -0,0 +1,119 @@
<script setup lang="ts">
import { computed } from 'vue'
import { getPasswordStrength } from '@/utils/password-strength'
defineOptions({
options: {
virtualHost: true,
},
})
const props = defineProps<{
password: string
}>()
const strength = computed(() => getPasswordStrength(props.password))
const showIndicator = computed(() => props.password.length > 0)
</script>
<template>
<view
v-if="showIndicator"
class="password-strength"
>
<view class="password-strength__row">
<view class="password-strength__bars">
<view
v-for="index in 4"
:key="index"
class="password-strength__bar"
:class="{ 'password-strength__bar--active': index <= strength.level }"
:style="index <= strength.level ? { background: strength.color } : undefined"
/>
</view>
<text
class="password-strength__label"
:style="{ color: strength.color }"
>
{{ strength.label }}
</text>
</view>
<view
v-if="strength.isTooWeak"
class="password-strength__error"
>
<view class="password-strength__error-icon">
<text class="password-strength__error-mark">
<img src="/static/icons/icon-mark.png" alt="error" />
</text>
</view>
<text class="password-strength__error-text">
密码等级过低,请重新输入
</text>
</view>
</view>
</template>
<style scoped lang="scss">
.password-strength {
width: 100%;
margin-top: 16rpx;
}
.password-strength__row {
display: flex;
align-items: center;
}
.password-strength__bars {
display: flex;
flex: 1;
gap: 12rpx;
}
.password-strength__bar {
flex: 1;
height: 8rpx;
background: #e8e8e8;
border-radius: 4rpx;
}
.password-strength__label {
flex-shrink: 0;
margin-left: 16rpx;
font-size: 24rpx;
font-weight: 500;
}
.password-strength__error {
display: flex;
align-items: center;
margin-top: 12rpx;
}
.password-strength__error-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 28rpx;
height: 28rpx;
margin-right: 8rpx;
// background: #ff4d4f;
border-radius: 6rpx;
}
.password-strength__error-mark {
font-size: 20rpx;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.password-strength__error-text {
font-size: 24rpx;
color: #ff4d4f;
}
</style>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
icon: string
placeholder: string
inputType?: 'text' | 'number'
password?: boolean
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
</script>
<template>
<view class="register-input">
<image
class="register-input__icon"
:src="icon"
mode="aspectFit"
/>
<input
class="register-input__field"
:type="inputType || 'text'"
:password="password"
:placeholder="placeholder"
placeholder-class="register-input__placeholder"
:value="modelValue"
@input="onInput"
>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.register-input {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.register-input__icon {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.register-input__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.register-input__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
</style>

View File

@@ -0,0 +1,285 @@
<script setup lang="ts">
import { ref } from 'vue'
import { getPasswordStrength } from '@/utils/password-strength'
import { navigateToResultPage } from '@/utils/navigate-result'
import CodeInput from './components/CodeInput.vue'
import PasswordField from './components/PasswordField.vue'
import PasswordStrength from './components/PasswordStrength.vue'
import RegisterInput from './components/RegisterInput.vue'
const phone = ref('')
const code = ref('')
const password = ref('')
const confirmPassword = ref('')
const agreed = ref(false)
function toggleAgreed() {
agreed.value = !agreed.value
}
function handleRegister() {
if (!agreed.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
return
}
if (!phone.value || !code.value || !password.value || !confirmPassword.value) {
uni.showToast({ title: '请填写完整信息', icon: 'none' })
return
}
if (password.value !== confirmPassword.value) {
uni.showToast({ title: '两次密码输入不一致', icon: 'none' })
return
}
if (getPasswordStrength(password.value).isTooWeak) {
uni.showToast({ title: '密码等级过低,请重新输入', icon: 'none' })
return
}
navigateToResultPage('register')
}
function handleLogin() {
uni.navigateBack()
}
function handleUserAgreement() {
uni.navigateTo({
url: '/pages/Login/user-agreement/index',
})
}
</script>
<template>
<view class="register-page">
<view class="register-page__header">
<text class="register-page__title">
创建账号
</text>
<text class="register-page__subtitle">
注册后即可开始使用全部功能
</text>
</view>
<view class="register-page__form">
<view class="register-page__field">
<RegisterInput
v-model="phone"
icon="/static/icons/icon-phone.png"
placeholder="请输入手机号"
input-type="number"
/>
</view>
<view class="register-page__field">
<CodeInput v-model="code" />
</view>
<view class="register-page__field">
<PasswordField
v-model="password"
placeholder="设置密码"
/>
<PasswordStrength :password="password" />
</view>
<view class="register-page__field">
<PasswordField
v-model="confirmPassword"
placeholder="再次确认密码"
/>
</view>
<view
class="register-page__agreement"
@tap="toggleAgreed"
>
<view
class="register-page__checkbox"
:class="{ 'register-page__checkbox--checked': agreed }"
>
<text
v-if="agreed"
class="register-page__checkmark"
>
✓
</text>
</view>
<view class="register-page__agreement-text">
<text class="register-page__agreement-normal">
我已阅读并同意
</text>
<text
class="register-page__agreement-link"
@tap.stop="handleUserAgreement"
>
用户协议
</text>
<text class="register-page__agreement-normal">
和
</text>
<text class="register-page__agreement-link">
隐私政策
</text>
<text class="register-page__agreement-normal">
以及
</text>
<text class="register-page__agreement-link">
定制条款
</text>
</view>
</view>
<button
class="register-page__submit"
@tap="handleRegister"
>
注册
</button>
</view>
<view class="register-page__footer">
<text class="register-page__footer-normal">
已有账号?
</text>
<text
class="register-page__footer-link"
@tap="handleLogin"
>
立即登录
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.register-page {
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 0 64rpx;
background: $color-bg-page;
}
.register-page__header {
padding-top: 120rpx;
text-align: center;
}
.register-page__title {
display: block;
font-size: 56rpx;
font-weight: 700;
color: $color-text-primary;
letter-spacing: 2rpx;
}
.register-page__subtitle {
display: block;
margin-top: 16rpx;
font-size: 28rpx;
font-weight: 400;
color: $color-text-secondary;
}
.register-page__form {
width: 100%;
margin-top: 80rpx;
box-sizing: border-box;
}
.register-page__field {
width: 100%;
margin-top: 24rpx;
box-sizing: border-box;
&:first-child {
margin-top: 0;
}
}
.register-page__agreement {
display: flex;
align-items: flex-start;
margin-top: 32rpx;
}
.register-page__checkbox {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 32rpx;
height: 32rpx;
margin-top: 4rpx;
margin-right: 12rpx;
border: 2rpx solid #cccccc;
border-radius: 50%;
transition: all 0.2s;
&--checked {
background: $color-primary;
border-color: $color-primary;
}
}
.register-page__checkmark {
font-size: 20rpx;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.register-page__agreement-text {
flex: 1;
font-size: 24rpx;
line-height: 1.6;
}
.register-page__agreement-normal {
color: $color-text-secondary;
}
.register-page__agreement-link {
color: $color-text-link;
}
.register-page__submit {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
margin-top: 48rpx;
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
background: $color-primary;
border: none;
border-radius: 48rpx;
line-height: 96rpx;
&::after {
border: none;
}
}
.register-page__footer {
display: flex;
align-items: center;
justify-content: center;
margin-top: auto;
padding: 60rpx 0 calc(60rpx + env(safe-area-inset-bottom));
}
.register-page__footer-normal {
font-size: 28rpx;
color: $color-text-secondary;
}
.register-page__footer-link {
margin-left: 4rpx;
font-size: 28rpx;
color: $color-text-link;
}
</style>

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
import { ref } from 'vue'
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const countdown = ref(0)
let timer: ReturnType<typeof setInterval> | null = null
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
function handleGetCode() {
if (countdown.value > 0)
return
uni.showToast({ title: '验证码已发送', icon: 'none' })
countdown.value = 60
timer = setInterval(() => {
countdown.value -= 1
if (countdown.value <= 0 && timer) {
clearInterval(timer)
timer = null
}
}, 1000)
}
</script>
<template>
<view class="code-input">
<image
class="code-input__icon"
src="/static/icons/icon-shield-check.png"
mode="aspectFit"
/>
<input
class="code-input__field"
type="number"
placeholder="请输入验证码"
placeholder-class="code-input__placeholder"
:value="modelValue"
@input="onInput"
>
<view
class="code-input__btn"
:class="{ 'code-input__btn--disabled': countdown > 0 }"
@tap="handleGetCode"
>
<text class="code-input__btn-text">
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.code-input {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 16rpx 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.code-input__icon {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.code-input__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.code-input__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
.code-input__btn {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
height: 64rpx;
padding: 0 24rpx;
background: $color-primary;
border-radius: 32rpx;
&--disabled {
opacity: 0.6;
}
}
.code-input__btn-text {
font-size: 24rpx;
font-weight: 500;
color: #ffffff;
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,77 @@
<script setup lang="ts">
defineOptions({
options: {
virtualHost: true,
},
})
defineProps<{
icon: string
placeholder: string
inputType?: 'text' | 'number'
password?: boolean
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
function onInput(e: { detail: { value: string } }) {
emit('update:modelValue', e.detail.value)
}
</script>
<template>
<view class="login-input">
<image
class="login-input__icon"
:src="icon"
mode="aspectFit"
/>
<input
class="login-input__field"
:type="inputType || 'text'"
:password="password"
:placeholder="placeholder"
placeholder-class="login-input__placeholder"
:value="modelValue"
@input="onInput"
>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.login-input {
display: flex;
align-items: center;
width: 100%;
height: 96rpx;
padding: 0 32rpx;
box-sizing: border-box;
background: $color-bg-input;
border-radius: 48rpx;
}
.login-input__icon {
flex-shrink: 0;
width: 40rpx;
height: 40rpx;
margin-right: 20rpx;
}
.login-input__field {
flex: 1;
height: 96rpx;
font-size: 30rpx;
color: $color-text-primary;
background: transparent;
}
.login-input__placeholder {
font-size: 30rpx;
color: $color-text-placeholder;
}
</style>

View File

@@ -0,0 +1,335 @@
<script setup lang="ts">
import { ref } from 'vue'
import CodeInput from './components/CodeInput.vue'
import LoginInput from './components/LoginInput.vue'
const phone = ref('')
const code = ref('')
const agreed = ref(false)
function toggleAgreed() {
agreed.value = !agreed.value
}
function handleLogin() {
if (!agreed.value) {
uni.showToast({ title: '请先同意用户协议', icon: 'none' })
return
}
if (!phone.value || !code.value) {
uni.showToast({ title: '请输入手机号和验证码', icon: 'none' })
return
}
}
function handlePasswordLogin() {
uni.navigateBack()
}
function handleRegister() {
uni.navigateTo({
url: '/pages/Login/user-register/index',
})
}
function handleUserAgreement() {
uni.navigateTo({
url: '/pages/Login/user-agreement/index',
})
}
</script>
<template>
<view class="sms-login-page">
<view class="sms-login-page__header">
<text class="sms-login-page__title">
欢迎回来
</text>
<text class="sms-login-page__subtitle">
使用手机号登录您的账号
</text>
</view>
<view class="sms-login-page__form">
<LoginInput
v-model="phone"
icon="/static/icons/icon-phone.png"
placeholder="请输入手机号"
input-type="number"
/>
<view class="sms-login-page__code-group">
<CodeInput v-model="code" />
</view>
<view
class="sms-login-page__agreement"
@tap="toggleAgreed"
>
<view
class="sms-login-page__checkbox"
:class="{ 'sms-login-page__checkbox--checked': agreed }"
>
<text
v-if="agreed"
class="sms-login-page__checkmark"
>
✓
</text>
</view>
<view class="sms-login-page__agreement-text">
<text class="sms-login-page__agreement-normal">
我已阅读并同意
</text>
<text
class="sms-login-page__agreement-link"
@tap.stop="handleUserAgreement"
>
用户协议
</text>
<text class="sms-login-page__agreement-normal">
和
</text>
<text class="sms-login-page__agreement-link">
隐私政策
</text>
<text class="sms-login-page__agreement-normal">
以及
</text>
<text class="sms-login-page__agreement-link">
定制条款
</text>
</view>
</view>
<button
class="sms-login-page__submit"
@tap="handleLogin"
>
登录
</button>
</view>
<view class="sms-login-page__other">
<view class="sms-login-page__divider">
<view class="sms-login-page__divider-line" />
<text class="sms-login-page__divider-text">
其他登录方式
</text>
<view class="sms-login-page__divider-line" />
</view>
<view
class="sms-login-page__password"
@tap="handlePasswordLogin"
>
<view class="sms-login-page__password-icon-wrap">
<image
class="sms-login-page__password-icon"
src="/static/icons/icon-password-login.png"
mode="aspectFit"
/>
</view>
<text class="sms-login-page__password-text">
密码登录
</text>
</view>
</view>
<view class="sms-login-page__footer">
<text class="sms-login-page__footer-normal">
没有账号?
</text>
<text
class="sms-login-page__footer-link"
@tap="handleRegister"
>
立即注册
</text>
</view>
</view>
</template>
<style scoped lang="scss">
@import '@/styles/variables.scss';
.sms-login-page {
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 0 64rpx;
background: $color-bg-page;
}
.sms-login-page__header {
padding-top: 120rpx;
text-align: center;
}
.sms-login-page__title {
display: block;
font-size: 56rpx;
font-weight: 700;
color: $color-text-primary;
letter-spacing: 2rpx;
}
.sms-login-page__subtitle {
display: block;
margin-top: 16rpx;
font-size: 28rpx;
font-weight: 400;
color: $color-text-secondary;
}
.sms-login-page__form {
width: 100%;
margin-top: 80rpx;
box-sizing: border-box;
}
.sms-login-page__code-group {
margin-top: 24rpx;
}
.sms-login-page__agreement {
display: flex;
align-items: flex-start;
margin-top: 32rpx;
}
.sms-login-page__checkbox {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 32rpx;
height: 32rpx;
margin-top: 4rpx;
margin-right: 12rpx;
border: 2rpx solid #cccccc;
border-radius: 50%;
transition: all 0.2s;
&--checked {
background: $color-primary;
border-color: $color-primary;
}
}
.sms-login-page__checkmark {
font-size: 20rpx;
font-weight: 700;
color: #ffffff;
line-height: 1;
}
.sms-login-page__agreement-text {
flex: 1;
font-size: 24rpx;
line-height: 1.6;
}
.sms-login-page__agreement-normal {
color: $color-text-secondary;
}
.sms-login-page__agreement-link {
color: $color-text-link;
}
.sms-login-page__submit {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
margin-top: 48rpx;
font-size: 32rpx;
font-weight: 600;
color: #ffffff;
background: $color-primary;
border: none;
border-radius: 48rpx;
line-height: 96rpx;
&::after {
border: none;
}
}
.sms-login-page__other {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 0;
}
.sms-login-page__divider {
display: flex;
align-items: center;
width: 100%;
}
.sms-login-page__divider-line {
flex: 1;
height: 1rpx;
background: #e8e8e8;
}
.sms-login-page__divider-text {
padding: 0 24rpx;
font-size: 24rpx;
color: $color-text-secondary;
white-space: nowrap;
}
.sms-login-page__password {
display: flex;
flex-direction: column;
align-items: center;
width: fit-content;
margin-top: 48rpx;
}
.sms-login-page__password-icon-wrap {
display: flex;
align-items: center;
justify-content: center;
width: 96rpx;
height: 96rpx;
background: #B6CDFF;
border-radius: 50%;
}
.sms-login-page__password-icon {
width: 48rpx;
height: 48rpx;
}
.sms-login-page__password-text {
margin-top: 16rpx;
font-size: 24rpx;
color: #999999;
}
.sms-login-page__footer {
display: flex;
align-items: center;
justify-content: center;
padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
}
.sms-login-page__footer-normal {
font-size: 28rpx;
color: $color-text-secondary;
}
.sms-login-page__footer-link {
margin-left: 4rpx;
font-size: 28rpx;
color: $color-text-link;
}
</style>