2026-06-14 16:49:18 +08:00
|
|
|
|
<!-- =============================================================================
|
2026-07-11 23:13:10 +08:00
|
|
|
|
// 企微IT智能服务台 — 坐席端统一登录页
|
2026-06-14 16:49:18 +08:00
|
|
|
|
// =============================================================================
|
2026-07-11 23:13:10 +08:00
|
|
|
|
// 说明:统一认证登录页面
|
|
|
|
|
|
// - 企微内:自动跳转 OAuth2 授权
|
|
|
|
|
|
// - 企微外:展示扫码登录页面,轮询扫码状态
|
|
|
|
|
|
// - 支持账号绑定(互联企业用户)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
// ============================================================================= -->
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-06-21 00:46:50 +08:00
|
|
|
|
<div class="login-page">
|
2026-07-11 23:13:10 +08:00
|
|
|
|
<!-- 测试环境标识 -->
|
2026-07-07 21:52:11 +08:00
|
|
|
|
<div v-if="isTestEnv" class="test-env-badge">
|
|
|
|
|
|
🔧 测试环境
|
|
|
|
|
|
</div>
|
2026-07-11 23:13:10 +08:00
|
|
|
|
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<div class="login-card">
|
2026-07-11 23:13:10 +08:00
|
|
|
|
<!-- 标题区:logo-block + 渐变文字(与 TopBar 统一) -->
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<div class="login-title">
|
2026-07-11 23:13:10 +08:00
|
|
|
|
<div class="login-logo-row">
|
|
|
|
|
|
<span class="logo-block">IT</span>
|
|
|
|
|
|
<h1 class="title-gradient">智能IT服务台</h1>
|
|
|
|
|
|
</div>
|
2026-07-05 17:03:36 +08:00
|
|
|
|
<p>坐席工作台</p>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
<!-- 扫码登录面板 -->
|
|
|
|
|
|
<div v-if="showQrPanel" class="qr-login">
|
2026-07-05 17:03:36 +08:00
|
|
|
|
<div class="qr-container">
|
|
|
|
|
|
<div v-if="qrLoading" class="qr-loading">
|
|
|
|
|
|
<el-icon class="is-loading"><Loading /></el-icon>
|
|
|
|
|
|
<p>加载中...</p>
|
|
|
|
|
|
</div>
|
2026-07-11 23:13:10 +08:00
|
|
|
|
<div v-else-if="qrCodeUrl" class="qr-code">
|
|
|
|
|
|
<img :src="qrCodeUrl" alt="企微扫码登录" />
|
2026-07-05 17:03:36 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="qr-error">
|
|
|
|
|
|
<p>获取二维码失败</p>
|
|
|
|
|
|
<el-button size="small" @click="fetchQrCode">重试</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="qr-hint">请使用企业微信扫码登录</p>
|
2026-07-11 23:13:10 +08:00
|
|
|
|
<p v-if="scanStatus === 'scanned'" class="scan-tip">
|
|
|
|
|
|
<el-icon><CircleCheckFilled /></el-icon> 已扫码,请在手机确认
|
|
|
|
|
|
</p>
|
2026-07-05 17:03:36 +08:00
|
|
|
|
</div>
|
2026-06-21 00:46:50 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
<!-- 加载状态 -->
|
|
|
|
|
|
<div v-if="loading" class="loading-container">
|
|
|
|
|
|
<el-icon class="is-loading" size="32"><Loading /></el-icon>
|
|
|
|
|
|
<p>{{ loadingText }}</p>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-07-11 23:13:10 +08:00
|
|
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
2026-06-21 00:46:50 +08:00
|
|
|
|
import { useRouter } from 'vue-router'
|
2026-06-14 16:49:18 +08:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
2026-07-11 23:13:10 +08:00
|
|
|
|
import { Loading, CircleCheckFilled } from '@element-plus/icons-vue'
|
|
|
|
|
|
import { getQrcode, getScanStatus, handleOAuthCallback, type QrcodeResponse, type ScanStatusResponse } from '@/api/auth'
|
2026-07-04 21:01:39 +08:00
|
|
|
|
import { useAgentStore } from '@/stores/agent'
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
2026-06-21 00:46:50 +08:00
|
|
|
|
const router = useRouter()
|
2026-07-04 21:01:39 +08:00
|
|
|
|
const agentStore = useAgentStore()
|
2026-07-11 23:13:10 +08:00
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
|
|
|
|
// 状态
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
2026-06-21 00:46:50 +08:00
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
/** 测试环境标识 */
|
|
|
|
|
|
const isTestEnv = import.meta.env.DEV || window.location.hostname.includes('localhost')
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/** 是否显示扫码面板 */
|
|
|
|
|
|
const showQrPanel = ref(true)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/** 二维码 URL */
|
|
|
|
|
|
const qrCodeUrl = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
/** 二维码加载中 */
|
2026-07-05 17:03:36 +08:00
|
|
|
|
const qrLoading = ref(false)
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/** 扫码状态 */
|
|
|
|
|
|
const scanStatus = ref<'waiting' | 'scanned' | ''>('')
|
2026-06-21 00:46:50 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/** 全局加载状态 */
|
|
|
|
|
|
const loading = ref(false)
|
2026-07-04 21:01:39 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/** 加载提示文字 */
|
|
|
|
|
|
const loadingText = ref('请稍候...')
|
2026-07-04 21:01:39 +08:00
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
|
/** 轮询定时器 */
|
|
|
|
|
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
2026-07-11 23:13:10 +08:00
|
|
|
|
|
|
|
|
|
|
/** 当前扫码登录票据 */
|
2026-07-05 17:03:36 +08:00
|
|
|
|
let currentTicket = ''
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
// --------------------------------------------------------------------------
|
|
|
|
|
|
// 方法
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检测是否在企微环境
|
|
|
|
|
|
*/
|
|
|
|
|
|
function isWecomEnv(): boolean {
|
|
|
|
|
|
if (typeof navigator === 'undefined') return false
|
|
|
|
|
|
return /wxwork/i.test(navigator.userAgent)
|
2026-06-21 00:46:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
|
/**
|
2026-07-11 23:13:10 +08:00
|
|
|
|
* 获取扫码登录二维码
|
2026-07-05 17:03:36 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async function fetchQrCode(): Promise<void> {
|
|
|
|
|
|
qrLoading.value = true
|
2026-07-11 23:13:10 +08:00
|
|
|
|
scanStatus.value = 'waiting'
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-07-11 23:13:10 +08:00
|
|
|
|
const data: QrcodeResponse = await getQrcode()
|
|
|
|
|
|
qrCodeUrl.value = `data:image/png;base64,${data.qrcode_png_base64}`
|
|
|
|
|
|
currentTicket = data.ticket
|
|
|
|
|
|
startPolling()
|
2026-07-05 17:03:36 +08:00
|
|
|
|
} catch (error) {
|
2026-07-11 23:13:10 +08:00
|
|
|
|
console.error('获取二维码失败:', error)
|
|
|
|
|
|
ElMessage.error('获取二维码失败,请重试')
|
2026-07-05 17:03:36 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
qrLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-11 23:13:10 +08:00
|
|
|
|
* 轮询扫码状态
|
2026-07-05 17:03:36 +08:00
|
|
|
|
*/
|
2026-07-11 23:13:10 +08:00
|
|
|
|
async function pollScanStatus(): Promise<void> {
|
2026-07-05 17:03:36 +08:00
|
|
|
|
if (!currentTicket) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-07-11 23:13:10 +08:00
|
|
|
|
const result: ScanStatusResponse = await getScanStatus(currentTicket)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
if (result.status === 'scanned') {
|
|
|
|
|
|
scanStatus.value = 'scanned'
|
|
|
|
|
|
} else if (result.status === 'confirmed' && result.token) {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
await handleLoginSuccess(result.token, result.user_info)
|
|
|
|
|
|
} else if (result.status === 'expired') {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
ElMessage.warning('二维码已过期,请重新扫码')
|
|
|
|
|
|
fetchQrCode()
|
|
|
|
|
|
} else if (result.status === 'cancelled') {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
ElMessage.info('扫码已取消')
|
|
|
|
|
|
scanStatus.value = 'waiting'
|
|
|
|
|
|
fetchQrCode()
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-07-07 21:52:11 +08:00
|
|
|
|
console.warn('轮询扫码状态失败:', error)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 启动轮询
|
|
|
|
|
|
*/
|
2026-07-05 17:03:36 +08:00
|
|
|
|
function startPolling(): void {
|
|
|
|
|
|
stopPolling()
|
2026-07-11 23:13:10 +08:00
|
|
|
|
pollTimer = setInterval(pollScanStatus, 2000)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 停止轮询
|
|
|
|
|
|
*/
|
2026-07-05 17:03:36 +08:00
|
|
|
|
function stopPolling(): void {
|
|
|
|
|
|
if (pollTimer) {
|
|
|
|
|
|
clearInterval(pollTimer)
|
|
|
|
|
|
pollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
/**
|
2026-07-11 23:13:10 +08:00
|
|
|
|
* 处理登录成功
|
2026-07-04 21:01:39 +08:00
|
|
|
|
*/
|
2026-07-11 23:13:10 +08:00
|
|
|
|
async function handleLoginSuccess(token: string, userInfo?: any): Promise<void> {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
loadingText.value = '登录中...'
|
2026-07-04 21:01:39 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-07-11 23:13:10 +08:00
|
|
|
|
// 检查是否需要账号绑定
|
|
|
|
|
|
if (userInfo?.is_new_user) {
|
|
|
|
|
|
// 跳转绑定页面,传递用户信息
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
name: 'Bind',
|
|
|
|
|
|
query: {
|
|
|
|
|
|
userid: userInfo.userid,
|
|
|
|
|
|
name: userInfo.name,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2026-07-08 21:54:57 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
// 保存 token
|
|
|
|
|
|
localStorage.setItem('agent_token', token)
|
|
|
|
|
|
agentStore.token = token
|
|
|
|
|
|
if (userInfo) {
|
|
|
|
|
|
localStorage.setItem('agent_user_id', userInfo.userid)
|
|
|
|
|
|
agentStore.agentUserId = userInfo.userid
|
|
|
|
|
|
agentStore.agentInfo = {
|
|
|
|
|
|
user_id: userInfo.userid,
|
|
|
|
|
|
name: userInfo.name,
|
|
|
|
|
|
status: 'online',
|
|
|
|
|
|
}
|
2026-07-04 21:01:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ElMessage.success('登录成功')
|
|
|
|
|
|
router.push('/workspace')
|
2026-07-11 23:13:10 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('登录失败:', error)
|
|
|
|
|
|
ElMessage.error('登录失败,请重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理 OAuth2 回调
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function processOAuthCallback(code: string, state?: string): Promise<void> {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
loadingText.value = '授权中...'
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await handleOAuthCallback(code, state)
|
|
|
|
|
|
|
|
|
|
|
|
if (result.is_new_user) {
|
|
|
|
|
|
// 需要账号绑定
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
name: 'Bind',
|
|
|
|
|
|
query: {
|
|
|
|
|
|
userid: result.user_info.userid,
|
|
|
|
|
|
name: result.user_info.name,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
2026-07-05 17:03:36 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
// 登录成功
|
|
|
|
|
|
localStorage.setItem('agent_token', result.token)
|
|
|
|
|
|
agentStore.token = result.token
|
|
|
|
|
|
localStorage.setItem('agent_user_id', result.user_info.userid)
|
|
|
|
|
|
agentStore.agentUserId = result.user_info.userid
|
|
|
|
|
|
agentStore.agentInfo = {
|
|
|
|
|
|
user_id: result.user_info.userid,
|
|
|
|
|
|
name: result.user_info.name,
|
|
|
|
|
|
status: 'online',
|
2026-07-07 21:52:11 +08:00
|
|
|
|
}
|
2026-07-11 23:13:10 +08:00
|
|
|
|
|
|
|
|
|
|
ElMessage.success('登录成功')
|
|
|
|
|
|
router.push('/workspace')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('OAuth回调处理失败:', error)
|
|
|
|
|
|
ElMessage.error('授权失败,请重试')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 跳转到 OAuth2 授权页面
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function redirectToOAuth(): Promise<void> {
|
|
|
|
|
|
const currentRedirectUri = window.location.origin + '/itagent/'
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { getOAuthAuthorizeUrl } = await import('@/api/auth')
|
|
|
|
|
|
const data = await getOAuthAuthorizeUrl(currentRedirectUri)
|
|
|
|
|
|
if (data.authorize_url) {
|
|
|
|
|
|
window.location.href = data.authorize_url
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('从后端获取授权URL失败,尝试本地构造:', error)
|
2026-07-07 21:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
// 降级:本地构造授权URL
|
|
|
|
|
|
const corpId = import.meta.env.VITE_WECOM_CORP_ID || ''
|
|
|
|
|
|
if (!corpId) {
|
|
|
|
|
|
console.warn('未配置 VITE_WECOM_CORP_ID,无法跳转授权')
|
2026-07-07 21:52:11 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
const redirectUri = encodeURIComponent(currentRedirectUri)
|
|
|
|
|
|
const oauthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${corpId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`
|
|
|
|
|
|
window.location.href = oauthUrl
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
|
|
|
|
// 生命周期
|
|
|
|
|
|
// --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
// 检查 URL 中的 code 参数(OAuth2 回调)
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
|
|
|
|
const code = urlParams.get('code')
|
|
|
|
|
|
const state = urlParams.get('state')
|
|
|
|
|
|
|
|
|
|
|
|
if (code) {
|
|
|
|
|
|
// 有 OAuth2 code,处理回调
|
|
|
|
|
|
await processOAuthCallback(code, state || undefined)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检测企微环境
|
|
|
|
|
|
if (isWecomEnv()) {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
loadingText.value = '正在跳转企微授权...'
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 企微内,跳转 OAuth2 授权
|
|
|
|
|
|
await redirectToOAuth()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('跳转授权失败:', error)
|
|
|
|
|
|
// 降级显示扫码登录
|
|
|
|
|
|
showQrPanel.value = true
|
|
|
|
|
|
fetchQrCode()
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 企微外,显示扫码登录
|
|
|
|
|
|
showQrPanel.value = true
|
|
|
|
|
|
fetchQrCode()
|
|
|
|
|
|
}
|
2026-07-05 17:03:36 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
})
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/* 测试环境标识 */
|
2026-07-07 21:52:11 +08:00
|
|
|
|
.test-env-badge {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 16px;
|
|
|
|
|
|
left: 16px;
|
|
|
|
|
|
z-index: 100;
|
|
|
|
|
|
background: linear-gradient(135deg, #ff6b6b 0%, #ffa500 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
2026-07-07 21:52:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-21 00:46:50 +08:00
|
|
|
|
.login-page {
|
2026-07-07 21:52:11 +08:00
|
|
|
|
position: relative;
|
2026-06-21 00:46:50 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-height: 100vh;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
background: linear-gradient(135deg, #07C160 0%, #06AD56 100%);
|
2026-06-21 00:46:50 +08:00
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-card {
|
|
|
|
|
|
width: 100%;
|
2026-07-04 21:01:39 +08:00
|
|
|
|
max-width: 400px;
|
2026-06-21 00:46:50 +08:00
|
|
|
|
background: var(--bg-secondary, #ffffff);
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
padding: 40px 32px;
|
|
|
|
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-title {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/* logo + 标题水平排列 */
|
|
|
|
|
|
.login-logo-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* IT logo 方块 — 复用 TopBar 样式 */
|
|
|
|
|
|
.logo-block {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 7px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
|
|
|
color: #07C160;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
letter-spacing: -0.5px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 渐变文字 — 复用 TopBar 样式 */
|
|
|
|
|
|
.title-gradient {
|
2026-06-21 00:46:50 +08:00
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: 700;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
color: #ffffff;
|
|
|
|
|
|
margin: 0;
|
2026-06-21 00:46:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-title p {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--text-tertiary, #909399);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/* 扫码登录 */
|
2026-07-05 17:03:36 +08:00
|
|
|
|
.qr-login {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-container {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
height: 200px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
background: var(--bg-tertiary, #f5f5f5);
|
2026-07-05 17:03:36 +08:00
|
|
|
|
border-radius: 12px;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
margin-bottom: 16px;
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-loading {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
color: var(--text-tertiary, #909399);
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-code img {
|
|
|
|
|
|
width: 180px;
|
|
|
|
|
|
height: 180px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-error {
|
|
|
|
|
|
text-align: center;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
color: var(--error-color, #f56c6c);
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-hint {
|
|
|
|
|
|
font-size: 14px;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
color: var(--text-tertiary, #909399);
|
|
|
|
|
|
margin: 0;
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
.scan-tip {
|
2026-07-05 17:03:36 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
gap: 4px;
|
|
|
|
|
|
color: var(--success-color, #07c160);
|
2026-07-05 17:03:36 +08:00
|
|
|
|
font-size: 14px;
|
2026-07-11 23:13:10 +08:00
|
|
|
|
margin-top: 12px;
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 23:13:10 +08:00
|
|
|
|
/* 加载状态 */
|
|
|
|
|
|
.loading-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
padding: 40px 0;
|
|
|
|
|
|
color: var(--text-secondary, #606266);
|
2026-06-21 00:46:50 +08:00
|
|
|
|
}
|
2026-07-04 21:01:39 +08:00
|
|
|
|
</style>
|