2026-06-14 16:49:18 +08:00
|
|
|
|
<!-- =============================================================================
|
2026-07-07 21:52:11 +08:00
|
|
|
|
// IT智能服务台 — 坐席登录页 (三端认证重构 AUTH-09 + CTRT)
|
2026-06-14 16:49:18 +08:00
|
|
|
|
// =============================================================================
|
2026-07-07 21:52:11 +08:00
|
|
|
|
// 说明: 坐席工作台登录,仅保留两种受控登录方式(决策见 system_design.md):
|
|
|
|
|
|
// - ① 企微扫码登录(/auth_qrcode/create + /auth_qrcode/poll 轮询)
|
|
|
|
|
|
// - ② 账号密码 + OTP 二次验证(POST /agents/login,所有登录强制 OTP)
|
|
|
|
|
|
//
|
|
|
|
|
|
// 已移除(AUTH-09):
|
|
|
|
|
|
// - 企微 JS-SDK "免密登录" 分支(checkWecomClient / handleWecomQuickLogin / autoLoginWithWecomUser)
|
|
|
|
|
|
// - "智能检测自动跳转" 分支(isInWecom 自动跳转 sso/init)
|
|
|
|
|
|
// 理由:统一安全水位,所有登录均需经过扫码或账密+OTP,不再有免密直入通道。
|
|
|
|
|
|
//
|
|
|
|
|
|
// 响应契约(CTRT-01/02):apiClient 拦截器已统一返回 inner data,
|
|
|
|
|
|
// 故此页面直接读取 response 字段,不再访问 response.data / response.data.data。
|
2026-06-14 16:49:18 +08:00
|
|
|
|
// ============================================================================= -->
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-06-21 00:46:50 +08:00
|
|
|
|
<div class="login-page">
|
2026-07-07 21:52:11 +08:00
|
|
|
|
<!-- 测试环境标识 - 右上角覆盖层 -->
|
|
|
|
|
|
<div v-if="isTestEnv" class="test-env-badge">
|
|
|
|
|
|
🔧 测试环境
|
|
|
|
|
|
</div>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<div class="login-card">
|
2026-06-21 00:46:50 +08:00
|
|
|
|
<!-- 标题区 -->
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<div class="login-title">
|
|
|
|
|
|
<h1>🛠️ IT智能服务台</h1>
|
2026-07-05 17:03:36 +08:00
|
|
|
|
<p>坐席工作台</p>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
<!-- 扫码登录面板(默认展示) -->
|
|
|
|
|
|
<div v-if="showQrLoginPanel" 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>
|
|
|
|
|
|
<div v-else-if="qrCode" class="qr-code">
|
|
|
|
|
|
<img :src="qrCode" alt="企微扫码登录" />
|
|
|
|
|
|
</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-06-14 16:49:18 +08:00
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
|
<!-- 分隔线 -->
|
|
|
|
|
|
<div class="divider">
|
|
|
|
|
|
<span>其他登录方式</span>
|
|
|
|
|
|
</div>
|
2026-06-21 00:46:50 +08:00
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
class="password-login-btn"
|
|
|
|
|
|
@click="showPasswordLogin = true"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span>🔐</span>
|
2026-07-07 21:52:11 +08:00
|
|
|
|
账号密码登录
|
2026-07-05 17:03:36 +08:00
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
2026-06-21 00:46:50 +08:00
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
<!-- 账号密码登录表单 -->
|
|
|
|
|
|
<div v-if="showPasswordLogin" class="password-login">
|
2026-07-05 17:03:36 +08:00
|
|
|
|
<el-button
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="back-btn"
|
|
|
|
|
|
@click="handleBackToQrCode"
|
|
|
|
|
|
>
|
|
|
|
|
|
← 返回扫码登录
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form
|
|
|
|
|
|
ref="formRef"
|
|
|
|
|
|
:model="loginForm"
|
|
|
|
|
|
:rules="rules"
|
|
|
|
|
|
label-position="top"
|
|
|
|
|
|
@submit.prevent="handleLogin"
|
|
|
|
|
|
>
|
|
|
|
|
|
<el-form-item label="账号" prop="userId">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="loginForm.userId"
|
|
|
|
|
|
placeholder="请输入账号(如 sxn)"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:prefix-icon="User"
|
|
|
|
|
|
@keydown.enter="handleLogin"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item label="密码" prop="password">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="loginForm.password"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
placeholder="请输入密码"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:prefix-icon="Lock"
|
|
|
|
|
|
show-password
|
|
|
|
|
|
@keydown.enter="handleLogin"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- OTP 输入区: 仅在 require_otp 时显示 -->
|
|
|
|
|
|
<el-form-item v-if="requireOtp" label="OTP 验证码" prop="otpCode">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="loginForm.otpCode"
|
|
|
|
|
|
placeholder="请输入 Google Authenticator 验证码"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:prefix-icon="Key"
|
|
|
|
|
|
maxlength="6"
|
|
|
|
|
|
@keydown.enter="handleLogin"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:loading="logging"
|
|
|
|
|
|
:disabled="logging"
|
|
|
|
|
|
@click="handleLogin"
|
|
|
|
|
|
class="login-btn"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ logging ? '登录中...' : (requireOtp ? '验证 OTP' : '账号密码登录') }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</div>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
<!-- 错误提示 -->
|
|
|
|
|
|
<el-alert
|
|
|
|
|
|
v-if="errorMsg"
|
|
|
|
|
|
:title="errorMsg"
|
|
|
|
|
|
type="error"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:closable="true"
|
|
|
|
|
|
@close="errorMsg = ''"
|
|
|
|
|
|
style="margin-top: 16px"
|
|
|
|
|
|
/>
|
2026-06-21 00:46:50 +08:00
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
<!-- 提示信息 -->
|
2026-06-14 16:49:18 +08:00
|
|
|
|
<div class="login-hint">
|
2026-06-21 00:46:50 +08:00
|
|
|
|
<p>登录即表示同意《IT智能服务台使用规范》</p>
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-07-05 17:03:36 +08:00
|
|
|
|
import { ref, reactive, 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-04 21:01:39 +08:00
|
|
|
|
import type { FormInstance, FormRules } from 'element-plus'
|
2026-07-05 17:03:36 +08:00
|
|
|
|
import { User, Lock, Key, Loading } from '@element-plus/icons-vue'
|
2026-07-04 21:01:39 +08:00
|
|
|
|
import { useAgentStore } from '@/stores/agent'
|
2026-07-07 21:52:11 +08:00
|
|
|
|
import { useWebSocket } from '@/composables/useWebSocket'
|
|
|
|
|
|
import apiClient from '@/api/index'
|
2026-06-14 16:49:18 +08:00
|
|
|
|
|
2026-06-21 00:46:50 +08:00
|
|
|
|
const router = useRouter()
|
2026-07-07 21:52:11 +08:00
|
|
|
|
const { connect: connectWebSocket } = useWebSocket()
|
2026-07-04 21:01:39 +08:00
|
|
|
|
const agentStore = useAgentStore()
|
|
|
|
|
|
const formRef = ref<FormInstance>()
|
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')
|
|
|
|
|
|
|
|
|
|
|
|
/** 登录面板显示控制 */
|
|
|
|
|
|
const showQrLoginPanel = ref(true) // 默认展示扫码登录
|
|
|
|
|
|
const showPasswordLogin = ref(false) // 是否显示账号密码登录
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
/** 企微二维码 */
|
|
|
|
|
|
const qrCode = ref('')
|
|
|
|
|
|
const qrLoading = ref(false)
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
/** 登录表单数据 */
|
|
|
|
|
|
const loginForm = reactive({
|
|
|
|
|
|
userId: '',
|
|
|
|
|
|
password: '',
|
|
|
|
|
|
otpCode: '',
|
2026-06-21 00:46:50 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
/** 是否需要 OTP 验证 */
|
|
|
|
|
|
const requireOtp = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
/** 登录中状态 */
|
|
|
|
|
|
const logging = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
/** 错误信息 */
|
|
|
|
|
|
const errorMsg = ref<string>('')
|
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
|
/** 轮询定时器 */
|
|
|
|
|
|
let pollTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
|
let currentTicket = ''
|
|
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
/** 表单校验规则 */
|
|
|
|
|
|
const rules: FormRules = {
|
2026-07-05 17:03:36 +08:00
|
|
|
|
userId: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
|
|
|
|
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
2026-07-04 21:01:39 +08:00
|
|
|
|
otpCode: [
|
|
|
|
|
|
{ required: true, message: '请输入 OTP 验证码', trigger: 'blur' },
|
|
|
|
|
|
{ len: 6, message: '验证码为 6 位数字', trigger: 'blur' },
|
|
|
|
|
|
],
|
2026-06-21 00:46:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
|
/**
|
2026-07-07 21:52:11 +08:00
|
|
|
|
* 获取企微登录二维码(CTRT: apiClient 直接返回 inner data)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async function fetchQrCode(): Promise<void> {
|
|
|
|
|
|
qrLoading.value = true
|
|
|
|
|
|
errorMsg.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-07-07 21:52:11 +08:00
|
|
|
|
const response = await apiClient.post('/auth_qrcode/create')
|
|
|
|
|
|
const result = response
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
if (result?.qrcode_url || result?.qrcode_png_base64) {
|
|
|
|
|
|
qrCode.value = result.qrcode_png_base64
|
|
|
|
|
|
? `data:image/png;base64,${result.qrcode_png_base64}`
|
|
|
|
|
|
: result.qrcode_url
|
|
|
|
|
|
currentTicket = result.ticket
|
2026-07-05 17:03:36 +08:00
|
|
|
|
startPolling()
|
|
|
|
|
|
} else {
|
2026-07-07 21:52:11 +08:00
|
|
|
|
throw new Error('获取二维码失败:响应数据为空')
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取企微二维码失败:', error)
|
2026-07-07 21:52:11 +08:00
|
|
|
|
if (!errorMsg.value) {
|
|
|
|
|
|
errorMsg.value = error instanceof Error ? error.message : '获取二维码失败,请重试'
|
|
|
|
|
|
}
|
2026-07-05 17:03:36 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
qrLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-07 21:52:11 +08:00
|
|
|
|
* 轮询扫码状态(CTRT: apiClient 直接返回 inner data)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async function pollQrCode(): Promise<void> {
|
|
|
|
|
|
if (!currentTicket) return
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-07-07 21:52:11 +08:00
|
|
|
|
const response = await apiClient.get(`/auth_qrcode/poll/${currentTicket}`)
|
|
|
|
|
|
const result = response
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
if (result) {
|
|
|
|
|
|
const { status, token, employee_id, name } = result
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
if (status === 'confirmed' && token) {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
localStorage.setItem('agent_token', token)
|
2026-07-07 21:52:11 +08:00
|
|
|
|
if (employee_id) {
|
|
|
|
|
|
localStorage.setItem('agent_user_id', employee_id)
|
|
|
|
|
|
}
|
|
|
|
|
|
const agentStore = useAgentStore()
|
|
|
|
|
|
agentStore.token = token
|
|
|
|
|
|
if (employee_id) {
|
|
|
|
|
|
agentStore.agentUserId = employee_id
|
|
|
|
|
|
}
|
|
|
|
|
|
if (name) {
|
|
|
|
|
|
agentStore.agentInfo = { user_id: employee_id, name, status: 'online' }
|
|
|
|
|
|
}
|
2026-07-05 17:03:36 +08:00
|
|
|
|
ElMessage.success('登录成功')
|
2026-07-07 21:52:11 +08:00
|
|
|
|
connectWebSocket()
|
2026-07-05 17:03:36 +08:00
|
|
|
|
router.push('/workspace')
|
|
|
|
|
|
} else if (status === 'expired') {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
ElMessage.warning('二维码已过期,请重新获取')
|
|
|
|
|
|
fetchQrCode()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-07-07 21:52:11 +08:00
|
|
|
|
console.warn('轮询扫码状态失败:', error)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
/** 启动轮询 */
|
2026-07-05 17:03:36 +08:00
|
|
|
|
function startPolling(): void {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
pollTimer = setInterval(pollQrCode, 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
/** 停止轮询 */
|
2026-07-05 17:03:36 +08:00
|
|
|
|
function stopPolling(): void {
|
|
|
|
|
|
if (pollTimer) {
|
|
|
|
|
|
clearInterval(pollTimer)
|
|
|
|
|
|
pollTimer = null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
/** 返回扫码登录 */
|
2026-07-05 17:03:36 +08:00
|
|
|
|
function handleBackToQrCode(): void {
|
2026-07-07 21:52:11 +08:00
|
|
|
|
showPasswordLogin.value = false
|
|
|
|
|
|
showQrLoginPanel.value = true
|
|
|
|
|
|
fetchQrCode()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 显示扫码登录面板 */
|
|
|
|
|
|
function showQrLogin(): void {
|
|
|
|
|
|
showQrLoginPanel.value = true
|
2026-07-05 17:03:36 +08:00
|
|
|
|
showPasswordLogin.value = false
|
|
|
|
|
|
fetchQrCode()
|
|
|
|
|
|
}
|
2026-06-21 00:46:50 +08:00
|
|
|
|
|
2026-07-04 21:01:39 +08:00
|
|
|
|
/**
|
2026-07-07 21:52:11 +08:00
|
|
|
|
* 账号密码 + OTP 登录
|
2026-07-04 21:01:39 +08:00
|
|
|
|
*/
|
|
|
|
|
|
async function handleLogin(): Promise<void> {
|
|
|
|
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
|
|
|
|
if (!valid) return
|
|
|
|
|
|
|
|
|
|
|
|
logging.value = true
|
|
|
|
|
|
errorMsg.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await agentStore.login(
|
|
|
|
|
|
loginForm.userId.trim(),
|
|
|
|
|
|
loginForm.password,
|
|
|
|
|
|
loginForm.otpCode.trim() || undefined
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (result && result.require_otp) {
|
|
|
|
|
|
requireOtp.value = true
|
|
|
|
|
|
loginForm.otpCode = ''
|
|
|
|
|
|
ElMessage.info('请输入 OTP 验证码')
|
|
|
|
|
|
logging.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ElMessage.success('登录成功')
|
2026-07-07 21:52:11 +08:00
|
|
|
|
connectWebSocket()
|
2026-07-04 21:01:39 +08:00
|
|
|
|
router.push('/workspace')
|
|
|
|
|
|
} catch (error: unknown) {
|
2026-07-05 17:03:36 +08:00
|
|
|
|
if (error instanceof Error && error.message === 'require_otp') {
|
|
|
|
|
|
requireOtp.value = true
|
|
|
|
|
|
loginForm.otpCode = ''
|
|
|
|
|
|
ElMessage.info('请输入 OTP 验证码')
|
|
|
|
|
|
logging.value = false
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-04 21:01:39 +08:00
|
|
|
|
const errMsg = error instanceof Error ? error.message : '登录失败,请重试'
|
|
|
|
|
|
errorMsg.value = errMsg
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
logging.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
2026-07-07 21:52:11 +08:00
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
// === OAuth 重定向计数清除 ===
|
|
|
|
|
|
const existingToken = localStorage.getItem('agent_token')
|
|
|
|
|
|
if (existingToken) {
|
|
|
|
|
|
console.log('[Login] 检测到已有 token,清除 OAuth 重定向计数')
|
|
|
|
|
|
localStorage.removeItem('oauth_redirect_count')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// === OAuth2.0 自动登录流程(参考ITSM系统)===
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
|
|
|
|
const code = urlParams.get('code')
|
|
|
|
|
|
const ssoToken = urlParams.get('sso_token')
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 如果有 SSO token,说明已经通过 OAuth 登录成功(CTRT: 直接读取 inner data)
|
|
|
|
|
|
if (ssoToken) {
|
|
|
|
|
|
console.log('[Login] 检测到 SSO token,验证身份...')
|
|
|
|
|
|
logging.value = true
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await apiClient.get('/auth_wecom/sso/verify', {
|
|
|
|
|
|
params: { sso_token: ssoToken },
|
|
|
|
|
|
})
|
|
|
|
|
|
const result = response
|
|
|
|
|
|
|
|
|
|
|
|
// 拦截器已保证成功即有效数据
|
|
|
|
|
|
if (result) {
|
|
|
|
|
|
const { user_id, name, role } = result
|
|
|
|
|
|
const token = ssoToken
|
|
|
|
|
|
|
|
|
|
|
|
localStorage.setItem('agent_token', token)
|
|
|
|
|
|
localStorage.setItem('agent_user_id', user_id)
|
|
|
|
|
|
|
|
|
|
|
|
agentStore.token = token
|
|
|
|
|
|
agentStore.agentUserId = user_id
|
|
|
|
|
|
agentStore.agentInfo = { user_id, name, status: 'online' }
|
|
|
|
|
|
|
|
|
|
|
|
window.history.replaceState({}, '', window.location.pathname)
|
|
|
|
|
|
|
|
|
|
|
|
ElMessage.success('登录成功')
|
|
|
|
|
|
connectWebSocket()
|
|
|
|
|
|
router.push('/workspace')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('SSO token 验证失败:', error)
|
|
|
|
|
|
window.history.replaceState({}, '', window.location.pathname)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
logging.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 如果有 code,跳转到后端 OAuth 回调
|
|
|
|
|
|
if (code) {
|
|
|
|
|
|
console.log('[Login] 检测到 OAuth code,跳转授权...')
|
|
|
|
|
|
window.location.replace(`/api/auth_wecom/sso/callback?code=${code}&state=`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 默认展示扫码登录(AUTH-09:移除 JS-SDK 免密与智能检测自动跳转)
|
|
|
|
|
|
showQrLogin()
|
2026-07-05 17:03:36 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
stopPolling()
|
|
|
|
|
|
})
|
2026-06-14 16:49:18 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
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;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-title h1 {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--text-primary, #303133);
|
|
|
|
|
|
margin: 0 0 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-title p {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--text-tertiary, #909399);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-loading {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-code img {
|
|
|
|
|
|
width: 180px;
|
|
|
|
|
|
height: 180px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-error {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #f56c6c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.qr-hint {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
margin: 0 0 24px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.divider {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin: 0 0 24px 0;
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.divider::before,
|
|
|
|
|
|
.divider::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
background: #e4e7ed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.divider span {
|
|
|
|
|
|
padding: 0 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.password-login-btn,
|
2026-07-04 21:01:39 +08:00
|
|
|
|
.login-btn {
|
2026-06-21 00:46:50 +08:00
|
|
|
|
width: 100%;
|
2026-07-05 17:03:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.password-login {
|
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.back-btn {
|
|
|
|
|
|
margin-bottom: 16px;
|
2026-06-21 00:46:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 16:49:18 +08:00
|
|
|
|
.login-hint {
|
|
|
|
|
|
text-align: center;
|
2026-06-21 00:46:50 +08:00
|
|
|
|
color: var(--text-placeholder, #c0c4cc);
|
2026-06-14 16:49:18 +08:00
|
|
|
|
font-size: 12px;
|
2026-06-21 00:46:50 +08:00
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
border-top: 1px solid var(--border-color-lighter, #ebeef5);
|
|
|
|
|
|
padding-top: 16px;
|
2026-07-04 21:01:39 +08:00
|
|
|
|
margin-top: 24px;
|
2026-06-14 16:49:18 +08:00
|
|
|
|
}
|
2026-06-21 00:46:50 +08:00
|
|
|
|
|
|
|
|
|
|
.login-hint p {
|
|
|
|
|
|
margin: 4px 0;
|
|
|
|
|
|
}
|
2026-07-04 21:01:39 +08:00
|
|
|
|
</style>
|