ForcePilot/web/src/views/LoginView.vue

1056 lines
27 KiB
Vue
Raw Normal View History

2025-05-02 23:56:59 +08:00
<template>
<div class="login-view" :class="{ 'has-alert': serverStatus === 'error' }">
<!-- 服务状态提示 -->
<div v-if="serverStatus === 'error'" class="server-status-alert">
<div class="alert-content">
2026-03-16 21:40:19 +08:00
<exclamation-circle-icon class="alert-icon" size="20" />
<div class="alert-text">
<div class="alert-title">服务端连接失败</div>
<div class="alert-message">{{ serverError }}</div>
</div>
<a-button type="link" size="small" @click="checkServerHealth" :loading="healthChecking">
重试
</a-button>
</div>
</div>
2026-01-24 12:06:49 +08:00
<!-- 顶部导航品牌名称 & 操作按钮 -->
<nav class="login-navbar">
<div class="navbar-content">
<div class="brand-container" @click="goHome" style="cursor: pointer">
<img v-if="brandLogo" :src="brandLogo" alt="logo" class="brand-logo" />
<h1 class="brand-text">
<span v-if="brandOrgName" class="brand-org">{{ brandOrgName }}</span>
<span v-if="brandOrgName && brandName !== brandOrgName" class="brand-separator"></span>
<span class="brand-main">{{ brandName }}</span>
</h1>
2026-01-24 12:06:49 +08:00
</div>
</div>
2026-01-24 12:06:49 +08:00
</nav>
<!-- 主要内容区居中卡片 -->
<main class="login-main">
<div class="login-card">
<!-- 左侧图片 -->
<div class="card-side is-image">
<img :src="loginBgImage" alt="登录背景" class="login-bg-image" />
</div>
2026-01-24 12:06:49 +08:00
<!-- 右侧表单 -->
<div class="card-side is-form">
<div class="form-wrapper">
<header class="form-header">
<!-- 如果是在初始化显示特定标题 -->
<h2 v-if="isFirstRun" class="init-title">系统初始化请创建超级管理员</h2>
<p v-else class="welcome-text">欢迎登录</p>
2026-01-24 12:06:49 +08:00
</header>
<div class="login-content" :class="{ 'is-initializing': isFirstRun }">
<!-- 初始化管理员表单 -->
<div v-if="isFirstRun" class="login-form login-form--init">
<a-form :model="adminForm" @finish="handleInitialize" layout="vertical">
<a-form-item
label="UID"
name="uid"
2026-01-24 12:06:49 +08:00
:rules="[
{ required: true, message: '请输入UID' },
2026-01-24 12:06:49 +08:00
{
pattern: /^[a-zA-Z0-9_]+$/,
message: 'UID只能包含字母、数字和下划线'
2026-01-24 12:06:49 +08:00
},
{
min: 3,
max: 20,
message: 'UID长度必须在3-20个字符之间'
2026-01-24 12:06:49 +08:00
}
]"
>
<a-input
v-model:value="adminForm.uid"
placeholder="请输入UID3-20个字符"
2026-01-24 12:06:49 +08:00
:maxlength="20"
/>
</a-form-item>
<a-form-item
label="手机号(可选)"
name="phone_number"
:rules="[
{
validator: async (rule, value) => {
if (!value || value.trim() === '') {
return // 空值允许
}
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(value)) {
throw new Error('请输入正确的手机号格式')
}
2025-10-15 22:24:33 +08:00
}
}
2026-01-24 12:06:49 +08:00
]"
>
2026-01-24 12:06:49 +08:00
<a-input
v-model:value="adminForm.phone_number"
placeholder="可用于登录,可不填写"
:max-length="11"
/>
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: true, message: '请输入密码' }]"
>
<a-input-password v-model:value="adminForm.password" prefix-icon="lock" />
</a-form-item>
<a-form-item
label="确认密码"
name="confirmPassword"
:rules="[
{ required: true, message: '请确认密码' },
{ validator: validateConfirmPassword }
]"
>
<a-input-password
v-model:value="adminForm.confirmPassword"
prefix-icon="lock"
/>
</a-form-item>
<a-form-item v-if="showAgreementConsent" class="agreement-form-item">
<div class="agreement-row">
<a-checkbox v-model:checked="agreementAccepted">
登录即代表同意
<a
class="agreement-link"
:href="userAgreementUrl"
target="_blank"
rel="noopener noreferrer"
@click.stop
>用户协议</a
>
<a
class="agreement-link"
:href="privacyPolicyUrl"
target="_blank"
rel="noopener noreferrer"
@click.stop
>隐私协议</a
>
</a-checkbox>
</div>
</a-form-item>
2026-01-24 12:06:49 +08:00
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loading" block
>创建管理员账户</a-button
>
2026-01-24 12:06:49 +08:00
</a-form-item>
</a-form>
</div>
<!-- 登录表单 -->
<div v-else class="login-form">
<a-form :model="loginForm" @finish="handleLogin" layout="vertical">
<a-form-item
label="登录账号"
name="loginId"
:rules="[{ required: true, message: '请输入UID或手机号' }]"
2025-10-15 22:24:33 +08:00
>
<a-input v-model:value="loginForm.loginId" placeholder="UID或手机号">
2026-01-24 12:06:49 +08:00
<template #prefix>
2026-03-16 21:40:19 +08:00
<user-icon size="18" />
2026-01-24 12:06:49 +08:00
</template>
</a-input>
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: true, message: '请输入密码' }]"
>
<a-input-password v-model:value="loginForm.password">
<template #prefix>
<lock-icon size="18" />
2026-01-24 12:06:49 +08:00
</template>
</a-input-password>
</a-form-item>
<a-form-item v-if="showAgreementConsent" class="agreement-form-item">
<div class="agreement-row">
<a-checkbox v-model:checked="agreementAccepted">
登录即代表同意
<a
class="agreement-link"
:href="userAgreementUrl"
target="_blank"
rel="noopener noreferrer"
@click.stop
>用户协议</a
>
<a
class="agreement-link"
:href="privacyPolicyUrl"
target="_blank"
rel="noopener noreferrer"
@click.stop
>隐私协议</a
>
</a-checkbox>
2026-01-24 12:06:49 +08:00
</div>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:loading="loading"
:disabled="isLocked"
block
size="large"
>
<span v-if="isLocked">账户已锁定 {{ formatTime(lockRemainingTime) }}</span>
<span v-else>登录</span>
</a-button>
</a-form-item>
</a-form>
<!-- OIDC 登录选项 -->
<div v-if="oidcChecking || oidcEnabled" class="third-party-login">
<div class="divider">
<span>或使用以下方式登录</span>
</div>
<div class="login-icons">
<!-- 检查中显示骨架屏 -->
<div v-if="oidcChecking" class="login-skeleton">
<a-skeleton-button block size="large" :active="true" />
</div>
<!-- 检查完成后显示按钮 -->
<a-button
v-else
type="default"
size="large"
block
:loading="oidcLoading"
@click="handleOIDCLogin"
>
<template #icon>
<key-icon size="18" />
</template>
{{ oidcButtonText }}
</a-button>
</div>
</div>
2026-01-24 12:06:49 +08:00
</div>
2025-10-15 22:24:33 +08:00
2026-01-24 12:06:49 +08:00
<!-- 错误提示 -->
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
2025-05-10 01:02:23 +08:00
</div>
</div>
</div>
2025-05-10 01:02:23 +08:00
</div>
2026-01-24 12:06:49 +08:00
</main>
<!-- 页面底部版权信息等 -->
<footer class="page-footer">
<div class="footer-links">
<a href="https://github.com/xerrors" target="_blank">联系我们</a>
<span class="divider">|</span>
<a href="https://github.com/xerrors/Yuxi" target="_blank">使用帮助</a>
2026-01-24 12:06:49 +08:00
</div>
<div class="copyright">
&copy; {{ new Date().getFullYear() }} {{ brandName }}. All Rights Reserved.
</div>
</footer>
2025-05-02 23:56:59 +08:00
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { useInfoStore } from '@/stores/info'
import { useAgentStore } from '@/stores/agent'
import { message } from 'ant-design-vue'
import { healthApi } from '@/apis/system_api'
import { authApi } from '@/apis/auth_api'
import {
User as UserIcon,
Lock as LockIcon,
Key as KeyIcon,
AlertCircle as ExclamationCircleIcon
} from 'lucide-vue-next'
import { tryAutoStartOIDC, sanitizeRedirect } from '@/utils/oidcAutoStart'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const infoStore = useInfoStore()
const agentStore = useAgentStore()
2025-10-15 22:24:33 +08:00
// 品牌展示数据
const loginBgImage = computed(() => {
return infoStore.organization?.login_bg || '/login-bg.jpg'
})
const brandLogo = computed(() => {
return infoStore.organization?.logo || ''
})
const brandOrgName = computed(() => {
return infoStore.organization?.name?.trim() || ''
})
2025-10-15 22:24:33 +08:00
const brandName = computed(() => {
const orgName = brandOrgName.value
const brandNameRaw = infoStore.branding?.name?.trim() || 'Yuxi'
2026-01-24 12:06:49 +08:00
if (orgName && brandNameRaw && orgName !== brandNameRaw) {
return brandNameRaw
2026-01-24 12:06:49 +08:00
}
return orgName || brandNameRaw
})
const userAgreementUrl = computed(() => {
return infoStore.footer?.user_agreement_url?.trim() || ''
})
const privacyPolicyUrl = computed(() => {
return infoStore.footer?.privacy_policy_url?.trim() || ''
})
const showAgreementConsent = computed(() => {
return Boolean(userAgreementUrl.value && privacyPolicyUrl.value)
})
2025-05-02 23:56:59 +08:00
// 状态
const isFirstRun = ref(false)
const loading = ref(false)
const errorMessage = ref('')
const agreementAccepted = ref(false)
const serverStatus = ref('loading')
const serverError = ref('')
const healthChecking = ref(false)
2025-05-02 23:56:59 +08:00
// OIDC 相关状态
const oidcEnabled = ref(false)
const oidcLoading = ref(false)
const oidcChecking = ref(true)
const oidcButtonText = ref('OIDC 登录')
// 登录锁定相关状态
const isLocked = ref(false)
const lockRemainingTime = ref(0)
const lockCountdown = ref(null)
2025-05-02 23:56:59 +08:00
// 登录表单
const loginForm = reactive({
loginId: '', // 支持uid或phone_number登录
2025-05-02 23:56:59 +08:00
password: ''
})
2025-05-02 23:56:59 +08:00
// 管理员初始化表单
const adminForm = reactive({
uid: '', // 改为直接输入uid
2025-05-02 23:56:59 +08:00
password: '',
confirmPassword: '',
phone_number: '' // 手机号字段(可选)
})
2025-05-02 23:56:59 +08:00
const goHome = () => {
router.push('/')
}
// 清理倒计时器
const clearLockCountdown = () => {
if (lockCountdown.value) {
clearInterval(lockCountdown.value)
lockCountdown.value = null
}
}
// 启动锁定倒计时
const startLockCountdown = (remainingSeconds) => {
clearLockCountdown()
isLocked.value = true
lockRemainingTime.value = remainingSeconds
lockCountdown.value = setInterval(() => {
lockRemainingTime.value--
if (lockRemainingTime.value <= 0) {
clearLockCountdown()
isLocked.value = false
errorMessage.value = ''
}
}, 1000)
}
// 格式化时间显示
const formatTime = (seconds) => {
if (seconds < 60) {
return `${seconds}`
} else if (seconds < 3600) {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = seconds % 60
return `${minutes}${remainingSeconds}`
} else if (seconds < 86400) {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
return `${hours}小时${minutes}分钟`
} else {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
return `${days}${hours}小时`
}
}
2025-05-02 23:56:59 +08:00
// 密码确认验证
const validateConfirmPassword = async (rule, value) => {
2025-05-02 23:56:59 +08:00
if (value === '') {
throw new Error('请确认密码')
2025-05-02 23:56:59 +08:00
}
if (value !== adminForm.password) {
throw new Error('两次输入的密码不一致')
2025-05-02 23:56:59 +08:00
}
}
2025-05-02 23:56:59 +08:00
const ensureAgreementAccepted = () => {
if (!showAgreementConsent.value || agreementAccepted.value) {
return true
}
const warningMessage = '请先阅读并同意《用户协议》《隐私协议》'
message.warning(warningMessage)
return false
}
2025-05-02 23:56:59 +08:00
// 处理登录
const handleLogin = async () => {
// 如果当前被锁定,不允许登录
if (isLocked.value) {
message.warning(`账户被锁定,请等待 ${formatTime(lockRemainingTime.value)}`)
return
}
if (!ensureAgreementAccepted()) {
return
}
2025-05-02 23:56:59 +08:00
try {
loading.value = true
errorMessage.value = ''
clearLockCountdown()
2025-05-02 23:56:59 +08:00
await userStore.login({
loginId: loginForm.loginId,
2025-05-02 23:56:59 +08:00
password: loginForm.password
})
2025-05-02 23:56:59 +08:00
message.success('登录成功')
2025-05-02 23:56:59 +08:00
// 获取重定向路径
const redirectPath = sessionStorage.getItem('redirect') || '/'
sessionStorage.removeItem('redirect') // 清除重定向信息
2025-05-05 19:48:55 +08:00
2025-05-02 23:56:59 +08:00
// 根据用户角色决定重定向目标
if (redirectPath === '/') {
// 统一跳转到聊天页面(管理员与普通用户共享同一聊天界面)
2025-05-02 23:56:59 +08:00
try {
await agentStore.initialize()
router.push('/agent')
2025-05-02 23:56:59 +08:00
} catch (error) {
console.error('获取智能体信息失败:', error)
router.push('/agent')
2025-05-02 23:56:59 +08:00
}
} else {
// 跳转到其他预设的路径
router.push(redirectPath)
2025-05-02 23:56:59 +08:00
}
} catch (error) {
console.error('登录失败:', error)
// 检查是否是锁定错误HTTP 423
if (error.status === 423) {
// 尝试从响应头中获取剩余时间
let remainingTime = 0
if (error.headers && error.headers.get) {
const lockRemainingHeader = error.headers.get('X-Lock-Remaining')
if (lockRemainingHeader) {
remainingTime = parseInt(lockRemainingHeader)
}
}
// 如果没有从头中获取到,尝试从错误消息中解析
if (remainingTime === 0) {
const lockTimeMatch = error.message.match(/(\d+)\s*秒/)
if (lockTimeMatch) {
remainingTime = parseInt(lockTimeMatch[1])
}
}
if (remainingTime > 0) {
startLockCountdown(remainingTime)
errorMessage.value = `由于多次登录失败,账户已被锁定 ${formatTime(remainingTime)}`
} else {
errorMessage.value = error.message || '账户被锁定,请稍后再试'
}
} else {
errorMessage.value = error.message || '登录失败,请检查用户名和密码'
}
2025-05-02 23:56:59 +08:00
} finally {
loading.value = false
2025-05-02 23:56:59 +08:00
}
}
2025-05-02 23:56:59 +08:00
// 处理 OIDC 登录
const handleOIDCLogin = async () => {
if (!ensureAgreementAccepted()) {
return
}
try {
oidcLoading.value = true
errorMessage.value = ''
// 获取 OIDC 登录 URL
const response = await authApi.getOIDCLoginUrl()
if (response.login_url) {
// 保存当前路径,以便登录后返回
const redirectPath =
sessionStorage.getItem('redirect') || router.currentRoute.value.query.redirect || '/'
sessionStorage.setItem('oidc_redirect', redirectPath)
// 跳转到 OIDC Provider
window.location.href = response.login_url
} else {
errorMessage.value = '获取 OIDC 登录地址失败'
}
} catch (error) {
console.error('OIDC 登录失败:', error)
errorMessage.value = error.message || 'OIDC 登录失败,请重试'
} finally {
oidcLoading.value = false
}
}
// 检查 OIDC 配置
const checkOIDCConfig = async () => {
oidcChecking.value = true
try {
const config = await authApi.getOIDCConfig()
oidcEnabled.value = config.enabled
if (config.provider_name) {
oidcButtonText.value = config.provider_name
}
return config
} catch (error) {
console.error('检查 OIDC 配置失败:', error)
oidcEnabled.value = false
return null
} finally {
oidcChecking.value = false
}
}
2025-05-02 23:56:59 +08:00
// 处理初始化管理员
const handleInitialize = async () => {
if (!ensureAgreementAccepted()) {
return
}
2025-05-02 23:56:59 +08:00
try {
loading.value = true
errorMessage.value = ''
2025-05-02 23:56:59 +08:00
if (adminForm.password !== adminForm.confirmPassword) {
errorMessage.value = '两次输入的密码不一致'
return
2025-05-02 23:56:59 +08:00
}
await userStore.initialize({
uid: adminForm.uid,
password: adminForm.password,
phone_number: adminForm.phone_number || null // 空字符串转为null
})
2025-05-02 23:56:59 +08:00
message.success('管理员账户创建成功')
router.push('/')
2025-05-02 23:56:59 +08:00
} catch (error) {
console.error('初始化失败:', error)
errorMessage.value = error.message || '初始化失败,请重试'
2025-05-02 23:56:59 +08:00
} finally {
loading.value = false
2025-05-02 23:56:59 +08:00
}
}
2025-05-02 23:56:59 +08:00
// 检查是否是首次运行
const checkFirstRunStatus = async () => {
try {
loading.value = true
const isFirst = await userStore.checkFirstRun()
isFirstRun.value = isFirst
2025-05-02 23:56:59 +08:00
} catch (error) {
console.error('检查首次运行状态失败:', error)
errorMessage.value = '系统出错,请稍后重试'
2025-05-02 23:56:59 +08:00
} finally {
loading.value = false
2025-05-02 23:56:59 +08:00
}
}
2025-05-02 23:56:59 +08:00
// 检查服务器健康状态
const checkServerHealth = async () => {
try {
healthChecking.value = true
const response = await healthApi.checkHealth()
if (response.status === 'ok') {
serverStatus.value = 'ok'
} else {
serverStatus.value = 'error'
serverError.value = response.message || '服务端状态异常'
}
} catch (error) {
console.error('检查服务器健康状态失败:', error)
serverStatus.value = 'error'
serverError.value = error.message || '无法连接到服务端,请检查网络连接'
} finally {
healthChecking.value = false
}
}
2025-05-02 23:56:59 +08:00
// 组件挂载时
onMounted(async () => {
// 如果已登录,按 redirect 参数跳转(不固定跳首页)
2025-05-02 23:56:59 +08:00
if (userStore.isLoggedIn) {
router.push(sanitizeRedirect(route.query.redirect))
return
2025-05-02 23:56:59 +08:00
}
// 显示 OIDC 认证失败的错误信息(由后端重定向携带)
if (route.query.oidc_error) {
errorMessage.value = String(route.query.oidc_error)
}
// 首先检查服务器健康状态
await checkServerHealth()
2025-05-02 23:56:59 +08:00
// 检查是否是首次运行
await checkFirstRunStatus()
// 如果处于首次运行状态,不需要 OIDC 自动登录
if (isFirstRun.value) {
return
}
// 检查 OIDC 配置完成后,尝试自动触发 OIDC 登录(跨系统跳转场景)
const config = await checkOIDCConfig()
if (config && config.enabled) {
const autoStarted = await tryAutoStartOIDC(async () => await authApi.getOIDCLoginUrl(), config)
// 如果已发起 OIDC 跳转,页面会被重定向,不需要继续
if (autoStarted) return
}
})
// 组件卸载时清理定时器
onUnmounted(() => {
clearLockCountdown()
})
2025-05-02 23:56:59 +08:00
</script>
<style lang="less" scoped>
.login-view {
2026-01-24 12:06:49 +08:00
min-height: 100vh;
2025-05-02 23:56:59 +08:00
width: 100%;
2025-05-10 01:02:23 +08:00
position: relative;
2026-01-24 12:06:49 +08:00
display: flex;
flex-direction: column;
background-color: var(--gray-10);
background-image: radial-gradient(var(--gray-200) 1px, transparent 1px);
background-size: 24px 24px;
&.has-alert {
padding-top: 60px;
}
}
2025-05-10 01:02:23 +08:00
2026-01-24 12:06:49 +08:00
/* Unified Navbar */
.login-navbar {
position: absolute;
2026-01-24 12:06:49 +08:00
top: 0;
left: 0;
width: 100%;
padding: 32px 0;
z-index: 10;
2026-01-24 12:06:49 +08:00
.navbar-content {
max-width: 1500px; /* Constraint the width */
margin: 0 auto;
padding: 0 40px;
display: flex;
justify-content: space-between;
align-items: center;
.brand-container {
display: flex;
align-items: center;
gap: 12px;
}
2026-01-24 12:06:49 +08:00
}
}
.brand-text {
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 1;
display: flex;
align-items: center;
gap: 12px;
.brand-org {
color: var(--gray-700);
font-weight: 600;
}
.brand-separator {
width: 4px;
height: 4px;
background-color: var(--gray-400);
border-radius: 50%;
font-weight: 600;
}
.brand-main {
color: var(--main-color);
font-weight: 600;
}
}
.brand-logo {
height: 32px;
width: auto;
object-fit: contain;
}
.top-logo {
height: 32px;
width: auto;
object-fit: contain;
}
.back-home-btn {
2025-10-15 22:24:33 +08:00
color: var(--gray-600);
font-size: 14px;
2026-01-24 12:06:49 +08:00
&:hover {
color: var(--main-color);
background-color: transparent;
}
}
2026-01-24 12:06:49 +08:00
/* Main Content: Card Layout */
.login-main {
flex: 1;
display: flex;
2026-01-24 12:06:49 +08:00
align-items: center;
justify-content: center;
padding: 20px;
padding-top: 80px; /* Add space for navbar */
}
.login-card {
width: 900px;
max-width: 95vw;
height: 560px;
background: var(--gray-0);
border-radius: 16px;
box-shadow: 0 0px 40px var(--shadow-1);
display: flex;
overflow: hidden;
}
2026-01-24 12:06:49 +08:00
.card-side {
position: relative;
2026-01-24 12:06:49 +08:00
}
/* Image Side */
.card-side.is-image {
flex: 1.4;
background-color: var(--main-10);
overflow: hidden;
.login-bg-image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
2025-05-10 01:02:23 +08:00
}
}
2026-01-24 12:06:49 +08:00
/* Form Side */
.card-side.is-form {
flex: 1;
display: flex;
align-items: center;
2026-01-24 12:06:49 +08:00
justify-content: center;
padding: 40px;
}
2026-01-24 12:06:49 +08:00
.form-wrapper {
width: 100%;
2026-01-24 12:06:49 +08:00
max-width: 320px;
2025-10-15 22:24:33 +08:00
display: flex;
flex-direction: column;
gap: 32px;
}
2026-01-24 12:06:49 +08:00
.form-header {
2025-10-15 22:24:33 +08:00
text-align: left;
2026-01-24 12:06:49 +08:00
.welcome-text {
font-size: 14px;
font-weight: 600;
color: var(--gray-500);
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 1px;
}
.init-title {
font-size: 18px;
font-weight: 600;
color: var(--main-color);
margin: 0;
line-height: 1.4;
2025-05-02 23:56:59 +08:00
}
}
.login-form {
2025-05-10 01:02:23 +08:00
:deep(.ant-input-affix-wrapper) {
2026-01-24 12:06:49 +08:00
padding: 10px 12px;
border-radius: 8px;
2025-05-10 01:02:23 +08:00
}
:deep(.ant-btn) {
2026-01-24 12:06:49 +08:00
height: 44px;
2025-05-10 01:02:23 +08:00
font-size: 16px;
2026-01-24 12:06:49 +08:00
border-radius: 8px;
2025-05-10 01:02:23 +08:00
}
:deep(.ant-input-prefix) {
margin-right: 8px;
color: var(--gray-500);
}
2025-05-10 01:02:23 +08:00
}
.login-form.login-form--init :deep(.ant-form-item) {
margin-bottom: 14px;
}
2025-05-10 01:02:23 +08:00
.third-party-login {
2026-01-24 12:06:49 +08:00
margin-top: 16px;
2025-05-10 01:02:23 +08:00
.divider {
position: relative;
text-align: center;
2026-01-24 12:06:49 +08:00
margin: 24px 0 16px;
&::before,
&::after {
2025-05-10 01:02:23 +08:00
content: '';
position: absolute;
top: 50%;
2026-01-24 12:06:49 +08:00
width: 30%;
2025-05-10 01:02:23 +08:00
height: 1px;
2025-10-15 22:24:33 +08:00
background-color: var(--gray-200);
2025-05-10 01:02:23 +08:00
}
&::before {
left: 0;
}
&::after {
right: 0;
}
span {
display: inline-block;
2026-01-24 12:06:49 +08:00
padding: 0 8px;
2025-10-15 22:24:33 +08:00
background-color: var(--gray-0);
2026-01-24 12:06:49 +08:00
color: var(--gray-400);
font-size: 12px;
2025-05-10 01:02:23 +08:00
}
}
.login-icons {
:deep(.ant-btn) {
2026-01-24 12:06:49 +08:00
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border-color: var(--gray-300);
color: var(--gray-700);
2025-05-10 01:02:23 +08:00
&:hover {
border-color: var(--main-color);
color: var(--main-color);
2026-01-24 12:06:49 +08:00
background-color: var(--main-10);
2025-05-10 01:02:23 +08:00
}
.anticon,
svg {
color: var(--main-color);
}
}
}
/* 修复:添加骨架屏样式 */
.login-skeleton {
:deep(.ant-skeleton-button) {
width: 100% !important;
height: 44px;
border-radius: 8px;
2025-05-10 01:02:23 +08:00
}
}
}
.agreement-form-item {
margin-bottom: 12px;
}
.agreement-row {
font-size: 13px;
color: var(--gray-600);
line-height: 1.6;
:deep(.ant-checkbox-wrapper) {
display: inline-flex;
align-items: flex-start;
}
:deep(.ant-checkbox + span) {
padding-inline-start: 8px;
}
2026-01-24 12:06:49 +08:00
}
.agreement-link {
2026-01-24 12:06:49 +08:00
color: var(--main-color);
2026-01-24 12:06:49 +08:00
&:hover {
text-decoration: underline;
}
}
.error-message {
2025-10-15 22:24:33 +08:00
margin-top: 16px;
2026-01-24 12:06:49 +08:00
padding: 10px 12px;
background-color: var(--color-error-50);
border: 1px solid color-mix(in srgb, var(--color-error-500) 25%, transparent);
border-radius: 6px;
color: var(--color-error-700);
font-size: 13px;
text-align: center;
}
/* Page Footer */
.page-footer {
padding: 24px;
text-align: center;
}
.footer-links {
2025-10-15 22:24:33 +08:00
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
2026-01-24 12:06:49 +08:00
margin-bottom: 8px;
2025-05-10 01:02:23 +08:00
a {
2026-01-24 12:06:49 +08:00
color: var(--gray-500);
font-size: 13px;
2025-05-10 01:02:23 +08:00
&:hover {
color: var(--main-color);
}
}
2026-01-24 12:06:49 +08:00
.divider {
color: var(--gray-300);
font-size: 12px;
}
}
.copyright {
font-size: 12px;
color: var(--gray-400);
2025-05-10 01:02:23 +08:00
}
2026-01-24 12:06:49 +08:00
/* Server Status Alert */
.server-status-alert {
2025-10-15 22:24:33 +08:00
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 12px 20px;
2026-01-24 12:06:49 +08:00
background: var(--color-error-500);
Add dark/light theme toggle feature (#343) * Add dark/light theme toggle feature * update: refine dark/light theme and related components * 调整语义化主题变量,优化暗/亮模式切换效果 * Revert "调整语义化主题变量,优化暗/亮模式切换效果" This reverts commit 85d9373297686fdbacb252a880b2847d20a39641. * 深色模式适配:减少 !important,迁移 CSS 变量,优化布局 * style: 替换硬编码颜色值为CSS变量以支持主题切换 将多处硬编码的颜色值(如#fff、#f0f0f0等)替换为CSS变量(如--gray-0、--gray-150等),统一管理颜色样式,便于主题切换和样式维护。主要修改包括背景色、边框色、文字色等视觉元素,同时移除不再需要的深色模式适配代码。 * style: 使用CSS变量替换硬编码颜色值 * refactor(theme): 重构主题系统,统一使用CSS变量并优化暗色模式实现 - 移除冗余的theme.js配置文件,将主题配置集中到theme store - 使用ant-design-vue的darkAlgorithm实现暗色模式 - 在多个组件中替换硬编码颜色值为CSS变量 - 优化用户信息组件,整合文档中心和主题切换功能 - 更新基础CSS样式,完善颜色系统和滚动条样式 * reset: 恢复被覆盖的修改(96d257a7ace38c94e2b113d56472d211d1141087) * feat(theme): 实现深色主题支持并重构样式系统 重构颜色变量系统,添加深色主题支持 移除硬编码颜色值,统一使用CSS变量 优化图表组件以响应主题切换 清理无用样式代码,提升可维护性 * docs: 更新开发规范文档 * style: 调整边框和背景颜色样式 --------- Co-authored-by: Wenjie Zhang <xerrors@163.com>
2025-11-23 01:39:44 +08:00
color: var(--gray-0);
z-index: 1000;
.alert-content {
display: flex;
align-items: center;
2026-01-24 12:06:49 +08:00
max-width: 1500px;
margin: 0 auto;
.alert-icon {
font-size: 20px;
margin-right: 12px;
Add dark/light theme toggle feature (#343) * Add dark/light theme toggle feature * update: refine dark/light theme and related components * 调整语义化主题变量,优化暗/亮模式切换效果 * Revert "调整语义化主题变量,优化暗/亮模式切换效果" This reverts commit 85d9373297686fdbacb252a880b2847d20a39641. * 深色模式适配:减少 !important,迁移 CSS 变量,优化布局 * style: 替换硬编码颜色值为CSS变量以支持主题切换 将多处硬编码的颜色值(如#fff、#f0f0f0等)替换为CSS变量(如--gray-0、--gray-150等),统一管理颜色样式,便于主题切换和样式维护。主要修改包括背景色、边框色、文字色等视觉元素,同时移除不再需要的深色模式适配代码。 * style: 使用CSS变量替换硬编码颜色值 * refactor(theme): 重构主题系统,统一使用CSS变量并优化暗色模式实现 - 移除冗余的theme.js配置文件,将主题配置集中到theme store - 使用ant-design-vue的darkAlgorithm实现暗色模式 - 在多个组件中替换硬编码颜色值为CSS变量 - 优化用户信息组件,整合文档中心和主题切换功能 - 更新基础CSS样式,完善颜色系统和滚动条样式 * reset: 恢复被覆盖的修改(96d257a7ace38c94e2b113d56472d211d1141087) * feat(theme): 实现深色主题支持并重构样式系统 重构颜色变量系统,添加深色主题支持 移除硬编码颜色值,统一使用CSS变量 优化图表组件以响应主题切换 清理无用样式代码,提升可维护性 * docs: 更新开发规范文档 * style: 调整边框和背景颜色样式 --------- Co-authored-by: Wenjie Zhang <xerrors@163.com>
2025-11-23 01:39:44 +08:00
color: var(--gray-0);
}
.alert-text {
flex: 1;
.alert-title {
font-weight: 600;
font-size: 16px;
margin-bottom: 2px;
}
.alert-message {
font-size: 14px;
opacity: 0.9;
}
}
:deep(.ant-btn-link) {
Add dark/light theme toggle feature (#343) * Add dark/light theme toggle feature * update: refine dark/light theme and related components * 调整语义化主题变量,优化暗/亮模式切换效果 * Revert "调整语义化主题变量,优化暗/亮模式切换效果" This reverts commit 85d9373297686fdbacb252a880b2847d20a39641. * 深色模式适配:减少 !important,迁移 CSS 变量,优化布局 * style: 替换硬编码颜色值为CSS变量以支持主题切换 将多处硬编码的颜色值(如#fff、#f0f0f0等)替换为CSS变量(如--gray-0、--gray-150等),统一管理颜色样式,便于主题切换和样式维护。主要修改包括背景色、边框色、文字色等视觉元素,同时移除不再需要的深色模式适配代码。 * style: 使用CSS变量替换硬编码颜色值 * refactor(theme): 重构主题系统,统一使用CSS变量并优化暗色模式实现 - 移除冗余的theme.js配置文件,将主题配置集中到theme store - 使用ant-design-vue的darkAlgorithm实现暗色模式 - 在多个组件中替换硬编码颜色值为CSS变量 - 优化用户信息组件,整合文档中心和主题切换功能 - 更新基础CSS样式,完善颜色系统和滚动条样式 * reset: 恢复被覆盖的修改(96d257a7ace38c94e2b113d56472d211d1141087) * feat(theme): 实现深色主题支持并重构样式系统 重构颜色变量系统,添加深色主题支持 移除硬编码颜色值,统一使用CSS变量 优化图表组件以响应主题切换 清理无用样式代码,提升可维护性 * docs: 更新开发规范文档 * style: 调整边框和背景颜色样式 --------- Co-authored-by: Wenjie Zhang <xerrors@163.com>
2025-11-23 01:39:44 +08:00
color: var(--gray-0);
border-color: var(--gray-0);
&:hover {
Add dark/light theme toggle feature (#343) * Add dark/light theme toggle feature * update: refine dark/light theme and related components * 调整语义化主题变量,优化暗/亮模式切换效果 * Revert "调整语义化主题变量,优化暗/亮模式切换效果" This reverts commit 85d9373297686fdbacb252a880b2847d20a39641. * 深色模式适配:减少 !important,迁移 CSS 变量,优化布局 * style: 替换硬编码颜色值为CSS变量以支持主题切换 将多处硬编码的颜色值(如#fff、#f0f0f0等)替换为CSS变量(如--gray-0、--gray-150等),统一管理颜色样式,便于主题切换和样式维护。主要修改包括背景色、边框色、文字色等视觉元素,同时移除不再需要的深色模式适配代码。 * style: 使用CSS变量替换硬编码颜色值 * refactor(theme): 重构主题系统,统一使用CSS变量并优化暗色模式实现 - 移除冗余的theme.js配置文件,将主题配置集中到theme store - 使用ant-design-vue的darkAlgorithm实现暗色模式 - 在多个组件中替换硬编码颜色值为CSS变量 - 优化用户信息组件,整合文档中心和主题切换功能 - 更新基础CSS样式,完善颜色系统和滚动条样式 * reset: 恢复被覆盖的修改(96d257a7ace38c94e2b113d56472d211d1141087) * feat(theme): 实现深色主题支持并重构样式系统 重构颜色变量系统,添加深色主题支持 移除硬编码颜色值,统一使用CSS变量 优化图表组件以响应主题切换 清理无用样式代码,提升可维护性 * docs: 更新开发规范文档 * style: 调整边框和背景颜色样式 --------- Co-authored-by: Wenjie Zhang <xerrors@163.com>
2025-11-23 01:39:44 +08:00
color: var(--gray-0);
background-color: color-mix(in srgb, var(--gray-0) 10%, transparent);
}
}
}
}
2026-01-24 12:06:49 +08:00
/* Responsive */
@media (max-width: 1280px) {
.login-navbar .navbar-content {
padding: 0 40px;
}
}
@media (max-width: 768px) {
.login-navbar .navbar-content {
padding: 0 20px;
}
.brand-text {
font-size: 20px;
}
.login-card {
flex-direction: column;
height: auto;
max-height: none;
width: 100%;
margin-top: 20px;
}
.card-side.is-image {
display: none;
}
.card-side.is-form {
padding: 40px 20px;
}
}
</style>