Files
taimed-international-app/pages/login/login.vue

697 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="login-page">
<!-- Logo 背景区域 -->
<view class="logo-bg">
<text class="welcome-text">Hello! Welcome to<br>Taimed International</text>
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-1"></image>
<image src="@/static/icon/login_icon.png" mode="aspectFit" class="icon-hua-2"></image>
</view>
<!-- 登录表单区域 -->
<view class="form-box">
<!-- 登录方式标题 -->
<view class="login-method">
<view class="title active">
<template v-if="loginType === 2000">{{ $t('login.codeLogin') }}</template>
<template v-if="loginType === 1000">{{ $t('login.passwordLogin') }}</template>
</view>
</view>
<!-- 验证码登录 -->
<view v-if="loginType === 2000">
<view class="input-tit">{{ $t('login.email') }}</view>
<view class="input-box">
<input
v-model="email"
:placeholder="$t('login.emailPlaceholder')"
placeholder-class="grey"
/>
</view>
<view class="input-tit">{{ $t('login.code') }}</view>
<view class="input-box">
<input
v-model="code"
:placeholder="$t('login.codePlaceholder')"
placeholder-class="grey"
maxlength="6"
@confirm="onSubmit"
/>
<wd-button type="info" :class="['code-btn', { active: !readonly }]" @click="onSetCode">
{{ t('login.getCode') }}
</wd-button>
</view>
</view>
<!-- 密码登录 -->
<view v-if="loginType === 1000">
<view class="input-tit">{{ $t('login.email') }}</view>
<view class="input-box">
<input
v-model="phoneEmail"
:placeholder="$t('login.emailPlaceholder')"
placeholder-class="grey"
/>
</view>
<view class="input-tit">{{ $t('login.password') }}</view>
<view class="input-box">
<input
class="input-item"
v-model="password"
:password="!isSee"
:placeholder="$t('login.passwordPlaceholder')"
placeholder-class="grey"
@confirm="onSubmit"
/>
<image
v-if="isSee"
src="@/static/icon/ic_logon_display.png"
mode="aspectFit"
class="eye-icon"
@click="isSee = false"
></image>
<image
v-else
src="@/static/icon/ic_logon_hide.png"
mode="aspectFit"
class="eye-icon"
@click="isSee = true"
></image>
</view>
</view>
<!-- 协议同意 -->
<view class="protocol-box">
<view class="select" :class="{ active: agree }" @click="agreeAgreements"></view>
<view class="protocol-text">
{{ $t('login.agree') }}
<text class="highlight" @click="yhxy">{{ $t('login.userAgreement') }}</text>
and
<text class="highlight" @click="yszc">{{ $t('login.privacyPolicy') }}</text>
</view>
</view>
<!-- 登录按钮 -->
<view class="btn-box">
<button @click="onSubmit" class="login-btn" :class="{ active: btnShow }">
{{ $t('login.goLogin') }}
</button>
</view>
<view class="loginHelp" v-if="submitClickNum > 0">
<text>登录遇到问题</text><text class="link" @click="onPageJump('/pages/user/workOrder?name=login')">去反馈</text>
</view>
<!-- 切换登录方式 -->
<view class="qie-huan">
<view v-if="loginType === 2000" @click="changeLoginType(1000)">
{{ $t('login.switchToPassword') }}
</view>
<view v-if="loginType === 1000" class="switch-links">
<text @click="changeLoginType(2000)">{{ $t('login.switchToCode') }}</text>
<text @click="onPageJump('/pages/login/forget')">{{ $t('login.forgotPassword') }}</text>
</view>
</view>
<!-- 游客体验 -->
<view class="youke-l">
<view @click="onPageJump('/pages/visitor/visitor')">
{{ $t('login.noLogin') }}
</view>
</view>
</view>
<!-- 用户协议弹窗 -->
<wd-popup v-model="yhxyShow" position="bottom">
<view class="tanchu">
<view class="dp-title" v-html="yhxyText.title"></view>
<view class="dp-content" v-html="yhxyText.content"></view>
</view>
</wd-popup>
<!-- 隐私政策弹窗 -->
<wd-popup v-model="yszcShow" position="bottom">
<view class="tanchu">
<view class="dp-title" v-html="yszcText.title"></view>
<view class="dp-content" v-html="yszcText.content"></view>
</view>
</wd-popup>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { loginWithCode, loginWithPassword } from '@/api/modules/auth'
import { commonApi } from '@/api/modules/common'
import { validateEmail } from '@/utils/validator'
import { onPageJump } from '@/utils'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const userStore = useUserStore()
// 登录类型2000-验证码登录1000-密码登录
const loginType = ref<1000 | 2000>(2000)
// 表单数据
const email = ref('')
const code = ref('')
const password = ref('')
const phoneEmail = ref('')
const agree = ref(false)
const isSee = ref(false)
// 验证码相关
const codeText = ref('Get Code')
const readonly = ref(false)
const btnShow = ref(true)
// 用户协议和隐私政策
const yhxyShow = ref(false)
const yszcShow = ref(false)
const yhxyText = ref<any>({
title: '',
content: ''
})
const yszcText = ref<any>({
title: '',
content: ''
})
let codeTimer: any = null
// 提交点击次数
const submitClickNum = ref(0)
/**
* 切换登录方式
* @param type 1000-密码登录2000-验证码登录
*/
const changeLoginType = (type: 1000 | 2000) => {
loginType.value = type
code.value = ''
password.value = ''
const temporaryEmail = email.value || phoneEmail.value
email.value = temporaryEmail
phoneEmail.value = temporaryEmail
}
/**
* 表单验证
*/
// 是否同意协议
const isAgree = () => {
if (!agree.value) {
uni.showToast({
title: t('login.agreeFirst'),
icon: 'none'
})
return false
} else {
return true
}
}
// 是否填写邮箱
const isEmailEmpty = () => {
if (!email.value) {
uni.showToast({
title: t('login.emailPlaceholder'),
icon: 'none'
})
return false
} else {
return true
}
}
// 是否填写验证码
const isCodeEmpty = () => {
if (!code.value) {
uni.showToast({
title: t('login.codePlaceholder'),
icon: 'none'
})
return false
} else {
return true
}
}
// 邮箱格式验证
const isEmailVerified = (email: string) => {
console.log(email, validateEmail(email))
if (!validateEmail(email)) {
uni.showToast({
title: t('login.emailError'),
icon: 'none'
})
return false
} else {
return true
}
}
// 是否填写手机号或邮箱
const isPhoneEmailEmpty = () => {
if (!phoneEmail.value) {
uni.showToast({
title: t('login.emailPlaceholder'),
icon: 'none'
})
return false
} else {
return true
}
}
// 是否填写密码
const isPasswordEmpty = () => {
if (!password.value) {
uni.showToast({
title: t('login.passwordPlaceholder'),
icon: 'none'
})
return false
} else {
return true
}
}
/**
* 提交登录
*/
// 验证码登录
const verifyCodeLogin = async () => {
if (!isEmailEmpty()) return false
if (!isEmailVerified(email.value)) return false
if (!isCodeEmpty()) return false
try {
const res = await loginWithCode(email.value, code.value)
return res
} catch (error) {
console.error('验证码登录失败:', error)
return null
}
}
// 密码登录
const passwordLogin = async () => {
if (!isPhoneEmailEmpty()) return false
if (!isEmailVerified(phoneEmail.value)) return false
if (!isPasswordEmpty()) return false
try {
const res = await loginWithPassword(phoneEmail.value, password.value)
return res
} catch (error) {
console.error('密码登录失败:', error)
return null
}
}
// 提交登录
const onSubmit = async () => {
if(!isAgree()) return false
let res = null
switch (loginType.value) {
case 2000:
res = await verifyCodeLogin()
break
case 1000:
res = await passwordLogin()
break
}
if (res && res.userInfo && res.token) {
res.userInfo.token = res.token.token
userStore.setUserInfo(res.userInfo)
uni.showToast({
title: t('login.loginSuccess'),
duration: 600,
})
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
})
}, 600)
} else {
// 登录失败时增加提交点击次数
submitClickNum.value++
}
}
/**
* 发送验证码
*/
const onSetCode = async () => {
if (readonly.value) {
return
}
if(!isAgree()) return false
if (!isEmailEmpty()) return false
if (!isEmailVerified(email.value)) return false
try {
uni.showLoading()
await commonApi.sendMailCaptcha(email.value)
uni.hideLoading()
uni.showToast({
title: t('login.sendCodeSuccess'),
icon: 'none'
})
getCodeState()
} catch (error) {
uni.hideLoading()
submitClickNum.value++ // 发送验证码失败时增加提交点击次数
console.error('Send code error:', error)
}
}
/**
* 验证码倒计时
*/
const getCodeState = () => {
if (codeTimer) {
clearInterval(codeTimer)
}
readonly.value = true
let countdown = 60
codeText.value = '60S'
codeTimer = setInterval(() => {
countdown--
codeText.value = `${countdown}S`
if (countdown <= 0) {
clearInterval(codeTimer)
codeText.value = t('login.getCode')
readonly.value = false
}
}, 1000)
}
/**
* 显示用户协议
*/
const yhxy = () => {
yhxyShow.value = true
}
/**
* 显示隐私政策
*/
const yszc = () => {
yszcShow.value = true
}
/**
* 页面跳转
*/
const onPageJump = (url: string) => {
uni.navigateTo({
url: url,
})
}
/**
* 获取协议内容
*/
const getAgreements = async (id: number) => {
const res = await commonApi.getAgreement(id)
if (!res.content) return false
let content = res.content || ''
content = content.replace(
/<h5>/g,
'<view style="font-weight: bold;font-size: 32rpx;margin-top: 20rpx;margin-bottom: 20rpx;">'
)
content = content.replace(/<\/h5>/g, '</view>')
return {
title: res.title,
content: content
}
}
const loadAgreements = async () => {
// 获取用户协议
const yhxyRes = await getAgreements(111)
yhxyText.value = yhxyRes
// 获取隐私政策
const yszcRes = await getAgreements(112)
yszcText.value = yszcRes
}
// 同意协议
const agreeAgreements = () => {
agree.value = !agree.value;
uni.setStorageSync('Agreements_agreed', agree.value);
}
onMounted(() => {
loadAgreements()
})
</script>
<style lang="scss" scoped>
.login-page {
background-color: #fff;
min-height: 100vh;
}
.logo-bg {
background-image: url('@/static/icon/login_bg.png');
background-repeat: no-repeat;
background-size: 100% 100%;
height: 25vh;
position: relative;
.welcome-text {
font-size: 45rpx;
line-height: 65rpx;
position: absolute;
top: 120rpx;
left: 60rpx;
color: #fff;
letter-spacing: 6rpx;
}
.icon-hua-1 {
position: absolute;
bottom: 60rpx;
left: 245rpx;
width: 150rpx;
height: 150rpx;
opacity: 0.08;
}
.icon-hua-2 {
position: absolute;
bottom: 10rpx;
right: 30rpx;
width: 250rpx;
height: 250rpx;
opacity: 0.15;
}
}
.form-box {
padding: calc(var(--status-bar-height) + 40rpx) 60rpx 50rpx 60rpx;
background-color: #fff;
min-height: 75vh;
.login-method {
text-align: center;
padding: 0 96rpx;
.title {
margin: 0 auto;
font-size: 40rpx;
letter-spacing: 3rpx;
color: #666;
&.active {
position: relative;
color: $app-theme-color;
padding-bottom: 35rpx;
font-weight: bold;
}
}
}
.input-tit {
margin-top: 20rpx;
font-size: 34rpx;
font-weight: bold;
color: $app-theme-color;
}
.input-box {
display: flex;
align-items: center;
border-radius: 8rpx;
border-bottom: solid 2rpx #efeef4;
margin: 30rpx 0;
input {
flex: 1;
font-size: 28rpx;
color: #333;
height: 70rpx;
}
.input-item {
font-size: 28rpx;
flex: 1;
height: 70rpx;
}
.code-btn {
height: 60rpx;
background-color: #f8f9fb;
font-size: 28rpx;
padding: 0 14rpx;
min-width: 0;
border-radius: 10rpx;
color: #999;
line-height: 60rpx;
margin-left: 20rpx;
border: none;
&.active {
color: $app-theme-color;
}
}
.eye-icon {
width: 36rpx;
height: 24rpx;
margin-left: 20rpx;
}
.grey {
color: #999999;
}
}
.protocol-box {
line-height: 38rpx;
margin-top: 40rpx;
display: flex;
width: 100%;
font-size: 28rpx;
color: #333333;
.select {
width: 36rpx;
height: 36rpx;
background-image: url('@/static/icon/ic_gender_unselected.png');
background-position: center center;
background-repeat: no-repeat;
background-size: 100% auto;
margin-right: 15rpx;
margin-top: 2rpx;
flex-shrink: 0;
&.active {
background-image: url('@/static/icon/ic_agreed.png');
}
}
.protocol-text {
flex: 1;
}
.highlight {
color: $app-theme-color;
}
}
.btn-box {
margin-top: 40rpx;
.login-btn {
font-size: 32rpx;
background-color: #e5e5e5;
color: #fff;
height: 80rpx;
line-height: 80rpx;
border-radius: 50rpx;
border: none;
&.active {
background: linear-gradient(90deg, #54a966 0%, #54a966 100%);
color: #fff;
}
}
}
.qie-huan {
font-size: 26rpx;
margin: 20rpx 0 0 0;
text-align: center;
color: #333;
.switch-links {
display: flex;
justify-content: space-between;
width: 100%;
}
}
.youke-l {
display: flex;
justify-content: center;
margin: 60rpx 0 0 0;
font-size: 26rpx;
color: $app-theme-color;
view {
font-weight: bold;
border: 1px solid $app-theme-color;
border-radius: 10rpx;
padding: 5rpx 15rpx;
}
}
}
.tanchu {
padding: 40rpx 10rpx;
position: relative;
background: #fff;
.dp-title {
font-size: 36rpx;
margin-bottom: 50rpx;
color: #555;
text-align: center;
font-weight: bold;
}
.dp-content {
max-height: 70vh;
overflow-y: auto;
padding: 0 20rpx;
font-size: 28rpx;
color: #555;
line-height: 45rpx;
}
}
.loginHelp{
border: 1px solid #f5dab1;
margin-top: 16rpx;
font-size: 26rpx;
text-align: center;
padding: 10rpx;
background-color: #fdf6ec;
border-radius: 15rpx;
.link{
color: #e6a23c;
}
}
</style>