2026-04-01 00:42:25 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="oidc-callback-view">
|
|
|
|
|
|
<div class="callback-container">
|
|
|
|
|
|
<div v-if="loading" class="loading-section">
|
|
|
|
|
|
<a-spin size="large" />
|
|
|
|
|
|
<p class="loading-text">正在处理登录...</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="error" class="error-section">
|
|
|
|
|
|
<a-result status="error" :title="errorTitle" :sub-title="errorMessage">
|
|
|
|
|
|
<template #extra>
|
2026-04-06 12:09:22 +08:00
|
|
|
|
<a-button type="primary" @click="goToLogin"> 返回登录页 </a-button>
|
2026-04-01 00:42:25 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</a-result>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="success-section">
|
|
|
|
|
|
<a-result status="success" title="登录成功" sub-title="正在跳转...">
|
|
|
|
|
|
<template #icon>
|
|
|
|
|
|
<a-spin />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-result>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, onMounted } from 'vue'
|
|
|
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
|
|
|
|
import { useUserStore } from '@/stores/user'
|
|
|
|
|
|
import { useAgentStore } from '@/stores/agent'
|
2026-04-01 22:10:24 +08:00
|
|
|
|
import { authApi } from '@/apis/auth_api'
|
2026-04-01 00:42:25 +08:00
|
|
|
|
import { message } from 'ant-design-vue'
|
2026-06-08 10:26:33 +08:00
|
|
|
|
import { clearAutoStartAttempt } from '@/utils/oidcAutoStart'
|
2026-04-01 00:42:25 +08:00
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const userStore = useUserStore()
|
|
|
|
|
|
const agentStore = useAgentStore()
|
|
|
|
|
|
|
|
|
|
|
|
// 状态
|
|
|
|
|
|
const loading = ref(true)
|
|
|
|
|
|
const error = ref(false)
|
|
|
|
|
|
const errorTitle = ref('登录失败')
|
|
|
|
|
|
const errorMessage = ref('处理登录请求时发生错误')
|
|
|
|
|
|
|
|
|
|
|
|
// 返回登录页
|
|
|
|
|
|
const goToLogin = () => {
|
|
|
|
|
|
router.push('/login')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 22:10:24 +08:00
|
|
|
|
// 处理 OIDC 回调 - 从 URL 参数中获取一次性 code
|
|
|
|
|
|
const handleCallback = async () => {
|
2026-04-01 00:42:25 +08:00
|
|
|
|
try {
|
2026-04-01 22:10:24 +08:00
|
|
|
|
const code = route.query.code
|
2026-04-01 00:42:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 检查必要的参数
|
2026-04-01 22:10:24 +08:00
|
|
|
|
if (!code || typeof code !== 'string') {
|
2026-04-01 00:42:25 +08:00
|
|
|
|
loading.value = false
|
|
|
|
|
|
error.value = true
|
|
|
|
|
|
errorTitle.value = '参数错误'
|
2026-04-01 22:10:24 +08:00
|
|
|
|
errorMessage.value = '缺少有效的登录 code,请重新登录'
|
2026-04-01 00:42:25 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 22:10:24 +08:00
|
|
|
|
const tokenData = await authApi.exchangeOIDCCode(code)
|
|
|
|
|
|
|
|
|
|
|
|
await router.replace({ path: route.path, query: {} })
|
|
|
|
|
|
|
2026-04-01 00:42:25 +08:00
|
|
|
|
// 更新用户状态
|
2026-04-01 22:10:24 +08:00
|
|
|
|
userStore.token = tokenData.access_token
|
2026-05-17 22:44:05 +08:00
|
|
|
|
userStore.userId = tokenData.uid
|
2026-04-01 22:10:24 +08:00
|
|
|
|
userStore.username = tokenData.username
|
2026-05-17 22:44:05 +08:00
|
|
|
|
userStore.uid = tokenData.uid || ''
|
2026-04-01 22:10:24 +08:00
|
|
|
|
userStore.phoneNumber = tokenData.phone_number || ''
|
|
|
|
|
|
userStore.avatar = tokenData.avatar || ''
|
|
|
|
|
|
userStore.userRole = tokenData.role || 'user'
|
|
|
|
|
|
userStore.departmentId = tokenData.department_id || null
|
|
|
|
|
|
userStore.departmentName = tokenData.department_name || ''
|
2026-04-01 00:42:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 保存 token 到 localStorage
|
2026-04-01 22:10:24 +08:00
|
|
|
|
localStorage.setItem('user_token', tokenData.access_token)
|
2026-04-01 00:42:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 显示成功消息
|
|
|
|
|
|
message.success('登录成功')
|
|
|
|
|
|
|
2026-06-08 10:26:33 +08:00
|
|
|
|
// 获取重定向路径并清理 OIDC 相关标记
|
2026-04-01 00:42:25 +08:00
|
|
|
|
const redirectPath = sessionStorage.getItem('oidc_redirect') || '/'
|
|
|
|
|
|
sessionStorage.removeItem('oidc_redirect')
|
2026-06-08 10:26:33 +08:00
|
|
|
|
clearAutoStartAttempt()
|
2026-04-01 00:42:25 +08:00
|
|
|
|
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
|
|
|
|
|
|
// 延迟跳转,让用户看到成功提示
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
|
// 跳转
|
|
|
|
|
|
if (redirectPath === '/') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await agentStore.initialize()
|
|
|
|
|
|
router.push('/agent')
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('获取智能体信息失败:', err)
|
|
|
|
|
|
router.push('/agent')
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
router.push(redirectPath)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 500)
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('OIDC 回调处理失败:', err)
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
error.value = true
|
|
|
|
|
|
errorTitle.value = '登录失败'
|
2026-04-01 22:10:24 +08:00
|
|
|
|
errorMessage.value = err?.message || '处理登录请求时发生错误,请重试'
|
2026-04-01 00:42:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 组件挂载时处理回调
|
2026-04-01 22:10:24 +08:00
|
|
|
|
onMounted(async () => {
|
2026-04-01 00:42:25 +08:00
|
|
|
|
// 如果已登录,跳转到首页
|
|
|
|
|
|
if (userStore.isLoggedIn) {
|
|
|
|
|
|
router.push('/')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 22:10:24 +08:00
|
|
|
|
await handleCallback()
|
2026-04-01 00:42:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
|
.oidc-callback-view {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background-color: var(--gray-10);
|
|
|
|
|
|
background-image: radial-gradient(var(--gray-200) 1px, transparent 1px);
|
|
|
|
|
|
background-size: 24px 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.callback-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 500px;
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
background: var(--gray-0);
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
box-shadow: 0 4px 20px var(--shadow-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
|
|
|
|
|
|
.loading-text {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
color: var(--gray-600);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.error-section,
|
|
|
|
|
|
.success-section {
|
|
|
|
|
|
:deep(.ant-result) {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
|
|
|
|
.ant-result-title {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
color: var(--gray-800);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ant-result-subtitle {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--gray-500);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 576px) {
|
|
|
|
|
|
.callback-container {
|
|
|
|
|
|
margin: 20px;
|
|
|
|
|
|
padding: 30px 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|