1093 lines
35 KiB
Vue
1093 lines
35 KiB
Vue
<template>
|
||
<view class="login-page" :style="{ background: themeConfig.bgColor }">
|
||
<!-- 顶部品牌区 -->
|
||
<view class="brand-container">
|
||
<image class="logo-image" :src="logoConfig.path" mode="aspectFit" />
|
||
<view class="logo-text" :style="{ color: themeConfig.textPrimary }">{{ logoConfig.name }}</view>
|
||
<view class="desc" :style="{ color: themeConfig.textSecondary }">{{ logoConfig.subtitle }}</view>
|
||
</view>
|
||
|
||
<!-- 登录方式切换 -->
|
||
<view class="login-tabs" :style="{ background: themeConfig.cardBgColor }">
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: loginType === 'wechat' }"
|
||
:style="loginType === 'wechat' ? {
|
||
background: themeConfig.gradientSoft,
|
||
color: '#fff',
|
||
boxShadow: `0 4rpx 16rpx ${themeConfig.shadowColor}`
|
||
} : { color: themeConfig.textSecondary }"
|
||
@click="switchToWechat"
|
||
>微信登录</view>
|
||
<view
|
||
class="tab-item"
|
||
:class="{ active: loginType === 'email' }"
|
||
:style="loginType === 'email' ? {
|
||
background: themeConfig.gradientSoft,
|
||
color: '#fff',
|
||
boxShadow: `0 4rpx 16rpx ${themeConfig.shadowColor}`
|
||
} : { color: themeConfig.textSecondary }"
|
||
@click="openEmailModal"
|
||
>邮箱登录</view>
|
||
</view>
|
||
|
||
<!-- 微信登录区 -->
|
||
<view v-if="loginType === 'wechat'" class="login-container">
|
||
<!-- 协议勾选框(微信登录必须勾选) -->
|
||
<view
|
||
class="agreement-checkbox"
|
||
:class="{ shake: wechatShake }"
|
||
@click="toggleWechatAgree"
|
||
>
|
||
<view
|
||
class="checkbox-box"
|
||
:class="{ checked: isWechatAgree }"
|
||
:style="isWechatAgree ? {
|
||
background: themeConfig.primaryColor,
|
||
borderColor: themeConfig.primaryColor
|
||
} : {
|
||
background: themeConfig.cardBgColor,
|
||
borderColor: themeConfig.borderColor
|
||
}"
|
||
>
|
||
<FaIcon v-if="isWechatAgree" icon="check" :size="16" color="#fff" />
|
||
</view>
|
||
<view class="checkbox-text-wrapper">
|
||
<text class="checkbox-text" :style="{ color: themeConfig.textTertiary }">我已阅读并同意</text>
|
||
<text class="link-text" :style="{ color: themeConfig.primaryColor }" @click.stop="gotoUserAgreement">《用户协议》</text>
|
||
<text class="checkbox-text" :style="{ color: themeConfig.textTertiary }">和</text>
|
||
<text class="link-text" :style="{ color: themeConfig.primaryColor }" @click.stop="gotoPrivacyPolicy">《隐私政策》</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view
|
||
class="login-btn btn btn--primary btn--block"
|
||
:class="{ disabled: isLoading }"
|
||
@click="wxLogin"
|
||
>
|
||
<text v-if="isLoading" class="btn-text">登录中...</text>
|
||
<template v-else>
|
||
<FaIcon icon="comment" :size="20" color="#fff" />
|
||
<text class="btn-text">微信一键登录</text>
|
||
</template>
|
||
</view>
|
||
|
||
<view class="tips" :style="{ color: themeConfig.textTertiary }">登录后即可使用全部功能</view>
|
||
</view>
|
||
|
||
<!-- 邮箱登录引导区(tab 选中后引导打开弹窗) -->
|
||
<view v-else class="login-container email-quick">
|
||
<view class="email-subtitle" :style="{ color: themeConfig.textTertiary }">点击下方按钮,在弹窗中输入邮箱和密码</view>
|
||
<view
|
||
class="login-btn btn btn--primary btn--block"
|
||
:class="{ disabled: isLoading }"
|
||
@click="openEmailModal"
|
||
>
|
||
<FaIcon icon="envelope" :size="20" color="#fff" />
|
||
<text class="btn-text">打开邮箱登录</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 邮箱登录弹窗 -->
|
||
<CenterModal
|
||
:visible.sync="showEmailModal"
|
||
title="邮箱登录"
|
||
cancelText="取消"
|
||
confirmText="登录"
|
||
:confirmDisabled="!canEmailLogin"
|
||
@confirm="handleEmailLogin"
|
||
@cancel="showEmailModal = false"
|
||
>
|
||
<view class="modal-form">
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">邮箱地址</text>
|
||
<input
|
||
v-model="emailForm.email"
|
||
type="text"
|
||
placeholder="请输入邮箱"
|
||
class="form-input"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">密码</text>
|
||
<view class="password-input-wrapper">
|
||
<input
|
||
v-model="emailForm.password"
|
||
:type="showPassword ? 'text' : 'password'"
|
||
placeholder="请输入密码"
|
||
class="form-input password-input"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
<FaIcon class="password-toggle" :style="{ color: themeConfig.textTertiary }" :icon="showPassword ? 'eye-slash' : 'eye'" :size="20" @click="showPassword = !showPassword" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">图形验证码</text>
|
||
<view class="captcha-row">
|
||
<input
|
||
v-model="emailForm.captcha"
|
||
type="text"
|
||
placeholder="请输入验证码"
|
||
class="form-input captcha-input"
|
||
maxlength="4"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
<view
|
||
class="captcha-image"
|
||
:style="{ background: themeConfig.inputBgColor, borderColor: themeConfig.borderColor }"
|
||
@click="refreshEmailCaptcha"
|
||
>
|
||
<image :src="emailCaptchaUrl" mode="aspectFit" class="captcha-img" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="agreement-checkbox" :class="{ shake: emailShake }" @click="isEmailAgree = !isEmailAgree">
|
||
<view
|
||
class="checkbox-box"
|
||
:class="{ checked: isEmailAgree }"
|
||
:style="isEmailAgree ? {
|
||
background: themeConfig.primaryColor,
|
||
borderColor: themeConfig.primaryColor
|
||
} : {
|
||
background: themeConfig.cardBgColor,
|
||
borderColor: themeConfig.borderColor
|
||
}"
|
||
>
|
||
<FaIcon v-if="isEmailAgree" icon="check" :size="16" color="#fff" />
|
||
</view>
|
||
<view class="checkbox-text-wrapper">
|
||
<text class="checkbox-text" :style="{ color: themeConfig.textTertiary }">我已阅读并同意</text>
|
||
<text class="link-text" :style="{ color: themeConfig.primaryColor }" @click.stop="gotoUserAgreement">《用户协议》</text>
|
||
<text class="checkbox-text" :style="{ color: themeConfig.textTertiary }">和</text>
|
||
<text class="link-text" :style="{ color: themeConfig.primaryColor }" @click.stop="gotoPrivacyPolicy">《隐私政策》</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 弹窗内的底部快捷链接 -->
|
||
<view class="modal-bottom-links">
|
||
<view class="register-link" :style="{ color: themeConfig.primaryColor }" @click="switchToRegister">
|
||
还没有账号?立即注册
|
||
</view>
|
||
<view class="forgot-link" :style="{ color: themeConfig.textTertiary }" @click="switchToForgot">
|
||
忘记密码?
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</CenterModal>
|
||
|
||
<!-- 注册弹窗 -->
|
||
<CenterModal
|
||
:visible.sync="showRegisterModal"
|
||
title="注册账号"
|
||
:showClose="true"
|
||
:showCancel="true"
|
||
cancelText="取消"
|
||
confirmText="注册"
|
||
:confirmDisabled="!canRegister"
|
||
@confirm="handleRegister"
|
||
@cancel="showRegisterModal = false"
|
||
>
|
||
<view class="modal-form">
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">邮箱地址</text>
|
||
<input
|
||
v-model="registerForm.email"
|
||
type="text"
|
||
placeholder="请输入邮箱"
|
||
class="form-input"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">设置密码</text>
|
||
<view class="password-input-wrapper">
|
||
<input
|
||
v-model="registerForm.password"
|
||
:type="showRegisterPassword ? 'text' : 'password'"
|
||
placeholder="请设置密码(6位以上)"
|
||
class="form-input password-input"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
<FaIcon class="password-toggle" :style="{ color: themeConfig.textTertiary }" :icon="showRegisterPassword ? 'eye-slash' : 'eye'" :size="20" @click="showRegisterPassword = !showRegisterPassword" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">确认密码</text>
|
||
<view class="password-input-wrapper">
|
||
<input
|
||
v-model="registerForm.confirmPassword"
|
||
:type="showRegisterPassword ? 'text' : 'password'"
|
||
placeholder="请再次输入密码"
|
||
class="form-input password-input"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">图形验证码</text>
|
||
<view class="captcha-row">
|
||
<input
|
||
v-model="registerForm.captcha"
|
||
type="text"
|
||
placeholder="请输入验证码"
|
||
class="form-input captcha-input"
|
||
maxlength="4"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
<view
|
||
class="captcha-image"
|
||
:style="{ background: themeConfig.inputBgColor, borderColor: themeConfig.borderColor }"
|
||
@click="refreshCaptcha"
|
||
>
|
||
<image :src="captchaUrl" mode="aspectFit" class="captcha-img" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">邮箱验证码</text>
|
||
<view class="captcha-row">
|
||
<input
|
||
v-model="registerForm.code"
|
||
type="text"
|
||
placeholder="请输入邮箱验证码"
|
||
class="form-input captcha-input"
|
||
maxlength="6"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
<view
|
||
class="send-code-btn"
|
||
:class="{ disabled: registerCountdown > 0 || !registerForm.email || !registerForm.captcha }"
|
||
:style="(registerCountdown > 0 || !registerForm.email || !registerForm.captcha) ? { background: themeConfig.borderColor, color: themeConfig.textTertiary } : { background: themeConfig.primaryColor, color: '#fff' }"
|
||
@click="sendRegisterCode"
|
||
>
|
||
<text>{{ registerCountdown > 0 ? registerCountdown + 's' : '发送验证码' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="registerForm.password && registerForm.confirmPassword && registerForm.password !== registerForm.confirmPassword" class="form-error" :style="{ color: themeConfig.dangerColor }">两次密码不一致</view>
|
||
</view>
|
||
</CenterModal>
|
||
|
||
<!-- 找回密码弹窗 -->
|
||
<CenterModal
|
||
:visible.sync="showForgotModal"
|
||
title="找回密码"
|
||
:showClose="true"
|
||
:showCancel="true"
|
||
cancelText="取消"
|
||
confirmText="重置密码"
|
||
:confirmDisabled="!canForgot"
|
||
@confirm="handleForgotPassword"
|
||
@cancel="showForgotModal = false"
|
||
>
|
||
<view class="modal-form">
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">邮箱地址</text>
|
||
<input
|
||
v-model="forgotForm.email"
|
||
type="text"
|
||
placeholder="请输入注册邮箱"
|
||
class="form-input"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">图形验证码</text>
|
||
<view class="captcha-row">
|
||
<input
|
||
v-model="forgotForm.captcha"
|
||
type="text"
|
||
placeholder="请输入验证码"
|
||
class="form-input captcha-input"
|
||
maxlength="4"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
<view
|
||
class="captcha-image"
|
||
:style="{ background: themeConfig.inputBgColor, borderColor: themeConfig.borderColor }"
|
||
@click="refreshForgotCaptcha"
|
||
>
|
||
<image :src="forgotCaptchaUrl" mode="aspectFit" class="captcha-img" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">邮箱验证码</text>
|
||
<view class="captcha-row">
|
||
<input
|
||
v-model="forgotForm.code"
|
||
type="text"
|
||
placeholder="请输入邮箱验证码"
|
||
class="form-input captcha-input"
|
||
maxlength="6"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
<view
|
||
class="send-code-btn"
|
||
:class="{ disabled: forgotCountdown > 0 || !forgotForm.email || !forgotForm.captcha }"
|
||
:style="(forgotCountdown > 0 || !forgotForm.email || !forgotForm.captcha) ? { background: themeConfig.borderColor, color: themeConfig.textTertiary } : { background: themeConfig.primaryColor, color: '#fff' }"
|
||
@click="sendForgotCode"
|
||
>
|
||
<text>{{ forgotCountdown > 0 ? forgotCountdown + 's' : '发送验证码' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">新密码</text>
|
||
<view class="password-input-wrapper">
|
||
<input
|
||
v-model="forgotForm.password"
|
||
:type="showForgotPassword ? 'text' : 'password'"
|
||
placeholder="请设置新密码(6位以上)"
|
||
class="form-input password-input"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
<FaIcon class="password-toggle" :style="{ color: themeConfig.textTertiary }" :icon="showForgotPassword ? 'eye-slash' : 'eye'" :size="20" @click="showForgotPassword = !showForgotPassword" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="form-label" :style="{ color: themeConfig.textSecondary }">确认新密码</text>
|
||
<view class="password-input-wrapper">
|
||
<input
|
||
v-model="forgotForm.confirmPassword"
|
||
:type="showForgotPassword ? 'text' : 'password'"
|
||
placeholder="请再次输入新密码"
|
||
class="form-input password-input"
|
||
:style="{ background: themeConfig.inputBgColor, color: themeConfig.textPrimary, borderColor: themeConfig.borderColor }"
|
||
placeholder-class="form-input-placeholder"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="forgotForm.password && forgotForm.confirmPassword && forgotForm.password !== forgotForm.confirmPassword" class="form-error" :style="{ color: themeConfig.dangerColor }">两次密码不一致</view>
|
||
</view>
|
||
</CenterModal>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { login, getUserConfig, emailLogin, register, sendEmailCode, getCaptcha, resetPassword } from '@/api/index.js'
|
||
import { setToken } from '@/utils/auth.js'
|
||
import { injectTheme, THEME_CHANGE_EVENT, getThemeConfigFromStorage, THEME_PRESETS, DEFAULT_THEME_KEY } from '@/utils/theme.js'
|
||
import { LOGO_CONFIG } from '@/utils/constants.js'
|
||
import FaIcon from '@/components/FaIcon.vue'
|
||
import CenterModal from '@/components/CenterModal.vue'
|
||
|
||
export default {
|
||
components: {
|
||
FaIcon,
|
||
CenterModal
|
||
},
|
||
data() {
|
||
return {
|
||
logoConfig: LOGO_CONFIG,
|
||
themeConfig: getThemeConfigFromStorage() || THEME_PRESETS[DEFAULT_THEME_KEY],
|
||
loginType: 'wechat',
|
||
isLoading: false,
|
||
isEmailAgree: false,
|
||
isWechatAgree: false,
|
||
wechatShake: false,
|
||
emailShake: false,
|
||
showPassword: false,
|
||
|
||
emailForm: {
|
||
email: '',
|
||
password: '',
|
||
captcha: '',
|
||
captchaId: ''
|
||
},
|
||
|
||
showEmailModal: false,
|
||
showRegisterModal: false,
|
||
showForgotModal: false,
|
||
showRegisterPassword: false,
|
||
showForgotPassword: false,
|
||
registerCountdown: 0,
|
||
forgotCountdown: 0,
|
||
captchaUrl: '',
|
||
emailCaptchaUrl: '',
|
||
forgotCaptchaUrl: '',
|
||
registerForm: {
|
||
email: '',
|
||
password: '',
|
||
confirmPassword: '',
|
||
captcha: '',
|
||
captchaId: '',
|
||
code: ''
|
||
},
|
||
|
||
forgotForm: {
|
||
email: '',
|
||
captcha: '',
|
||
captchaId: '',
|
||
code: '',
|
||
password: '',
|
||
confirmPassword: ''
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
canRegister() {
|
||
const { email, password, confirmPassword, captcha, code } = this.registerForm
|
||
return !!(email && password && confirmPassword && captcha && code && password === confirmPassword)
|
||
},
|
||
canEmailLogin() {
|
||
const { email, password, captcha } = this.emailForm
|
||
// 协议勾选不在这里校验,统一在 handleEmailLogin 中通过抖动+toast 提示
|
||
return !!(email && password && captcha && !this.isLoading)
|
||
},
|
||
canForgot() {
|
||
const { email, captcha, code, password, confirmPassword } = this.forgotForm
|
||
return !!(email && captcha && code && password && confirmPassword && password === confirmPassword)
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.syncThemeConfig()
|
||
},
|
||
onShow() {
|
||
this.syncThemeConfig()
|
||
},
|
||
beforeUnmount() {
|
||
uni.$off(THEME_CHANGE_EVENT, this.themeChangeHandler)
|
||
},
|
||
methods: {
|
||
syncThemeConfig() {
|
||
const appInstance = getApp({ allowDefault: true })
|
||
const globalTheme = appInstance?.globalData?.themeConfig
|
||
this.themeConfig = globalTheme
|
||
|| getThemeConfigFromStorage()
|
||
|| THEME_PRESETS[DEFAULT_THEME_KEY]
|
||
|
||
if (!this.themeChangeHandler) {
|
||
this.themeChangeHandler = ({ key, config }) => {
|
||
if (key && config) this.themeConfig = config
|
||
}
|
||
uni.$on(THEME_CHANGE_EVENT, this.themeChangeHandler)
|
||
}
|
||
},
|
||
|
||
gotoUserAgreement() {
|
||
uni.navigateTo({ url: '/pages/userAgreement/userAgreement' })
|
||
},
|
||
|
||
gotoPrivacyPolicy() {
|
||
uni.navigateTo({ url: '/pages/privacyPolicy/privacyPolicy' })
|
||
},
|
||
|
||
switchToWechat() {
|
||
this.loginType = 'wechat'
|
||
this.showEmailModal = false
|
||
},
|
||
|
||
openEmailModal() {
|
||
this.loginType = 'email'
|
||
this.showEmailModal = true
|
||
if (!this.emailCaptchaUrl) {
|
||
this.refreshEmailCaptcha()
|
||
}
|
||
},
|
||
|
||
// 切换到注册(从邮箱弹窗底部链接)
|
||
switchToRegister() {
|
||
this.showEmailModal = false
|
||
// 等弹窗关闭动画结束再打开注册弹窗
|
||
this.$nextTick(() => {
|
||
setTimeout(() => this.openRegisterModal(), 250)
|
||
})
|
||
},
|
||
|
||
// 切换到忘记密码(从邮箱弹窗底部链接)
|
||
switchToForgot() {
|
||
this.showEmailModal = false
|
||
this.$nextTick(() => {
|
||
setTimeout(() => this.openForgotModal(), 250)
|
||
})
|
||
},
|
||
|
||
// 微信登录协议勾选切换
|
||
toggleWechatAgree() {
|
||
this.isWechatAgree = !this.isWechatAgree
|
||
},
|
||
|
||
// 触发抖动动画(提示用户需勾选协议)
|
||
triggerShake(type) {
|
||
const flag = type === 'wechat' ? 'wechatShake' : 'emailShake'
|
||
this[flag] = false
|
||
this.$nextTick(() => {
|
||
this[flag] = true
|
||
setTimeout(() => { this[flag] = false }, 600)
|
||
})
|
||
},
|
||
|
||
async wxLogin() {
|
||
// 微信登录必须先勾选协议
|
||
if (!this.isWechatAgree) {
|
||
this.triggerShake('wechat')
|
||
uni.showToast({ title: '请先勾选并同意《用户协议》和《隐私政策》', icon: 'none', duration: 2000 })
|
||
return
|
||
}
|
||
if (this.isLoading) return
|
||
this.isLoading = true
|
||
|
||
try {
|
||
const loginRes = await uni.login().catch(() => { throw new Error('获取微信登录凭证失败,请检查网络') })
|
||
const code = loginRes.code
|
||
if (!code) throw new Error('微信登录凭证为空')
|
||
|
||
const result = await login(code)
|
||
if (!result?.token) throw new Error('登录失败,未获取到token')
|
||
|
||
setToken(result.token)
|
||
await this.syncUserConfig()
|
||
|
||
uni.showToast({ title: '登录成功', icon: 'success', duration: 1500 })
|
||
|
||
setTimeout(() => {
|
||
uni.switchTab({
|
||
url: '/pages/index/index',
|
||
fail: () => { uni.redirectTo({ url: '/pages/index/index' }) }
|
||
})
|
||
}, 1500)
|
||
} catch (err) {
|
||
console.error('登录失败:', err)
|
||
uni.showToast({ title: err?.message || '登录失败,请重试', icon: 'none', duration: 2000 })
|
||
} finally {
|
||
this.isLoading = false
|
||
}
|
||
},
|
||
|
||
async handleEmailLogin() {
|
||
const { email, password } = this.emailForm
|
||
if (!email || !password) {
|
||
return uni.showToast({ title: '请填写邮箱和密码', icon: 'none' })
|
||
}
|
||
// 邮箱登录必须先勾选协议
|
||
if (!this.isEmailAgree) {
|
||
this.triggerShake('email')
|
||
uni.showToast({ title: '请先勾选并同意《用户协议》和《隐私政策》', icon: 'none', duration: 2000 })
|
||
return
|
||
}
|
||
|
||
this.isLoading = true
|
||
try {
|
||
const result = await emailLogin({
|
||
email,
|
||
password,
|
||
captcha: this.emailForm.captcha,
|
||
captchaId: this.emailForm.captchaId
|
||
})
|
||
if (!result?.token) throw new Error('登录失败')
|
||
|
||
setToken(result.token)
|
||
await this.syncUserConfig()
|
||
|
||
uni.showToast({ title: '登录成功', icon: 'success', duration: 1500 })
|
||
|
||
this.showEmailModal = false
|
||
|
||
setTimeout(() => {
|
||
uni.switchTab({
|
||
url: '/pages/index/index',
|
||
fail: () => { uni.redirectTo({ url: '/pages/index/index' }) }
|
||
})
|
||
}, 1500)
|
||
} catch (err) {
|
||
console.error('邮箱登录失败:', err)
|
||
if (err.message && err.message.includes('账号不存在')) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: '该邮箱尚未注册,是否立即注册?',
|
||
confirmText: '去注册',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
this.showEmailModal = false
|
||
setTimeout(() => this.openRegisterModal(), 300)
|
||
}
|
||
}
|
||
})
|
||
} else {
|
||
uni.showToast({ title: err?.message || '登录失败,请重试', icon: 'none', duration: 2000 })
|
||
}
|
||
} finally {
|
||
this.isLoading = false
|
||
}
|
||
},
|
||
|
||
async refreshCaptcha() {
|
||
try {
|
||
const res = await getCaptcha()
|
||
if (res?.image) {
|
||
this.captchaUrl = res.image
|
||
this.registerForm.captchaId = res.captchaId
|
||
}
|
||
} catch (err) { console.error('获取验证码失败:', err) }
|
||
},
|
||
|
||
async sendRegisterCode() {
|
||
if (this.registerCountdown > 0) return
|
||
const { email, captcha } = this.registerForm
|
||
if (!email) return uni.showToast({ title: '请先输入邮箱', icon: 'none' })
|
||
if (!captcha) return uni.showToast({ title: '请先输入图形验证码', icon: 'none' })
|
||
|
||
try {
|
||
await sendEmailCode({ email, captcha, captchaId: this.registerForm.captchaId, type: 'register' })
|
||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||
|
||
this.registerCountdown = 60
|
||
const timer = setInterval(() => {
|
||
this.registerCountdown--
|
||
if (this.registerCountdown <= 0) clearInterval(timer)
|
||
}, 1000)
|
||
} catch (err) {
|
||
uni.showToast({ title: err?.message || '发送失败', icon: 'none' })
|
||
this.refreshCaptcha()
|
||
this.registerForm.captcha = ''
|
||
}
|
||
},
|
||
|
||
async handleRegister() {
|
||
const { email, password, confirmPassword, code } = this.registerForm
|
||
if (!email || !password || !confirmPassword || !code) {
|
||
return uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||
}
|
||
if (password !== confirmPassword) return uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||
if (password.length < 6) return uni.showToast({ title: '密码长度不能少于6位', icon: 'none' })
|
||
|
||
this.isLoading = true
|
||
try {
|
||
const result = await register({ email, password, confirmPassword, code })
|
||
if (!result?.token) throw new Error('注册失败')
|
||
|
||
setToken(result.token)
|
||
await this.syncUserConfig()
|
||
|
||
uni.showToast({ title: '注册成功', icon: 'success', duration: 1500 })
|
||
|
||
this.showRegisterModal = false
|
||
setTimeout(() => {
|
||
uni.switchTab({
|
||
url: '/pages/index/index',
|
||
fail: () => { uni.redirectTo({ url: '/pages/index/index' }) }
|
||
})
|
||
}, 1500)
|
||
} catch (err) {
|
||
uni.showToast({ title: err?.message || '注册失败,请重试', icon: 'none', duration: 2000 })
|
||
this.refreshCaptcha()
|
||
this.registerForm.captcha = ''
|
||
} finally {
|
||
this.isLoading = false
|
||
}
|
||
},
|
||
|
||
openRegisterModal() {
|
||
this.showRegisterModal = true
|
||
if (!this.captchaUrl) this.refreshCaptcha()
|
||
},
|
||
|
||
openForgotModal() {
|
||
this.showForgotModal = true
|
||
if (!this.forgotCaptchaUrl) this.refreshForgotCaptcha()
|
||
},
|
||
|
||
async refreshEmailCaptcha() {
|
||
try {
|
||
const res = await getCaptcha()
|
||
if (res?.image) {
|
||
this.emailCaptchaUrl = res.image
|
||
this.emailForm.captchaId = res.captchaId
|
||
}
|
||
} catch (err) { console.error('获取邮箱登录验证码失败:', err) }
|
||
},
|
||
|
||
async refreshForgotCaptcha() {
|
||
try {
|
||
const res = await getCaptcha()
|
||
if (res?.image) {
|
||
this.forgotCaptchaUrl = res.image
|
||
this.forgotForm.captchaId = res.captchaId
|
||
}
|
||
} catch (err) { console.error('获取找回密码验证码失败:', err) }
|
||
},
|
||
|
||
async sendForgotCode() {
|
||
if (this.forgotCountdown > 0) return
|
||
const { email, captcha } = this.forgotForm
|
||
if (!email) return uni.showToast({ title: '请先输入邮箱', icon: 'none' })
|
||
if (!captcha) return uni.showToast({ title: '请先输入图形验证码', icon: 'none' })
|
||
|
||
try {
|
||
await sendEmailCode({ email, captcha, captchaId: this.forgotForm.captchaId, type: 'forgot' })
|
||
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||
|
||
this.forgotCountdown = 60
|
||
const timer = setInterval(() => {
|
||
this.forgotCountdown--
|
||
if (this.forgotCountdown <= 0) clearInterval(timer)
|
||
}, 1000)
|
||
} catch (err) {
|
||
uni.showToast({ title: err?.message || '发送失败', icon: 'none' })
|
||
this.refreshForgotCaptcha()
|
||
this.forgotForm.captcha = ''
|
||
}
|
||
},
|
||
|
||
closeForgotModal() {
|
||
this.showForgotModal = false
|
||
},
|
||
|
||
async handleForgotPassword() {
|
||
const { email, captcha, code, password, confirmPassword } = this.forgotForm
|
||
if (!email || !captcha || !code || !password || !confirmPassword) {
|
||
return uni.showToast({ title: '请填写完整信息', icon: 'none' })
|
||
}
|
||
if (password !== confirmPassword) return uni.showToast({ title: '两次密码不一致', icon: 'none' })
|
||
if (password.length < 6) return uni.showToast({ title: '密码长度不能少于6位', icon: 'none' })
|
||
|
||
this.isLoading = true
|
||
try {
|
||
await resetPassword({ email, password, confirmPassword, code, captchaId: this.forgotForm.captchaId })
|
||
uni.showToast({ title: '密码重置成功', icon: 'success', duration: 1500 })
|
||
|
||
this.showForgotModal = false
|
||
setTimeout(() => {
|
||
this.openEmailModal()
|
||
this.emailForm.email = email
|
||
}, 1500)
|
||
} catch (err) {
|
||
uni.showToast({ title: err?.message || '重置失败,请重试', icon: 'none', duration: 2000 })
|
||
this.refreshForgotCaptcha()
|
||
this.forgotForm.captcha = ''
|
||
} finally {
|
||
this.isLoading = false
|
||
}
|
||
},
|
||
|
||
async syncUserConfig() {
|
||
try {
|
||
const res = await getUserConfig()
|
||
if (res?.theme) injectTheme(res.theme)
|
||
} catch (error) {
|
||
console.warn('用户配置同步失败:', error)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/styles/buttons.scss';
|
||
|
||
.login-page {
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 80rpx 40rpx 40rpx;
|
||
box-sizing: border-box;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
position: relative;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
.brand-container {
|
||
text-align: center;
|
||
margin-bottom: 48rpx;
|
||
z-index: 1;
|
||
}
|
||
|
||
.logo-image {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
display: block;
|
||
margin: 0 auto 28rpx;
|
||
}
|
||
|
||
.logo-text {
|
||
font-size: 52rpx;
|
||
font-weight: 700;
|
||
letter-spacing: 4rpx;
|
||
margin-bottom: 14rpx;
|
||
display: block;
|
||
}
|
||
|
||
.desc {
|
||
font-size: 26rpx;
|
||
line-height: 1.5;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
.login-tabs {
|
||
display: flex;
|
||
border-radius: 16rpx;
|
||
padding: 8rpx;
|
||
margin-bottom: 40rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
||
z-index: 1;
|
||
}
|
||
|
||
.tab-item {
|
||
padding: 20rpx 56rpx;
|
||
font-size: 28rpx;
|
||
border-radius: 12rpx;
|
||
transition: background-color 0.3s ease, color 0.3s ease, box-shadow 0.3s ease;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.login-container {
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
z-index: 1;
|
||
flex: 1;
|
||
justify-content: flex-start;
|
||
padding-top: 16rpx;
|
||
}
|
||
|
||
.email-quick {
|
||
align-items: center;
|
||
text-align: center;
|
||
padding-top: 0;
|
||
justify-content: flex-start;
|
||
min-height: 30vh;
|
||
}
|
||
|
||
/* 微信登录区的小字协议 */
|
||
.agreement-link {
|
||
margin-top: 32rpx;
|
||
font-size: 24rpx;
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.email-icon-wrap {
|
||
width: 140rpx;
|
||
height: 140rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 32rpx;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.email-subtitle {
|
||
font-size: 26rpx;
|
||
margin-bottom: 48rpx;
|
||
line-height: 1.6;
|
||
padding: 0 40rpx;
|
||
}
|
||
|
||
.modal-form {
|
||
padding: 8rpx 0;
|
||
}
|
||
|
||
.form-item {
|
||
margin-bottom: 28rpx;
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 26rpx;
|
||
margin-bottom: 12rpx;
|
||
display: block;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
height: 84rpx;
|
||
border-radius: 14rpx;
|
||
padding: 0 24rpx;
|
||
font-size: 28rpx;
|
||
box-sizing: border-box;
|
||
border: 2rpx solid;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.form-input-placeholder {
|
||
color: var(--textTertiary, #C0C4CC);
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.password-input-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.password-input {
|
||
padding-right: 80rpx;
|
||
}
|
||
|
||
.password-toggle {
|
||
position: absolute;
|
||
right: 24rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
font-size: 36rpx;
|
||
padding: 8rpx;
|
||
}
|
||
|
||
.agreement-checkbox {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin: 8rpx 0 24rpx;
|
||
padding: 0 4rpx;
|
||
}
|
||
|
||
/* 未勾选协议抖动提示 */
|
||
.agreement-checkbox.shake {
|
||
animation: shakeX 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||
transform: translateZ(0);
|
||
}
|
||
|
||
@keyframes shakeX {
|
||
0%, 100% { transform: translateX(0); }
|
||
10%, 30%, 50%, 70%, 90% { transform: translateX(-8rpx); }
|
||
20%, 40%, 60%, 80% { transform: translateX(8rpx); }
|
||
}
|
||
|
||
/* 弹窗内底部快捷链接(注册 / 忘记密码) */
|
||
.modal-bottom-links {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-top: 24rpx;
|
||
padding: 0 4rpx;
|
||
font-size: 26rpx;
|
||
}
|
||
|
||
.checkbox-box {
|
||
width: 38rpx;
|
||
height: 38rpx;
|
||
border: 2rpx solid;
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 14rpx;
|
||
margin-top: 4rpx;
|
||
flex-shrink: 0;
|
||
transition: all 0.25s;
|
||
}
|
||
|
||
.checkbox-text-wrapper {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.checkbox-text {
|
||
font-size: 24rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.link-text {
|
||
font-size: 24rpx;
|
||
text-decoration: underline;
|
||
margin: 0 4rpx;
|
||
}
|
||
|
||
/* 登录页主按钮:直接使用 .btn + .btn--primary 统一样式 */
|
||
|
||
.btn-text {
|
||
font-size: 30rpx;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
margin-left: 10rpx;
|
||
}
|
||
|
||
.tips {
|
||
text-align: center;
|
||
font-size: 24rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
|
||
/* .login-bottom-links 已迁移到 .modal-bottom-links(弹窗内),此处保留以防引用 */
|
||
.login-bottom-links {
|
||
display: none;
|
||
}
|
||
|
||
.register-link,
|
||
.forgot-link {
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.captcha-row {
|
||
display: flex;
|
||
gap: 14rpx;
|
||
align-items: center;
|
||
}
|
||
|
||
.captcha-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.captcha-image {
|
||
width: 220rpx;
|
||
height: 84rpx;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 2rpx solid;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.captcha-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.send-code-btn {
|
||
width: 200rpx;
|
||
height: 84rpx;
|
||
border-radius: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 24rpx;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.send-code-btn.disabled {
|
||
pointer-events: none;
|
||
}
|
||
|
||
.form-error {
|
||
font-size: 24rpx;
|
||
margin-top: 8rpx;
|
||
display: block;
|
||
text-align: center;
|
||
}
|
||
</style>
|