问题: 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>
986 lines
29 KiB
Vue
986 lines
29 KiB
Vue
<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 京公网安备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>
|