Files
happy-life-star/mini-program/src/pages/login/index.vue
T

554 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="login-page">
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
<view class="bg-decoration">
<view class="orb orb-primary"></view>
<view class="orb orb-secondary"></view>
<view class="star-field"></view>
</view>
<view class="content" :style="{ paddingTop: safeAreaTop + 'px', paddingBottom: safeAreaBottom + 24 + 'px' }">
<view class="brand-lockup">
<text class="brand-kicker">EMOTION MUSEUM</text>
<text class="brand-title">数字生命档案</text>
</view>
<view class="login-card">
<view class="header">
<text class="title font-serif">欢迎回来</text>
<text class="subtitle">开启你的数字生命档案</text>
</view>
<view
class="btn-primary wechat-login-btn"
:class="{ disabled: loading }"
@click="handleWechatLogin"
>
<text class="wechat-mark"></text>
<text v-if="loading && activeMethod === 'wechat'">微信登录中...</text>
<text v-else>微信一键登录</text>
</view>
<view class="login-divider" v-if="loginConfig.smsLoginEnabled">
<view></view>
<text>或使用手机号登录</text>
<view></view>
</view>
<view class="form" v-if="loginConfig.smsLoginEnabled">
<view class="input-group">
<text class="input-label">手机号码</text>
<input
class="glass-input"
type="number"
maxlength="11"
placeholder="输入手机号"
placeholder-class="input-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="请输入验证码"
placeholder-class="input-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
v-if="loginConfig.smsLoginEnabled"
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, getLoginConfig } 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)
const phone = ref('')
const code = ref('')
const loading = ref(false)
const activeMethod = ref('')
const countdown = ref(60)
const isCountingDown = ref(false)
const loginConfig = ref({ wechatLoginEnabled: true, smsLoginEnabled: false })
onMounted(async () => {
const windowInfo = uni.getWindowInfo()
statusBarHeight.value = windowInfo.statusBarHeight || 20
safeAreaTop.value = windowInfo.safeAreaInsets?.top || windowInfo.statusBarHeight || 20
safeAreaBottom.value = windowInfo.safeAreaInsets?.bottom || 0
try {
const res = await getLoginConfig()
if (res.data) {
loginConfig.value = res.data
}
} catch (e) {
// 接口失败时默认只显示微信登录
loginConfig.value = { wechatLoginEnabled: true, smsLoginEnabled: 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 routeAfterLogin = () => {
uni.redirectTo({ url: '/pages/main/index?tab=script' })
}
const getWechatLoginCode = () => {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
if (res.code) {
resolve(res.code)
} else {
reject(new Error('未获取到微信登录凭证'))
}
},
fail: reject
})
})
}
const handleWechatLogin = async () => {
if (loading.value) return
loading.value = true
activeMethod.value = 'wechat'
try {
const loginCode = await getWechatLoginCode()
const result = await store.loginWithWechat({ code: loginCode })
if (result.success) {
routeAfterLogin()
} else {
uni.showToast({ title: result.error || '微信登录失败', icon: 'none' })
}
} catch (error) {
uni.showToast({ title: error.message || '微信登录失败', icon: 'none' })
} finally {
loading.value = false
activeMethod.value = ''
}
}
const handleLogin = async () => {
if (!canSubmit.value || loading.value) return
loading.value = true
activeMethod.value = 'phone'
try {
const result = await store.login(phone.value, code.value)
if (result.success) {
routeAfterLogin()
} else {
uni.showToast({ title: result.error || '登录失败', icon: 'none' })
}
} catch (error) {
uni.showToast({ title: '登录失败', icon: 'none' })
} finally {
loading.value = false
activeMethod.value = ''
}
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
background:
radial-gradient(circle at 50% -8%, rgba(180, 129, 255, 0.28), transparent 28%),
linear-gradient(180deg, #13091f 0%, #1b0b31 46%, #100719 100%);
position: relative;
overflow: visible;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
.title.font-serif {
font-family: 'Songti SC', 'STSong', 'Noto Serif SC', serif;
}
.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;
}
.orb {
position: absolute;
border-radius: 999rpx;
filter: blur(72rpx);
}
.orb-primary {
top: 96rpx;
right: -120rpx;
width: 360rpx;
height: 360rpx;
background: rgba(162, 91, 255, 0.24);
}
.orb-secondary {
left: -180rpx;
bottom: 18%;
width: 420rpx;
height: 420rpx;
background: rgba(62, 98, 255, 0.14);
}
.star-field {
position: absolute;
top: 132rpx;
left: 64rpx;
right: 64rpx;
height: 360rpx;
opacity: 0.42;
background-image:
radial-gradient(circle, rgba(255, 255, 255, 0.5) 0 1rpx, transparent 2rpx),
radial-gradient(circle, rgba(219, 196, 255, 0.38) 0 1rpx, transparent 2rpx);
background-size: 108rpx 96rpx, 148rpx 132rpx;
}
.content {
position: relative;
z-index: 1;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-left: 32rpx;
padding-right: 32rpx;
box-sizing: border-box;
}
.brand-lockup {
margin-top: 136rpx;
margin-bottom: 50rpx;
text-align: center;
}
.brand-kicker {
display: block;
margin-bottom: 14rpx;
color: rgba(219, 200, 255, 0.58);
font-size: 18rpx;
font-weight: 600;
letter-spacing: 5rpx;
}
.brand-title {
display: block;
color: rgba(255, 255, 255, 0.9);
font-size: 34rpx;
font-weight: 500;
letter-spacing: 3rpx;
}
.login-card {
width: 100%;
max-width: 686rpx;
margin: 0 auto;
padding: 46rpx 32rpx 36rpx;
box-sizing: border-box;
background:
linear-gradient(180deg, rgba(58, 29, 91, 0.76), rgba(31, 15, 52, 0.82)),
rgba(34, 15, 57, 0.86);
border: 1rpx solid rgba(217, 190, 255, 0.22);
border-radius: 40rpx;
box-shadow:
0 28rpx 72rpx rgba(0, 0, 0, 0.32),
inset 0 1rpx 0 rgba(255, 255, 255, 0.12);
backdrop-filter: blur(34rpx);
}
.header {
text-align: center;
margin-bottom: 42rpx;
}
.title {
display: block;
margin-bottom: 12rpx;
color: rgba(255, 255, 255, 0.94);
font-size: 42rpx;
font-weight: 500;
letter-spacing: 5rpx;
line-height: 1.25;
}
.subtitle {
display: block;
color: rgba(225, 209, 255, 0.66);
font-size: 24rpx;
line-height: 1.45;
}
.form {
margin-bottom: 36rpx;
}
.wechat-login-btn {
gap: 16rpx;
margin-bottom: 26rpx;
background: linear-gradient(135deg, #35c76f 0%, #159552 100%);
box-shadow:
0 14rpx 36rpx rgba(21, 149, 82, 0.26),
inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
.wechat-mark {
width: 42rpx;
height: 42rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #159552;
font-size: 24rpx;
line-height: 42rpx;
font-weight: 900;
background: rgba(255, 255, 255, 0.94);
}
.login-divider {
display: flex;
align-items: center;
gap: 18rpx;
margin-bottom: 30rpx;
}
.login-divider view {
flex: 1;
height: 1rpx;
background: rgba(224, 205, 255, 0.16);
}
.login-divider text {
color: rgba(225, 209, 255, 0.48);
font-size: 22rpx;
line-height: 1;
}
.input-group {
margin-bottom: 24rpx;
}
.input-group:last-child {
margin-bottom: 0;
}
.input-label {
display: block;
margin-bottom: 12rpx;
color: rgba(234, 223, 255, 0.64);
font-size: 22rpx;
font-weight: 500;
letter-spacing: 1rpx;
}
.glass-input {
width: 100%;
height: 84rpx;
padding: 0 28rpx;
box-sizing: border-box;
color: #fff7ff;
font-size: 27rpx;
background: rgba(255, 255, 255, 0.085);
border: 1rpx solid rgba(255, 255, 255, 0.14);
border-radius: 24rpx;
}
.input-placeholder {
color: rgba(230, 213, 255, 0.38);
}
.code-row {
display: flex;
flex-direction: row;
gap: 16rpx;
align-items: stretch;
width: 100%;
box-sizing: border-box;
}
.code-input {
flex: 1;
width: auto;
min-width: 0;
}
.code-btn {
width: 196rpx;
height: 84rpx;
color: rgba(255, 255, 255, 0.94);
font-size: 22rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 24rpx;
background: rgba(132, 72, 204, 0.58);
border: 1rpx solid rgba(217, 190, 255, 0.28);
box-sizing: border-box;
transition: all 0.2s ease;
}
.code-btn:active {
transform: scale(0.97);
opacity: 0.86;
}
.code-btn.disabled {
color: rgba(226, 213, 255, 0.36);
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.09);
}
.countdown {
color: rgba(235, 224, 255, 0.7);
font-size: 26rpx;
font-weight: 500;
}
.btn-primary {
width: 100%;
height: 88rpx;
margin-bottom: 24rpx;
padding: 0;
box-sizing: border-box;
color: white;
font-weight: 700;
font-size: 28rpx;
letter-spacing: 1rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
border-radius: 28rpx;
background: linear-gradient(135deg, #a855f7 0%, #6d3be8 100%);
box-shadow:
0 14rpx 36rpx rgba(133, 68, 255, 0.34),
inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
transition: all 0.2s ease;
}
.btn-primary:active {
transform: scale(0.98);
box-shadow:
0 8rpx 24rpx rgba(133, 68, 255, 0.24),
inset 0 1rpx 0 rgba(255, 255, 255, 0.18);
}
.btn-primary.disabled {
opacity: 0.48;
}
.agreement {
display: block;
text-align: center;
color: rgba(220, 205, 246, 0.44);
font-size: 22rpx;
line-height: 1.6;
}
@media screen and (max-height: 700px) {
.brand-lockup {
margin-top: 76rpx;
margin-bottom: 34rpx;
}
.login-card {
padding-top: 38rpx;
padding-bottom: 30rpx;
}
.header {
margin-bottom: 32rpx;
}
}
</style>