287 lines
8.4 KiB
Vue
287 lines
8.4 KiB
Vue
<template>
|
||
<div class="user-card">
|
||
<UserHeader />
|
||
<UserText :user-text="userText" @back="handleBack" />
|
||
<UserTabs v-model="activeTab" :hide-tab="hideTab" :tab-list="tabList" />
|
||
|
||
<el-form class="user-form" ref="loginFormRef" :model="loginForm" :rules="loginRules" @submit.prevent="handleLogin">
|
||
<Transition name="form-switch" mode="out-in">
|
||
<el-form-item v-if="activeTab === 'email'" key="form-email-item" prop="email">
|
||
<el-input v-model="loginForm.email" placeholder="请输入邮箱" clearable size="large" />
|
||
</el-form-item>
|
||
|
||
<el-form-item
|
||
class="phone-item"
|
||
prop="phone"
|
||
v-else-if="activeTab === 'phone'"
|
||
key="form-phone-item"
|
||
>
|
||
<el-select
|
||
v-model="loginForm.countryCode"
|
||
:teleported="false"
|
||
value-key="code"
|
||
placeholder="国家"
|
||
size="large"
|
||
>
|
||
<el-option
|
||
v-for="item in countryOptions"
|
||
:key="item.code"
|
||
:label="item.code"
|
||
:value="item.code"
|
||
>
|
||
<div class="country-option">
|
||
<span class="country-name">{{ item.name }}</span>
|
||
<span class="country-code">{{ item.code }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
<el-input v-model="loginForm.phone" placeholder="请输入手机号" clearable size="large" @keyup.enter.prevent="handleLogin"/>
|
||
</el-form-item>
|
||
|
||
<el-form-item v-else-if="activeTab === 'password'" key="form-password-item" prop="password">
|
||
<el-input
|
||
ref="passwordInputRef"
|
||
v-model="loginForm.password"
|
||
placeholder="请输入密码"
|
||
type="password"
|
||
show-password
|
||
clearable
|
||
size="large"
|
||
/>
|
||
<router-link to="/user/forgot-password" class="forgot-password">忘记密码?</router-link>
|
||
</el-form-item>
|
||
</Transition>
|
||
<el-form-item class="form-submit">
|
||
<el-button type="primary" @click="handleLogin" :loading="activeTab === 'password' && buttonLoading">登录</el-button>
|
||
</el-form-item>
|
||
<router-link to="/user/register" class="to-register">没有账号?去注册</router-link>
|
||
</el-form>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
|
||
defineOptions({
|
||
name: 'Login'
|
||
})
|
||
// type类型
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
import { useRouter } from 'vue-router'
|
||
// 组件
|
||
import UserHeader from './components/Header.vue'
|
||
import UserText from './components/Text.vue'
|
||
import UserTabs from './components/Tabs.vue'
|
||
|
||
//公共方法/store/配置文件
|
||
import { useUserStore } from '@/stores/modules/user'
|
||
import { useButtonLoading } from '@/composables/useLoading'
|
||
import { setStorage, getStorage } from '@/utils/storage'
|
||
import { md5Encrypt } from '@/utils/crypto'
|
||
|
||
//用户校验规则
|
||
import { countryOptions } from '@/views/user/config/mock'
|
||
import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules'
|
||
|
||
const router = useRouter()
|
||
|
||
// 按钮 loading
|
||
const { buttonLoading, startButtonLoading, stopButtonLoading } = useButtonLoading()
|
||
|
||
//userText 实例
|
||
const userText = ref({
|
||
backText: '',
|
||
title: '',
|
||
text: '初次登录时,您的电子邮箱或手机号码将被用于注册。',
|
||
account: '',
|
||
})
|
||
|
||
//tabs 实例
|
||
const tabList = ref([
|
||
{ label: '邮箱', name: 'email' },
|
||
{ label: '手机', name: 'phone' },
|
||
])
|
||
const activeTab = ref<string>('email')
|
||
const hideTab = ref<boolean>(false)
|
||
|
||
// login form 实例
|
||
const userStore = useUserStore()
|
||
const loginFormRef = ref<FormInstance>()
|
||
const passwordInputRef = ref()
|
||
const loginForm = ref({
|
||
email: '',
|
||
countryCode: '+86',
|
||
phone: '',
|
||
password: '',
|
||
code: '123456',
|
||
})
|
||
const loginRules = ref<FormRules>({
|
||
email: emailRules().email,
|
||
phone: phoneRules(loginForm.value).phone,
|
||
code: codeRules().code,
|
||
password: passwordRules(loginForm.value).password,
|
||
})
|
||
|
||
//方法
|
||
|
||
/**
|
||
* 生成时间戳拼接6位随机数字的字符串
|
||
* @returns {string} 格式示例:1710688800000123456
|
||
*/
|
||
function generateTimestampWithRandomNum() {
|
||
// 1. 获取当前时间戳(毫秒级)
|
||
const timestamp = Date.now()
|
||
|
||
// 2. 生成6位随机数字(范围:000000 ~ 999999)
|
||
// Math.random()生成0~1的随机数,乘以1000000后取整,不足6位补0
|
||
const random6Num = Math.floor(Math.random() * 1000000)
|
||
.toString()
|
||
.padStart(6, '0')
|
||
|
||
// 3. 拼接并返回
|
||
return timestamp + random6Num
|
||
}
|
||
|
||
// 登录
|
||
const handleLogin = async () => {
|
||
console.log('触发登录');
|
||
|
||
if (!loginFormRef.value) return
|
||
|
||
try {
|
||
const currentTab = activeTab.value
|
||
//验证邮箱/手机号 → 切换到密码页
|
||
if (currentTab === 'email' || currentTab === 'phone') {
|
||
//校验当前账号字段(email/phone)
|
||
await loginFormRef.value.validateField(currentTab)
|
||
//存储账号类型和账号值(方便后续登录取值)
|
||
const loginValue =
|
||
currentTab === 'phone'
|
||
? loginForm.value.countryCode + loginForm.value[currentTab]
|
||
: loginForm.value[currentTab]
|
||
setStorage('loginType', currentTab, 'session') // 存储账号类型(email/phone)
|
||
setStorage('login', loginValue, 'session') // 存储账号值
|
||
//切换到密码页,更新提示
|
||
hideTab.value = true
|
||
userText.value = {
|
||
backText: '返回',
|
||
title: '欢迎回来!',
|
||
text: '请输入您的登录密码',
|
||
account: loginValue,
|
||
}
|
||
activeTab.value = 'password'
|
||
|
||
if (currentTab === 'phone' && loginForm.value.email !== '') {
|
||
loginForm.value.email = ''
|
||
}
|
||
|
||
// 切换到密码页后自动聚焦密码输入框
|
||
setTimeout(() => {
|
||
passwordInputRef.value?.focus()
|
||
},400)
|
||
}
|
||
// 验证密码+验证码 → 执行登录
|
||
else if (currentTab === 'password') {
|
||
// 校验密码+验证码
|
||
await loginFormRef.value.validateField(['password', 'code'])
|
||
// 开启 loading
|
||
startButtonLoading()
|
||
try {
|
||
// 生成device_no并保存到变量(关键修改1)
|
||
const deviceNo = generateTimestampWithRandomNum()
|
||
// 登录参数
|
||
const loginParams = {
|
||
email: loginForm.value.email,
|
||
mobile: loginForm.value.countryCode + loginForm.value.phone,
|
||
login_type: getStorage('loginType', 'session') === 'email' ? 1 : 2,
|
||
code: loginForm.value.code,
|
||
password: loginForm.value.password,
|
||
device_no: deviceNo,
|
||
}
|
||
await userStore.login(loginParams)
|
||
setStorage('device_no', deviceNo)
|
||
} finally {
|
||
// 关闭 loading
|
||
stopButtonLoading()
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('表单校验失败:', error)
|
||
}
|
||
}
|
||
|
||
//返回登录页
|
||
const handleBack = () => {
|
||
if (activeTab.value === 'password') {
|
||
// 从sessionStorage取账号类型(而非用password作为key)
|
||
const loginType = getStorage('loginType', 'session') || 'email'
|
||
// 清空密码、验证码字段,避免残留
|
||
loginForm.value.password = ''
|
||
loginForm.value.code = ''
|
||
// 清空表单校验提示
|
||
loginFormRef.value?.clearValidate(['password', 'code'])
|
||
// 重置文本和tab状态
|
||
hideTab.value = false
|
||
userText.value = {
|
||
backText: '',
|
||
title: '',
|
||
text: '初次登录时,您的电子邮箱或手机号码将被用于注册。',
|
||
account: '',
|
||
}
|
||
activeTab.value = loginType as string
|
||
} else {
|
||
// 非password页:仅重置为email(可选,也可保留当前tab)
|
||
activeTab.value = 'email'
|
||
// 清空当前账号字段和校验提示
|
||
loginForm.value.email = ''
|
||
loginForm.value.phone = ''
|
||
loginFormRef.value?.clearValidate(['email', 'phone'])
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.user-form {
|
||
.phone-item {
|
||
:deep(.el-form-item__content) {
|
||
flex-wrap: nowrap;
|
||
}
|
||
|
||
.el-select {
|
||
width: 120px;
|
||
margin-right: 10px;
|
||
|
||
.country-option {
|
||
width: 120px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
:deep(.el-select-dropdown__item) {
|
||
padding: 0 16px 0 16px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.forgot-password {
|
||
font-size: 14px;
|
||
color: #409eff;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.form-submit {
|
||
margin-top: 18px;
|
||
|
||
:deep(.el-form-item__content) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
.to-register {
|
||
display: flex;
|
||
justify-content: center;
|
||
text-decoration: none;
|
||
color: #409eff;
|
||
margin-top: 24px;
|
||
}
|
||
}
|
||
</style>
|