697 lines
15 KiB
Vue
697 lines
15 KiB
Vue
<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>
|