This commit is contained in:
2026-06-11 16:55:33 +08:00
parent 2baa76d893
commit 95280644a4
2 changed files with 269 additions and 234 deletions

View File

@ -458,7 +458,7 @@ const handleCreateOrder = async () => {
}) })
// URL // URL
const payUrl = res.payment_urls[0] const payUrl = res.payment_urls[1]
if (payUrl) { if (payUrl) {
if (isMobileDevice.value) { if (isMobileDevice.value) {
// //

View File

@ -1,300 +1,335 @@
<template> <template>
<div class="user-card"> <div class="login-wrapper">
<UserHeader /> <div class="user-card">
<UserText :user-text="userText" @back="handleBack" /> <div class="brand">
<UserTabs v-model="activeTab" :hide-tab="hideTab" :tab-list="tabList" /> <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"> <div class="form-section">
<Transition name="form-switch" mode="out-in"> <h2 class="section-title">欢迎回来</h2>
<el-form-item v-if="activeTab === 'email'" key="form-email-item" prop="email"> <p class="section-tip">请输入您的账号信息</p>
<el-input v-model="loginForm.email" placeholder="请输入邮箱" clearable size="large" />
</el-form-item>
<el-form-item <div class="login-tabs">
class="phone-item" <div
prop="phone" v-for="tab in tabList"
v-else-if="activeTab === 'phone'" :key="tab.name"
key="form-phone-item" :class="['tab-item', { active: activeTab === tab.name }]"
> @click="activeTab = tab.name"
<el-select
v-model="loginForm.countryCode"
:teleported="false"
value-key="code"
placeholder="国家"
size="large"
> >
<el-option {{ tab.label }}
v-for="item in countryOptions" </div>
:key="item.code" </div>
: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-form class="form-body" ref="loginFormRef" :model="loginForm" :rules="rules" @submit.prevent="handleLogin">
<el-input <el-form-item v-if="activeTab === 'email'" prop="email">
ref="passwordInputRef" <el-input v-model="loginForm.email" placeholder="邮箱地址" size="large" clearable />
v-model="loginForm.password" </el-form-item>
placeholder="请输入密码"
type="password" <el-form-item class="phone-item" prop="phone" v-else>
show-password <el-select class="country-select" v-model="loginForm.countryCode" :teleported="false" value-key="code" size="large">
clearable <el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code" />
size="large" </el-select>
/> <el-input v-model="loginForm.phone" placeholder="手机号码" size="large" clearable @keyup.enter.prevent="handleLogin" />
</el-form-item> </el-form-item>
</Transition>
<el-form-item class="form-submit"> <el-form-item prop="password">
<el-button type="primary" @click="handleLogin" :loading="activeTab === 'password' && buttonLoading">登录</el-button> <el-input v-model="loginForm.password" placeholder="登录密码" type="password" show-password size="large" @keyup.enter.prevent="handleLogin" />
</el-form-item> </el-form-item>
<span class="forgot-password" @click="handleForgotPassword">忘记密码</span>
<span class="to-register" @click="handleRegister">没有账号去注册</span> <el-button type="primary" class="submit-btn" @click="handleLogin" :loading="buttonLoading"> </el-button>
</el-form>
<div class="link-row">
<span class="link" @click="handleForgotPassword">忘记密码</span>
<span class="link" @click="handleRegister">没有账号立即注册</span>
</div>
</el-form>
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
defineOptions({
name: 'Login'
})
// type
import type { FormInstance, FormRules } from 'element-plus'
import { useRouter } from 'vue-router' 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 { useUserStore } from '@/stores/modules/user'
import { useButtonLoading } from '@/composables/useLoading' import { useButtonLoading } from '@/composables/useLoading'
import { setStorage, getStorage } from '@/utils/storage' import { setStorage, getStorage } from '@/utils/storage'
import { md5Encrypt } from '@/utils/crypto'
//
import { countryOptions } from '@/views/user/config/mock' 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() const router = useRouter()
// loading
const { buttonLoading, startButtonLoading, stopButtonLoading } = useButtonLoading() const { buttonLoading, startButtonLoading, stopButtonLoading } = useButtonLoading()
const userStore = useUserStore()
//userText
const userText = ref({
backText: '',
title: '',
text: '初次登录时,您的电子邮箱或手机号码将被用于注册。',
account: '',
})
//tabs
const tabList = ref([ const tabList = ref([
{ label: '邮箱', name: 'email' }, { label: '邮箱登录', name: 'email' },
{ label: '手机', name: 'phone' }, { label: '手机登录', name: 'phone' },
]) ])
const activeTab = ref<string>('email') const activeTab = ref<string>('email')
const hideTab = ref<boolean>(false) const loginFormRef = ref()
// login form
const userStore = useUserStore()
const loginFormRef = ref<FormInstance>()
const passwordInputRef = ref()
const loginForm = ref({ const loginForm = ref({
email: '', email: '',
countryCode: '+86', countryCode: '+86',
phone: '', phone: '',
password: '', password: '',
code: '123456',
}) })
const loginRules = ref<FormRules>({
const rules = {
email: emailRules().email, email: emailRules().email,
phone: phoneRules(loginForm.value).phone, phone: phoneRules(loginForm.value).phone,
code: codeRules().code,
password: passwordRules(loginForm.value).password, password: passwordRules(loginForm.value).password,
})
//
/**
* 生成时间戳拼接6位随机数字的字符串
* @returns {string} 格式示例1710688800000123456
*/
function generateTimestampWithRandomNum() {
// 1.
const timestamp = Date.now()
// 2. 6000000 ~ 999999
// Math.random()0~1100000060
const random6Num = Math.floor(Math.random() * 1000000)
.toString()
.padStart(6, '0')
// 3.
return timestamp + random6Num
} }
// function generateDeviceNo() {
const handleLogin = async () => { return Date.now().toString() + Math.floor(Math.random() * 1000000).toString().padStart(6, '0')
console.log('触发登录'); }
async function handleLogin() {
if (!loginFormRef.value) return if (!loginFormRef.value) return
try { try {
const currentTab = activeTab.value await loginFormRef.value.validate()
/// startButtonLoading()
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 !== '') { try {
loginForm.value.email = '' const deviceNo = getStorage('device_no', 'session') || generateDeviceNo()
} await userStore.login({
email: loginForm.value.email,
// mobile: loginForm.value.countryCode + loginForm.value.phone,
setTimeout(() => { login_type: activeTab.value === 'email' ? 1 : 2,
passwordInputRef.value?.focus() password: loginForm.value.password,
},400) device_no: deviceNo+'',
} })
// + setStorage('device_no', deviceNo)
else if (currentTab === 'password') { } finally {
// + stopButtonLoading()
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()
}
} }
} catch (error) { } catch (error) {
console.error('表单校验失败:', error) console.error('登录失败:', error)
} }
} }
// function handleRegister() {
const handleBack = () => {
if (activeTab.value === 'password') {
// sessionStoragepasswordkey
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 {
// passwordemailtab
activeTab.value = 'email'
//
loginForm.value.email = ''
loginForm.value.phone = ''
loginFormRef.value?.clearValidate(['email', 'phone'])
}
}
// -
const handleRegister = () => {
userStore.clearUserInfo() userStore.clearUserInfo()
router.push('/user/register') router.push('/user/register')
} }
// - function handleForgotPassword() {
const handleForgotPassword = () => {
userStore.clearUserInfo() userStore.clearUserInfo()
router.push('/user/forgot-password') router.push('/user/forgot-password')
} }
</script> </script>
<style scoped lang="scss"> <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 { .phone-item {
:deep(.el-form-item__content) { :deep(.el-form-item__content) {
flex-wrap: nowrap; flex-wrap: nowrap;
} }
.el-select { :deep(.el-select) {
width: 120px; width: 130px;
margin-right: 10px; margin-right: 10px;
}
.country-option { :deep(.el-select--large .el-select__wrapper) {
width: 120px; border-radius: 10px;
display: flex; border: 1px solid #e2e8f0;
justify-content: space-between; background: #fafbfc;
} box-shadow: none !important;
padding: 18px 12px;
}
:deep(.el-select-dropdown__item) { :deep(.el-select:hover .el-input__wrapper) {
padding: 0 16px 0 16px; 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 { .submit-btn {
font-size: 14px; width: 100%;
color: #409eff; height: 46px;
cursor: pointer; 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 { &:hover {
margin-top: 18px; 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) { &:active {
width: 100%; transform: scale(1);
box-shadow: 0 3px 10px rgba(30, 64, 175, 0.25);
} }
} }
.to-register {
.link-row {
display: flex; display: flex;
justify-content: center; justify-content: space-between;
cursor: pointer;
color: #409eff;
margin-top: 24px; margin-top: 24px;
.link {
font-size: 14px;
color: #1e40af;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
color: #1e3a8a;
text-decoration: underline;
}
}
} }
} }
</style> </style>