kboss/f/web-kboss/src/views/LoginDialog/LoginDialog.vue
2026-07-16 18:05:03 +08:00

810 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>