554 lines
12 KiB
Vue
554 lines
12 KiB
Vue
<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>
|