Files
happy-life-star/UniApp/src/pages/login/index.vue
T
2026-02-27 11:32:50 +08:00

344 lines
7.7 KiB
Vue

<template>
<view class="login-page">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="bg-decoration">
<view class="aurora-top"></view>
<view class="aurora-bottom"></view>
</view>
<view class="content" :style="{ paddingTop: safeAreaTop + 20 + 'px' }">
<view class="login-card">
<view class="header">
<text class="title font-serif">欢迎回来</text>
<text class="subtitle">开启你的数字生命档案</text>
</view>
<view class="form">
<view class="input-group">
<text class="input-label">手机号码</text>
<input
class="glass-input"
type="number"
maxlength="11"
placeholder="输入手机号"
v-model="phone"
/>
</view>
<view class="input-group">
<text class="input-label">验证码</text>
<view class="code-row">
<input
class="glass-input code-input"
type="number"
maxlength="6"
placeholder="请输入验证码"
v-model="code"
/>
<view
class="code-btn"
:class="{ disabled: isCountingDown || phone.length !== 11 }"
@click="handleGetCode"
>
<text v-if="isCountingDown" class="countdown">{{ countdown }}s</text>
<text v-else>获取验证码</text>
</view>
</view>
</view>
</view>
<view
class="btn-primary login-btn"
:class="{ disabled: !canSubmit || loading }"
@click="handleLogin"
>
<text v-if="loading">登录中...</text>
<text v-else>开启旅程</text>
</view>
<text class="agreement">
登录即代表同意用户协议隐私政策
</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { getSmsCode } from '../../services/auth.js'
const store = useAppStore()
const statusBarHeight = ref(uni.getStorageSync('statusBarHeight') || 20)
const safeAreaTop = ref(uni.getStorageSync('safeAreaTop') || 20)
const safeAreaBottom = ref(uni.getStorageSync('safeAreaBottom') || 0)
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 20
safeAreaTop.value = systemInfo.safeAreaInsets?.top || systemInfo.statusBarHeight || 20
safeAreaBottom.value = systemInfo.safeAreaInsets?.bottom || 0
})
const phone = ref('')
const code = ref('')
const loading = ref(false)
const countdown = ref(60)
const isCountingDown = ref(false)
const canSubmit = computed(() => {
return phone.value.length === 11 && code.value.length === 6
})
const handleGetCode = async () => {
if (isCountingDown.value || loading.value) return
if (phone.value.length !== 11) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
return
}
try {
await getSmsCode(phone.value)
uni.showToast({ title: '验证码已发送', icon: 'success' })
startCountdown()
} catch (error) {
uni.showToast({ title: '验证码已发送 (模拟: 888888)', icon: 'none' })
startCountdown()
}
}
const startCountdown = () => {
isCountingDown.value = true
countdown.value = 60
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
isCountingDown.value = false
}
}, 1000)
}
const handleLogin = async () => {
if (!canSubmit.value || loading.value) return
loading.value = true
try {
const result = await store.login(phone.value, code.value)
if (result.success) {
if (result.hasProfile) {
uni.redirectTo({ url: '/pages/main/index' })
} else {
uni.redirectTo({ url: '/pages/onboarding/index' })
}
} else {
uni.showToast({ title: result.error || '登录失败', icon: 'none' })
}
} catch (error) {
uni.showToast({ title: '登录失败', icon: 'none' })
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
background: linear-gradient(180deg, #0F071A 0%, #1A0B2E 50%, #0F071A 100%);
position: relative;
}
.status-bar {
height: constant(safe-area-inset-top);
height: env(safe-area-inset-top);
width: 100%;
background: transparent;
flex-shrink: 0;
}
.bg-decoration {
position: absolute;
inset: 0;
pointer-events: none;
}
.aurora-top {
position: absolute;
top: -10%;
left: -10%;
width: 120%;
height: 60%;
background: rgba(168, 85, 247, 0.08);
filter: blur(120rpx);
border-radius: 50%;
}
.aurora-bottom {
position: absolute;
bottom: -10%;
right: -10%;
width: 100%;
height: 50%;
background: rgba(139, 92, 246, 0.05);
filter: blur(100rpx);
border-radius: 50%;
}
.content {
position: relative;
z-index: 1;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
padding-top: calc(40rpx + constant(safe-area-inset-top));
padding-top: calc(40rpx + env(safe-area-inset-top));
}
.login-card {
width: 100%;
max-width: 720rpx;
background: rgba(168, 85, 247, 0.05);
backdrop-filter: blur(40rpx);
border: 1px solid rgba(168, 85, 247, 0.15);
border-radius: 48rpx;
padding: 64rpx 40rpx;
}
.header {
text-align: center;
margin-bottom: 64rpx;
}
.title {
display: block;
font-size: 48rpx;
font-weight: 300;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 6rpx;
margin-bottom: 16rpx;
}
.subtitle {
display: block;
font-size: 28rpx;
color: rgba(255, 255, 255, 0.4);
font-style: italic;
}
.form {
margin-bottom: 48rpx;
}
.input-group {
margin-bottom: 32rpx;
}
.input-label {
display: block;
font-size: 18rpx;
color: rgba(255, 255, 255, 0.35);
margin-bottom: 16rpx;
letter-spacing: 4rpx;
text-transform: uppercase;
}
.glass-input {
width: 100%;
height: 92rpx;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
padding: 0 32rpx;
color: #F3E8FF;
font-size: 28rpx;
}
.glass-input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
.code-row {
display: flex;
flex-direction: row;
gap: 16rpx;
align-items: stretch;
width: 100%;
}
.code-input {
flex: 1;
width: auto;
min-width: 0;
}
.code-btn {
width: 220rpx;
height: 92rpx;
background: linear-gradient(135deg, rgba(147, 51, 234, 0.5), rgba(124, 58, 237, 0.4));
border: 1px solid rgba(168, 85, 247, 0.4);
border-radius: 24rpx;
color: rgba(255, 255, 255, 0.95);
font-size: 22rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
letter-spacing: 2rpx;
transition: all 0.2s ease;
}
.code-btn:active {
transform: scale(0.97);
opacity: 0.85;
}
.code-btn.disabled {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.3);
}
.countdown {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.6);
}
.btn-primary {
width: 100%;
height: 100rpx;
background: linear-gradient(135deg, #9333EA 0%, #7C3AED 100%);
border-radius: 32rpx;
color: white;
font-weight: 600;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
box-shadow: 0 8rpx 32rpx rgba(168, 85, 247, 0.3);
margin-bottom: 32rpx;
}
.btn-primary.disabled {
opacity: 0.5;
}
.agreement {
display: block;
text-align: center;
font-size: 22rpx;
color: rgba(255, 255, 255, 0.25);
line-height: 1.6;
}
</style>