981 lines
24 KiB
Vue
981 lines
24 KiB
Vue
<template>
|
||
<div class="login-page">
|
||
<!-- 左侧品牌展示区 -->
|
||
<div :style="bgStyle" class="login-left">
|
||
<div class="left-overlay" />
|
||
<!-- 装饰网格线 -->
|
||
<div class="left-grid" />
|
||
<!-- 浮动装饰圆点 -->
|
||
<div class="dot dot-1" />
|
||
<div class="dot dot-2" />
|
||
<div class="dot dot-3" />
|
||
<div class="left-content">
|
||
<!-- 品牌 logo -->
|
||
<div class="brand">
|
||
<div class="brand-logo-text">决策咨询网</div>
|
||
<div class="brand-site-name">GX Decision Consulting</div>
|
||
</div>
|
||
|
||
<!-- 中央文案 -->
|
||
<div class="hero-text">
|
||
<div class="hero-tag">广西决策咨询中心</div>
|
||
<h1 class="hero-title">汇聚专家智慧<br>服务政府决策</h1>
|
||
<p class="hero-desc">广西决策咨询网是自治区党委政府决策咨询服务的重要平台<br>汇聚各领域专家学者,提供权威决策咨询服务</p>
|
||
<div class="hero-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-num">200+</span>
|
||
<span class="stat-label">认证专家</span>
|
||
</div>
|
||
<div class="stat-divider" />
|
||
<div class="stat-item">
|
||
<span class="stat-num">500+</span>
|
||
<span class="stat-label">会员单位</span>
|
||
</div>
|
||
<div class="stat-divider" />
|
||
<div class="stat-item">
|
||
<span class="stat-num">1000+</span>
|
||
<span class="stat-label">建言献策</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部 -->
|
||
<div class="left-footer">
|
||
<span>© {{ new Date().getFullYear() }} 广西决策咨询中心 保留所有权利</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧登录表单区 -->
|
||
<div class="login-right">
|
||
<div class="form-wrapper">
|
||
<!-- 移动端 logo(仅小屏显示) -->
|
||
<div class="mobile-brand">
|
||
<img :src="config?.sysLogo || defaultLogo" alt="logo" class="mobile-logo" />
|
||
<span class="mobile-brand-name">{{ config?.siteName || '广西决策咨询网' }}</span>
|
||
</div>
|
||
|
||
<!-- 登录卡片头部 -->
|
||
<div class="form-header">
|
||
<h2 class="form-title">{{ '欢迎回来' }}</h2>
|
||
<p class="form-subtitle">{{ '请登录您的账号以继续' }}</p>
|
||
</div>
|
||
|
||
<!-- 切换标签 -->
|
||
<div class="login-tabs">
|
||
<button
|
||
:class="{ active: loginType === 'scan' }"
|
||
class="login-tab"
|
||
@click="setLoginType('scan')"
|
||
>
|
||
<QrcodeOutlined />
|
||
{{ '扫码登录' }}
|
||
</button>
|
||
<button
|
||
:class="{ active: loginType === 'sms' }"
|
||
class="login-tab"
|
||
@click="setLoginType('sms')"
|
||
>
|
||
<MobileOutlined />
|
||
{{ '手机号登录' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 表单区域 -->
|
||
<a-form ref="formRef" :model="form" :rules="rules" class="login-form">
|
||
<!-- 手机号登录 -->
|
||
<template v-if="loginType === 'sms'">
|
||
<a-form-item name="phone">
|
||
<a-input
|
||
v-model:value="form.phone"
|
||
:maxlength="11"
|
||
:placeholder="'请输入手机号码'"
|
||
allow-clear
|
||
class="form-input"
|
||
size="large"
|
||
>
|
||
<template #prefix>
|
||
<span class="phone-prefix">+86</span>
|
||
<span class="phone-prefix-divider" />
|
||
</template>
|
||
</a-input>
|
||
</a-form-item>
|
||
|
||
<a-form-item name="smsCode">
|
||
<div class="captcha-row">
|
||
<a-input
|
||
v-model:value="form.smsCode"
|
||
:maxlength="6"
|
||
:placeholder="'请输入验证码'"
|
||
allow-clear
|
||
class="form-input"
|
||
size="large"
|
||
@press-enter="submitSms"
|
||
/>
|
||
<button
|
||
:class="{ disabled: countdown > 0 }"
|
||
:disabled="countdown > 0"
|
||
class="sms-btn"
|
||
@click.prevent="openImgCodeModal"
|
||
>
|
||
<span v-if="countdown <= 0">{{ '发送验证码' }}</span>
|
||
<span v-else>{{ countdown }}{{ 's 后重发' }}</span>
|
||
</button>
|
||
</div>
|
||
</a-form-item>
|
||
|
||
<!-- 注册协议和隐私政策 -->
|
||
<div class="agreement-row">
|
||
<a-checkbox v-model:checked="form.agreement">
|
||
<span class="agreement-text">
|
||
{{ '我已阅读并同意' }}
|
||
<NuxtLink class="agreement-link" target="_blank" to="/agreement" @click.stop>{{ '《注册协议》' }}</NuxtLink>
|
||
{{ '和' || '和' }}
|
||
<NuxtLink class="agreement-link" target="_blank" to="/privacy" @click.stop>{{ '《隐私政策》' }}</NuxtLink>
|
||
</span>
|
||
</a-checkbox>
|
||
</div>
|
||
|
||
<a-form-item>
|
||
<a-button
|
||
:loading="loading"
|
||
block
|
||
class="submit-btn"
|
||
size="large"
|
||
type="primary"
|
||
@click="submitSms"
|
||
>
|
||
{{ loading ? '登录中…' : '立即登录' }}
|
||
</a-button>
|
||
</a-form-item>
|
||
</template>
|
||
|
||
<!-- 扫码登录 -->
|
||
<template v-else>
|
||
<QrLogin @login-success="onQrLoginSuccess" @login-error="onQrLoginError" />
|
||
</template>
|
||
</a-form>
|
||
|
||
<!-- 底部切换扫码 -->
|
||
<div class="form-footer">
|
||
<button class="switch-scan-btn" @click="toggleScan">
|
||
<QrcodeOutlined v-if="loginType !== 'scan'" />
|
||
<MobileOutlined v-else />
|
||
{{ loginType === 'scan' ? '切换到手机号登录' : '切换到扫码登录' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图形验证码弹窗(发送短信用) -->
|
||
<a-modal v-model:open="imgCodeModalOpen" :footer="null" :title="'安全验证'" :width="360">
|
||
<p class="modal-tip">{{ '请先完成图形验证码验证' }}</p>
|
||
<div class="captcha-row modal-captcha">
|
||
<a-input
|
||
v-model:value="imgCode"
|
||
:maxlength="5"
|
||
:placeholder="'请输入图形验证码'"
|
||
allow-clear
|
||
size="large"
|
||
@press-enter="sendSmsCode"
|
||
/>
|
||
<button :title="'点击刷新'" class="captcha-img-btn" @click.prevent="changeCaptcha">
|
||
<img :src="captcha" alt="captcha" />
|
||
</button>
|
||
</div>
|
||
<a-button :loading="sendingSms" block class="submit-btn" size="large" type="primary" @click="sendSmsCode">
|
||
{{ '发送验证码' }}
|
||
</a-button>
|
||
</a-modal>
|
||
|
||
<!-- 选择账号弹窗 -->
|
||
<a-modal v-model:open="selectUserOpen" :footer="null" :title="'选择账号登录'" :width="520">
|
||
<a-list :data-source="admins" item-layout="horizontal">
|
||
<template #renderItem="{ item }">
|
||
<a-list-item class="list-item" @click="selectUser(item)">
|
||
<a-list-item-meta :description="`${'租户ID'}: ${item.tenantId}`">
|
||
<template #title>{{ item.tenantName || item.username }}</template>
|
||
<template #avatar>
|
||
<a-avatar :src="item.avatar" />
|
||
</template>
|
||
</a-list-item-meta>
|
||
<template #actions><RightOutlined /></template>
|
||
</a-list-item>
|
||
</template>
|
||
</a-list>
|
||
</a-modal>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||
import { message, type FormInstance } from 'ant-design-vue'
|
||
import {
|
||
MobileOutlined,
|
||
QrcodeOutlined,
|
||
RightOutlined
|
||
} from '@ant-design/icons-vue'
|
||
import QrLogin from '@/components/QrLogin.vue'
|
||
import { configWebsiteField, type Config } from '@/api/cms/cmsWebsiteField'
|
||
import { loginBySms, sendSmsCaptcha, getCaptcha } from '@/api/passport/login'
|
||
import type { LoginParam } from '@/api/passport/login/model'
|
||
import { listAdminsByPhoneAll } from '@/api/system/user'
|
||
import type { User } from '@/api/system/user/model'
|
||
import { getUserInfo } from '@/api/layout'
|
||
import { TEMPLATE_ID } from '@/config/setting'
|
||
import { setToken } from '@/utils/token-util'
|
||
import type { QrCodeStatusResponse } from '@/api/passport/qrLogin'
|
||
definePageMeta({ layout: false })
|
||
|
||
const route = useRoute()
|
||
|
||
const defaultLogo = 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg'
|
||
const config = ref<Config>()
|
||
const loading = ref(false)
|
||
const loginType = ref<'scan' | 'sms'>('scan')
|
||
|
||
const captcha = ref('')
|
||
const captchaText = ref('')
|
||
|
||
const imgCodeModalOpen = ref(false)
|
||
const imgCode = ref('')
|
||
const sendingSms = ref(false)
|
||
|
||
const countdown = ref(0)
|
||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||
|
||
const selectUserOpen = ref(false)
|
||
const admins = ref<User[]>([])
|
||
|
||
const formRef = ref<FormInstance>()
|
||
const form = reactive<LoginParam & { smsCode?: string; agreement?: boolean }>({
|
||
phone: '',
|
||
smsCode: '',
|
||
remember: true,
|
||
agreement: false
|
||
})
|
||
|
||
const phoneReg = /^1[3-9]\d{9}$/
|
||
const rules = reactive({
|
||
phone: [
|
||
{ required: true, message: '请输入手机号码', type: 'string' },
|
||
{ pattern: phoneReg, message: '手机号格式不正确', trigger: 'blur' }
|
||
],
|
||
smsCode: [{ required: true, message: '请输入短信验证码', type: 'string' }]
|
||
})
|
||
|
||
const bgStyle = computed(() => {
|
||
const bg = config.value?.loginBgImg
|
||
if (!bg) return {}
|
||
return { backgroundImage: `url(${bg})` }
|
||
})
|
||
|
||
function setLoginType(type: 'scan' | 'sms') {
|
||
loginType.value = type
|
||
}
|
||
|
||
function toggleScan() {
|
||
loginType.value = loginType.value === 'scan' ? 'sms' : 'scan'
|
||
}
|
||
|
||
function stopCountdown() {
|
||
if (countdownTimer) clearInterval(countdownTimer)
|
||
countdownTimer = null
|
||
countdown.value = 0
|
||
}
|
||
|
||
async function changeCaptcha() {
|
||
try {
|
||
const data = await getCaptcha()
|
||
captcha.value = data.base64
|
||
captchaText.value = data.text
|
||
} catch (e: unknown) {
|
||
message.error(e instanceof Error ? e.message : '登录失败')
|
||
}
|
||
}
|
||
|
||
function openImgCodeModal() {
|
||
if (!form.phone) return message.error('请输入手机号码')
|
||
imgCode.value = ''
|
||
changeCaptcha()
|
||
imgCodeModalOpen.value = true
|
||
}
|
||
|
||
async function sendSmsCode() {
|
||
if (!imgCode.value) return message.error('请输入图形验证码')
|
||
if (captchaText.value && imgCode.value.toLowerCase() !== captchaText.value.toLowerCase()) {
|
||
return message.error('图形验证码不正确')
|
||
}
|
||
|
||
sendingSms.value = true
|
||
try {
|
||
await sendSmsCaptcha({ phone: form.phone })
|
||
message.success('短信验证码发送成功,请注意查收')
|
||
imgCodeModalOpen.value = false
|
||
countdown.value = 30
|
||
stopCountdown()
|
||
countdown.value = 30
|
||
countdownTimer = setInterval(() => {
|
||
countdown.value -= 1
|
||
if (countdown.value <= 0) stopCountdown()
|
||
}, 1000)
|
||
} catch (e: unknown) {
|
||
message.error(e instanceof Error ? e.message : '发送失败')
|
||
} finally {
|
||
sendingSms.value = false
|
||
}
|
||
}
|
||
|
||
async function goAfterLogin() {
|
||
const from = typeof route.query.from === 'string' ? route.query.from : ''
|
||
await navigateTo(from || '/')
|
||
}
|
||
|
||
function persistUserId(userId: unknown) {
|
||
if (!import.meta.client) return
|
||
const n = typeof userId === 'number' ? userId : Number(userId)
|
||
if (Number.isFinite(n) && n > 0) localStorage.setItem('UserId', String(n))
|
||
}
|
||
|
||
async function ensureUserIdPersisted(seed?: unknown) {
|
||
if (!import.meta.client) return
|
||
persistUserId(seed)
|
||
try {
|
||
if (localStorage.getItem('UserId')) return
|
||
} catch {
|
||
// ignore
|
||
}
|
||
|
||
try {
|
||
const me = await getUserInfo()
|
||
persistUserId(me.userId)
|
||
} catch {
|
||
// ignore (don't block login redirect)
|
||
}
|
||
}
|
||
|
||
async function submitSms() {
|
||
if (!formRef.value) return
|
||
// 校验协议勾选
|
||
if (!form.agreement) {
|
||
return message.error('请先阅读并同意《注册协议》和《隐私政策》')
|
||
}
|
||
loading.value = true
|
||
try {
|
||
await formRef.value.validate()
|
||
const msg = await loginBySms({
|
||
phone: form.phone,
|
||
code: String(form.smsCode || '').toLowerCase(),
|
||
tenantId: form.tenantId,
|
||
remember: !!form.remember
|
||
})
|
||
|
||
if (msg === '请选择登录用户') {
|
||
selectUserOpen.value = true
|
||
admins.value = await listAdminsByPhoneAll({
|
||
phone: form.phone,
|
||
templateId: Number(TEMPLATE_ID)
|
||
})
|
||
return
|
||
}
|
||
|
||
message.success(msg || '登录成功')
|
||
await ensureUserIdPersisted()
|
||
await goAfterLogin()
|
||
} catch (e: unknown) {
|
||
message.error(e instanceof Error ? e.message : '登录失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function selectUser(user: User) {
|
||
form.tenantId = user.tenantId
|
||
selectUserOpen.value = false
|
||
await submitSms()
|
||
}
|
||
|
||
async function onQrLoginSuccess(payload: QrCodeStatusResponse) {
|
||
const accessToken = payload.accessToken || payload.access_token
|
||
if (accessToken) setToken(String(accessToken), true)
|
||
if (payload.tenantId && import.meta.client) localStorage.setItem('TenantId', String(payload.tenantId))
|
||
const seedUserId =
|
||
typeof payload.userInfo === 'object' && payload.userInfo && 'userId' in payload.userInfo
|
||
? (payload.userInfo as { userId?: unknown }).userId
|
||
: undefined
|
||
await ensureUserIdPersisted(seedUserId)
|
||
message.success('扫码登录成功')
|
||
await goAfterLogin()
|
||
}
|
||
|
||
function onQrLoginError(error: string) {
|
||
message.error(error || '扫码登录失败')
|
||
}
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
config.value = await configWebsiteField({ lang: 'zh-CN' })
|
||
} catch {
|
||
// ignore config errors
|
||
}
|
||
changeCaptcha()
|
||
|
||
if (typeof route.query.loginPhone === 'string') {
|
||
form.phone = route.query.loginPhone
|
||
loginType.value = 'sms'
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
stopCountdown()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ===== 整体布局 ===== */
|
||
.login-page {
|
||
display: flex;
|
||
min-height: 100vh;
|
||
background: #f7f8fa;
|
||
}
|
||
|
||
/* ===== 左侧品牌区 ===== */
|
||
.login-left {
|
||
flex: 1;
|
||
position: relative;
|
||
background: linear-gradient(150deg, #0d1b2a 0%, #1e3a5f 30%, #2563eb 60%, #1e3a5f 100%);
|
||
background-size: cover;
|
||
background-position: center;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.left-overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(150deg, rgba(13, 27, 42, 0.75) 0%, rgba(30, 58, 95, 0.55) 100%);
|
||
}
|
||
|
||
/* 装饰网格 */
|
||
.left-grid {
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px);
|
||
background-size: 48px 48px;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 浮动装饰圆点 */
|
||
.dot {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
pointer-events: none;
|
||
}
|
||
.dot-1 {
|
||
width: 420px;
|
||
height: 420px;
|
||
background: radial-gradient(circle, rgba(37, 99, 235, 0.22) 0%, transparent 65%);
|
||
top: -120px;
|
||
right: -80px;
|
||
}
|
||
.dot-2 {
|
||
width: 320px;
|
||
height: 320px;
|
||
background: radial-gradient(circle, rgba(99, 179, 237, 0.18) 0%, transparent 65%);
|
||
bottom: 60px;
|
||
left: -60px;
|
||
}
|
||
.dot-3 {
|
||
width: 180px;
|
||
height: 180px;
|
||
background: radial-gradient(circle, rgba(249, 115, 22, 0.14) 0%, transparent 65%);
|
||
top: 45%;
|
||
left: 55%;
|
||
}
|
||
|
||
/* 光晕大圆 */
|
||
.login-left::before {
|
||
content: '';
|
||
position: absolute;
|
||
width: 560px;
|
||
height: 560px;
|
||
border-radius: 50%;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
pointer-events: none;
|
||
}
|
||
.login-left::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 380px;
|
||
height: 380px;
|
||
border-radius: 50%;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.left-content {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-height: 100vh;
|
||
padding: 48px 56px;
|
||
}
|
||
|
||
/* 品牌区 */
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
}
|
||
/* 品牌 logo 图片 */
|
||
.brand-logo-img {
|
||
height: 22px;
|
||
width: auto;
|
||
object-fit: contain;
|
||
}
|
||
/* 品牌文字 logo */
|
||
.brand-logo-text {
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
letter-spacing: 0.04em;
|
||
text-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||
}
|
||
/* site-name 渐变文字,与导航栏保持一致 */
|
||
.brand-site-name {
|
||
color: #fff;
|
||
font-family: 'Alimama FangYuanTi VF,sans-serif', sans-serif;
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
letter-spacing: 0.04em;
|
||
white-space: nowrap;
|
||
line-height: 1;
|
||
background: linear-gradient(135deg, #ffffff 0%, #93c5fd 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
/* Hero 文案 */
|
||
.hero-text {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding: 48px 0 36px;
|
||
}
|
||
|
||
.hero-tag {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 5px 14px;
|
||
border-radius: 20px;
|
||
background: rgba(37, 99, 235, 0.2);
|
||
border: 1px solid rgba(37, 99, 235, 0.35);
|
||
color: #93c5fd;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 24px;
|
||
width: fit-content;
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: 46px;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
line-height: 1.18;
|
||
margin-bottom: 22px;
|
||
letter-spacing: -1px;
|
||
white-space: pre-line;
|
||
}
|
||
.hero-desc {
|
||
font-size: 15px;
|
||
color: rgba(255, 255, 255, 0.55);
|
||
margin-bottom: 44px;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
/* 统计数字 */
|
||
.hero-stats {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 14px;
|
||
padding: 20px 28px;
|
||
backdrop-filter: blur(8px);
|
||
width: fit-content;
|
||
}
|
||
.stat-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 0 28px;
|
||
}
|
||
.stat-item:first-child {
|
||
padding-left: 0;
|
||
}
|
||
.stat-item:last-child {
|
||
padding-right: 0;
|
||
}
|
||
.stat-num {
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
letter-spacing: -0.5px;
|
||
line-height: 1;
|
||
}
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, 0.45);
|
||
}
|
||
.stat-divider {
|
||
width: 1px;
|
||
height: 36px;
|
||
background: rgba(255, 255, 255, 0.12);
|
||
}
|
||
|
||
.left-footer {
|
||
color: rgba(255, 255, 255, 0.25);
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* ===== 右侧表单区 ===== */
|
||
.login-right {
|
||
width: 65%;
|
||
min-width: 380px;
|
||
background: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 64px 48px;
|
||
box-shadow: -1px 0 0 0 #f0f0f0;
|
||
}
|
||
|
||
.form-wrapper {
|
||
width: 100%;
|
||
max-width: 400px;
|
||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08);
|
||
padding: 40px;
|
||
}
|
||
|
||
/* 移动端品牌 */
|
||
.mobile-brand {
|
||
display: none;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 40px;
|
||
}
|
||
.mobile-logo {
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 8px;
|
||
}
|
||
.mobile-brand-name {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: #111;
|
||
}
|
||
|
||
/* 表单头部 */
|
||
.form-header {
|
||
margin-bottom: 36px;
|
||
}
|
||
.form-title {
|
||
font-size: 28px;
|
||
font-weight: 800;
|
||
color: #0d0d0d;
|
||
margin: 0 0 10px;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
.form-subtitle {
|
||
font-size: 14px;
|
||
color: #8c8c8c;
|
||
margin: 0;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 切换 Tab */
|
||
.login-tabs {
|
||
display: flex;
|
||
gap: 0;
|
||
margin-bottom: 32px;
|
||
border-bottom: 1.5px solid #f0f0f0;
|
||
}
|
||
.login-tab {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding: 10px 0 12px;
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
background: transparent;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #a0a0a0;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
outline: none;
|
||
margin-bottom: -1.5px;
|
||
}
|
||
.login-tab.active {
|
||
color: #0d0d0d;
|
||
border-bottom-color: #6366f1;
|
||
font-weight: 600;
|
||
}
|
||
.login-tab:hover:not(.active) {
|
||
color: #555;
|
||
}
|
||
|
||
/* 表单输入框 */
|
||
.login-form :deep(.ant-input-affix-wrapper),
|
||
.login-form :deep(.ant-input),
|
||
.login-form :deep(.ant-input-password) {
|
||
border-radius: 10px;
|
||
border-color: #ebebeb;
|
||
background: #fafafa;
|
||
transition: all 0.2s;
|
||
padding-top: 6px;
|
||
padding-bottom: 6px;
|
||
align-items: center;
|
||
}
|
||
.login-form :deep(.ant-input-affix-wrapper:focus),
|
||
.login-form :deep(.ant-input-affix-wrapper-focused) {
|
||
border-color: #2563eb;
|
||
background: #fff;
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||
}
|
||
.login-form :deep(.ant-form-item) {
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.input-icon {
|
||
color: #bfbfbf;
|
||
font-size: 15px;
|
||
}
|
||
|
||
/* 验证码行 */
|
||
.captcha-row {
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
.captcha-row :deep(.ant-input-affix-wrapper),
|
||
.captcha-row :deep(.ant-input) {
|
||
flex: 1;
|
||
min-width: 0;
|
||
border-radius: 10px !important;
|
||
}
|
||
|
||
/* 图形验证码按钮 */
|
||
.captcha-img-btn {
|
||
width: 148px;
|
||
height: 48px;
|
||
flex-shrink: 0;
|
||
border: 1px solid #ebebeb;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
background: #fafafa;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: border-color 0.2s;
|
||
}
|
||
.captcha-img-btn:hover {
|
||
border-color: #6366f1;
|
||
}
|
||
.captcha-img-btn img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
.captcha-placeholder {
|
||
font-size: 12px;
|
||
color: #bfbfbf;
|
||
}
|
||
|
||
/* 发送验证码按钮 */
|
||
.sms-btn {
|
||
flex-shrink: 0;
|
||
padding: 0 18px;
|
||
height: 48px;
|
||
border: 1px solid #2563eb;
|
||
border-radius: 10px;
|
||
background: transparent;
|
||
color: #2563eb;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: all 0.2s;
|
||
outline: none;
|
||
}
|
||
.sms-btn:hover:not(.disabled) {
|
||
background: #2563eb;
|
||
color: #fff;
|
||
}
|
||
.sms-btn.disabled {
|
||
border-color: #e0e0e0;
|
||
color: #bfbfbf;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 手机号前缀 +86(prefix 模式) */
|
||
.phone-prefix {
|
||
font-size: 14px;
|
||
color: #555;
|
||
font-weight: 500;
|
||
margin-right: 2px;
|
||
}
|
||
.phone-prefix-divider {
|
||
display: inline-block;
|
||
width: 1px;
|
||
height: 14px;
|
||
background: #d9d9d9;
|
||
margin-left: 8px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* 记住登录 */
|
||
.form-options {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 24px;
|
||
margin-top: -4px;
|
||
}
|
||
|
||
/* 提交按钮 */
|
||
.submit-btn.ant-btn-primary {
|
||
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%) !important;
|
||
border: none !important;
|
||
border-radius: 10px !important;
|
||
height: 48px !important;
|
||
font-size: 15px !important;
|
||
font-weight: 600 !important;
|
||
letter-spacing: 0.3px !important;
|
||
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.28) !important;
|
||
transition: all 0.22s !important;
|
||
}
|
||
.submit-btn.ant-btn-primary:hover:not(:disabled) {
|
||
transform: translateY(-1px) !important;
|
||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.38) !important;
|
||
}
|
||
|
||
/* 扫码切换 */
|
||
.form-footer {
|
||
margin-top: 28px;
|
||
text-align: center;
|
||
}
|
||
.switch-scan-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 18px;
|
||
border: 1px solid #ebebeb;
|
||
border-radius: 20px;
|
||
background: transparent;
|
||
color: #8c8c8c;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
outline: none;
|
||
}
|
||
.switch-scan-btn:hover {
|
||
border-color: #2563eb;
|
||
color: #2563eb;
|
||
background: rgba(37, 99, 235, 0.04);
|
||
}
|
||
|
||
/* 弹窗 */
|
||
.modal-tip {
|
||
font-size: 13px;
|
||
color: #8c8c8c;
|
||
margin-bottom: 16px;
|
||
}
|
||
.modal-captcha {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 账号列表 */
|
||
.list-item {
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
}
|
||
.list-item:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
/* 注册协议和隐私政策勾选 */
|
||
.agreement-row {
|
||
margin-bottom: 20px;
|
||
line-height: 1.6;
|
||
}
|
||
.agreement-row :deep(.ant-checkbox-wrapper) {
|
||
font-size: 13px;
|
||
color: #666;
|
||
align-items: flex-start;
|
||
}
|
||
.agreement-row :deep(.ant-checkbox) {
|
||
margin-top: 2px;
|
||
}
|
||
.agreement-text {
|
||
font-size: 13px;
|
||
color: #666;
|
||
line-height: 1.6;
|
||
}
|
||
.agreement-link {
|
||
color: #6366f1;
|
||
text-decoration: none;
|
||
font-size: 13px;
|
||
}
|
||
.agreement-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* ===== 响应式 ===== */
|
||
@media (max-width: 900px) {
|
||
.login-left {
|
||
display: none;
|
||
}
|
||
.login-right {
|
||
width: 100%;
|
||
min-width: unset;
|
||
padding: 48px 32px;
|
||
min-height: 100vh;
|
||
box-shadow: none;
|
||
}
|
||
.mobile-brand {
|
||
display: flex;
|
||
}
|
||
.form-wrapper {
|
||
max-width: 400px;
|
||
margin: 0 auto;
|
||
}
|
||
}
|
||
</style>
|