395 lines
8.5 KiB
Vue
395 lines
8.5 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="11"
|
|
:placeholder="$t('auth.phonePlaceholder')"
|
|
/>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 验证码输入 -->
|
|
<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>
|
|
|
|
<!-- 区域提示 -->
|
|
<view class="region-notice">
|
|
<text>{{ $t('auth.regionNotSupported') }}</text>
|
|
</view>
|
|
|
|
<!-- 登录按钮 -->
|
|
<view class="login-btn" @click="handleLogin">
|
|
<text class="login-btn-text">{{ $t('auth.loginBtn') }}</text>
|
|
</view>
|
|
|
|
<!-- 协议勾选 -->
|
|
<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>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import { sendVerifyCode, loginWithCode } from '@/config/api/user.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('+86') // 国家区号
|
|
const countdown = ref(0) // 验证码倒计时
|
|
let timer = null // 计时器
|
|
|
|
// 勾选协议变化
|
|
const onAgreementChange = (e) => {
|
|
isAgreed.value = e.detail.value.includes('agreed')
|
|
}
|
|
|
|
// 显示国家区号选择器(暂时仅支持+86)
|
|
const showCountryPicker = () => {
|
|
uni.showToast({
|
|
title: t('auth.onlyMainlandSupported'),
|
|
icon: 'none'
|
|
})
|
|
}
|
|
|
|
// 验证手机号格式
|
|
const validatePhone = () => {
|
|
if (!phone.value) {
|
|
uni.showToast({ title: t('auth.phoneRequired'), icon: 'none' })
|
|
return false
|
|
}
|
|
const phoneReg = /^1[3-9]\d{9}$/
|
|
if (!phoneReg.test(phone.value)) {
|
|
uni.showToast({ title: t('auth.phoneInvalid'), icon: 'none' })
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// 发送验证码
|
|
const handleSendCode = async () => {
|
|
if (countdown.value > 0) return
|
|
|
|
if (!validatePhone()) return
|
|
|
|
try {
|
|
uni.showLoading({ title: t('common.sending') })
|
|
await sendVerifyCode(phone.value)
|
|
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
|
|
|
|
if (!verifyCode.value) {
|
|
uni.showToast({ title: t('auth.codeRequired'), icon: 'none' })
|
|
return
|
|
}
|
|
|
|
if (!isAgreed.value) {
|
|
uni.showToast({ title: t('auth.pleaseAgreeToTerms'), icon: 'none' })
|
|
return
|
|
}
|
|
|
|
try {
|
|
uni.showLoading({ title: t('common.loggingIn') })
|
|
const res = await loginWithCode(phone.value, verifyCode.value)
|
|
|
|
// 保存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>
|