Files
kra-new/web/src/view/login/index.vue
T

990 lines
24 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 class="login">
<div class="bg-aurora" />
<div class="grid-overlay" />
<div class="login-wrap">
<div class="login-card">
<section class="brand-panel">
<div class="brand-panel__mask" />
<div class="brand-top">
<img :src="brandLogo" alt="遨游龙翔游戏平台" class="brand-logo-img" />
</div>
<div class="brand-hero">
<h1>
渠道商务
<br />
<span class="hl">一站式数据中枢</span>
</h1>
<p>
聚合渠道注册、充值、活跃与预警数据,让每一笔商务合作清晰可查、可控、可追溯。
</p>
<div class="feature-list">
<div v-for="item in featureList" :key="item.text" class="feature-item">
<span class="feature-ico">
<!-- 趋势看板 -->
<svg
v-if="item.icon === 'trend'"
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M4 19V9m5 10V5m5 14v-7m5 7V3"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
<!-- 商务层级 -->
<svg
v-else-if="item.icon === 'user'"
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
>
<circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2" />
<path
d="M4 20c0-3.3 3.6-6 8-6s8 2.7 8 6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
<!-- 风险监控 -->
<svg
v-else
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M12 3l8 4v5c0 4.5-3.2 7.8-8 9-4.8-1.2-8-4.5-8-9V7l8-4z"
stroke="currentColor"
stroke-width="2"
stroke-linejoin="round"
/>
<path
d="M9 12l2 2 4-4"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span>{{ item.text }}</span>
</div>
</div>
</div>
<div class="brand-foot">
<p>© {{ currentYear }} 遨游龙翔游戏平台 · 内部商务系统</p>
</div>
</section>
<section class="form-panel">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<div class="form-head">
<div>
<h3 class="form-title">欢迎登录</h3>
<p class="form-desc">请输入您的商务账号以继续</p>
</div>
</div>
<div class="form-body">
<div class="field-block">
<label class="field-label">商务账号</label>
<el-form-item prop="username" class="form-item">
<el-input
v-model="loginForm.username"
type="text"
size="large"
auto-complete="off"
placeholder="请输入商务账号"
>
<template #prefix><Icon icon="lucide:user-round" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
</div>
<div class="field-block">
<label class="field-label">登录密码</label>
<el-form-item prop="password" class="form-item">
<el-input
v-model="loginForm.password"
type="password"
size="large"
autocomplete="new-password"
:readonly="passwordReadonly"
show-password
placeholder="请输入登录密码"
@focus="passwordReadonly = false"
@keyup.enter="handleLogin"
>
<template #prefix><Icon icon="lucide:lock-keyhole" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
</div>
<div v-if="captchaEnabled" class="field-block">
<label class="field-label">验证码</label>
<el-form-item prop="code" class="form-item captcha-row">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
placeholder="请输入验证码"
@keyup.enter="handleLogin"
>
<template #prefix><Icon icon="lucide:shield-check" class="el-input__icon input-icon" /></template>
</el-input>
<div v-if="codeUrl" class="login-code">
<img :src="codeUrl" class="login-code-img" @click="getCode" />
</div>
</el-form-item>
</div>
<div class="form-meta">
<el-checkbox v-model="loginForm.rememberMe">记住账号</el-checkbox>
<button type="button" class="link-type link-button" @click="handleOpenForgotPassword">
忘记密码?
</button>
</div>
<el-form-item class="submit-row">
<el-button
:loading="loading"
size="large"
type="primary"
class="submit-button"
@click.prevent="handleLogin"
>
<span v-if="!loading">登录</span>
<span v-else>登录中…</span>
</el-button>
</el-form-item>
</div>
</el-form>
</section>
</div>
</div>
<el-dialog
v-model="forgotPasswordVisible"
title="找回密码"
width="400px"
class="forgot-password-dialog"
header-class="forgot-password-dialog__header"
body-class="forgot-password-dialog__body"
destroy-on-close
@closed="handleForgotPasswordClosed"
>
<div class="forgot-password-panel">
<!-- 步骤1:输入商务账号 -->
<template v-if="forgotPasswordStep === 'account'">
<el-form ref="forgotAccountRef" :model="forgotPasswordForm" :rules="forgotAccountRules" label-position="top" hide-required-asterisk>
<el-form-item
label="商务账号"
prop="username"
class="forgot-password-field"
:error="forgotAccountError || undefined"
>
<el-input
v-model="forgotPasswordForm.username"
size="large"
placeholder="请输入您注册的商务账号"
@input="forgotAccountError = ''"
@keyup.enter="handleForgotAccountNext"
>
<template #prefix><Icon icon="lucide:user-round" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
</el-form>
<el-button
type="primary"
class="forgot-password-submit"
:loading="forgotSubmitting"
@click="handleForgotAccountNext"
>
下一步
</el-button>
</template>
<!-- 步骤2:短信验证码(账号校验通过后) -->
<template v-else-if="forgotPasswordStep === 'sms'">
<p class="forgot-sms-tip">
验证码已发送至手机号
<span class="forgot-sms-phone">{{ forgotMaskedPhone }}</span>
</p>
<el-form ref="forgotSmsRef" :model="forgotPasswordForm" :rules="forgotSmsRules" label-position="top" hide-required-asterisk>
<el-form-item label="短信验证码" prop="smsCode" class="forgot-password-field">
<div class="forgot-sms-row">
<el-input
v-model="forgotPasswordForm.smsCode"
size="large"
maxlength="6"
placeholder="请输入 6 位验证码"
@keyup.enter="handleForgotSmsNext"
>
<template #prefix><Icon icon="lucide:shield-check" class="el-input__icon input-icon" /></template>
</el-input>
<el-button
class="forgot-sms-send"
:disabled="smsCountdown > 0 || forgotSubmitting"
:loading="forgotSubmitting && smsCountdown <= 0"
@click="handleSendSmsCode"
>
{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}
</el-button>
</div>
</el-form-item>
</el-form>
<el-button
type="primary"
class="forgot-password-submit"
:loading="forgotSubmitting"
@click="handleForgotSmsNext"
>
下一步
</el-button>
</template>
<!-- 步骤3:设置新密码(验证码校验通过后) -->
<template v-else>
<el-form ref="forgotResetRef" :model="forgotPasswordForm" :rules="forgotResetRules" label-position="top">
<el-form-item label="新密码" prop="newPassword" class="forgot-password-field">
<el-input
v-model="forgotPasswordForm.newPassword"
type="password"
size="large"
show-password
maxlength="15"
placeholder="6-15 位字符"
@keyup.enter="handleForgotResetSubmit"
>
<template #prefix><Icon icon="lucide:lock-keyhole" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="confirmPassword" class="forgot-password-field">
<el-input
v-model="forgotPasswordForm.confirmPassword"
type="password"
size="large"
show-password
maxlength="15"
placeholder="请再次输入新密码"
@keyup.enter="handleForgotResetSubmit"
>
<template #prefix><Icon icon="lucide:lock-keyhole" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
</el-form>
<el-button
type="primary"
class="forgot-password-submit"
:loading="forgotSubmitting"
@click="handleForgotResetSubmit"
>
完成
</el-button>
</template>
</div>
</el-dialog>
<div class="el-login-footer">
<span>Copyright © 2018-{{ currentYear }} {{ title }} All Rights Reserved.</span>
</div>
</div>
</template>
<script setup>
import { Icon } from '@iconify/vue'
import brandLogo from '@/assets/business-login-logo.png'
import { useBusinessLogin } from './useBusinessLogin'
defineOptions({ name: 'Login' })
const {
title, currentYear, featureList, loginRef, loginForm, loginRules, loading, passwordReadonly, captchaEnabled, codeUrl, getCode, handleLogin,
forgotPasswordVisible, forgotPasswordStep, forgotAccountRef, forgotSmsRef, forgotResetRef, forgotPasswordForm, forgotAccountError, forgotMaskedPhone,
forgotSubmitting, smsCountdown, forgotAccountRules, forgotSmsRules, forgotResetRules, handleOpenForgotPassword, handleForgotPasswordClosed,
handleForgotAccountNext, handleSendSmsCode, handleForgotSmsNext, handleForgotResetSubmit
} = useBusinessLogin()
</script>
<style lang="scss" scoped>
.login {
--login-bg-primary: #eef1f7;
--login-bg-secondary: rgba(255, 255, 255, 0.92);
--login-bg-card: #ffffff;
--login-bg-input: #f3f5f9;
--login-bg-hover: #e9eef9;
--login-text-primary: #1d2330;
--login-text-secondary: #5b6172;
--login-text-muted: #9aa1b2;
--login-accent: #3b7bff;
--login-accent-hover: #2f6af0;
--login-accent-soft: rgba(59, 123, 255, 0.1);
--login-border: rgba(17, 24, 39, 0.08);
--login-shadow: 0 30px 80px rgba(31, 41, 55, 0.16);
--login-radius: 24px;
position: relative;
min-height: 100%;
overflow: hidden;
background: var(--login-bg-primary);
}
.bg-aurora,
.grid-overlay {
position: fixed;
inset: 0;
pointer-events: none;
}
.bg-aurora {
z-index: 0;
&::before,
&::after {
content: '';
position: absolute;
border-radius: 50%;
filter: blur(90px);
opacity: 0.42;
animation: aurora-float 18s ease-in-out infinite;
}
&::before {
top: -140px;
left: -120px;
width: 560px;
height: 560px;
background: radial-gradient(circle at 30% 30%, rgba(79, 140, 255, 0.45), transparent 70%);
}
&::after {
right: -160px;
bottom: -180px;
width: 620px;
height: 620px;
background: radial-gradient(circle at 70% 70%, rgba(124, 92, 255, 0.36), transparent 70%);
animation-delay: -9s;
}
}
.grid-overlay {
z-index: 1;
background-image:
linear-gradient(rgba(0, 0, 0, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 0, 0, 0.035) 1px, transparent 1px);
background-size: 46px 46px;
mask-image: radial-gradient(circle at 50% 50%, #000 30%, transparent 80%);
}
.login-wrap {
position: relative;
z-index: 2;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 28px 24px 92px;
}
.login-card {
width: min(960px, 100%);
height: 560px;
min-height: 560px;
display: grid;
grid-template-columns: 499px minmax(0, 1fr);
background: var(--login-bg-secondary);
border: 1px solid rgba(255, 255, 255, 0.7);
border-radius: var(--login-radius);
box-shadow: var(--login-shadow);
backdrop-filter: blur(22px);
overflow: hidden;
}
.brand-panel {
position: relative;
height: 558px;
padding: 44px;
display: flex;
flex-direction: column;
justify-content: space-between;
color: var(--login-text-primary);
background: linear-gradient(150deg, rgba(79, 140, 255, .10), rgba(124, 92, 255, .06) 60%, transparent);
border-right: 1px solid rgba(17, 24, 39, 0.06);
}
.brand-panel__mask {
position: absolute;
inset: 0;
}
.brand-top,
.brand-hero,
.brand-foot {
position: relative;
z-index: 1;
}
.brand-top {
display: flex;
align-items: center;
}
.brand-logo-img {
display: block;
max-width: min(100%, 420px);
max-height: 110px;
object-fit: contain;
}
.brand-hero {
margin-top: auto;
h1 {
margin: 18px 0 7px;
font-size: clamp(34px, 3.4vw, 35px);
line-height: 1.18;
letter-spacing: -0.03em;
}
p {
max-width: 420px;
margin: 0;
color: var(--login-text-secondary);
font-size: 15px;
line-height: 1.9;
}
}
.brand-badge {
display: inline-flex;
align-items: center;
padding: 8px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(59, 123, 255, 0.12);
color: var(--login-accent);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hl {
color: var(--login-accent);
}
.feature-list {
margin-top: 28px;
display: flex;
flex-direction: column;
gap: 14px;
}
.feature-item {
display: flex;
align-items: center;
gap: 12px;
color: var(--login-text-secondary);
font-size: 14px;
}
.feature-ico {
width: 34px;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 10px;
background: var(--login-accent-soft);
color: var(--login-accent);
flex: none;
}
.brand-foot {
margin-top: 36px;
p {
margin: 18px 0 0;
color: var(--login-text-muted);
font-size: 12px;
}
}
.brand-metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.metric-card {
padding: 18px 16px;
display: flex;
flex-direction: column;
gap: 6px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.72);
strong {
color: var(--login-text-primary);
font-size: 24px;
font-weight: 700;
}
span {
color: var(--login-text-secondary);
font-size: 13px;
}
}
.form-panel {
display: flex;
align-items: center;
justify-content: center;
padding: 44px;
background: rgba(255, 255, 255, 0.82);
}
.login-form {
width: 100%;
max-width: 100%;
}
.form-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 28px;
}
.form-kicker {
margin: 0 0 8px;
color: var(--login-accent);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.form-title {
margin: 0;
color: var(--login-text-primary);
font-size: 30px;
font-weight: 700;
}
.form-desc {
margin: 8px 0 0;
color: var(--login-text-muted);
font-size: 14px;
line-height: 1.8;
}
.form-body {
border-radius: 24px;
background: rgba(255, 255, 255, 0.72);
}
.field-block + .field-block {
margin-top: 18px;
}
.field-label {
display: block;
margin-bottom: 8px;
color: var(--login-text-secondary);
font-size: 13px;
font-weight: 500;
}
.form-item {
margin-bottom: 0;
}
.login-form .el-input {
height: 48px;
}
.login-form .input-icon {
width: 15px;
height: 46px;
margin-left: 0;
color: var(--login-text-muted);
}
.login-form :deep(.el-input__wrapper) {
min-height: 48px;
padding: 0 14px;
border-radius: 14px;
background: var(--login-bg-input);
box-shadow: 0 0 0 1px var(--login-border) inset;
}
.login-form :deep(.el-input__wrapper.is-focus) {
box-shadow:
0 0 0 1px rgba(59, 123, 255, 0.28) inset,
0 0 0 4px rgba(59, 123, 255, 0.12);
}
.login-form :deep(.el-input__inner) {
color: var(--login-text-primary);
}
.login-form :deep(.el-input__inner::placeholder) {
color: var(--login-text-muted);
}
.login-form :deep(.el-input__suffix-inner) {
color: var(--login-text-muted);
}
.captcha-row {
:deep(.el-form-item__content) {
display: grid;
grid-template-columns: minmax(0, 1fr) 124px;
gap: 12px;
}
}
.login-code {
height: 48px;
overflow: hidden;
border-radius: 14px;
border: 1px solid var(--login-border);
background: var(--login-bg-card);
img {
display: block;
width: 100%;
height: 100%;
cursor: pointer;
object-fit: cover;
}
}
.login-code-img {
padding-left: 0;
}
.form-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin: 18px 0 24px;
}
.login-form :deep(.el-checkbox__label) {
color: var(--login-text-secondary);
}
.login-form :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: var(--login-accent);
border-color: var(--login-accent);
}
.link-type {
color: #2f6af0;
font-size: 13px;
text-decoration: none;
&:hover {
color: #2f6af0;
}
}
.link-button {
padding: 0;
background: transparent;
border: none;
cursor: pointer;
}
.submit-row {
margin-bottom: 0;
}
.submit-button {
width: 100%;
height: 48px;
border: none;
border-radius: 14px;
background: linear-gradient(135deg, var(--login-accent), #6ba0ff);
box-shadow: none;
&:hover,
&:focus {
box-shadow: none;
}
}
.divider {
display: flex;
align-items: center;
gap: 12px;
margin: 24px 0 18px;
color: var(--login-text-muted);
font-size: 12px;
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: rgba(17, 24, 39, 0.08);
}
}
.social-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.login-form :deep(.el-button.is-circle) {
width: 42px;
height: 42px;
margin: 0;
background: var(--login-bg-card);
border: 1px solid var(--login-border);
color: var(--login-text-secondary);
}
.login-form :deep(.el-button.is-circle:hover) {
background: var(--login-bg-hover);
border-color: rgba(59, 123, 255, 0.24);
color: var(--login-accent);
}
.login-form :deep(.el-form-item__error) {
padding-top: 7px;
}
.el-login-footer {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
height: 42px;
line-height: 42px;
text-align: center;
color: rgba(91, 97, 114, 0.78);
font-size: 12px;
}
/* 弹窗根节点经 Teleport 挂到 body,用 :global 覆盖全局 dialog 样式 */
:global(.forgot-password-dialog.el-dialog) {
border-radius: 18px;
overflow: hidden;
}
:global(.forgot-password-dialog__header) {
margin-right: 0 !important;
padding: 24px 28px !important;
border-bottom: 1px solid rgba(17, 24, 39, 0.08);
}
:global(.forgot-password-dialog__header .el-dialog__title) {
color: #1d2330;
font-size: 16px;
font-weight: 700;
}
:global(.forgot-password-dialog__header .el-dialog__headerbtn) {
top: 22px;
right: 20px;
}
:global(.forgot-password-dialog__body) {
padding: 22px 28px 36px !important;
max-height: none !important;
overflow: visible !important;
}
/* 插槽内节点带 scoped 标记,可稳定约束输入框高度 */
.forgot-password-panel {
:deep(.el-form-item__label) {
padding-bottom: 10px;
color: var(--login-text-secondary);
font-size: 14px;
line-height: 1.4;
}
/* 生产构建中 EP `.el-input .el-input__icon{height:inherit}` 会压过 SvgIcon 的 1em,
SVG 回退为默认约 150px 高,把输入框撑变形;与登录表单同样锁死尺寸 */
:deep(.el-input) {
height: 40px;
}
:deep(.el-input__wrapper) {
min-height: 40px;
height: 40px;
border-radius: 12px;
box-shadow: none;
background: var(--login-bg-input, #f3f5f9);
}
:deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px rgba(59, 123, 255, 0.35) inset;
}
:deep(.el-input__inner::placeholder) {
color: var(--login-text-muted, #9aa1b2);
}
:deep(.el-input .input-icon) {
width: 15px;
height: 20px;
margin-left: 0;
}
}
.forgot-password-field {
margin-bottom: 18px;
}
.forgot-sms-tip {
margin: 0 0 18px;
color: var(--login-text-secondary);
font-size: 14px;
line-height: 1.6;
}
.forgot-sms-phone {
margin-left: 4px;
color: var(--login-accent);
font-weight: 600;
}
.forgot-sms-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
width: 100%;
align-items: center;
}
.forgot-sms-send {
height: 40px;
padding: 0 14px;
border: none;
border-radius: 10px;
background: #eaf1ff;
color: var(--login-accent);
font-weight: 500;
}
.forgot-sms-send:hover,
.forgot-sms-send:focus {
background: #dde9ff;
color: var(--login-accent);
}
.forgot-sms-send.is-disabled,
.forgot-sms-send:disabled {
background: #f0f3f8;
color: var(--login-text-muted);
}
.forgot-password-submit {
width: 100%;
height: 48px;
border: none;
border-radius: 14px;
background: linear-gradient(135deg, var(--login-accent), #6ba0ff);
box-shadow: none;
&:hover,
&:focus {
box-shadow: none;
}
}
.forgot-password-tips {
margin: 16px 0 0;
color: #99a1b3;
font-size: 12px;
line-height: 1.8;
}
@keyframes aurora-float {
0%,
100% {
transform: translate(0, 0);
}
50% {
transform: translate(40px, 30px);
}
}
@media (max-width: 960px) {
.login-wrap {
padding: 20px 16px 82px;
}
.login-card {
width: min(960px, 100%);
height: auto;
min-height: 0;
grid-template-columns: 1fr;
}
.brand-panel {
height: auto;
min-height: 360px;
}
.brand-metrics {
grid-template-columns: 1fr;
}
}
@media (max-width: 760px) {
.brand-panel {
display: none;
}
.form-panel {
padding: 28px 20px;
}
.form-body {
padding: 22px 18px;
}
}
@media (max-width: 560px) {
.form-head {
flex-direction: column;
}
.captcha-row :deep(.el-form-item__content) {
grid-template-columns: 1fr;
}
.social-actions {
justify-content: flex-start;
}
}
</style>