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
const payUrl = res.payment_urls[0]
const payUrl = res.payment_urls[1]
if (payUrl) {
if (isMobileDevice.value) {
//

View File

@ -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. 6000000 ~ 999999
// Math.random()0~1100000060
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'
await loginFormRef.value.validate()
startButtonLoading()
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()
}
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') {
// 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 = () => {
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--large .el-select__wrapper) {
border-radius: 10px;
border: 1px solid #e2e8f0;
background: #fafbfc;
box-shadow: none !important;
padding: 18px 12px;
}
:deep(.el-select-dropdown__item) {
padding: 0 16px 0 16px;
}
: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>