433 lines
9.6 KiB
Vue
433 lines
9.6 KiB
Vue
<template>
|
||
<view class="login-container">
|
||
<view class="header">
|
||
<view class="title">Hello,</view>
|
||
<view class="subtitle">{{ $t('app.welcome') }}</view>
|
||
</view>
|
||
|
||
<!-- 国家区号选择器 -->
|
||
<view class="form-group">
|
||
<view class="phone-input-wrapper">
|
||
<view class="country-code" @click="showCountryPicker">
|
||
<text>{{ countryCode }}</text>
|
||
<text class="arrow">▼</text>
|
||
</view>
|
||
<view class="divider"></view>
|
||
<input
|
||
class="phone-input"
|
||
v-model="phone"
|
||
type="number"
|
||
:maxlength="countryCode === '+86' ? 11 : 12"
|
||
:placeholder="$t('auth.phonePlaceholder')"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 验证码输入 -->
|
||
<!-- #ifndef H5 -->
|
||
<view class="form-group">
|
||
<view class="code-input-wrapper">
|
||
<input
|
||
class="code-input"
|
||
v-model="verifyCode"
|
||
type="number"
|
||
maxlength="6"
|
||
:placeholder="$t('auth.codePlaceholder')"
|
||
/>
|
||
<view class="code-btn" @click="handleSendCode" :class="{ disabled: countdown > 0 }">
|
||
<text class="code-btn-text">{{ countdown > 0 ? `${countdown}s` : $t('auth.getCode') }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
|
||
<!-- 区域提示 -->
|
||
<view class="region-notice">
|
||
<!-- <text>当前支持印尼(+62)和中国(+86)手机号登录</text> -->
|
||
</view>
|
||
|
||
<!-- 登录按钮 -->
|
||
<view class="login-btn" @click="handleLogin">
|
||
<text class="login-btn-text">{{ $t('auth.loginBtn') }}</text>
|
||
</view>
|
||
|
||
<!-- 协议勾选 -->
|
||
<!-- #ifndef H5 -->
|
||
<view class="agreement-box">
|
||
<checkbox-group @change="onAgreementChange">
|
||
<label class="agreement-label">
|
||
<checkbox value="agreed" :checked="isAgreed" color="#07c160" class="agreement-checkbox" />
|
||
<text class="agreement-text">
|
||
{{ $t('auth.agreeToTerms') }}
|
||
<text class="link" @tap.stop="go('/pages/legal/agreement')">{{ $t('user.userAgreement') }}</text>
|
||
{{ $t('common.and') }}
|
||
<text class="link" @tap.stop="go('/pages/legal/privacy')">{{ $t('user.privacyPolicy') }}</text>
|
||
</text>
|
||
</label>
|
||
</checkbox-group>
|
||
</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { sendVerifyCode, quickLogin } from '@/config/api/user.js'
|
||
import { appid } from '@/config/url.js'
|
||
import { fetchAndCacheCustomerPhone } from '@/util/index.js'
|
||
import { useI18n } from '@/utils/i18n.js'
|
||
|
||
const { t } = useI18n()
|
||
|
||
// 设置页面标题
|
||
onMounted(() => {
|
||
uni.setNavigationBarTitle({
|
||
title: t('auth.phoneLogin')
|
||
})
|
||
})
|
||
|
||
const redirect = ref('/pages/index/index')
|
||
const isAgreed = ref(false) // 是否同意协议
|
||
const phone = ref('') // 手机号
|
||
const verifyCode = ref('') // 验证码
|
||
const countryCode = ref('+62') // 国家区号,默认印尼
|
||
const countdown = ref(0) // 验证码倒计时
|
||
let timer = null // 计时器
|
||
const countryOptions = [{
|
||
label: '印尼 +62',
|
||
value: '+62'
|
||
}, {
|
||
label: '中国 +86',
|
||
value: '+86'
|
||
}]
|
||
|
||
// 勾选协议变化
|
||
const onAgreementChange = (e) => {
|
||
isAgreed.value = e.detail.value.includes('agreed')
|
||
}
|
||
|
||
// 显示国家区号选择器
|
||
const showCountryPicker = () => {
|
||
uni.showActionSheet({
|
||
itemList: countryOptions.map(item => item.label),
|
||
success: ({
|
||
tapIndex
|
||
}) => {
|
||
const selected = countryOptions[tapIndex]
|
||
if (!selected || selected.value === countryCode.value) return
|
||
countryCode.value = selected.value
|
||
phone.value = ''
|
||
}
|
||
})
|
||
}
|
||
|
||
// 验证手机号格式
|
||
const validatePhone = () => {
|
||
if (!phone.value) {
|
||
uni.showToast({ title: t('auth.phoneRequired'), icon: 'none' })
|
||
return false
|
||
}
|
||
const phoneReg = countryCode.value === '+86' ? /^1[3-9]\d{9}$/ : /^8\d{7,11}$/
|
||
if (!phoneReg.test(phone.value)) {
|
||
uni.showToast({ title: t('auth.phoneInvalid'), icon: 'none' })
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
const getSubmitPhoneNumber = () => {
|
||
// 中国沿用原有 11 位手机号格式;印尼附带国家区号
|
||
return countryCode.value === '+86' ? phone.value : `${countryCode.value}${phone.value}`
|
||
}
|
||
|
||
// 发送验证码
|
||
const handleSendCode = async () => {
|
||
if (countdown.value > 0) return
|
||
|
||
if (!validatePhone()) return
|
||
|
||
try {
|
||
uni.showLoading({ title: t('common.sending') })
|
||
await sendVerifyCode(getSubmitPhoneNumber())
|
||
uni.hideLoading()
|
||
uni.showToast({ title: t('auth.codeSent'), icon: 'success' })
|
||
|
||
// 启动60秒倒计时
|
||
countdown.value = 60
|
||
timer = setInterval(() => {
|
||
countdown.value--
|
||
if (countdown.value <= 0) {
|
||
clearInterval(timer)
|
||
timer = null
|
||
}
|
||
}, 1000)
|
||
} catch (error) {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: error.message || t('auth.sendCodeFailed'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 登录
|
||
const handleLogin = async () => {
|
||
if (!validatePhone()) return
|
||
|
||
// #ifndef H5
|
||
if (!verifyCode.value) {
|
||
uni.showToast({ title: t('auth.codeRequired'), icon: 'none' })
|
||
return
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
if (!isAgreed.value) {
|
||
uni.showToast({ title: t('auth.pleaseAgreeToTerms'), icon: 'none' })
|
||
return
|
||
}
|
||
// #endif
|
||
|
||
try {
|
||
uni.showLoading({ title: t('common.loggingIn') })
|
||
const res = await quickLogin({
|
||
loginType: 'SMS',
|
||
appid,
|
||
phonenumber: getSubmitPhoneNumber(),
|
||
// H5 按产品要求不做验证码发送与校验,小程序端保持原流程
|
||
smsCode: verifyCode.value || ''
|
||
})
|
||
|
||
if (res && res.code !== 200) {
|
||
throw new Error(res.msg || res.message || t('auth.loginFailed'))
|
||
}
|
||
|
||
// 保存token和client_id
|
||
// 兼容多种返回格式:res.data.token, res.token, res.data.access_token
|
||
const token = res.token || (res.data && (res.data.token || res.data.access_token))
|
||
const clientId = res.client_id || (res.data && (res.data.client_id || res.data.clientId))
|
||
|
||
if (token) {
|
||
uni.setStorageSync('token', token)
|
||
if (clientId) {
|
||
uni.setStorageSync('client_id', clientId)
|
||
}
|
||
|
||
// 登录成功后获取并缓存客服电话
|
||
fetchAndCacheCustomerPhone().catch(err => {
|
||
console.error(t('auth.getServicePhoneFailed'), err)
|
||
})
|
||
} else {
|
||
throw new Error(t('auth.noAuthToken'))
|
||
}
|
||
|
||
uni.hideLoading()
|
||
uni.showToast({ title: t('auth.loginSuccess'), icon: 'success' })
|
||
|
||
// 跳转到首页
|
||
setTimeout(() => {
|
||
uni.reLaunch({ url: redirect.value })
|
||
}, 1500)
|
||
} catch (error) {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: error.message || t('auth.loginFailed'),
|
||
icon: 'none'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 页面加载
|
||
onLoad((opts) => {
|
||
if (opts && opts.redirect) {
|
||
try {
|
||
redirect.value = decodeURIComponent(opts.redirect)
|
||
} catch (_) {}
|
||
}
|
||
})
|
||
|
||
const go = (url) => {
|
||
uni.navigateTo({ url })
|
||
}
|
||
|
||
// 清理定时器
|
||
onUnmounted(() => {
|
||
if (timer) {
|
||
clearInterval(timer)
|
||
timer = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.login-container {
|
||
min-height: 100vh;
|
||
background: linear-gradient(180deg, #C8F4D9 0%, #FFFFFF 100%);
|
||
padding: 0 48rpx;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
|
||
.header {
|
||
padding-top: 120rpx;
|
||
margin-bottom: 80rpx;
|
||
|
||
.title {
|
||
font-size: 64rpx;
|
||
font-weight: 700;
|
||
color: #000000;
|
||
line-height: 1.2;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 64rpx;
|
||
font-weight: 700;
|
||
color: #000000;
|
||
line-height: 1.2;
|
||
}
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 32rpx;
|
||
|
||
.phone-input-wrapper {
|
||
background: #FFFFFF;
|
||
border-radius: 48rpx;
|
||
height: 96rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 32rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||
|
||
.country-code {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
padding-right: 16rpx;
|
||
|
||
.arrow {
|
||
margin-left: 8rpx;
|
||
font-size: 20rpx;
|
||
color: #999999;
|
||
}
|
||
}
|
||
|
||
.divider {
|
||
width: 2rpx;
|
||
height: 40rpx;
|
||
background: #E5E5E5;
|
||
margin: 0 16rpx;
|
||
}
|
||
|
||
.phone-input {
|
||
flex: 1;
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
}
|
||
}
|
||
|
||
.code-input-wrapper {
|
||
background: #FFFFFF;
|
||
border-radius: 48rpx;
|
||
height: 96rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 32rpx;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||
|
||
.code-input {
|
||
flex: 1;
|
||
font-size: 32rpx;
|
||
color: #333333;
|
||
}
|
||
|
||
.code-btn {
|
||
padding-left: 24rpx;
|
||
border-left: 2rpx solid #E5E5E5;
|
||
|
||
&.disabled {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.code-btn-text {
|
||
font-size: 28rpx;
|
||
color: #07c160;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.region-notice {
|
||
margin-bottom: 48rpx;
|
||
padding: 0 8rpx;
|
||
|
||
text {
|
||
font-size: 24rpx;
|
||
color: #666666;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
|
||
.login-btn {
|
||
background: #07c160;
|
||
border-radius: 60rpx;
|
||
height: 112rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 8rpx 24rpx rgba(7, 193, 96, 0.3);
|
||
margin-bottom: 48rpx;
|
||
|
||
&:active {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.login-btn-text {
|
||
font-size: 36rpx;
|
||
color: #FFFFFF;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.agreement-box {
|
||
position: absolute;
|
||
left: 48rpx;
|
||
right: 48rpx;
|
||
bottom: 60rpx;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
.agreement-label {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 100%;
|
||
|
||
.agreement-checkbox {
|
||
flex-shrink: 0;
|
||
transform: scale(0.75);
|
||
margin-right: 4rpx;
|
||
}
|
||
|
||
.agreement-text {
|
||
flex: 1;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
line-height: 1.8;
|
||
word-break: break-all;
|
||
|
||
.link {
|
||
color: #07c160;
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|