125 lines
5.6 KiB
Vue
125 lines
5.6 KiB
Vue
<template>
|
||
<view class="page sms-login">
|
||
<view class="card">
|
||
<view class="title">邮箱验证码登录</view>
|
||
<view class="form">
|
||
<input class="input" type="text" placeholder="请输入邮箱地址" v-model="email"/>
|
||
<view class="row">
|
||
<input class="input code" type="number" maxlength="6" placeholder="请输入验证码" v-model="code"/>
|
||
<button class="send" :disabled="countdown>0 || sending" @click="sendCode">{{ btnText }}</button>
|
||
</view>
|
||
<button class="login" type="primary" :disabled="logging" @click="doLogin">登录/注册</button>
|
||
</view>
|
||
<view class="hint">首次登录将自动创建店铺与店主用户。</view>
|
||
|
||
<view class="debug">
|
||
<view class="debug-title" @click="toggleDebug">请求体示例(点击{{ showDebug? '收起':'展开' }})</view>
|
||
<view v-if="showDebug" class="debug-body">
|
||
<view class="code-title">POST /api/auth/email/send</view>
|
||
<view class="code-wrap">
|
||
<text class="code">{{ sendBodyJson }}</text>
|
||
<button size="mini" class="copy" @click="copy(sendBodyJson)">复制</button>
|
||
</view>
|
||
<view class="code-title">POST /api/auth/email/login</view>
|
||
<view class="code-wrap">
|
||
<text class="code">{{ loginBodyJson }}</text>
|
||
<button size="mini" class="copy" @click="copy(loginBodyJson)">复制</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { post } from '../../common/http.js'
|
||
|
||
export default {
|
||
data(){
|
||
return { email: '', code: '', countdown: 0, timer: null, sending: false, logging: false, showDebug: true }
|
||
},
|
||
computed:{
|
||
btnText(){ return this.countdown>0 ? `${this.countdown}s` : (this.sending ? '发送中...' : '获取验证码') }
|
||
,
|
||
trimmedEmail(){ return String(this.email||'').trim() },
|
||
sendBodyJson(){ return JSON.stringify({ email: this.trimmedEmail, scene: 'login' }, null, 2) },
|
||
loginBodyJson(){ return JSON.stringify({ email: this.trimmedEmail, code: String(this.code||'').trim() }, null, 2) }
|
||
},
|
||
onUnload(){ if (this.timer) clearInterval(this.timer) },
|
||
methods:{
|
||
validateEmail(e){ return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(String(e||'').trim()) },
|
||
startCountdown(sec){
|
||
this.countdown = sec
|
||
if (this.timer) clearInterval(this.timer)
|
||
this.timer = setInterval(() => {
|
||
if (this.countdown<=1) { clearInterval(this.timer); this.timer=null; this.countdown=0; return }
|
||
this.countdown--
|
||
}, 1000)
|
||
},
|
||
async sendCode(){
|
||
if (this.sending || this.countdown>0) return
|
||
const e = String(this.email||'').trim()
|
||
if (!this.validateEmail(e)) return uni.showToast({ title: '请输入正确的邮箱地址', icon: 'none' })
|
||
this.sending = true
|
||
try {
|
||
const res = await post('/api/auth/email/send', { email: e, scene: 'login' })
|
||
const cd = Number(res && res.cooldownSec || 60)
|
||
this.startCountdown(cd)
|
||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
||
} catch(e) {
|
||
const msg = (e && e.message) || '发送失败'
|
||
uni.showToast({ title: msg, icon: 'none' })
|
||
} finally { this.sending=false }
|
||
},
|
||
async doLogin(){
|
||
if (this.logging) return
|
||
const e = String(this.email||'').trim()
|
||
const c = String(this.code||'').trim()
|
||
if (!this.validateEmail(e)) return uni.showToast({ title: '请输入正确的邮箱地址', icon: 'none' })
|
||
if (!/^\d{6}$/.test(c)) return uni.showToast({ title: '验证码格式不正确', icon: 'none' })
|
||
this.logging = true
|
||
try {
|
||
const data = await post('/api/auth/email/login', { email: e, code: c })
|
||
if (data && data.token) {
|
||
uni.setStorageSync('TOKEN', data.token)
|
||
if (data.user && data.user.email) uni.setStorageSync('USER_EMAIL', data.user.email)
|
||
uni.showToast({ title: '登录成功', icon: 'none' })
|
||
setTimeout(() => { uni.reLaunch({ url: '/pages/index/index' }) }, 300)
|
||
}
|
||
} catch(e) {
|
||
const msg = (e && e.message) || '登录失败'
|
||
uni.showToast({ title: msg, icon: 'none' })
|
||
} finally { this.logging=false }
|
||
}
|
||
,
|
||
copy(text){
|
||
try { uni.setClipboardData({ data: String(text||'') }); uni.showToast({ title: '已复制', icon: 'none' }) } catch(e) {}
|
||
}
|
||
,
|
||
toggleDebug(){ this.showDebug = !this.showDebug }
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.sms-login { padding: 24rpx; }
|
||
.card { background: $uni-bg-color-grey; border-radius: 16rpx; padding: 28rpx; }
|
||
.title { font-size: 32rpx; font-weight: 700; margin-bottom: 20rpx; }
|
||
.form { display: flex; flex-direction: column; gap: 16rpx; }
|
||
.row { display: flex; gap: 12rpx; }
|
||
.input { background: #fff; border: 1rpx solid $uni-border-color; border-radius: 12rpx; padding: 20rpx; font-size: 28rpx; flex: 1; }
|
||
.input.code { flex: 1; }
|
||
.send { min-width: 220rpx; }
|
||
.login { margin-top: 8rpx; }
|
||
.hint { margin-top: 12rpx; font-size: 22rpx; color: $uni-text-color-grey; }
|
||
.debug { margin-top: 20rpx; }
|
||
.debug-title { font-size: 26rpx; color: $uni-text-color-grey; }
|
||
.debug-body { margin-top: 12rpx; display: flex; flex-direction: column; gap: 12rpx; }
|
||
.code-title { font-size: 24rpx; color: $uni-text-color-grey; }
|
||
.code-wrap { position: relative; background: #fff; border: 1rpx solid $uni-border-color; border-radius: 12rpx; padding: 16rpx; }
|
||
.code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; font-size: 24rpx; white-space: pre-wrap; }
|
||
.copy { position: absolute; top: 8rpx; right: 8rpx; }
|
||
</style>
|
||
|
||
|