This commit is contained in:
那些花儿
2025-04-28 15:32:41 +08:00
parent cb3bcb57bc
commit 12c3afb4a6
42 changed files with 3272 additions and 1627 deletions
+330 -151
View File
@@ -1,51 +1,118 @@
<template>
<view class="login-container">
<image
class="banner-image"
src="https://res.firstui.cn/template/loginB/login/img_banner_3x.png"
mode="aspectFill"
/>
<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)" />
<view class="login-form">
<text class="title">登录</text>
<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>
<t-input
class="input-item"
v-model="loginForm.phone"
placeholder="请输入手机号码"
>
</t-input>
<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>
<t-input
class="input-item"
v-model="loginForm.smsCode"
placeholder="请输入验证码"
>
<template #suffix>
<t-button variant="text" class="verify-btn" @click="getSmsCode" :disabled="isCountingDown">
<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 }}
</t-button>
</template>
</t-input>
</view>
</view>
<!-- <view class="login-options">-->
<!-- <text>账号登录</text>-->
<!-- <text>注册</text>-->
<!-- </view>-->
<t-button block theme="primary" size="large" class="login-btn" @click="handleLogin">
登录
</t-button>
<!-- <view class="other-login">-->
<!-- <text class="other-text">其他登录方式</text>-->
<!-- <view class="login-methods">-->
<!-- <t-button shape="circle" variant="outline" icon="qq" class="method-btn" />-->
<!-- <t-button shape="circle" variant="outline" icon="wechat" class="method-btn" />-->
<!-- <t-button shape="circle" variant="outline" icon="image" class="method-btn" />-->
<!-- </view>-->
<!-- </view>-->
<!-- 登录按钮 -->
<view class="submit-btn" @click="handleLogin">
立即登录
</view>
<!-- &lt;!&ndash; 注册链接 &ndash;&gt;-->
<!-- <view class="register-link">-->
<!-- <text>没有账号?</text>-->
<!-- <text class="link">去注册</text>-->
<!-- </view>-->
</view>
</view>
</view>
</template>
@@ -56,12 +123,17 @@ import {Toast} from 'tdesign-mobile-vue';
import {useRequest} from "@/utils/request";
const request = useRequest()
const activeMethod = ref('phone')
const loginForm = reactive({
phone: '18571562330',
phone: '',
smsCode: ''
})
const setActiveMethod = (method) => {
activeMethod.value = method
}
// 倒计时相关
const countdown = ref(0)
const timer = ref(null)
@@ -84,7 +156,7 @@ const saveCountdownState = (seconds, phone) => {
uni.removeStorageSync(SMS_PHONE_KEY)
return
}
// 保存结束时间戳和手机号
const endTime = Date.now() + seconds * 1000
uni.setStorageSync(SMS_COUNTDOWN_KEY, endTime)
@@ -96,13 +168,18 @@ const restoreCountdownState = () => {
const endTime = uni.getStorageSync(SMS_COUNTDOWN_KEY)
const savedPhone = uni.getStorageSync(SMS_PHONE_KEY)
if (endTime && savedPhone && savedPhone === loginForm.phone) {
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)
@@ -116,13 +193,13 @@ 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)
@@ -137,50 +214,69 @@ const startCountDown = () => {
startCountDownTimer()
}
// 在页面挂载时恢复倒计时状态
onMounted(() => {
restoreCountdownState()
})
// 在页面卸载前清除定时器
onBeforeUnmount(() => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
// 获取验证码
async function getSmsCode() {
if (!loginForm.phone) {
Toast.error('请输入手机号码');
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.success('验证码发送成功')
Toast({message: '验证码发送成功', theme: 'success'})
// 开始倒计时
startCountDown()
} else {
Toast.error(msg)
Toast({message: msg, theme: 'error'})
}
} catch (e) {
Toast.error(e.message)
Toast({message: e.message, theme: 'error'})
}
}
async function handleLogin() {
if (!loginForm.phone) {
Toast.error('请输入手机号码');
Toast('请输入手机号码');
return
}
if (!loginForm.smsCode) {
Toast.error('请输入验证码');
Toast('请输入验证码');
return
}
try {
const {data, code, msg} = await request.post('/client/auth/sms-login', loginForm)
if (code === 0) {
Toast.success('登录成功')
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/')) {
@@ -196,119 +292,202 @@ async function handleLogin() {
}
}, 1000)
} else {
Toast.error(msg)
Toast({message: msg, theme: 'error'})
}
} catch (e) {
console.error(e)
Toast.error(e.message)
Toast({message: e.message, theme: 'error'})
}
}
// 页面加载时恢复倒计时状态
onMounted(() => {
restoreCountdownState()
})
// 页面卸载前清理定时器
onBeforeUnmount(() => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
})
</script>
<style lang="less" scoped>
.login-container {
min-height: 100vh;
background-color: #fff;
position: relative;
}
.banner-image {
body {
height: 100%;
overflow: hidden;
position: fixed;
width: 100%;
height: 260px;
background-color: #4080ff;
top: 0;
left: 0;
}
.login-form {
padding: 20px;
.login-container {
height: 100vh;
background: #f4f2f2;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
.title {
font-size: 24px;
font-weight: bold;
margin: 20px 0;
display: block;
color: var(--td-brand-color-5);
.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;
}
}
.input-item {
margin-bottom: 20px;
}
.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;
}
.country-code {
color: #333;
font-weight: bold;
}
.register-link {
margin-top: 16px;
text-align: center;
font-size: 14px;
color: #666;
.verify-btn {
color: var(--td-brand-color-5);
}
.login-options {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
font-size: 14px;
color: #666;
}
.login-btn {
margin: 20px 0;
}
.other-login {
margin-top: 40px;
text-align: center;
.other-text {
color: #999;
font-size: 14px;
position: relative;
display: inline-block;
padding: 0 10px;
background: #fff;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
width: 60px;
height: 1px;
background: #eee;
}
&::before {
left: -70px;
}
&::after {
right: -70px;
}
}
.login-methods {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 20px;
.method-btn {
width: 40px;
height: 40px;
}
}
.link {
color: #f45a5a;
margin-left: 4px;
}
}
</style>