This commit is contained in:
parent
2baa76d893
commit
95280644a4
|
|
@ -458,7 +458,7 @@ const handleCreateOrder = async () => {
|
|||
})
|
||||
|
||||
// 获取跳转URL并打开支付页面
|
||||
const payUrl = res.payment_urls[0]
|
||||
const payUrl = res.payment_urls[1]
|
||||
if (payUrl) {
|
||||
if (isMobileDevice.value) {
|
||||
// 移动端:直接跳转支付页面
|
||||
|
|
|
|||
|
|
@ -1,300 +1,335 @@
|
|||
<template>
|
||||
<div class="user-card">
|
||||
<UserHeader />
|
||||
<UserText :user-text="userText" @back="handleBack" />
|
||||
<UserTabs v-model="activeTab" :hide-tab="hideTab" :tab-list="tabList" />
|
||||
<div class="login-wrapper">
|
||||
<div class="user-card">
|
||||
<div class="brand">
|
||||
<img src="@/assets/images/logo.png" alt="logo" class="logo" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="form-section">
|
||||
<h2 class="section-title">欢迎回来</h2>
|
||||
<p class="section-tip">请输入您的账号信息</p>
|
||||
|
||||
<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"
|
||||
<div class="login-tabs">
|
||||
<div
|
||||
v-for="tab in tabList"
|
||||
:key="tab.name"
|
||||
:class="['tab-item', { active: activeTab === tab.name }]"
|
||||
@click="activeTab = tab.name"
|
||||
>
|
||||
<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>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
/>
|
||||
</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>
|
||||
<span class="forgot-password" @click="handleForgotPassword">忘记密码?</span>
|
||||
<span class="to-register" @click="handleRegister">没有账号?去注册</span>
|
||||
</el-form>
|
||||
<el-form class="form-body" ref="loginFormRef" :model="loginForm" :rules="rules" @submit.prevent="handleLogin">
|
||||
<el-form-item v-if="activeTab === 'email'" prop="email">
|
||||
<el-input v-model="loginForm.email" placeholder="邮箱地址" size="large" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="phone-item" prop="phone" v-else>
|
||||
<el-select class="country-select" v-model="loginForm.countryCode" :teleported="false" value-key="code" size="large">
|
||||
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code" />
|
||||
</el-select>
|
||||
<el-input v-model="loginForm.phone" placeholder="手机号码" size="large" clearable @keyup.enter.prevent="handleLogin" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="loginForm.password" placeholder="登录密码" type="password" show-password size="large" @keyup.enter.prevent="handleLogin" />
|
||||
</el-form-item>
|
||||
|
||||
<el-button type="primary" class="submit-btn" @click="handleLogin" :loading="buttonLoading">登 录</el-button>
|
||||
|
||||
<div class="link-row">
|
||||
<span class="link" @click="handleForgotPassword">忘记密码?</span>
|
||||
<span class="link" @click="handleRegister">没有账号?立即注册</span>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</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'
|
||||
import { passwordRules, emailRules, phoneRules } from './config/rules'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 按钮 loading
|
||||
const { buttonLoading, startButtonLoading, stopButtonLoading } = useButtonLoading()
|
||||
const userStore = useUserStore()
|
||||
|
||||
//userText 实例
|
||||
const userText = ref({
|
||||
backText: '',
|
||||
title: '',
|
||||
text: '初次登录时,您的电子邮箱或手机号码将被用于注册。',
|
||||
account: '',
|
||||
})
|
||||
|
||||
//tabs 实例
|
||||
const tabList = ref([
|
||||
{ label: '邮箱', name: 'email' },
|
||||
{ label: '手机', name: 'phone' },
|
||||
{ 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 loginFormRef = ref()
|
||||
const loginForm = ref({
|
||||
email: '',
|
||||
countryCode: '+86',
|
||||
phone: '',
|
||||
password: '',
|
||||
code: '123456',
|
||||
})
|
||||
const loginRules = ref<FormRules>({
|
||||
|
||||
const rules = {
|
||||
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('触发登录');
|
||||
|
||||
function generateDeviceNo() {
|
||||
return Date.now().toString() + Math.floor(Math.random() * 1000000).toString().padStart(6, '0')
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
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 {
|
||||
// 先从 storage 获取已存在的 device_no,如果没有则生成新的
|
||||
const existingDeviceNo = getStorage('device_no', 'session')
|
||||
console.log('existingDeviceNo:', existingDeviceNo);
|
||||
const deviceNo = existingDeviceNo || 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()
|
||||
}
|
||||
await loginFormRef.value.validate()
|
||||
startButtonLoading()
|
||||
|
||||
try {
|
||||
const deviceNo = getStorage('device_no', 'session') || generateDeviceNo()
|
||||
await userStore.login({
|
||||
email: loginForm.value.email,
|
||||
mobile: loginForm.value.countryCode + loginForm.value.phone,
|
||||
login_type: activeTab.value === 'email' ? 1 : 2,
|
||||
password: loginForm.value.password,
|
||||
device_no: deviceNo+'',
|
||||
})
|
||||
setStorage('device_no', deviceNo)
|
||||
} finally {
|
||||
stopButtonLoading()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单校验失败:', 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'])
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到注册页 - 清除登录状态
|
||||
const handleRegister = () => {
|
||||
function handleRegister() {
|
||||
userStore.clearUserInfo()
|
||||
router.push('/user/register')
|
||||
}
|
||||
|
||||
// 跳转到忘记密码页 - 清除登录状态
|
||||
const handleForgotPassword = () => {
|
||||
function handleForgotPassword() {
|
||||
userStore.clearUserInfo()
|
||||
router.push('/user/forgot-password')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-form {
|
||||
.login-wrapper {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: radial-gradient(ellipse at center, #f0f7ff 0%, #e8f4fd 40%, #f5f9ff 100%);
|
||||
}
|
||||
|
||||
.user-card {
|
||||
width: 600px;
|
||||
padding: 0px 36px 40px 36px;
|
||||
background: #ffffff;
|
||||
border-radius: 18px;
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.02),
|
||||
0 6px 12px rgba(0, 0, 0, 0.03),
|
||||
0 16px 32px rgba(0, 0, 0, 0.04);
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
padding-top: 8px;
|
||||
|
||||
.logo {
|
||||
width: 160px;
|
||||
height: auto;
|
||||
// margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1a365d;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
.section-title {
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.section-tip {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #a0aec0;
|
||||
margin: 0 0 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin-bottom: 28px;
|
||||
position: relative;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
padding: 12px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
color: #a0aec0;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
height: 3px;
|
||||
background: #1e40af;
|
||||
border-radius: 3px;
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #1e40af;
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
width: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active):hover {
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-body {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 8px 14px;
|
||||
border-radius: 10px;
|
||||
// border: 1.5px solid #e2e8f0;
|
||||
transition: all 0.2s ease;
|
||||
background: #fafbfc;
|
||||
|
||||
&:hover {
|
||||
border-color: #cbd5e1;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: #1e40af;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
|
||||
&::placeholder {
|
||||
color: #b0b8c4;
|
||||
}
|
||||
}
|
||||
|
||||
.phone-item {
|
||||
:deep(.el-form-item__content) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 120px;
|
||||
:deep(.el-select) {
|
||||
width: 130px;
|
||||
margin-right: 10px;
|
||||
|
||||
.country-option {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.el-select-dropdown__item) {
|
||||
padding: 0 16px 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-select--large .el-select__wrapper) {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fafbfc;
|
||||
box-shadow: none !important;
|
||||
padding: 18px 12px;
|
||||
}
|
||||
|
||||
:deep(.el-select:hover .el-input__wrapper) {
|
||||
border-color: #cbd5e1;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
:deep(.el-select.is-focus .el-input__wrapper) {
|
||||
border-color: #1e40af !important;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.08) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
font-size: 14px;
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 46px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 3px;
|
||||
border-radius: 10px;
|
||||
margin-top: 16px;
|
||||
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
|
||||
border: none;
|
||||
box-shadow: 0 4px 14px rgba(30, 64, 175, 0.25);
|
||||
transition: all 0.25s ease;
|
||||
|
||||
.form-submit {
|
||||
margin-top: 18px;
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
|
||||
transform: scale(1.01);
|
||||
box-shadow: 0 6px 18px rgba(30, 64, 175, 0.32);
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
width: 100%;
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 3px 10px rgba(30, 64, 175, 0.25);
|
||||
}
|
||||
}
|
||||
.to-register {
|
||||
|
||||
.link-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #409eff;
|
||||
justify-content: space-between;
|
||||
margin-top: 24px;
|
||||
|
||||
.link {
|
||||
font-size: 14px;
|
||||
color: #1e40af;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #1e3a8a;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in New Issue