Files
guoju0808/ry-vue3/src/views/auth/Login.vue
T
郭庆泰andClaude e59b512e4b fix(person): 子账号继承主账号 role_type + Login 不再硬编 leader
问题: sponsor 主账号新建的子账号 role_type 没写入,
      被 sys_user.role_type DEFAULT 'executor' 覆盖,
      导致子账号进入 executor 视角;另外 Login.vue
      autoRole/短信登录都硬编 'leader' 兜底, getInfo
      失败时静默跳 /leader/home。

修复:
- BizPersonServiceImpl.insert 读主账号 SysUser,
  把 mainUser.getRoleType() 写到新子账号
  (SysUserMapper.xml insertUser 已支持 role_type 列)
- Login.vue: 短信登录 fallbackRole 改 '',
  autoRole 不匹配返回 '', afterLogin 无 role 跳 /login
  (不再 fallback /leader/home)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-18 21:35:51 +08:00

986 lines
29 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">
<!-- 顶部导航 -->
<header class="header">
<a class="logo" @click.prevent="$router.push('/portal/home')">
<div class="logo-icon"></div>
<div class="logo-text">
<span class="logo-title">北京整合医学学会</span>
<span class="logo-subtitle">Beijing Association of Holistic Integrative Medicine</span>
</div>
</a>
</header>
<!-- 主体 -->
<main class="main-content">
<!-- 左侧 banner -->
<div class="banner-section">
<div class="banner-inner">
<span class="banner-tag">2025-2030</span>
<h2 class="banner-title">协同创新 共建共享<br>整合医学发展新格局</h2>
<div class="banner-divider"></div>
<p class="banner-desc">
聚焦医学整合创新系统推进七大专项计划<br>
全面构建覆盖诊疗科研人才管理公益<br>
政学协作与组织建设的协同发展体系
</p>
</div>
</div>
<!-- 右侧登录卡片 -->
<section class="login-section">
<div class="login-card">
<h1 class="login-title">{{ loginMode === 'password' ? '账号登录' : '手机号登录' }}</h1>
<p class="login-subtitle">{{ loginMode === 'password' ? '请输入您的账号信息' : '请输入手机号接收验证码' }}</p>
<div class="login-tabs">
<span class="tab" :class="{ active: loginMode === 'password' }" @click="switchMode('password')">账号密码登录</span>
<span class="tab" :class="{ active: loginMode === 'sms' }" @click="switchMode('sms')">手机验证码登录</span>
</div>
<form v-if="loginMode === 'password'" id="loginForm" @submit.prevent="onSubmit">
<div class="form-group">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<input v-model="form.username" type="text" id="username" class="form-input" placeholder="用户名 / 手机号" autocomplete="username">
</div>
<div class="form-group">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<input v-model="form.password" type="password" id="password" class="form-input" placeholder="密码" autocomplete="current-password">
</div>
<div v-if="captchaEnabled" class="captcha-group">
<div class="form-group" style="margin-bottom: 0;">
<input v-model="form.code" type="text" id="captcha" class="form-input captcha-input" placeholder="验证码" maxlength="6">
</div>
<img v-if="captchaImg" :src="captchaImg" class="captcha-image" @click="loadCaptcha" title="点击刷新" alt="验证码">
<div v-else class="captcha-image" @click="loadCaptcha" title="点击刷新">加载中</div>
</div>
<button type="submit" class="login-btn" :disabled="loading">{{ loading ? '登录中...' : '登 录' }}</button>
<div class="form-bottom-links">
<a class="bottom-link" @click="onRegister">新用户注册</a>
<a class="bottom-link" @click="onForgot">忘记密码</a>
</div>
</form>
<form v-else id="smsLoginForm" @submit.prevent="onSmsLoginSubmit">
<div class="form-group">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<input v-model="smsForm.phone" type="text" class="form-input" placeholder="请输入手机号" maxlength="11" autocomplete="tel">
</div>
<div class="form-group sms-group">
<input v-model="smsForm.smsCode" type="text" class="form-input sms-input" placeholder="短信验证码" maxlength="6">
<button type="button" class="sms-btn" :disabled="smsSmsLocked || smsSmsCountdown > 0 || !smsForm.phone" @click="sendLoginSmsCode">
{{ smsSmsLocked ? '发送中...' : smsSmsCountdown > 0 ? `${smsSmsCountdown}s 后重试` : '获取验证码' }}
</button>
</div>
<button type="submit" class="login-btn" :disabled="smsLoginLocked">{{ smsLoginLocked ? '登录中...' : '登 录' }}</button>
<div class="form-bottom-links">
<a class="bottom-link" @click="onRegister">新用户注册</a>
<a class="bottom-link" @click="onForgot">忘记密码</a>
</div>
</form>
</div>
</section>
</main>
<!-- 底部 -->
<footer class="footer">
<p>© 北京整合医学学会 BAHIM</p>
<p>京ICP备2020035479号-1 &nbsp;&nbsp; 京公网安备11010802034820</p>
</footer>
<!-- 注册弹窗 (原型: 选择用户类别) -->
<div class="modal-overlay" :class="{ active: showRoleModal }" @click.self="closeModal">
<div class="modal">
<h2 class="modal-title">选择用户类别</h2>
<div class="user-type-list">
<label v-for="t in registerTypes" :key="t.value" class="user-type-item">
<input type="radio" name="registerUserType" :value="t.value" v-model="registerUserType">
<div class="info">
<div class="name">{{ t.name }}</div>
<div class="desc">{{ t.desc }}</div>
</div>
</label>
</div>
<div class="modal-actions">
<button class="modal-btn secondary" type="button" @click="closeModal">取消</button>
<button class="modal-btn primary" type="button" @click="confirmRegister">确定</button>
</div>
</div>
</div>
<!-- 忘记密码弹窗 -->
<div class="modal-overlay" :class="{ active: showForgotModal }" @click.self="closeForgot">
<div class="modal">
<h2 class="modal-title">忘记密码</h2>
<p class="modal-subtitle">通过注册时填写的手机号验证后重置密码</p>
<form @submit.prevent="onResetSubmit">
<div class="form-group">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<input v-model="forgotForm.phone" type="text" class="form-input" placeholder="请输入注册时的手机号" maxlength="11" autocomplete="tel">
</div>
<div class="form-group sms-group">
<input v-model="forgotForm.smsCode" type="text" class="form-input sms-input" placeholder="短信验证码" maxlength="6">
<button type="button" class="sms-btn" :disabled="forgotSmsLocked || forgotSmsCountdown > 0 || !forgotForm.phone" @click="sendForgotSms">
{{ forgotSmsLocked ? '发送中...' : forgotSmsCountdown > 0 ? `${forgotSmsCountdown}s 后重试` : '获取验证码' }}
</button>
</div>
<div class="form-group">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<input v-model="forgotForm.password" type="password" class="form-input" placeholder="新密码 (6-20 位)" autocomplete="new-password">
</div>
<div class="form-group">
<svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<input v-model="forgotForm.confirmPassword" type="password" class="form-input" placeholder="确认新密码" autocomplete="new-password">
</div>
<div class="modal-actions">
<button class="modal-btn secondary" type="button" @click="closeForgot">取消</button>
<button class="modal-btn primary" type="submit" :disabled="forgotLoading">{{ forgotLoading ? '提交中...' : '重置密码' }}</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
import { login, getInfo, getCaptcha, sendLoginSms, smsLogin } from '@/api/auth'
import { useUserStore } from '@/store/user'
import { useAsyncLock } from '@/utils/useAsyncLock'
// 图形验证码刷新 + 忘记密码短信 + 短信登录 各自独立锁 (避免重复请求)
const { locked: captchaLocked, run: runCaptchaOnce } = useAsyncLock()
const { locked: forgotSmsLocked, run: runForgotSmsOnce } = useAsyncLock()
const { locked: smsSmsLocked, run: runSmsOnce } = useAsyncLock()
const { locked: smsLoginLocked, run: runSmsLoginOnce } = useAsyncLock()
const router = useRouter()
const userStore = useUserStore()
const form = reactive({ username: '', password: '', code: '', uuid: '', role: 'leader' })
const captchaImg = ref('')
const captchaEnabled = ref(true)
const loading = ref(false)
const showRoleModal = ref(false)
const registerUserType = ref('')
// 登录模式: 'password' | 'sms'
const loginMode = ref('password')
const smsForm = reactive({ phone: '', smsCode: '', uuid: '' })
const smsSmsCountdown = ref(0)
let smsSmsTimer = null
const registerTypes = [
{ value: 'expert', name: '项目参与专家', desc: '负责项目方案设计与评审' },
{ value: 'executor', name: '项目执行单位(供应商)', desc: '承担项目执行,需走入库流程' },
{ value: 'sponsor', name: '项目支持单位', desc: '注册后默认为管理员角色, 可建监察员子账号' }
]
const userTypes = [
{ value: 'admin', name: '系统管理员', desc: '系统配置、用户管理、权限控制' },
{ value: 'leader', name: '学会领导', desc: '查看全局数据、审批重要事项' },
{ value: 'manager', name: '项目经理', desc: '项目管理、方案审核、过程监督' },
{ value: 'doctor', name: '评审专家', desc: '项目评审、评分反馈、查看任务' },
{ value: 'executor', name: '执行单位', desc: '会议组织、人员安排、劳务结算' },
{ value: 'sponsor', name: '支持单位', desc: '支持函管理、人员管理、合作记录' },
]
async function loadCaptcha() {
await runCaptchaOnce(async () => {
try {
const res = await getCaptcha()
captchaEnabled.value = res?.captchaEnabled !== false
if (res?.uuid) {
form.uuid = res.uuid
captchaImg.value = 'data:image/png;base64,' + (res.img || '')
}
} catch (e) {
captchaImg.value = ''
}
})
}
async function onSubmit() {
if (!form.username || !form.password) {
ElMessage.warning('请输入用户名和密码')
return
}
if (captchaEnabled.value && !form.code) {
ElMessage.warning('请输入验证码')
return
}
loading.value = true
try {
const res = await login({ username: form.username, password: form.password, code: form.code || '', uuid: form.uuid || '' })
await afterLogin(res.token, form.username, autoRole(form.username))
} catch (e) {
// 错误提示已由 utils/request.js 拦截器统一弹 (ElMessage.error), 这里只刷新验证码
loadCaptcha()
} finally {
loading.value = false
}
}
/**
* 登录后置: 存 token → 调 /getInfo 拿真实 user → 跳角色首页
* 密码登录和短信登录共用
*/
async function afterLogin(token, displayName, fallbackRole) {
userStore.setToken(token)
let role = fallbackRole
try {
const info = await getInfo()
const u = info.user || {}
role = u.roleType || fallbackRole
userStore.setUser({
userId: u.userId,
userName: u.userName || displayName,
nickName: u.nickName || displayName,
accountType: u.accountType || 'MAIN',
parentUserId: u.parentUserId || null,
role
})
} catch {
// /getInfo 失败时回退: 只存基本信息
userStore.setUser({
userId: null,
userName: displayName,
nickName: displayName,
accountType: 'MAIN',
parentUserId: null,
role: fallbackRole
})
}
ElMessage.success(`欢迎,${displayName}${userTypes.find(u => u.value === role)?.name || role}`)
// 没拿到角色就跳回登录页 (之前 fallback 到 /leader/home 会让子账号误入 leader)
if (!role || !roleHome[role]) return router.replace({ name: 'login' })
router.replace(roleHome[role])
}
function switchMode(mode) {
if (loginMode.value === mode) return
loginMode.value = mode
// 切换时清空两条表单, 避免脏数据
if (mode === 'password') {
smsForm.phone = ''
smsForm.smsCode = ''
smsForm.uuid = ''
if (smsSmsTimer) { clearInterval(smsSmsTimer); smsSmsTimer = null }
smsSmsCountdown.value = 0
} else {
form.username = ''
form.password = ''
form.code = ''
// 切到短信登录时不需要图形验证码, 不再 loadCaptcha
}
}
async function sendLoginSmsCode() {
if (!/^1\d{10}$/.test(smsForm.phone)) {
return ElMessage.warning('请输入正确的手机号')
}
await runSmsOnce(async () => {
try {
const r = await sendLoginSms(smsForm.phone)
smsForm.uuid = r.data || ''
ElMessage.success('验证码已发送')
smsSmsCountdown.value = 60
smsSmsTimer = setInterval(() => {
smsSmsCountdown.value--
if (smsSmsCountdown.value <= 0) {
clearInterval(smsSmsTimer)
smsSmsTimer = null
}
}, 1000)
} catch (e) {
// request.js 已弹错误, 这里只重置 uuid
smsForm.uuid = ''
}
})
}
async function onSmsLoginSubmit() {
if (!/^1\d{10}$/.test(smsForm.phone)) return ElMessage.warning('请输入正确的手机号')
if (!smsForm.smsCode) return ElMessage.warning('请输入短信验证码')
if (!smsForm.uuid) return ElMessage.warning('请先获取验证码')
await runSmsLoginOnce(async () => {
try {
const res = await smsLogin({ phone: smsForm.phone, smsCode: smsForm.smsCode, uuid: smsForm.uuid })
// 短信登录后无 username, 用 phone 作为显示名; fallbackRole 留空, 让 /getInfo 决定
await afterLogin(res.token, smsForm.phone, '')
} catch (e) {
// request.js 已弹错误
}
})
}
function autoRole(username) {
const u = (username || '').toLowerCase()
if (u === 'admin' || u === 'ry') return 'admin'
if (u.startsWith('leader')) return 'leader'
if (u.startsWith('manager')) return 'manager'
if (u.startsWith('doctor')) return 'doctor'
if (u.startsWith('executor')) return 'executor'
if (u.startsWith('sponsor')) return 'sponsor'
// 不匹配返回空串, 让 /getInfo 决定 (避免名字不规范时误进 leader)
return ''
}
const roleHome = {
admin: '/admin/workbench',
leader: '/leader/home',
manager: '/manager/workbench',
doctor: '/doctor/home',
executor: '/executor/meetings',
sponsor: '/sponsor/home',
}
function onRegister() {
showRoleModal.value = true
registerUserType.value = ''
}
function closeModal() {
showRoleModal.value = false
}
function confirmRegister() {
if (!registerUserType.value) {
ElMessage.warning('请选择用户类别')
return
}
const routeMap = {
expert: '/register-expert',
executor: '/register-executor',
sponsor: '/register-sponsor'
}
const target = routeMap[registerUserType.value]
if (!target) {
ElMessage.warning('该类别暂未开放注册')
return
}
showRoleModal.value = false
router.push(target)
}
function onForgot() {
showForgotModal.value = true
}
// ===== 忘记密码弹窗 =====
const showForgotModal = ref(false)
const forgotLoading = ref(false)
const forgotSmsCountdown = ref(0)
let forgotSmsTimer = null
const forgotForm = reactive({ phone: '', smsCode: '', password: '', confirmPassword: '' })
let forgotSmsUuid = ''
function closeForgot() {
showForgotModal.value = false
forgotForm.phone = ''
forgotForm.smsCode = ''
forgotForm.password = ''
forgotForm.confirmPassword = ''
forgotSmsUuid = ''
if (forgotSmsTimer) { clearInterval(forgotSmsTimer); forgotSmsTimer = null }
forgotSmsCountdown.value = 0
}
async function sendForgotSms() {
if (!/^1\d{10}$/.test(forgotForm.phone)) {
return ElMessage.warning('请输入正确的手机号')
}
await runForgotSmsOnce(async () => {
try {
const r = await request({ url: '/business/auth/forgotSendSms', method: 'post', data: { phone: forgotForm.phone } })
forgotSmsUuid = r.data || ''
ElMessage.success('验证码已发送')
forgotSmsCountdown.value = 60
forgotSmsTimer = setInterval(() => {
forgotSmsCountdown.value--
if (forgotSmsCountdown.value <= 0) {
clearInterval(forgotSmsTimer)
forgotSmsTimer = null
}
}, 1000)
} catch (e) {
// request.js 拦截器已弹 ElMessage.error, 这里只重置
forgotSmsUuid = ''
}
})
}
async function onResetSubmit() {
if (!/^1\d{10}$/.test(forgotForm.phone)) return ElMessage.warning('请输入正确的手机号')
if (!forgotForm.smsCode) return ElMessage.warning('请输入短信验证码')
if (!forgotForm.password || forgotForm.password.length < 6 || forgotForm.password.length > 20) {
return ElMessage.warning('密码长度 6-20 位')
}
if (forgotForm.password !== forgotForm.confirmPassword) {
return ElMessage.warning('两次密码输入不一致')
}
forgotLoading.value = true
try {
await request({
url: '/business/auth/resetPassword',
method: 'post',
data: {
phone: forgotForm.phone,
smsCode: forgotForm.smsCode,
uuid: forgotSmsUuid,
password: forgotForm.password,
confirmPassword: forgotForm.confirmPassword
}
})
ElMessage.success('密码重置成功, 请用新密码登录')
closeForgot()
} catch (e) {
// request.js 已弹错误
} finally {
forgotLoading.value = false
}
}
onMounted(() => { loadCaptcha() })
onUnmounted(() => {
if (forgotSmsTimer) clearInterval(forgotSmsTimer)
if (smsSmsTimer) clearInterval(smsSmsTimer)
})
</script>
<style>
/* === 原型 CSS 1:1 复制自 /home/john/ry8080/proto/html/登录.html === */
.login-page *{
margin: 0;
padding: 0;
box-sizing: border-box;
}
.login-page{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #333;
background: #f5f6f8;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* 顶部导航 */
.login-page .header{
height: 64px;
padding: 0 60px;
background: #ffffff;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
}
.login-page .logo{
display: flex;
align-items: center;
gap: 12px;
}
.login-page .logo-icon{
width: 36px;
height: 36px;
background: #1e3a8a;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
}
.login-page .logo-text{
display: flex;
flex-direction: column;
line-height: 1.2;
}
.login-page .logo-title{
font-size: 16px;
font-weight: 600;
color: #1f2937;
letter-spacing: 0.5px;
}
.login-page .logo-subtitle{
font-size: 11px;
color: #6b7280;
margin-top: 2px;
letter-spacing: 0.3px;
}
/* 主体 */
.login-page .main-content{
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 60px;
gap: 60px;
}
.login-page .banner-section{
flex: 1;
max-width: 560px;
}
.login-page .banner-inner{
background: #1e3a8a;
color: #fff;
padding: 56px 56px;
height: 440px;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-page .banner-tag{
display: inline-block;
padding: 4px 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
font-size: 12px;
letter-spacing: 2px;
margin-bottom: 24px;
width: fit-content;
}
.login-page .banner-title{
font-size: 30px;
font-weight: 600;
line-height: 1.4;
margin-bottom: 20px;
letter-spacing: 1px;
}
.login-page .banner-divider{
width: 48px;
height: 2px;
background: #fff;
margin-bottom: 24px;
}
.login-page .banner-desc{
font-size: 14px;
line-height: 1.9;
color: rgba(255, 255, 255, 0.85);
letter-spacing: 0.5px;
}
/* 登录卡片 */
.login-page .login-section{
width: 400px;
flex-shrink: 0;
}
.login-page .login-card{
background: #ffffff;
padding: 40px 40px;
border: 1px solid #e5e7eb;
}
.login-page .login-title{
font-size: 22px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
letter-spacing: 1px;
}
.login-page .login-subtitle{
font-size: 13px;
color: #6b7280;
margin-bottom: 28px;
}
/* 登录模式切换 tabs */
.login-page .login-tabs{
display: flex;
gap: 24px;
margin-bottom: 24px;
border-bottom: 1px solid #e5e7eb;
}
.login-page .login-tabs .tab{
padding: 8px 0;
font-size: 14px;
color: #6b7280;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.2s, border-color 0.2s;
user-select: none;
}
.login-page .login-tabs .tab:hover{ color: #1e3a8a; }
.login-page .login-tabs .tab.active{
color: #1e3a8a;
border-bottom-color: #1e3a8a;
font-weight: 500;
}
.login-page .form-group{
margin-bottom: 18px;
position: relative;
}
.login-page .form-input{
width: 100%;
height: 44px;
padding: 0 14px 0 42px;
border: 1px solid #d1d5db;
background: #fff;
font-size: 14px;
color: #1f2937;
outline: none;
transition: border-color 0.2s;
}
.login-page .form-input:focus{
border-color: #1e3a8a;
}
.login-page .form-input::placeholder{
color: #9ca3af;
}
.login-page .input-icon{
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
width: 16px;
height: 16px;
color: #9ca3af;
pointer-events: none;
}
.login-page .captcha-group{
display: flex;
gap: 10px;
}
.login-page .captcha-group .form-group{
flex: 1;
margin-bottom: 0;
}
.login-page .captcha-input{
padding-left: 14px;
}
.login-page .captcha-input + .input-icon{
display: none;
}
.login-page .captcha-image{
width: 110px;
height: 44px;
border: 1px solid #d1d5db;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 600;
letter-spacing: 4px;
color: #1e3a8a;
cursor: pointer;
user-select: none;
font-family: 'Courier New', monospace;
}
.login-page .form-bottom-links{
display: flex;
justify-content: space-between;
margin-top: 14px;
font-size: 13px;
}
.login-page .form-bottom-links .bottom-link{
color: #1e3a8a;
cursor: pointer;
}
.login-page .form-bottom-links .bottom-link:hover{
text-decoration: underline;
}
.login-page .login-btn{
width: 100%;
height: 44px;
background: #1e3a8a;
border: none;
color: #fff;
font-size: 15px;
font-weight: 500;
letter-spacing: 4px;
cursor: pointer;
margin-top: 8px;
transition: background 0.2s;
}
.login-page .login-btn:hover{
background: #1e40af;
}
/* 底部 */
.login-page .footer{
padding: 20px 60px;
border-top: 1px solid #e5e7eb;
background: #fff;
text-align: center;
color: #6b7280;
font-size: 12px;
line-height: 1.8;
}
.login-page .footer p{ margin-bottom: 2px; }
/* 弹窗 */
.login-page .modal-overlay{
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.login-page .modal-overlay.active{ display: flex; }
.login-page .modal{
background: #fff;
padding: 32px 36px;
width: 440px;
max-width: 90vw;
border: 1px solid #e5e7eb;
}
.login-page .modal-title{
font-size: 18px;
font-weight: 600;
color: #1f2937;
text-align: center;
margin-bottom: 24px;
letter-spacing: 1px;
}
.login-page .user-type-list{
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.login-page .user-type-item{
display: flex;
align-items: center;
padding: 14px 18px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.2s;
}
.login-page .user-type-item:hover{
border-color: #1e3a8a;
background: #f9fafb;
}
.login-page .user-type-item input[type="radio"]{
margin-right: 12px;
cursor: pointer;
accent-color: #1e3a8a;
}
.login-page .user-type-item .info{ flex: 1; }
.login-page .user-type-item .name{
font-size: 14px;
font-weight: 500;
color: #1f2937;
margin-bottom: 2px;
}
.login-page .user-type-item .desc{
font-size: 12px;
color: #6b7280;
}
.login-page .modal-actions{
display: flex;
gap: 12px;
}
.login-page .modal-btn{
flex: 1;
height: 40px;
font-size: 14px;
cursor: pointer;
border: none;
}
.login-page .modal-btn.primary{
background: #1e3a8a;
color: #fff;
}
.login-page .modal-btn.primary:hover{ background: #1e40af; }
.login-page .modal-btn.secondary{
background: #fff;
color: #4b5563;
border: 1px solid #d1d5db;
}
.login-page .modal-btn.secondary:hover{
border-color: #1e3a8a;
color: #1e3a8a;
}
/* 响应式 */
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
padding: 30px 30px;
gap: 30px;
}
.banner-section { width: 100%; max-width: 100%; }
.banner-inner { height: auto; padding: 40px 30px; }
.login-section { width: 100%; max-width: 400px; }
}
@media (max-width: 640px) {
.header { padding: 0 20px; }
.main-content { padding: 20px; }
.login-card { padding: 30px 24px; }
.footer { padding: 16px 20px; }
}
/* === 用户要求: 左侧保持 440px 不动, 右侧登录卡片高度追平左侧, 两栏在 main-content 中居中 === */
.login-page .main-content { align-items: center; }
.login-page .login-card { height: 440px; display: flex; flex-direction: column; justify-content: center; }
/* === 注册选择类别弹窗 (原型 1:1 抄 /home/john/ry8080/proto/html/登录.html) === */
.modal-overlay {
position: fixed; inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex; align-items: center; justify-content: center;
z-index: 1000;
opacity: 0; visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.modal-overlay.active { opacity: 1; visibility: visible; }
.modal {
background: #fff;
padding: 32px 36px;
width: 440px;
max-width: 90vw;
border: 1px solid #e5e7eb;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
}
.modal-title { font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 20px; text-align: center; }
.user-type-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px; }
.user-type-item {
display: flex; align-items: center; gap: 12px;
padding: 14px 18px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.2s;
}
.user-type-item:hover { border-color: var(--brand-primary, #1890ff); background: #f7faff; }
.user-type-item input[type="radio"] {
width: 18px; height: 18px;
accent-color: var(--brand-primary, #1890ff);
cursor: pointer;
flex-shrink: 0;
}
.user-type-item .info { flex: 1; }
.user-type-item .name { font-size: 15px; color: #1a1a1a; font-weight: 500; margin-bottom: 2px; }
.user-type-item .desc { font-size: 12px; color: #909399; }
.modal-actions { display: flex; gap: 12px; }
.modal-btn {
flex: 1; height: 40px;
border: none; cursor: pointer;
font-size: 14px; font-weight: 500;
transition: all 0.2s;
}
.modal-btn.secondary { background: #f5f7fa; color: #606266; }
.modal-btn.secondary:hover { background: #ebeef5; }
.modal-btn.primary { background: var(--brand-primary, #1890ff); color: #fff; }
.modal-btn.primary:hover { background: var(--brand-primary-deep, #096dd9); }
/* ===== 忘记密码弹窗 ===== */
.modal-subtitle {
font-size: 13px;
color: #6b7280;
text-align: center;
margin: -12px 0 20px;
}
.sms-group {
display: flex;
gap: 10px;
margin-bottom: 18px;
}
.sms-group .sms-input {
flex: 1;
padding-left: 14px;
}
.sms-btn {
flex-shrink: 0;
width: 110px;
height: 44px;
border: 1px solid #d1d5db;
background: #f9fafb;
color: #1e3a8a;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.sms-btn:hover:not(:disabled) {
border-color: #1e3a8a;
background: #f3f4f6;
}
.sms-btn:disabled {
color: #9ca3af;
background: #f3f4f6;
cursor: not-allowed;
}
</style>