485 lines
11 KiB
Vue
485 lines
11 KiB
Vue
<script lang="ts" setup>
|
||
import type { FormInstance } from 'element-plus'
|
||
import { ElMessage } from 'element-plus'
|
||
import { computed, reactive, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { loginByEmailCodeApi, loginByPasswordApi } from '@/request/api/auth'
|
||
import { authCodeSendApi } from '@/request/api/auth/code'
|
||
import { useUserStore } from '@/store/user'
|
||
import { AuthCodeChannel, AuthCodePurpose } from '@/types/auth/code'
|
||
import { validateEmail } from '@/utils/validate'
|
||
|
||
const userStore = useUserStore()
|
||
const router = useRouter()
|
||
|
||
const loginOptions = [
|
||
{
|
||
label: '密码登录',
|
||
value: '0',
|
||
},
|
||
{
|
||
label: '验证码登录',
|
||
value: '1',
|
||
},
|
||
]
|
||
const loginMode = ref('0')
|
||
|
||
const carouselList = [
|
||
{
|
||
title: '值得信赖',
|
||
company: '恒星科技有限公司',
|
||
content: '我们已经连续三年使用该平台,数据处理稳定高效,团队协作体验非常顺畅。',
|
||
},
|
||
{
|
||
title: '安全 · 高效',
|
||
company: '博越信息技术',
|
||
content: '平台的权限管理做得很好,操作简单清晰,极大提升了内部管理效率。',
|
||
},
|
||
{
|
||
title: '稳定可靠',
|
||
company: '远航供应链集团',
|
||
content: '系统运行非常稳定,几乎没有出现过宕机情况,客服响应速度也很快。',
|
||
},
|
||
{
|
||
title: '智能化体验',
|
||
company: '蓝海数据服务中心',
|
||
content: '智能报表和自动化功能非常实用,减少了大量人工整理数据的时间。',
|
||
},
|
||
{
|
||
title: '灵活易用',
|
||
company: '中联创投有限公司',
|
||
content: 'UI设计简洁友好,新员工上手很快,各项业务流程也能灵活配置。',
|
||
},
|
||
]
|
||
|
||
// --- 发送验证码状态 & 倒计时 ---
|
||
const sending = ref(false)
|
||
const countdown = ref(0)
|
||
let timer: number | undefined
|
||
|
||
function startCountdown(seconds = 60) {
|
||
countdown.value = seconds
|
||
timer = window.setInterval(() => {
|
||
countdown.value -= 1
|
||
if (countdown.value <= 0) {
|
||
clearInterval(timer)
|
||
timer = undefined
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
const form = reactive<{
|
||
email: string
|
||
password: string
|
||
code: string
|
||
rememberMe: boolean
|
||
}>({
|
||
email: '',
|
||
password: '',
|
||
code: '',
|
||
rememberMe: true,
|
||
})
|
||
|
||
const formRef = ref<FormInstance>()
|
||
const formRules = {
|
||
email: [
|
||
{ required: true, message: '邮箱号不能为空', trigger: 'blur' },
|
||
{ type: 'email', required: true, message: '邮箱格式错误', trigger: 'blur' },
|
||
],
|
||
password: [
|
||
{
|
||
validator: (rule: any, value: any, callback: any) => {
|
||
if (loginMode.value === '0') {
|
||
if (!value) {
|
||
callback(new Error('密码不能为空'))
|
||
return
|
||
}
|
||
}
|
||
callback() // 校验通过
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
code: [
|
||
{
|
||
validator: (rule: any, value: any, callback: any) => {
|
||
if (loginMode.value === '1') {
|
||
if (!value) {
|
||
callback(new Error('验证码不能为空'))
|
||
return
|
||
}
|
||
}
|
||
callback() // 校验通过
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
}
|
||
|
||
const isEmailValid = computed(() => validateEmail(form.email))
|
||
|
||
const submitLoading = ref(false)
|
||
|
||
function handleSendEmailCode() {
|
||
if (sending.value || countdown.value > 0 || !isEmailValid.value)
|
||
return
|
||
|
||
sending.value = true
|
||
|
||
authCodeSendApi({
|
||
channel: AuthCodeChannel.EMAIL,
|
||
purpose: AuthCodePurpose.LOGIN,
|
||
target: form.email,
|
||
}).then((res) => {
|
||
if (res.isSuccess) {
|
||
ElMessage.success('发送成功')
|
||
startCountdown()
|
||
}
|
||
else {
|
||
sending.value = false
|
||
ElMessage.error(res.message || '发送失败,请检查邮箱是否正确或稍后再试!')
|
||
}
|
||
}).catch((err) => {
|
||
console.log(err)
|
||
|
||
ElMessage.error('发送失败,请稍后再试')
|
||
sending.value = false
|
||
}).finally(() => {
|
||
sending.value = false
|
||
})
|
||
}
|
||
|
||
async function submitForm(formEl: FormInstance | undefined) {
|
||
if (!formEl)
|
||
return
|
||
|
||
await formEl.validate(async (valid) => {
|
||
if (!valid)
|
||
return
|
||
|
||
submitLoading.value = true
|
||
|
||
try {
|
||
let res
|
||
if (loginMode.value === '0') {
|
||
// 密码登录
|
||
res = await loginByPasswordApi({
|
||
email: form.email,
|
||
password: form.password,
|
||
})
|
||
}
|
||
else {
|
||
// 验证码登录
|
||
res = await loginByEmailCodeApi({
|
||
email: form.email,
|
||
code: form.code,
|
||
})
|
||
}
|
||
|
||
if (res.isSuccess) {
|
||
ElMessage.success('登录成功')
|
||
|
||
userStore.setUserInfo({
|
||
user: {
|
||
id: res.data.userId,
|
||
name: res.data.userName,
|
||
position: res.data.userPosition,
|
||
role: res.data.userRole,
|
||
},
|
||
company: {
|
||
id: res.data.companyId,
|
||
logo: res.data.companyLogo,
|
||
name: res.data.companyName,
|
||
},
|
||
token: res.data.token,
|
||
})
|
||
|
||
router.push('/')
|
||
}
|
||
else {
|
||
ElMessage.error(res.message || '登录失败,请稍后再试')
|
||
}
|
||
}
|
||
catch (err) {
|
||
console.log(err)
|
||
ElMessage.error('登录失败,请稍后再试')
|
||
}
|
||
finally {
|
||
submitLoading.value = false
|
||
}
|
||
})
|
||
}
|
||
|
||
function navigateTo(path: string) {
|
||
router.push(path)
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="auth-wrapper">
|
||
<div class="auth-bg">
|
||
<img src="/auth-bg.jpg">
|
||
<div class="bg-content">
|
||
<el-carousel
|
||
height="150px"
|
||
indicator-position="none"
|
||
>
|
||
<el-carousel-item v-for="(item, index) in carouselList" :key="index">
|
||
<div class="carousel-item">
|
||
<h2 class="title">
|
||
{{ item.title }}
|
||
</h2>
|
||
|
||
<span class="content">{{ item.content }}</span>
|
||
|
||
<span class="company"> - {{ item.company }}</span>
|
||
</div>
|
||
</el-carousel-item>
|
||
</el-carousel>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="login-form">
|
||
<img src="/logo.png" class="logo" @click="navigateTo('/')">
|
||
|
||
<div class="main">
|
||
<h4 class="title">
|
||
登录
|
||
</h4>
|
||
<div style="padding: 8px 0px;">
|
||
<el-segmented
|
||
v-model="loginMode" :options="loginOptions" size="large"
|
||
/>
|
||
</div>
|
||
|
||
<p class="title-extra">
|
||
<template v-if="loginMode === '0'">
|
||
输入你的 邮箱 和 密码 登录系统
|
||
</template>
|
||
<template v-else-if="loginMode === '1'">
|
||
输入你的 邮箱 和 验证码 登录系统
|
||
</template>
|
||
</p>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="formRules"
|
||
label-width="auto"
|
||
label-position="top"
|
||
hide-required-asterisk
|
||
>
|
||
<el-form-item label="邮箱" prop="email">
|
||
<el-input v-model="form.email" placeholder="请输入邮箱" size="large" />
|
||
</el-form-item>
|
||
|
||
<el-form-item
|
||
v-if="loginMode === '0'"
|
||
prop="password"
|
||
>
|
||
<template #label>
|
||
<span class="form-label-flex">
|
||
<span>密码</span>
|
||
<span class="forgot" @click="navigateTo('/auth/forget')">忘记密码?</span>
|
||
</span>
|
||
</template>
|
||
<el-input
|
||
v-model="form.password"
|
||
placeholder="请输入密码"
|
||
show-password
|
||
size="large"
|
||
@keyup.enter="submitForm(formRef)"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item
|
||
v-else
|
||
prop="code"
|
||
>
|
||
<template #label>
|
||
<span class="form-label-flex">
|
||
<span>密码</span>
|
||
<span class="forgot" @click="navigateTo('/auth/forget')">忘记密码?</span>
|
||
</span>
|
||
</template>
|
||
<el-input
|
||
v-model="form.code"
|
||
placeholder="请输入邮箱验证码"
|
||
class="email-code"
|
||
size="large"
|
||
:class="(sending || countdown > 0 || !isEmailValid) ? 'disabled' : ''"
|
||
>
|
||
<template #append>
|
||
<el-button
|
||
:disabled="sending || countdown > 0 || !isEmailValid"
|
||
@click="handleSendEmailCode"
|
||
>
|
||
<template v-if="sending">
|
||
发送中...
|
||
</template>
|
||
<template v-else-if="countdown > 0">
|
||
{{ countdown }}s
|
||
</template>
|
||
<template v-else>
|
||
发送验证码
|
||
</template>
|
||
</el-button>
|
||
</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-checkbox v-model="form.rememberMe" label="记住我" class="remeber" />
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button type="primary" class="form-btn" :loading="submitLoading" @click="submitForm(formRef)">
|
||
登录
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
没有账号?前去
|
||
<span class="back" @click="navigateTo('/auth/register')">注册</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.auth-wrapper {
|
||
display: flex;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
|
||
.auth-bg {
|
||
width: 70%;
|
||
height: 100%;
|
||
position: relative;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.bg-content {
|
||
position: absolute;
|
||
left: 50%;
|
||
bottom: 10%;
|
||
transform: translateX(-50%);
|
||
text-align: center;
|
||
color: #fff;
|
||
width: 600px;
|
||
|
||
.carousel-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
.title {
|
||
font-size: 28px;
|
||
line-height: 28px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.content {
|
||
margin-bottom: 8px;
|
||
}
|
||
.company {
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.form-label-flex {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
align-items: center;
|
||
|
||
.forgot {
|
||
color: #0f6fec;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
|
||
.login-form {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 36px;
|
||
|
||
.logo {
|
||
height: 70px;
|
||
width: 160px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.main {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
:deep(.el-form-item__label) {
|
||
font-size: 16px;
|
||
width: 100%;
|
||
margin-bottom: 12px;
|
||
}
|
||
.email-code {
|
||
:deep(.el-input-group__append) {
|
||
background-color: #409eff;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
}
|
||
|
||
&.disabled {
|
||
:deep(.el-input-group__append) {
|
||
background-color: #f5f7fa;
|
||
color: #c0c4cc;
|
||
}
|
||
}
|
||
}
|
||
|
||
.remeber {
|
||
:deep(.el-checkbox__inner) {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
:deep(.el-checkbox__label) {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.title {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.title-extra {
|
||
font-size: 16px;
|
||
color: #666;
|
||
margin-bottom: 36px;
|
||
}
|
||
|
||
.form-btn {
|
||
width: 100%;
|
||
height: 36px;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.footer {
|
||
font-size: 18px;
|
||
color: #999;
|
||
height: 48px;
|
||
text-align: center;
|
||
line-height: 48px;
|
||
|
||
.back {
|
||
color: #0f6fec;
|
||
cursor: pointer;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|