Files
jczxw-pc/app/pages/login.vue
赵忠林 b334ad75cd feat(contact): 重构“联系我们”页面及表单交互体验
- 重新设计页面布局,增加顶部横幅及联系方式版块
- 优化在线咨询表单,增加字段并调整样式和验证规则
- 替换旧的提交逻辑为新异步模拟提交,提示更友好
- 移除旧二维码和联系卡片,增加温馨提示信息
- 添加详细样式,提升页面视觉效果和响应式布局
- 更新会员服务联系信息区,增加按钮链接和服务时间
- 会员服务列表使用模拟数据,支持按类型过滤展示
- 登录页左侧风格调整,突出广西决策咨询中心品牌形象
- 修改数值统计及底部版权声明,更新背景渐变颜色和按钮样式
2026-04-26 01:46:24 +08:00

983 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-page">
<!-- 左侧品牌展示区 -->
<div class="login-left" :style="bgStyle">
<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" class="mobile-logo" alt="logo" />
<span class="mobile-brand-name">{{ config?.siteName || $t('login.aiAppPlatform') }}</span>
</div>
<!-- 登录卡片头部 -->
<div class="form-header">
<h2 class="form-title">{{ $t('login.welcomeBack') }}</h2>
<p class="form-subtitle">{{ $t('login.loginToContinue') }}</p>
</div>
<!-- 切换标签 -->
<div class="login-tabs">
<button
class="login-tab"
:class="{ active: loginType === 'scan' }"
@click="setLoginType('scan')"
>
<QrcodeOutlined />
{{ $t('login.scanLogin') }}
</button>
<button
class="login-tab"
:class="{ active: loginType === 'sms' }"
@click="setLoginType('sms')"
>
<MobileOutlined />
{{ $t('login.phoneLogin') }}
</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"
size="large"
allow-clear
:maxlength="11"
:placeholder="$t('login.phonePlaceholder')"
class="form-input"
>
<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"
size="large"
allow-clear
:maxlength="6"
:placeholder="$t('login.smsCodePlaceholder')"
class="form-input"
@press-enter="submitSms"
/>
<button
class="sms-btn"
:disabled="countdown > 0"
:class="{ disabled: countdown > 0 }"
@click.prevent="openImgCodeModal"
>
<span v-if="countdown <= 0">{{ $t('login.sendCode') }}</span>
<span v-else>{{ countdown }}{{ $t('login.resendAfter') }}</span>
</button>
</div>
</a-form-item>
<!-- 注册协议和隐私政策 -->
<div class="agreement-row">
<a-checkbox v-model:checked="form.agreement">
<span class="agreement-text">
{{ $t('login.agreeTerms') }}
<NuxtLink to="/agreement" target="_blank" class="agreement-link" @click.stop>{{ $t('login.registerAgreement') }}</NuxtLink>
{{ $t('common.and') || '' }}
<NuxtLink to="/privacy" target="_blank" class="agreement-link" @click.stop>{{ $t('login.privacyPolicy') }}</NuxtLink>
</span>
</a-checkbox>
</div>
<a-form-item>
<a-button
block
size="large"
type="primary"
:loading="loading"
class="submit-btn"
@click="submitSms"
>
{{ loading ? $t('login.loggingIn') : $t('login.loginNow') }}
</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' ? $t('login.switchToPhone') : $t('login.switchToScan') }}
</button>
</div>
</div>
</div>
<!-- 图形验证码弹窗发送短信用 -->
<a-modal v-model:open="imgCodeModalOpen" :width="360" :footer="null" :title="$t('login.securityVerify')">
<p class="modal-tip">{{ $t('login.completeVerifyFirst') }}</p>
<div class="captcha-row modal-captcha">
<a-input
v-model:value="imgCode"
size="large"
allow-clear
:maxlength="5"
:placeholder="$t('login.enterImgCode')"
@press-enter="sendSmsCode"
/>
<button class="captcha-img-btn" @click.prevent="changeCaptcha" :title="$t('login.clickRefresh')">
<img alt="captcha" :src="captcha" />
</button>
</div>
<a-button block size="large" type="primary" :loading="sendingSms" class="submit-btn" @click="sendSmsCode">
{{ $t('login.sendCode') }}
</a-button>
</a-modal>
<!-- 选择账号弹窗 -->
<a-modal v-model:open="selectUserOpen" :width="520" :footer="null" :title="$t('login.selectAccount')">
<a-list item-layout="horizontal" :data-source="admins">
<template #renderItem="{ item }">
<a-list-item class="list-item" @click="selectUser(item)">
<a-list-item-meta :description="`${$t('login.tenantId')}: ${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 setup lang="ts">
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'
const { t } = useI18n()
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: t('login.enterPhone'), type: 'string' },
{ pattern: phoneReg, message: t('login.phoneFormatError'), trigger: 'blur' }
],
smsCode: [{ required: true, message: t('login.enterSmsCode'), 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 : t('login.loginFailed'))
}
}
function openImgCodeModal() {
if (!form.phone) return message.error(t('login.enterPhone'))
imgCode.value = ''
changeCaptcha()
imgCodeModalOpen.value = true
}
async function sendSmsCode() {
if (!imgCode.value) return message.error(t('login.enterImgCode'))
if (captchaText.value && imgCode.value.toLowerCase() !== captchaText.value.toLowerCase()) {
return message.error(t('login.imgCodeIncorrect'))
}
sendingSms.value = true
try {
await sendSmsCaptcha({ phone: form.phone })
message.success(t('login.smsSentSuccess'))
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 : t('login.sendFailed'))
} 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(t('login.agreeRequired'))
}
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 || t('login.loginSuccess'))
await ensureUserIdPersisted()
await goAfterLogin()
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : t('login.loginFailed'))
} 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(t('login.scanLoginSuccess'))
await goAfterLogin()
}
function onQrLoginError(error: string) {
message.error(error || t('login.scanLoginFailed'))
}
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(167, 139, 250, 0.18) 0%, transparent 65%);
bottom: 60px;
left: -60px;
}
.dot-3 {
width: 180px;
height: 180px;
background: radial-gradient(circle, rgba(236, 72, 153, 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%, #a5c8ff 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(99, 102, 241, 0.2);
border: 1px solid rgba(99, 102, 241, 0.35);
color: #a5b4fc;
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: #6366f1;
background: #fff;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 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 #6366f1;
border-radius: 10px;
background: transparent;
color: #6366f1;
font-size: 13px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
outline: none;
}
.sms-btn:hover:not(.disabled) {
background: #6366f1;
color: #fff;
}
.sms-btn.disabled {
border-color: #e0e0e0;
color: #bfbfbf;
cursor: not-allowed;
}
/* 手机号前缀 +86prefix 模式) */
.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: #6366f1;
color: #6366f1;
background: rgba(99, 102, 241, 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>