Files
hfb_sys/frontend/src/features/auth/views/LoginView.vue
T
yml2213 825ce83003 feat(auth): 优化PC与移动端登录注册界面并支持邀请码锁定绑定
- 重构 PC 端 LoginView.vue 与移动端 MobileRegisterView.vue,统一契合平台金橙色主体色
- 识别邀请链接并自动填入邀请码,锁定输入框为只读并展示「已绑定邀请码」专属徽章,防止篡改
- 短链 /i/:code 与 /m/i/:code 统一重定向至登录注册页,移除冗余独立落地页
- PC 端页脚完全对齐主站并固定沉底,左侧定制官方品质、安全上号、资金担保平台核心卖点
- 验证方式:npm run check(Prettier + ESLint + vue-tsc)全数通过
2026-10-10 13:48:14 +08:00

1274 lines
33 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.
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
ChatLineRound,
Hide,
Iphone,
Key,
Lock,
Medal,
Refresh,
Ticket,
View,
Wallet,
} from '@element-plus/icons-vue'
import { fetchAuthCaptcha, sendSmsCode, type AuthCaptcha } from '@/features/auth/api/auth'
import { useDistributionSiteStore } from '@/stores/distributionSite'
import { useSessionStore } from '@/stores/session'
import { readError } from '@/shared/utils/error'
const router = useRouter()
const route = useRoute()
const session = useSessionStore()
const siteStore = useDistributionSiteStore()
const loading = ref(false)
const sending = ref(false)
const captchaLoading = ref(false)
const captcha = ref<AuthCaptcha | null>(null)
const countDown = ref(0)
const topTab = ref<'login' | 'register'>('login')
const loginMode = ref<'password' | 'sms'>('password')
const isInviteLocked = ref(false)
const passwordVisible = ref(false)
const agreed = ref(true)
// 服务协议查看弹窗
const protocolDialogVisible = ref(false)
const protocolTitle = ref('')
const protocolContent = ref('')
const form = reactive({
phone: '',
captchaCode: '',
code: '',
password: '',
confirmPassword: '',
inviteCode: '',
})
let timer: ReturnType<typeof setInterval> | null = null
function startCountDown() {
countDown.value = 60
timer = setInterval(() => {
countDown.value--
if (countDown.value <= 0) {
if (timer) {
clearInterval(timer)
timer = null
}
}
}, 1000)
}
onUnmounted(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})
onMounted(() => {
void loadCaptcha()
if (route.query.tab === 'register') {
topTab.value = 'register'
}
const qInviteCode = (route.query.invite_code || route.query.code) as string | undefined
if (qInviteCode && qInviteCode.trim()) {
form.inviteCode = qInviteCode.trim()
isInviteLocked.value = true
topTab.value = 'register'
}
})
function switchTab(tab: 'login' | 'register') {
topTab.value = tab
form.code = ''
form.captchaCode = ''
form.password = ''
form.confirmPassword = ''
if (!isInviteLocked.value) {
form.inviteCode = ''
}
}
function switchLoginMode(mode: 'password' | 'sms') {
loginMode.value = mode
form.code = ''
form.captchaCode = ''
}
async function loadCaptcha() {
captchaLoading.value = true
try {
captcha.value = await fetchAuthCaptcha()
form.captchaCode = ''
} catch (error) {
ElMessage.error(readError(error, '图形验证码加载失败'))
} finally {
captchaLoading.value = false
}
}
async function refreshCaptcha() {
if (captchaLoading.value) return
await loadCaptcha()
}
async function handleSendCode() {
if (!form.phone.trim()) {
ElMessage.warning('请输入手机号')
return
}
if (!/^1[3-9]\d{9}$/.test(form.phone.trim())) {
ElMessage.warning('请输入正确的11位手机号码')
return
}
if (!captcha.value?.captcha_id || !form.captchaCode.trim()) {
ElMessage.warning('请输入图形验证码')
return
}
sending.value = true
try {
await sendSmsCode(form.phone.trim(), captcha.value.captcha_id, form.captchaCode.trim())
ElMessage.success('验证码已发送,请注意查收')
startCountDown()
} catch (error) {
ElMessage.error(readError(error, '验证码发送失败,请重试'))
await loadCaptcha()
} finally {
sending.value = false
}
}
async function handleLogin() {
if (!form.phone.trim()) {
ElMessage.warning('请输入手机号')
return
}
if (!form.code.trim()) {
ElMessage.warning('请输入短信验证码')
return
}
loading.value = true
try {
await session.login(form.phone.trim(), form.code.trim())
ElMessage.success('登录成功')
const redirect = (route.query.redirect as string) || '/'
await router.push(redirect)
} catch (error) {
ElMessage.error(readError(error, '登录失败'))
} finally {
loading.value = false
}
}
async function handlePasswordLogin() {
if (!form.phone.trim()) {
ElMessage.warning('请输入手机号')
return
}
if (!form.password) {
ElMessage.warning('请输入登录密码')
return
}
loading.value = true
try {
await session.loginByPassword(form.phone.trim(), form.password)
ElMessage.success('登录成功')
const redirect = (route.query.redirect as string) || '/'
await router.push(redirect)
} catch (error) {
ElMessage.error(readError(error, '登录失败'))
} finally {
loading.value = false
}
}
async function handleRegister() {
if (!form.phone.trim()) {
ElMessage.warning('请输入手机号')
return
}
if (!/^1[3-9]\d{9}$/.test(form.phone.trim())) {
ElMessage.warning('请输入正确的11位手机号码')
return
}
if (!form.code.trim()) {
ElMessage.warning('请输入短信验证码')
return
}
if (!form.password) {
ElMessage.warning('请设置登录密码')
return
}
if (form.password.length < 8 || form.password.length > 20) {
ElMessage.warning('登录密码长度应为 8-20 位')
return
}
if (!agreed.value) {
ElMessage.warning('请先勾选同意用户协议和隐私政策')
return
}
loading.value = true
try {
await session.register(
form.phone.trim(),
form.code.trim(),
form.password,
form.inviteCode.trim() || undefined
)
ElMessage.success('注册成功')
const redirect = (route.query.redirect as string) || '/'
await router.push(redirect)
} catch (error) {
ElMessage.error(readError(error, '注册失败'))
} finally {
loading.value = false
}
}
function openProtocol(title: string) {
protocolTitle.value = title
if (title === '用户协议') {
protocolContent.value =
'欢迎使用游戏服务平台。用户在使用本平台提供的各项服务前,请认真阅读本协议。用户注册、登录或使用服务即表示完全接受本协议条款。平台致力于为用户提供安全、透明、专业的数字资产租号与游戏服务,严禁任何破坏游戏平衡、恶意盗号、欺诈等违规行为。'
} else if (title === '隐私政策') {
protocolContent.value =
'本平台严格保护用户的个人隐私与信息安全。我们将按照合法、正当、必要的原则,仅收集为您提供服务所必需的信息(包括手机号、实名认证信息等),并通过行业领先的加密技术对数据进行安全存储与传输,绝不会向第三方未经授权披露您的个人信息。'
} else {
protocolContent.value =
'为保障租客与号主的合法权益,平台制定了严格的租号服务规则。租客在租用期间需遵守游戏守则,不得使用任何第三方辅助工具;号主应如实描述账号信息并保证账号安全可用。平台提供全流程智能上号与资金安全结算保障。'
}
protocolDialogVisible.value = true
}
</script>
<template>
<div class="login-page">
<!-- 中间核心卡片 -->
<main class="login-container">
<div class="login-card">
<!-- 左侧品牌背书区(品牌专属深色+金橙色系) -->
<div class="card-hero">
<div class="hero-top">
<div class="hero-brand">
<img
v-if="siteStore.logoUrl"
:src="siteStore.logoUrl"
alt=""
class="brand-logo-img"
/>
<div v-else class="brand-logo-badge">{{ siteStore.brandMark }}</div>
<span class="brand-name">{{ siteStore.siteName }}</span>
</div>
<div class="hero-header">
<h2 class="hero-title">安全租号 <span class="hero-sep">|</span> 畅享游戏</h2>
<p class="hero-desc">
高效、安全的哈夫币账号交易平台,7×24 小时智能订单系统随时为您服务。
</p>
</div>
</div>
<div class="hero-features">
<div class="feature-item">
<div class="feature-icon">
<el-icon><Medal /></el-icon>
</div>
<div class="feature-text">
<strong class="feature-name">官方品质保障</strong>
<span class="feature-sub">严格人工验号,真实账号资产保证</span>
</div>
</div>
<div class="feature-item">
<div class="feature-icon">
<el-icon><Lock /></el-icon>
</div>
<div class="feature-text">
<strong class="feature-name">智能安全上号</strong>
<span class="feature-sub">全程加密登录,号主租客双向守护</span>
</div>
</div>
<div class="feature-item">
<div class="feature-icon">
<el-icon><Wallet /></el-icon>
</div>
<div class="feature-text">
<strong class="feature-name">平台资金担保</strong>
<span class="feature-sub">专款安全托管,到期自动极速结算</span>
</div>
</div>
</div>
<!-- 3D 全息金橙盾牌发光元素 (原生矢量) -->
<div class="hero-shield-art">
<svg
class="shield-svg"
viewBox="0 0 160 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id="shieldGrad"
x1="20"
y1="20"
x2="140"
y2="150"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stop-color="#ffb84d" />
<stop offset="45%" stop-color="#ff6a00" />
<stop offset="100%" stop-color="#b83e00" />
</linearGradient>
<linearGradient
id="shieldRim"
x1="30"
y1="10"
x2="130"
y2="140"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stop-color="#ffd599" />
<stop offset="50%" stop-color="#ff8c26" />
<stop offset="100%" stop-color="#8f2b00" />
</linearGradient>
<linearGradient
id="baseGrad"
x1="20"
y1="130"
x2="140"
y2="155"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stop-color="#ff7a00" stop-opacity="0.6" />
<stop offset="100%" stop-color="#1e293b" stop-opacity="0.1" />
</linearGradient>
<filter id="glowFilter" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="8" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- 底座光晕台 -->
<ellipse cx="80" cy="140" rx="55" ry="14" fill="url(#baseGrad)" />
<ellipse
cx="80"
cy="140"
rx="42"
ry="10"
stroke="#ffaa40"
stroke-width="1.5"
stroke-opacity="0.8"
fill="none"
/>
<ellipse cx="80" cy="140" rx="26" ry="6" fill="#ea580c" fill-opacity="0.7" />
<!-- 外围环境辉光 -->
<path
d="M80 32 L120 48 C120 88 80 120 80 120 C80 120 40 88 40 48 Z"
fill="#ff7a00"
opacity="0.32"
filter="url(#glowFilter)"
/>
<!-- 盾牌外边框 -->
<path
d="M80 28 L124 45 C124 90 80 124 80 124 C80 124 36 90 36 45 Z"
fill="url(#shieldRim)"
/>
<!-- 盾牌主体 -->
<path
d="M80 34 L118 49 C118 86 80 118 80 118 C80 118 42 86 42 49 Z"
fill="url(#shieldGrad)"
/>
<!-- 内部光效反射 -->
<path
d="M80 38 L112 52 C112 70 94 98 80 112 C66 98 48 70 48 52 Z"
fill="#ffffff"
fill-opacity="0.12"
/>
<!-- 发光对勾图标 -->
<path
d="M64 74 L75 86 L98 62"
stroke="#ffffff"
stroke-width="6"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<div class="hero-footer">
<div class="quote-title">安全交易 · 全程留痕</div>
<div class="quote-sub">7×24 小时智能守护,租号更省心</div>
</div>
</div>
<!-- 右侧表单操作区 -->
<div class="card-form-box">
<div class="form-header">
<h1 class="form-title">{{ topTab === 'register' ? '注册账号' : '登录账号' }}</h1>
<div class="form-subtitle">
<template v-if="topTab === 'register'">
<span>已有账号?</span>
<button type="button" class="switch-link" @click="switchTab('login')">
立即登录
</button>
</template>
<template v-else>
<span>没有账号?</span>
<button type="button" class="switch-link" @click="switchTab('register')">
立即注册
</button>
</template>
</div>
</div>
<!-- 登录模式下的方式切换 -->
<div v-if="topTab === 'login'" class="login-sub-modes">
<button
type="button"
:class="{ active: loginMode === 'password' }"
@click="switchLoginMode('password')"
>
密码登录
</button>
<span class="mode-sep">|</span>
<button
type="button"
:class="{ active: loginMode === 'sms' }"
@click="switchLoginMode('sms')"
>
短信登录
</button>
</div>
<!-- 注册表单 -->
<form v-if="topTab === 'register'" class="auth-form" @submit.prevent="handleRegister">
<!-- 手机号 -->
<div class="input-shell">
<el-icon class="field-icon"><Iphone /></el-icon>
<input
v-model="form.phone"
type="tel"
maxlength="11"
placeholder="请输入手机号"
autocomplete="tel"
/>
</div>
<!-- 图形验证码 -->
<div class="input-shell captcha-shell">
<el-icon class="field-icon"><Key /></el-icon>
<input
v-model="form.captchaCode"
type="text"
maxlength="4"
placeholder="请输入图形验证码"
autocomplete="off"
/>
<button
type="button"
class="captcha-img-btn"
:disabled="captchaLoading"
title="点击刷新图形验证码"
@click="refreshCaptcha"
>
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
<el-icon v-else class="is-loading"><Refresh /></el-icon>
</button>
</div>
<!-- 短信验证码 -->
<div class="input-shell sms-shell">
<el-icon class="field-icon"><ChatLineRound /></el-icon>
<input v-model="form.code" type="text" maxlength="6" placeholder="请输入短信验证码" />
<button
type="button"
class="get-code-btn"
:disabled="countDown > 0 || sending"
@click="handleSendCode"
>
{{ countDown > 0 ? `${countDown}s 后获取` : sending ? '发送中...' : '发送验证码' }}
</button>
</div>
<!-- 登录密码 -->
<div class="input-shell">
<el-icon class="field-icon"><Lock /></el-icon>
<input
v-model="form.password"
:type="passwordVisible ? 'text' : 'password'"
maxlength="20"
placeholder="请设置登录密码(8-20位)"
autocomplete="new-password"
/>
<button
type="button"
class="toggle-pwd-btn"
aria-label="显示/隐藏密码"
@click="passwordVisible = !passwordVisible"
>
<el-icon><View v-if="passwordVisible" /><Hide v-else /></el-icon>
</button>
</div>
<!-- 邀请码 -->
<div class="input-shell invite-shell" :class="{ 'is-locked': isInviteLocked }">
<el-icon class="field-icon"><Ticket /></el-icon>
<input
v-model="form.inviteCode"
maxlength="32"
:readonly="isInviteLocked"
:placeholder="isInviteLocked ? '' : '请输入邀请码(选填)'"
/>
<span v-if="isInviteLocked" class="invite-locked-badge">已绑定邀请码</span>
</div>
<!-- 协议同意勾选 -->
<div class="agreement-box">
<el-checkbox v-model="agreed">
我已阅读并同意
<span class="protocol-link" @click.stop.prevent="openProtocol('用户协议')"
>《用户协议》</span
>
与
<span class="protocol-link" @click.stop.prevent="openProtocol('隐私政策')"
>《隐私政策》</span
>
以及
<span class="protocol-link" @click.stop.prevent="openProtocol('租号服务协议')"
>《租号服务协议》</span
>
</el-checkbox>
</div>
<!-- 提交按钮 -->
<button type="submit" class="submit-btn" :disabled="loading">
{{ loading ? '注册中...' : '注册' }}
</button>
</form>
<!-- 密码登录表单 -->
<form
v-else-if="topTab === 'login' && loginMode === 'password'"
class="auth-form"
@submit.prevent="handlePasswordLogin"
>
<div class="input-shell">
<el-icon class="field-icon"><Iphone /></el-icon>
<input
v-model="form.phone"
type="tel"
maxlength="11"
placeholder="请输入手机号"
autocomplete="tel"
/>
</div>
<div class="input-shell">
<el-icon class="field-icon"><Lock /></el-icon>
<input
v-model="form.password"
:type="passwordVisible ? 'text' : 'password'"
maxlength="20"
placeholder="请输入登录密码"
autocomplete="current-password"
/>
<button
type="button"
class="toggle-pwd-btn"
aria-label="显示/隐藏密码"
@click="passwordVisible = !passwordVisible"
>
<el-icon><View v-if="passwordVisible" /><Hide v-else /></el-icon>
</button>
</div>
<button type="submit" class="submit-btn" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
<p class="login-tip">未设置密码?可切换短信验证码登录</p>
</form>
<!-- 短信验证码登录表单 -->
<form
v-else-if="topTab === 'login' && loginMode === 'sms'"
class="auth-form"
@submit.prevent="handleLogin"
>
<div class="input-shell">
<el-icon class="field-icon"><Iphone /></el-icon>
<input
v-model="form.phone"
type="tel"
maxlength="11"
placeholder="请输入手机号"
autocomplete="tel"
/>
</div>
<!-- 图形验证码 -->
<div class="input-shell captcha-shell">
<el-icon class="field-icon"><Key /></el-icon>
<input
v-model="form.captchaCode"
type="text"
maxlength="4"
placeholder="请输入图形验证码"
autocomplete="off"
/>
<button
type="button"
class="captcha-img-btn"
:disabled="captchaLoading"
title="点击刷新图形验证码"
@click="refreshCaptcha"
>
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
<el-icon v-else class="is-loading"><Refresh /></el-icon>
</button>
</div>
<!-- 短信验证码 -->
<div class="input-shell sms-shell">
<el-icon class="field-icon"><ChatLineRound /></el-icon>
<input v-model="form.code" type="text" maxlength="6" placeholder="请输入短信验证码" />
<button
type="button"
class="get-code-btn"
:disabled="countDown > 0 || sending"
@click="handleSendCode"
>
{{ countDown > 0 ? `${countDown}s 后获取` : sending ? '发送中...' : '发送验证码' }}
</button>
</div>
<button type="submit" class="submit-btn" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
</form>
</div>
</div>
</main>
<!-- 底部版权备案信息(与主站一致) -->
<footer class="page-bottom-footer">
<template v-if="siteStore.footerInfo?.icp">
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
class="footer-icp"
>
{{ siteStore.footerInfo.icp }}
</a>
<span class="footer-divider">|</span>
<span class="footer-copyright">
{{ siteStore.footerInfo.copyright || `版权所有 ©2026 ${siteStore.siteName}` }}
</span>
<template v-if="siteStore.footerInfo.contact">
<span class="footer-divider">|</span>
<span class="footer-contact">{{ siteStore.footerInfo.contact }}</span>
</template>
</template>
<template v-else>
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
class="footer-icp"
>
湘ICP备2025146668号
</a>
<span class="footer-divider">|</span>
<span class="footer-copyright">版权所有 ©2026 {{ siteStore.siteName }}</span>
</template>
</footer>
<!-- 服务协议详情弹窗 -->
<el-dialog v-model="protocolDialogVisible" :title="protocolTitle" width="540px" append-to-body>
<div class="protocol-modal-body">
<p>{{ protocolContent }}</p>
</div>
<template #footer>
<el-button type="primary" @click="protocolDialogVisible = false">我已了解</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.login-page {
min-height: 100vh;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
background-color: #f8fafc;
background-image:
radial-gradient(at 10% 10%, rgba(255, 106, 0, 0.05) 0px, transparent 40%),
radial-gradient(at 90% 90%, rgba(255, 106, 0, 0.04) 0px, transparent 40%),
linear-gradient(rgba(226, 232, 240, 0.6) 1px, transparent 1px),
linear-gradient(90deg, rgba(226, 232, 240, 0.6) 1px, transparent 1px);
background-size:
100% 100%,
100% 100%,
36px 36px,
36px 36px;
padding: 32px 20px 20px;
}
/* 核心卡片容器:撑满剩余高度并居中卡片 */
.login-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.login-card {
width: 960px;
max-width: 100%;
min-height: 540px;
display: grid;
grid-template-columns: 440px 1fr;
background: #ffffff;
border-radius: 24px;
box-shadow:
0 24px 64px rgba(15, 23, 42, 0.1),
0 4px 16px rgba(15, 23, 42, 0.04);
overflow: hidden;
}
/* 左侧品牌背书区域(专属深色 + 金橙光辉) */
.card-hero {
position: relative;
overflow: hidden;
padding: 44px 38px;
display: flex;
flex-direction: column;
justify-content: space-between;
background:
radial-gradient(circle at 85% 75%, rgba(255, 106, 0, 0.18) 0%, transparent 55%),
linear-gradient(155deg, #0d121c 0%, #151d2d 45%, #1e293b 100%);
}
.hero-top {
position: relative;
z-index: 2;
}
.hero-brand {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 24px;
}
.brand-logo-badge {
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(135deg, #ff6a00, #ff852e);
color: #ffffff;
font-weight: 900;
font-size: 16px;
display: grid;
place-items: center;
box-shadow: 0 4px 12px rgba(255, 106, 0, 0.35);
}
.brand-logo-img {
width: 34px;
height: 34px;
border-radius: 10px;
object-fit: cover;
}
.brand-name {
font-size: 17px;
font-weight: 800;
color: #ffffff;
letter-spacing: 0.5px;
}
.hero-title {
font-size: 30px;
font-weight: 800;
color: #ffffff;
letter-spacing: 2px;
margin: 0 0 10px;
}
.hero-sep {
color: #ff6a00;
margin: 0 4px;
}
.hero-desc {
font-size: 13px;
color: #94a3b8;
line-height: 1.6;
margin: 0;
max-width: 320px;
}
.hero-features {
display: flex;
flex-direction: column;
gap: 18px;
margin: 24px 0;
position: relative;
z-index: 2;
}
.feature-item {
display: flex;
align-items: center;
gap: 14px;
}
.feature-icon {
width: 38px;
height: 38px;
border-radius: 50%;
border: 1.5px solid rgba(255, 106, 0, 0.4);
background: rgba(255, 106, 0, 0.08);
display: grid;
place-items: center;
color: #ff852e;
font-size: 18px;
flex-shrink: 0;
}
.feature-name {
display: block;
font-size: 15px;
font-weight: 700;
color: #ffffff;
line-height: 1.2;
}
.feature-sub {
display: block;
font-size: 11px;
color: #94a3b8;
margin-top: 3px;
}
/* 3D 悬浮金橙盾牌发光元素 */
.hero-shield-art {
position: absolute;
right: 18px;
bottom: 80px;
width: 150px;
height: 150px;
pointer-events: none;
z-index: 1;
}
.shield-svg {
width: 100%;
height: 100%;
filter: drop-shadow(0 12px 28px rgba(255, 106, 0, 0.4));
}
.hero-footer {
position: relative;
z-index: 2;
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding-top: 16px;
}
.quote-title {
font-size: 13px;
font-weight: 600;
color: #cbd5e1;
letter-spacing: 0.5px;
}
.quote-sub {
font-size: 13px;
color: #ffffff;
margin-top: 4px;
font-weight: 500;
}
/* 右侧表单操作区域 */
.card-form-box {
padding: 44px 44px 36px;
display: flex;
flex-direction: column;
justify-content: center;
}
.form-header {
margin-bottom: 20px;
}
.form-title {
font-size: 24px;
font-weight: 800;
color: #0f172a;
margin: 0 0 8px;
}
.form-subtitle {
font-size: 14px;
color: #64748b;
display: flex;
align-items: center;
gap: 4px;
}
.switch-link {
color: #ff6a00;
font-weight: 700;
border: none;
background: none;
padding: 0;
cursor: pointer;
font-size: 14px;
transition: opacity 0.2s;
}
.switch-link:hover {
opacity: 0.85;
text-decoration: underline;
}
/* 登录子模式切换 */
.login-sub-modes {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 18px;
}
.login-sub-modes button {
border: none;
background: none;
font-size: 14px;
color: #64748b;
cursor: pointer;
padding: 4px 0;
position: relative;
transition: color 0.2s;
}
.login-sub-modes button.active {
color: #ff6a00;
font-weight: 700;
}
.login-sub-modes button.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: #ff6a00;
border-radius: 2px;
}
.mode-sep {
color: #e2e8f0;
}
/* 表单输入控件 Shell */
.auth-form {
display: flex;
flex-direction: column;
}
.input-shell {
display: flex;
align-items: center;
height: 48px;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0 14px;
background: #ffffff;
transition:
border-color 0.2s,
box-shadow 0.2s;
margin-bottom: 14px;
}
.input-shell:focus-within {
border-color: #ff6a00;
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.12);
}
.field-icon {
color: #94a3b8;
font-size: 17px;
margin-right: 10px;
flex-shrink: 0;
}
.input-shell input {
flex: 1;
min-width: 0;
height: 100%;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #0f172a;
}
.input-shell input::placeholder {
color: #94a3b8;
}
.toggle-pwd-btn {
border: none;
background: none;
color: #94a3b8;
cursor: pointer;
font-size: 16px;
padding: 4px;
display: grid;
place-items: center;
}
.toggle-pwd-btn:hover {
color: #64748b;
}
/* 图形验证码行 */
.captcha-shell {
padding-right: 4px;
}
.captcha-img-btn {
width: 108px;
height: 38px;
border: 1px solid #e2e8f0;
border-radius: 6px;
overflow: hidden;
background: #f8fafc;
display: grid;
place-items: center;
cursor: pointer;
padding: 0;
flex-shrink: 0;
transition: opacity 0.15s;
}
.captcha-img-btn:hover {
opacity: 0.9;
}
.captcha-img-btn img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 短信验证码行 */
.sms-shell {
padding-right: 6px;
}
.get-code-btn {
border: 1px solid #fed7aa;
background: #fff7ed;
color: #ff6a00;
font-size: 13px;
font-weight: 600;
padding: 8px 14px;
border-radius: 6px;
cursor: pointer;
white-space: nowrap;
transition:
background 0.15s,
opacity 0.15s;
}
.get-code-btn:hover:not(:disabled) {
background: #ffedd5;
}
.get-code-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
border-color: #e2e8f0;
background: #f8fafc;
color: #94a3b8;
}
/* 邀请码锁定行 */
.invite-shell.is-locked {
background: #f8fafc;
border-color: #e2e8f0;
}
.invite-shell.is-locked input {
font-weight: 700;
color: #0f172a;
letter-spacing: 0.5px;
}
.invite-locked-badge {
font-size: 11px;
font-weight: 700;
color: #059669;
background: #ecfdf5;
border: 1px solid #a7f3d0;
padding: 2px 8px;
border-radius: 4px;
white-space: nowrap;
}
/* 协议同意行 */
.agreement-box {
margin: 2px 0 16px;
font-size: 12px;
}
.agreement-box :deep(.el-checkbox__label) {
font-size: 12px;
color: #64748b;
line-height: 1.5;
}
.agreement-box :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
background-color: #ff6a00;
border-color: #ff6a00;
}
.agreement-box :deep(.el-checkbox__inner:hover) {
border-color: #ff6a00;
}
.protocol-link {
color: #ff6a00;
cursor: pointer;
font-weight: 500;
}
.protocol-link:hover {
text-decoration: underline;
}
/* 提交主按钮(平台品牌金橙色) */
.submit-btn {
width: 100%;
height: 48px;
border-radius: 8px;
border: none;
background: linear-gradient(135deg, #ff6a00 0%, #ff852e 100%);
color: #ffffff;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition:
transform 0.15s,
box-shadow 0.2s,
background 0.2s;
box-shadow: 0 8px 24px rgba(255, 106, 0, 0.28);
letter-spacing: 1px;
}
.submit-btn:hover:not(:disabled) {
background: linear-gradient(135deg, #ff7a1a 0%, #ff9543 100%);
transform: translateY(-1px);
box-shadow: 0 12px 28px rgba(255, 106, 0, 0.35);
}
.submit-btn:active:not(:disabled) {
transform: translateY(0);
}
.submit-btn:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.login-tip {
margin: 14px 0 0;
color: #94a3b8;
font-size: 12px;
text-align: center;
}
/* 底部版权备案(与主站一致,固定在最底部) */
.page-bottom-footer {
width: 100%;
padding: 24px 0 10px;
text-align: center;
font-size: 13px;
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
.footer-icp {
color: #64748b;
text-decoration: none;
transition: color 0.2s;
}
.footer-icp:hover {
color: #ff6a00;
text-decoration: underline;
}
.footer-divider {
color: #cbd5e1;
}
.footer-copyright,
.footer-contact {
color: #64748b;
}
.protocol-modal-body {
font-size: 14px;
line-height: 1.7;
color: #334155;
max-height: 360px;
overflow-y: auto;
}
/* 响应式断点适配 */
@media (max-width: 900px) {
.login-card {
grid-template-columns: 1fr;
width: 440px;
border-radius: 18px;
}
.card-hero {
display: none;
}
.card-form-box {
padding: 36px 28px;
}
}
@media (max-width: 480px) {
.login-card {
border-radius: 14px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
}
.card-form-box {
padding: 28px 20px;
}
}
</style>