519 lines
13 KiB
Vue
519 lines
13 KiB
Vue
<template>
|
||
<view class="login-container">
|
||
<view class="title-area">
|
||
<div class="tech-animation">
|
||
<svg preserveAspectRatio="xMidYMid slice" viewBox="0 0 300 100">
|
||
<pattern id="pattern-hex" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"
|
||
patternTransform="scale(0.8)">
|
||
<rect x="0" y="0" width="10" height="10" fill="none"/>
|
||
<path d="M0,5 L2.5,0 L7.5,0 L10,5 L7.5,10 L2.5,10 Z" stroke-width="0.5" stroke="rgba(244, 90, 90, 0.2)"
|
||
fill="rgba(244, 90, 90, 0.1)">
|
||
<animate attributeName="stroke-opacity" values="0.1;0.3;0.1" dur="3s" repeatCount="indefinite"/>
|
||
</path>
|
||
</pattern>
|
||
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-hex)"/>
|
||
|
||
<g fill="none" stroke-width="0.5">
|
||
<path d="M20,50 Q60,20 100,50 T180,50 T260,50" stroke="rgba(244, 90, 90, 0.6)">
|
||
<animate attributeName="d"
|
||
values="M20,50 Q60,20 100,50 T180,50 T260,50;
|
||
M20,50 Q60,80 100,50 T180,50 T260,50;
|
||
M20,50 Q60,20 100,50 T180,50 T260,50"
|
||
dur="10s" repeatCount="indefinite"/>
|
||
</path>
|
||
|
||
<path d="M20,70 Q60,40 100,70 T180,70 T260,70" stroke="rgba(244, 90, 90, 0.4)">
|
||
<animate attributeName="d"
|
||
values="M20,70 Q60,40 100,70 T180,70 T260,70;
|
||
M20,70 Q60,100 100,70 T180,70 T260,70;
|
||
M20,70 Q60,40 100,70 T180,70 T260,70"
|
||
dur="12s" repeatCount="indefinite"/>
|
||
</path>
|
||
</g>
|
||
|
||
<circle cx="50" cy="50" r="1" fill="rgba(244, 90, 90, 0.8)">
|
||
<animate attributeName="cx" values="50;250;50" dur="20s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="50;30;80;50" dur="16s" repeatCount="indefinite"/>
|
||
<animate attributeName="r" values="1;1.5;1" dur="5s" repeatCount="indefinite"/>
|
||
</circle>
|
||
|
||
<circle cx="150" cy="30" r="1" fill="rgba(244, 90, 90, 0.8)">
|
||
<animate attributeName="cx" values="150;100;200;150" dur="25s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="30;60;20;30" dur="18s" repeatCount="indefinite"/>
|
||
<animate attributeName="r" values="1;1.8;1" dur="6s" repeatCount="indefinite"/>
|
||
</circle>
|
||
|
||
<circle cx="250" cy="70" r="1" fill="rgba(244, 90, 90, 0.8)">
|
||
<animate attributeName="cx" values="250;150;250" dur="22s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="70;40;70" dur="15s" repeatCount="indefinite"/>
|
||
<animate attributeName="r" values="1;1.2;1" dur="4s" repeatCount="indefinite"/>
|
||
</circle>
|
||
</svg>
|
||
</div>
|
||
|
||
<view class="title-content">
|
||
<text class="main-title">HELLO,</text>
|
||
<view class="sub-title-container">
|
||
<text class="sub-title">欢迎使用</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="login-card">
|
||
<view class="login-tabs">
|
||
<view
|
||
:class="['tab', activeMethod === 'phone' ? 'active' : '']"
|
||
@click="setActiveMethod('phone')"
|
||
>
|
||
手机快速登录
|
||
</view>
|
||
<view
|
||
:class="['tab', activeMethod === 'password' ? 'active' : '']"
|
||
@click="setActiveMethod('password')"
|
||
>
|
||
账号密码登录
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 登录表单区域 -->
|
||
<view class="form-container">
|
||
<!-- 手机号码输入框 -->
|
||
<view class="input-box">
|
||
<t-icon name="mobile" size="20px" class="input-icon"/>
|
||
<input
|
||
class="input"
|
||
type="tel"
|
||
v-model="loginForm.phone"
|
||
placeholder="请输入手机号"
|
||
maxlength="11"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 验证码输入框 -->
|
||
<view class="input-box">
|
||
<t-icon name="shield-error" size="20px" class="input-icon"/>
|
||
<input
|
||
class="input"
|
||
type="number"
|
||
v-model="loginForm.smsCode"
|
||
placeholder="请输入验证码"
|
||
maxlength="6"
|
||
/>
|
||
<view class="verify-code-btn" @click="getSmsCode" :class="{ disabled: isCountingDown }">
|
||
{{ countDownText }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 登录按钮 -->
|
||
<view class="submit-btn" @click="handleLogin">
|
||
立即登录
|
||
</view>
|
||
|
||
<!-- 注册链接 -->
|
||
<view class="register-link" @click="navigateToRegister">
|
||
<text>没有账号?</text>
|
||
<text class="link">去注册</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import {ref, reactive, computed, onMounted, onBeforeUnmount} from 'vue'
|
||
import {Toast} from 'tdesign-mobile-vue';
|
||
import {useRequest} from "@/utils/request";
|
||
import {useUserStore} from "@/store";
|
||
|
||
const request = useRequest()
|
||
const userStore = useUserStore()
|
||
const activeMethod = ref('phone')
|
||
|
||
const loginForm = reactive({
|
||
phone: '',
|
||
smsCode: ''
|
||
})
|
||
|
||
const setActiveMethod = (method) => {
|
||
activeMethod.value = method
|
||
}
|
||
|
||
// 倒计时相关
|
||
const countdown = ref(0)
|
||
const timer = ref(null)
|
||
|
||
// 本地存储键名
|
||
const SMS_COUNTDOWN_KEY = '__SMS_COUNTDOWN__'
|
||
const SMS_PHONE_KEY = '__SMS_PHONE__'
|
||
|
||
// 是否在倒计时中
|
||
const isCountingDown = computed(() => countdown.value > 0)
|
||
// 验证码按钮文字
|
||
const countDownText = computed(() => {
|
||
return isCountingDown.value ? `${countdown.value}秒后重试` : '获取验证码'
|
||
})
|
||
|
||
// 保存倒计时状态到本地存储
|
||
const saveCountdownState = (seconds, phone) => {
|
||
if (seconds <= 0) {
|
||
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
|
||
uni.removeStorageSync(SMS_PHONE_KEY)
|
||
return
|
||
}
|
||
|
||
// 保存结束时间戳和手机号
|
||
const endTime = Date.now() + seconds * 1000
|
||
uni.setStorageSync(SMS_COUNTDOWN_KEY, endTime)
|
||
uni.setStorageSync(SMS_PHONE_KEY, phone)
|
||
}
|
||
|
||
// 从本地存储恢复倒计时状态
|
||
const restoreCountdownState = () => {
|
||
const endTime = uni.getStorageSync(SMS_COUNTDOWN_KEY)
|
||
const savedPhone = uni.getStorageSync(SMS_PHONE_KEY)
|
||
|
||
if (endTime && savedPhone) {
|
||
const now = Date.now()
|
||
const remainingSeconds = Math.floor((endTime - now) / 1000)
|
||
|
||
if (remainingSeconds > 0) {
|
||
countdown.value = remainingSeconds
|
||
startCountDownTimer() // 重新启动定时器
|
||
|
||
// 如果用户还没输入手机号,填入保存的手机号
|
||
if (!loginForm.phone) {
|
||
loginForm.phone = savedPhone
|
||
}
|
||
} else {
|
||
// 倒计时已结束,清除存储
|
||
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
|
||
uni.removeStorageSync(SMS_PHONE_KEY)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 开始倒计时定时器
|
||
const startCountDownTimer = () => {
|
||
if (timer.value) {
|
||
clearInterval(timer.value)
|
||
}
|
||
|
||
timer.value = setInterval(() => {
|
||
countdown.value--
|
||
if (countdown.value <= 0) {
|
||
clearInterval(timer.value)
|
||
timer.value = null
|
||
|
||
// 清除存储
|
||
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
|
||
uni.removeStorageSync(SMS_PHONE_KEY)
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
// 开始倒计时
|
||
const startCountDown = () => {
|
||
countdown.value = 60
|
||
saveCountdownState(60, loginForm.phone)
|
||
startCountDownTimer()
|
||
}
|
||
|
||
// 在页面挂载时恢复倒计时状态
|
||
onMounted(() => {
|
||
restoreCountdownState()
|
||
})
|
||
|
||
// 在页面卸载前清除定时器
|
||
onBeforeUnmount(() => {
|
||
if (timer.value) {
|
||
clearInterval(timer.value)
|
||
timer.value = null
|
||
}
|
||
})
|
||
|
||
// 获取验证码
|
||
async function getSmsCode() {
|
||
if (!loginForm.phone) {
|
||
Toast('请输入手机号码');
|
||
return
|
||
}
|
||
|
||
// 验证手机号码
|
||
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
|
||
Toast('请输入正确的手机号码');
|
||
return
|
||
}
|
||
|
||
|
||
if (isCountingDown.value) return
|
||
try {
|
||
const {code, data, msg} = await request.post('/client/auth/send-sms-code/' + loginForm.phone)
|
||
if (code === 0) {
|
||
Toast({message: '验证码发送成功', theme: 'success'})
|
||
// 开始倒计时
|
||
startCountDown()
|
||
} else {
|
||
Toast({message: msg, theme: 'error'})
|
||
}
|
||
} catch (e) {
|
||
Toast({message: e.message, theme: 'error'})
|
||
}
|
||
}
|
||
|
||
async function handleLogin() {
|
||
if (!loginForm.phone) {
|
||
Toast('请输入手机号码');
|
||
return
|
||
}
|
||
if (!loginForm.smsCode) {
|
||
Toast('请输入验证码');
|
||
return
|
||
}
|
||
|
||
try {
|
||
userStore.smsLogin(loginForm).then(() => {
|
||
userStore.getUserInfo().then(() => {
|
||
Toast({message: '登录成功', theme: 'success'})
|
||
uni.switchTab({
|
||
url: '/pages/index/index'
|
||
})
|
||
}).catch(e => {
|
||
Toast({message: e.message, theme: 'error'})
|
||
})
|
||
}).catch(e => {
|
||
console.log(e)
|
||
Toast({message: e.message, theme: 'error'})
|
||
})
|
||
|
||
// const {data, code, msg} = await request.post('/client/auth/sms-login', loginForm)
|
||
// if (code === 0) {
|
||
// Toast({message: '登录成功', theme: 'success'})
|
||
// // 保存token
|
||
// uni.setStorageSync('token', data)
|
||
//
|
||
// // 获取之前保存的重定向URL
|
||
// const redirectUrl = uni.getStorageSync('REDIRECT_URL') || '/pages/index/index'
|
||
// uni.removeStorageSync('REDIRECT_URL') // 使用后删除
|
||
//
|
||
// // 延迟跳转,让用户看到成功提示
|
||
// setTimeout(() => {
|
||
// if (redirectUrl.startsWith('/pages/index/') || redirectUrl.startsWith('/pages/profile/')) {
|
||
// // 主页和个人中心,使用switchTab
|
||
// uni.switchTab({
|
||
// url: redirectUrl
|
||
// })
|
||
// } else {
|
||
// // 其他页面使用reLaunch
|
||
// uni.reLaunch({
|
||
// url: redirectUrl
|
||
// })
|
||
// }
|
||
// }, 1000)
|
||
// } else {
|
||
// Toast({message: msg, theme: 'error'})
|
||
// }
|
||
} catch (e) {
|
||
console.error(e)
|
||
Toast({message: e.message, theme: 'error'})
|
||
}
|
||
}
|
||
|
||
function navigateToRegister(){
|
||
uni.redirectTo({
|
||
url: '/pages/login/register'
|
||
})
|
||
}
|
||
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
body {
|
||
height: 100%;
|
||
overflow: hidden;
|
||
position: fixed;
|
||
width: 100%;
|
||
top: 0;
|
||
left: 0;
|
||
}
|
||
|
||
.login-container {
|
||
height: 100vh;
|
||
background: #f4f2f2;
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.title-area {
|
||
padding: 0;
|
||
height: 30vh;
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
background: linear-gradient(135deg, #7a1818 0%, #d03e3e 50%, #f45a5a 100%);
|
||
}
|
||
|
||
.tech-animation {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 0;
|
||
}
|
||
|
||
.tech-animation svg {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.title-content {
|
||
padding: 0 30px;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.main-title {
|
||
font-size: 36px;
|
||
font-weight: bold;
|
||
margin-bottom: 8px;
|
||
display: block;
|
||
position: relative;
|
||
z-index: 1;
|
||
color: #ffffff;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.sub-title-container {
|
||
display: flex;
|
||
align-items: center;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.sub-title {
|
||
font-size: 18px;
|
||
color: #ffffff;
|
||
position: relative;
|
||
z-index: 1;
|
||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.login-card {
|
||
flex: 1;
|
||
background-color: #ffffff;
|
||
border-radius: 30px 30px 0 0;
|
||
margin-top: -30px;
|
||
padding: 30px 0 0;
|
||
position: relative;
|
||
z-index: 2;
|
||
box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.login-tabs {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 24px;
|
||
position: relative;
|
||
gap: 10px;
|
||
padding: 0 30px;
|
||
}
|
||
|
||
.tab {
|
||
width: calc(50% - 5px);
|
||
height: 55px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 15px;
|
||
color: #f45a5a;
|
||
font-weight: 500;
|
||
background-color: #fff0f0;
|
||
transition: all 0.3s ease;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tab:first-child {
|
||
border-radius: 25px 15px 30px 25px;
|
||
clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%);
|
||
}
|
||
|
||
.tab:last-child {
|
||
border-radius: 30px 25px 25px 15px;
|
||
clip-path: polygon(10% 0, 100% 0, 100% 100%, 0 100%);
|
||
}
|
||
|
||
.tab.active {
|
||
color: #fff;
|
||
background-color: #f45a5a;
|
||
}
|
||
|
||
.form-container {
|
||
padding: 0 30px;
|
||
}
|
||
|
||
.input-box {
|
||
height: 50px;
|
||
background-color: #eceaea;
|
||
border-radius: 25px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 15px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.input-icon {
|
||
color: #999;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.input {
|
||
flex: 1;
|
||
height: 100%;
|
||
border: none;
|
||
font-size: 15px;
|
||
background-color: transparent;
|
||
}
|
||
|
||
.input::placeholder {
|
||
color: #bcbcbc;
|
||
}
|
||
|
||
.verify-code-btn {
|
||
color: #f45a5a;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
|
||
&.disabled {
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.submit-btn {
|
||
margin-top: 32px;
|
||
height: 50px;
|
||
background-color: #f45a5a;
|
||
border-radius: 25px;
|
||
color: white;
|
||
font-size: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.register-link {
|
||
margin-top: 16px;
|
||
text-align: center;
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
|
||
.link {
|
||
color: #f45a5a;
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
</style>
|