810 lines
23 KiB
Vue
810 lines
23 KiB
Vue
<template>
|
||
<div>
|
||
<el-dialog
|
||
:visible.sync="dialogVisible"
|
||
width="540px"
|
||
append-to-body
|
||
custom-class="glass-dialog"
|
||
:close-on-click-modal="true"
|
||
:close-on-press-escape="true"
|
||
@close="handleClose"
|
||
>
|
||
<div class="login-card">
|
||
<div class="brand-area">
|
||
|
||
<div class="brand-text">
|
||
<h2>{{ activeTab === 'login' ? '欢迎回来' : '创建账号' }}</h2>
|
||
<p>{{ activeTab === 'login' ? '登录后继续使用智能服务' : '注册开启全新体验' }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tab-group">
|
||
<button type="button" :class="{ active: activeTab === 'login' }" @click="switchTab('login')">登录</button>
|
||
<button type="button" :class="{ active: activeTab === 'register' }" @click="switchTab('register')">注册</button>
|
||
</div>
|
||
|
||
<div v-if="activeTab === 'login'" class="form-panel">
|
||
<div class="mode-tabs">
|
||
<button type="button" :class="{ active: loginMode === 'password' }" @click="switchLoginMode('password')">密码登录</button>
|
||
<button type="button" :class="{ active: loginMode === 'mobile' }" @click="switchLoginMode('mobile')">验证码登录</button>
|
||
</div>
|
||
|
||
<el-form ref="loginForm" :model="loginForm" :rules="activeLoginRules" label-position="top">
|
||
<template v-if="loginMode === 'password'">
|
||
<el-form-item prop="username">
|
||
<el-input v-model.trim="loginForm.username" placeholder="请输入账户" prefix-icon="el-icon-user" @keyup.enter.native="handleLogin" />
|
||
</el-form-item>
|
||
<el-form-item prop="password">
|
||
<el-input
|
||
v-model="loginForm.password"
|
||
:type="loginPwdVisible ? 'text' : 'password'"
|
||
placeholder="请输入密码"
|
||
prefix-icon="el-icon-lock"
|
||
@keyup.enter.native="handleLogin"
|
||
>
|
||
<i slot="suffix" class="el-icon-view password-eye" @click="loginPwdVisible = !loginPwdVisible"></i>
|
||
</el-input>
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<el-form-item prop="mobile">
|
||
<el-input v-model.trim="loginForm.mobile" placeholder="请输入手机号" prefix-icon="el-icon-mobile-phone" />
|
||
</el-form-item>
|
||
<el-form-item prop="vcode">
|
||
<div class="code-row">
|
||
<el-input v-model.trim="loginForm.vcode" placeholder="请输入验证码" prefix-icon="el-icon-key" @keyup.enter.native="handleLogin" />
|
||
<button type="button" class="code-btn" :disabled="loginCodeDisabled" @click="getLoginCode">{{ loginCodeText }}</button>
|
||
</div>
|
||
</el-form-item>
|
||
</template>
|
||
</el-form>
|
||
|
||
<div class="form-actions">
|
||
<button type="button" class="text-btn" @click="forgotPasswordVisible = true">忘记密码?</button>
|
||
<button type="button" class="text-btn" @click="switchTab('register')">没有账号?去注册</button>
|
||
</div>
|
||
|
||
<button type="button" class="main-btn" :disabled="loginLoading" @click="handleLogin">
|
||
{{ loginLoading ? '登录中...' : '立即登录' }}
|
||
</button>
|
||
</div>
|
||
|
||
<div v-else class="form-panel">
|
||
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" label-position="top">
|
||
<el-form-item prop="mobile">
|
||
<el-input v-model.trim="registerForm.mobile" class="phone-input" placeholder="请输入手机号">
|
||
<span slot="prefix" class="country-prefix">+86</span>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item prop="vcode">
|
||
<div class="code-row">
|
||
<el-input v-model.trim="registerForm.vcode" placeholder="请输入验证码" prefix-icon="el-icon-key" />
|
||
<button type="button" class="code-btn" :disabled="regCodeDisabled" @click="getRegisterCode">{{ regCodeText }}</button>
|
||
</div>
|
||
</el-form-item>
|
||
<el-form-item prop="username">
|
||
<el-input v-model.trim="registerForm.username" placeholder="请输入账户名" prefix-icon="el-icon-user" />
|
||
</el-form-item>
|
||
<el-form-item prop="password">
|
||
<el-input
|
||
v-model="registerForm.password"
|
||
:type="regPwdVisible ? 'text' : 'password'"
|
||
placeholder="请输入密码"
|
||
prefix-icon="el-icon-lock"
|
||
@keyup.enter.native="handleRegister"
|
||
>
|
||
<i slot="suffix" class="el-icon-view password-eye" @click="regPwdVisible = !regPwdVisible"></i>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<div class="agreement-wrap">
|
||
<el-checkbox v-model="registerForm.agree">
|
||
我已阅读并同意《用户协议》、《隐私政策》、《产品服务协议》
|
||
</el-checkbox>
|
||
</div>
|
||
|
||
<button type="button" class="main-btn" :disabled="registerLoading" @click="handleRegister">
|
||
{{ registerLoading ? '注册中...' : '立即注册' }}
|
||
</button>
|
||
|
||
<button type="button" class="bottom-link" @click="switchTab('login')">已有账号?前往登录</button>
|
||
</div>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<forgot-password-dialog :visible.sync="forgotPasswordVisible" />
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { JSEncrypt } from 'jsencrypt'
|
||
import router, { resetRouter } from '@/router'
|
||
import { getCodeAPI, logintypeAPI } from '@/api/login'
|
||
import { register, sendCode } from '@/api/login'
|
||
import { getHomePath } from '@/views/setting/tools'
|
||
import ForgotPasswordDialog from '@/views/login/components/ForgotPasswordDialog.vue'
|
||
|
||
export default {
|
||
name: 'LoginDialog',
|
||
components: { ForgotPasswordDialog },
|
||
props: {
|
||
visible: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
},
|
||
data() {
|
||
const validatePhone = (rule, value, callback) => {
|
||
if (!value) callback(new Error('请输入手机号'))
|
||
else if (!/^1[3-9]\d{9}$/.test(value)) callback(new Error('请输入正确的手机号'))
|
||
else callback()
|
||
}
|
||
|
||
return {
|
||
activeTab: 'login',
|
||
loginMode: 'password',
|
||
forgotPasswordVisible: false,
|
||
loginLoading: false,
|
||
registerLoading: false,
|
||
loginPwdVisible: false,
|
||
regPwdVisible: false,
|
||
loginCodeDisabled: false,
|
||
regCodeDisabled: false,
|
||
loginCodeText: '获取验证码',
|
||
regCodeText: '获取验证码',
|
||
loginTimer: null,
|
||
regTimer: null,
|
||
loginCount: 60,
|
||
regCount: 60,
|
||
loginForm: {
|
||
username: '',
|
||
password: '',
|
||
mobile: '',
|
||
vcode: '',
|
||
codeid: ''
|
||
},
|
||
registerForm: {
|
||
mobile: '',
|
||
vcode: '',
|
||
codeid: '',
|
||
username: '',
|
||
password: '',
|
||
org_type: '2',
|
||
agree: false,
|
||
wechat_openid: localStorage.getItem('wechat_openid') || '',
|
||
domain_name: window.location.hostname
|
||
},
|
||
loginRules: {
|
||
username: [{ required: true, message: '请输入账户', trigger: 'blur' }],
|
||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||
mobile: [{ required: true, validator: validatePhone, trigger: 'blur' }],
|
||
vcode: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
|
||
},
|
||
registerRules: {
|
||
mobile: [{ required: true, validator: validatePhone, trigger: 'blur' }],
|
||
vcode: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
||
username: [{ required: true, message: '请输入账户名', trigger: 'blur' }],
|
||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
dialogVisible: {
|
||
get() {
|
||
return this.visible
|
||
},
|
||
set(value) {
|
||
this.$emit('update:visible', value)
|
||
}
|
||
},
|
||
activeLoginRules() {
|
||
if (this.loginMode === 'mobile') {
|
||
return {
|
||
mobile: this.loginRules.mobile,
|
||
vcode: this.loginRules.vcode
|
||
}
|
||
}
|
||
return {
|
||
username: this.loginRules.username,
|
||
password: this.loginRules.password
|
||
}
|
||
}
|
||
},
|
||
beforeDestroy() {
|
||
clearInterval(this.loginTimer)
|
||
clearInterval(this.regTimer)
|
||
},
|
||
methods: {
|
||
handleClose() {
|
||
this.loginLoading = false
|
||
this.registerLoading = false
|
||
this.$emit('close')
|
||
},
|
||
switchTab(tab) {
|
||
this.activeTab = tab
|
||
this.$nextTick(() => {
|
||
if (this.$refs.loginForm) this.$refs.loginForm.clearValidate()
|
||
if (this.$refs.registerForm) this.$refs.registerForm.clearValidate()
|
||
})
|
||
},
|
||
switchLoginMode(mode) {
|
||
this.loginMode = mode
|
||
this.$nextTick(() => {
|
||
if (this.$refs.loginForm) this.$refs.loginForm.clearValidate()
|
||
})
|
||
},
|
||
passwordEncryption(passwordUser) {
|
||
const publicKey = '-----BEGIN PUBLIC KEY-----\n' +
|
||
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJ3ThUWT3CgvH0O8rrT6\n' +
|
||
'qycpqX0NTq4Q3CxBrvNxo9//qX2bKvhomoLNd+vdti8xNOK6/3zuTJIVt0RoNKwE\n' +
|
||
'0HWMR8H0jgp7ING54DtT5B8bhmUpbs/hownGzIBGOedhqeOPiv0Q5oSi9OIEE+PK\n' +
|
||
'2L8KdgFF2Z6Q1DQdv5Y1qvD/t2mJVjR+NPTwcwBIT8UJ0Cfu8lqHjjJbNF//smTj\n' +
|
||
'Q8v2pnqp19jItuHeD4G4u7a8fWC3/IGEv4+uc5rq5qhwdzRxHUveNmoE+nyh0T8R\n' +
|
||
'C8Y8/XLkEiD0nMvZZjBn7Bof6f1st0aqJX8R1VGvzdTJ8eTvJuyMNsR4wLoF5Pvx\n' +
|
||
'hQIDAQAB\n' +
|
||
'-----END PUBLIC KEY-----'
|
||
const encryptor = new JSEncrypt()
|
||
encryptor.setPublicKey(publicKey)
|
||
return encryptor.encrypt(passwordUser)
|
||
},
|
||
buildLoginParams() {
|
||
const commonParams = {
|
||
domain_name: window.location.hostname,
|
||
wechat_openid: ''
|
||
}
|
||
if (this.loginMode === 'mobile') {
|
||
return {
|
||
...commonParams,
|
||
mobile: this.loginForm.mobile,
|
||
vcode: this.loginForm.vcode,
|
||
codeid: this.loginForm.codeid
|
||
}
|
||
}
|
||
return {
|
||
...commonParams,
|
||
username: this.loginForm.username,
|
||
password: this.passwordEncryption(this.loginForm.password)
|
||
}
|
||
},
|
||
startCountdown(type) {
|
||
const isLogin = type === 'login'
|
||
const timerKey = isLogin ? 'loginTimer' : 'regTimer'
|
||
const countKey = isLogin ? 'loginCount' : 'regCount'
|
||
const textKey = isLogin ? 'loginCodeText' : 'regCodeText'
|
||
const disabledKey = isLogin ? 'loginCodeDisabled' : 'regCodeDisabled'
|
||
|
||
clearInterval(this[timerKey])
|
||
this[countKey] = 59
|
||
this[disabledKey] = true
|
||
this[textKey] = `重新发送 ${this[countKey]}s`
|
||
this[timerKey] = setInterval(() => {
|
||
if (this[countKey] > 0) {
|
||
this[countKey] -= 1
|
||
this[textKey] = `重新发送 ${this[countKey]}s`
|
||
return
|
||
}
|
||
clearInterval(this[timerKey])
|
||
this[timerKey] = null
|
||
this[countKey] = 60
|
||
this[disabledKey] = false
|
||
this[textKey] = '获取验证码'
|
||
}, 1000)
|
||
},
|
||
getLoginCode() {
|
||
this.$refs.loginForm.validateField('mobile', async error => {
|
||
if (error) return
|
||
try {
|
||
const res = await getCodeAPI({ mobile: this.loginForm.mobile, action_type: 'login' })
|
||
if (res.status) {
|
||
this.loginForm.codeid = res.codeid || (res.data && res.data.codeid) || ''
|
||
this.startCountdown('login')
|
||
this.$message.success('验证码已发送')
|
||
} else {
|
||
this.$message.error(res.msg || '验证码获取失败')
|
||
}
|
||
} catch (error) {
|
||
this.$message.error('验证码获取失败')
|
||
}
|
||
})
|
||
},
|
||
getRegisterCode() {
|
||
this.$refs.registerForm.validateField('mobile', async error => {
|
||
if (error) return
|
||
try {
|
||
const res = await sendCode({ mobile: this.registerForm.mobile, action_type: 'register' })
|
||
if (res.status) {
|
||
this.registerForm.codeid = res.codeid || (res.data && res.data.codeid) || res.data || ''
|
||
this.startCountdown('register')
|
||
this.$message.success('验证码已发送')
|
||
} else {
|
||
this.$message.error(res.msg || '验证码获取失败')
|
||
}
|
||
} catch (error) {
|
||
this.$message.error('验证码获取失败')
|
||
}
|
||
})
|
||
},
|
||
handleLogin() {
|
||
this.$refs.loginForm.validate(async valid => {
|
||
if (!valid) return
|
||
const loginParams = this.buildLoginParams()
|
||
this.loginLoading = true
|
||
try {
|
||
const check = await logintypeAPI(loginParams)
|
||
if (!check.status) {
|
||
this.$message.error(check.msg || '登录失败')
|
||
return
|
||
}
|
||
const res = await this.$store.dispatch('user/login', loginParams)
|
||
if (!res.status) {
|
||
this.$message.error(res.msg || '登录失败')
|
||
return
|
||
}
|
||
|
||
localStorage.setItem('user_info', JSON.stringify(res.user))
|
||
sessionStorage.setItem('userId', res.userId)
|
||
sessionStorage.setItem('orgid', res.user.orgid)
|
||
sessionStorage.setItem('org_type', res.org_type)
|
||
sessionStorage.setItem('username', res.user.username)
|
||
|
||
if (res.admin !== 1) {
|
||
sessionStorage.setItem('juese', res.roles[0])
|
||
sessionStorage.setItem('jueseNew', res.roles)
|
||
} else {
|
||
sessionStorage.setItem('juese', 'admin')
|
||
sessionStorage.setItem('jueseNew', 'admin')
|
||
}
|
||
sessionStorage.setItem('roles', JSON.stringify(res.admin !== 1 ? res.roles : ['admin']))
|
||
|
||
this.$store.commit('setLoginState', true)
|
||
resetRouter()
|
||
this.$store.commit('permission/RESET_ROUTES')
|
||
const routeRoles = res.admin !== 1 ? res.roles : ['admin']
|
||
const accessRoutes = await this.$store.dispatch('permission/generateRoutes', {
|
||
user: res.user.username,
|
||
auths: res.data,
|
||
admin: res.admin || '',
|
||
orgType: res.org_type,
|
||
roles: routeRoles
|
||
})
|
||
router.addRoutes(accessRoutes)
|
||
|
||
this.$message.success(res.msg || '登录成功')
|
||
this.dialogVisible = false
|
||
this.$emit('success', res)
|
||
this.redirectAfterLogin(res)
|
||
} catch (error) {
|
||
this.$message.error((error && error.msg) || '登录失败')
|
||
} finally {
|
||
this.loginLoading = false
|
||
}
|
||
})
|
||
},
|
||
redirectAfterLogin(res) {
|
||
const redirectPath = sessionStorage.getItem('loginRedirectPath')
|
||
if (redirectPath && redirectPath.startsWith('/') && !redirectPath.includes('/login')) {
|
||
sessionStorage.removeItem('loginRedirectPath')
|
||
this.$router.push(redirectPath).catch(() => {})
|
||
return
|
||
}
|
||
if (res.admin === 1) this.$router.push('/').catch(() => {})
|
||
else if (String(res.org_type) === '2' || String(res.org_type) === '3') this.$router.push(getHomePath()).catch(() => {})
|
||
else if (res.roles && res.roles.includes('运营')) this.$router.push('/operation/supplierManagement').catch(() => {})
|
||
else if (res.roles && res.roles.includes('销售')) this.$router.push('/sales/distributorManagement').catch(() => {})
|
||
else if (res.roles && res.roles.includes('运维')) this.$router.push('/operationAndMaintenance/workOrderProcessing').catch(() => {})
|
||
else this.$router.push('/').catch(() => {})
|
||
},
|
||
handleRegister() {
|
||
if (!this.registerForm.agree) {
|
||
this.$message.warning('请先阅读并同意相关协议')
|
||
return
|
||
}
|
||
this.$refs.registerForm.validate(async valid => {
|
||
if (!valid) return
|
||
this.registerLoading = true
|
||
try {
|
||
const registerData = {
|
||
mobile: this.registerForm.mobile,
|
||
vcode: this.registerForm.vcode,
|
||
codeid: this.registerForm.codeid,
|
||
org_type: '2',
|
||
username: this.registerForm.username || this.registerForm.mobile,
|
||
password: this.registerForm.password,
|
||
nick_name: this.registerForm.mobile,
|
||
wechat_openid: this.registerForm.wechat_openid,
|
||
domain_name: this.registerForm.domain_name || window.location.hostname
|
||
}
|
||
const res = await register(registerData)
|
||
if (res.status) {
|
||
this.$message.success('注册成功,请登录')
|
||
this.loginForm.mobile = this.registerForm.mobile
|
||
this.switchTab('login')
|
||
this.loginMode = 'mobile'
|
||
this.$refs.registerForm.resetFields()
|
||
this.registerForm.agree = false
|
||
} else {
|
||
this.$message.error(res.message || res.msg || '注册失败')
|
||
}
|
||
} catch (error) {
|
||
this.$message.error('注册失败,请重试')
|
||
} finally {
|
||
this.registerLoading = false
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
|
||
::v-deep .glass-dialog {
|
||
overflow: hidden;
|
||
// border: 1px solid rgba(255, 255, 255, 0.9);
|
||
border-radius: 28px;
|
||
background: #fff;
|
||
box-shadow:
|
||
0 30px 80px rgba(15, 23, 42, 0.16),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(28px) saturate(1.35);
|
||
-webkit-backdrop-filter: blur(28px) saturate(1.35);
|
||
|
||
.el-dialog__header {
|
||
display: none;
|
||
}
|
||
|
||
.el-dialog__body {
|
||
padding: 0;
|
||
}
|
||
|
||
.el-dialog__headerbtn {
|
||
position: absolute;
|
||
top: 28px;
|
||
right: 28px;
|
||
z-index: 10;
|
||
width: 42px;
|
||
height: 42px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.62);
|
||
backdrop-filter: blur(6px);
|
||
-webkit-backdrop-filter: blur(6px);
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
background: rgba(255, 255, 255, 0.86);
|
||
}
|
||
|
||
.el-dialog__close {
|
||
color: #1f2937;
|
||
font-size: 20px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.login-card {
|
||
position: relative;
|
||
padding: 40px 44px 36px;
|
||
}
|
||
|
||
.brand-area {
|
||
display: flex;
|
||
gap: 18px;
|
||
align-items: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.brand-badge {
|
||
width: 60px;
|
||
height: 60px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
font-size: 24px;
|
||
font-weight: 800;
|
||
letter-spacing: -0.5px;
|
||
border-radius: 20px;
|
||
background: linear-gradient(135deg, #2563eb, #7c3aed);
|
||
box-shadow: 0 14px 30px rgba(37, 99, 235, 0.3);
|
||
}
|
||
|
||
.brand-text h2 {
|
||
margin: 0;
|
||
color: #020817;
|
||
font-size: 30px;
|
||
line-height: 1.2;
|
||
font-weight: 800;
|
||
letter-spacing: -0.04em;
|
||
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.55);
|
||
}
|
||
|
||
.brand-text p {
|
||
margin: 4px 0 0;
|
||
color: #263548;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.tab-group {
|
||
display: flex;
|
||
height: 52px;
|
||
padding: 0;
|
||
margin-bottom: 34px;
|
||
background: transparent;
|
||
border-bottom: 1px solid rgba(15, 23, 42, 0.26);
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.tab-group button {
|
||
flex: 1;
|
||
padding: 0;
|
||
border: none;
|
||
border-bottom: 3px solid transparent;
|
||
border-radius: 0;
|
||
background: transparent;
|
||
color: #1f2937;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.tab-group button.active {
|
||
color: #020617;
|
||
background: transparent;
|
||
border-bottom-color: #020617;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.mode-tabs {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 10px;
|
||
margin-bottom: 22px;
|
||
}
|
||
|
||
.mode-tabs button {
|
||
height: 46px;
|
||
padding: 0;
|
||
border: none;
|
||
border: 1px solid #cbd5e1;
|
||
border-radius: 14px;
|
||
background: rgba(255, 255, 255, 0.66);
|
||
color: #94a3b8;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.mode-tabs button.active {
|
||
color: #020617;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-color: #020617;
|
||
}
|
||
|
||
.form-panel ::v-deep .el-form-item {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.form-panel ::v-deep .el-form-item__error {
|
||
color: #ef4444;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-panel ::v-deep .el-input__inner {
|
||
height: 48px;
|
||
padding-left: 44px;
|
||
border: 1px solid #c8d3e2;
|
||
border-radius: 14px;
|
||
background: rgba(255, 255, 255, 0.94);
|
||
color: #020817;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.form-panel ::v-deep .el-input__inner:focus {
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.12);
|
||
background: #ffffff;
|
||
}
|
||
|
||
.form-panel ::v-deep .el-input__inner::placeholder {
|
||
color: #64748b;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.form-panel ::v-deep .el-input__prefix {
|
||
left: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
color: #475569;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.form-panel ::v-deep .el-input__suffix {
|
||
right: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.password-eye {
|
||
color: #475569;
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
transition: color 0.2s;
|
||
|
||
&:hover {
|
||
color: #475569;
|
||
}
|
||
}
|
||
|
||
.code-row {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.code-row .el-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.code-btn {
|
||
height: 48px;
|
||
padding: 0 18px;
|
||
color: #1d4ed8;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
border: 1px solid rgba(37, 99, 235, 0.35);
|
||
border-radius: 14px;
|
||
background: rgba(255, 255, 255, 0.94);
|
||
transition: all 0.2s ease;
|
||
|
||
&:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin: 2px 0 20px;
|
||
}
|
||
|
||
.text-btn,
|
||
.bottom-link {
|
||
padding: 4px 0;
|
||
border: none;
|
||
background: transparent;
|
||
color: #1f2937;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
|
||
&:hover {
|
||
color: #2563eb;
|
||
}
|
||
}
|
||
|
||
.main-btn {
|
||
width: 100%;
|
||
height: 50px;
|
||
border: none;
|
||
border-radius: 999px;
|
||
// background: linear-gradient(90deg, #2463f6, #24b9f1);
|
||
background-color: #020617;
|
||
color: #fff;
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.3px;
|
||
cursor: pointer;
|
||
box-shadow: 0 14px 32px rgba(37, 99, 235, 0.32);
|
||
transition: all 0.25s ease;
|
||
}
|
||
|
||
.main-btn:hover:not(:disabled) {
|
||
box-shadow: 0 18px 40px rgba(37, 99, 235, 0.34);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.main-btn:disabled {
|
||
opacity: 0.72;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.country-prefix {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
height: 100%;
|
||
padding-left: 12px;
|
||
color: #1f2937;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-panel ::v-deep .phone-input .el-input__prefix {
|
||
left: 0;
|
||
width: 54px;
|
||
justify-content: center;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.form-panel ::v-deep .phone-input .el-input__inner {
|
||
padding-left: 58px;
|
||
}
|
||
|
||
.agreement-wrap {
|
||
margin: 4px 0 20px;
|
||
|
||
::v-deep .el-checkbox__label {
|
||
color: #1f2937;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
white-space: normal;
|
||
}
|
||
}
|
||
|
||
.bottom-link {
|
||
display: block;
|
||
width: 100%;
|
||
margin-top: 16px;
|
||
text-align: center;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
::v-deep .glass-dialog {
|
||
width: calc(100vw - 28px) !important;
|
||
border-radius: 22px;
|
||
}
|
||
|
||
.login-card {
|
||
padding: 32px 22px 28px;
|
||
}
|
||
|
||
.brand-badge {
|
||
width: 52px;
|
||
height: 52px;
|
||
font-size: 20px;
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.brand-text h2 {
|
||
font-size: 26px;
|
||
}
|
||
|
||
.brand-text p {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.tab-group {
|
||
height: 52px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.tab-group button {
|
||
font-size: 17px;
|
||
}
|
||
|
||
.mode-tabs {
|
||
gap: 18px;
|
||
}
|
||
|
||
.mode-tabs button {
|
||
height: 36px;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
</style>
|
||
|