| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- <template>
- <div class="login-container">
- <el-card class="login-card" v-loading="checkLoading">
- <template #header>
- <div class="card-header">
- <img src="/logo.png" alt="Logo" class="logo-img" />
- <h2>韫珠科技统一登录平台</h2>
- <el-tag v-if="loginChallenge" type="warning" size="small">OIDC 登录</el-tag>
- <div v-if="ssoAppName" style="color: #67C23A; background-color: #f0f9eb; border: 1px solid #e1f3d8; padding: 6px 12px; border-radius: 4px; font-size: 16px; margin: 10px 0; display: inline-block;">{{ ssoAppName }}</div>
- </div>
- </template>
- <!-- SMS Enabled and NOT OIDC/SSO (Simple implementation for now) -->
- <el-tabs v-if="smsEnabled && !loginChallenge" v-model="activeTab" class="login-tabs" stretch>
- <el-tab-pane label="密码登录" name="password">
- <el-form :model="loginForm" label-width="0px" class="login-form">
- <el-form-item>
- <el-input v-model="loginForm.mobile" placeholder="手机号" :prefix-icon="Iphone" />
- </el-form-item>
- <el-form-item>
- <el-input
- v-model="loginForm.password"
- type="password"
- placeholder="密码"
- :prefix-icon="Lock"
- show-password
- />
- </el-form-item>
- <el-form-item>
- <el-checkbox v-model="loginForm.remember_me">记住我 (30天免登录)</el-checkbox>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" :loading="loading" @click="handleLogin" style="width: 100%">
- 登录
- </el-button>
- </el-form-item>
- <div style="text-align: center; margin-top: 10px;">
- <router-link to="/reset-password">忘记密码?</router-link>
- </div>
- </el-form>
- </el-tab-pane>
- <el-tab-pane label="验证码登录" name="sms">
- <el-form :model="smsForm" label-width="0px" class="login-form">
- <el-form-item>
- <el-input v-model="smsForm.mobile" placeholder="手机号" :prefix-icon="Iphone" />
- </el-form-item>
- <el-form-item>
- <el-row :gutter="10" style="width: 100%">
- <el-col :span="15">
- <el-input v-model="smsForm.code" placeholder="验证码" :prefix-icon="Key" />
- </el-col>
- <el-col :span="9">
- <el-button @click="handleSendCode" :disabled="countdown > 0" style="width: 100%">
- {{ countdown > 0 ? `${countdown}s` : '发送验证码' }}
- </el-button>
- </el-col>
- </el-row>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" :loading="loading" @click="handleSmsLogin" style="width: 100%">
- 登录
- </el-button>
- </el-form-item>
- </el-form>
- </el-tab-pane>
- </el-tabs>
- <!-- Original Layout for fallback (OIDC/SSO or SMS disabled) -->
- <el-form v-else :model="loginForm" label-width="0px" class="login-form">
- <el-form-item>
- <el-input v-model="loginForm.mobile" placeholder="手机号" :prefix-icon="Iphone" />
- </el-form-item>
- <el-form-item>
- <el-input
- v-model="loginForm.password"
- type="password"
- placeholder="密码"
- :prefix-icon="Lock"
- show-password
- />
- </el-form-item>
- <el-form-item>
- <el-checkbox v-model="loginForm.remember_me">记住我 (30天免登录)</el-checkbox>
- </el-form-item>
- <el-form-item>
- <el-button type="primary" :loading="loading" @click="handleLogin" style="width: 100%">
- 登录
- </el-button>
- </el-form-item>
- <div style="text-align: center; margin-top: 10px;">
- <router-link to="/reset-password">忘记密码?</router-link>
- </div>
- </el-form>
- </el-card>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, reactive, onMounted } from 'vue'
- import { useRouter, useRoute } from 'vue-router'
- import { useAuthStore } from '../store/auth'
- import { getLoginRequest, acceptLogin } from '../api/oidc'
- import { getSystemStatus, getAppPublicInfo, ssoLogin } from '../api/public'
- import { getPublicConfigs } from '../api/systemConfig'
- import { sendSmsCode, loginWithSms } from '../api/smsAuth'
- import { ElMessage } from 'element-plus'
- import { Iphone, Lock, Key } from '@element-plus/icons-vue'
- const router = useRouter()
- const route = useRoute()
- const authStore = useAuthStore()
- const activeTab = ref('password')
- const smsEnabled = ref(false)
- const countdown = ref(0)
- let timer: any = null
- const loginForm = reactive({
- mobile: '',
- password: '',
- remember_me: true
- })
- const smsForm = reactive({
- mobile: '',
- code: ''
- })
- const loading = ref(false)
- const checkLoading = ref(true)
- const loginChallenge = ref('')
- const ssoAppId = ref('')
- const ssoAppName = ref('')
- const handleSendCode = async () => {
- if (!smsForm.mobile) {
- ElMessage.warning('请输入手机号')
- return
- }
- try {
- await sendSmsCode(smsForm.mobile, 'pc')
- ElMessage.success('验证码已发送')
- countdown.value = 60
- timer = setInterval(() => {
- countdown.value--
- if (countdown.value <= 0) {
- clearInterval(timer)
- }
- }, 1000)
- } catch (e) {
- // handled by interceptor
- }
- }
- const handleSmsLogin = async () => {
- if (!smsForm.mobile || !smsForm.code) {
- ElMessage.warning('请输入手机号和验证码')
- return
- }
- loading.value = true
- try {
- const res = await loginWithSms({
- mobile: smsForm.mobile,
- code: smsForm.code,
- platform: 'pc'
- })
-
- // Manual login success handling since authStore.login is for password
- const token = res.data.access_token
- authStore.token = token
- localStorage.setItem('token', token)
- await authStore.fetchUser()
-
- // SSO Redirect Check
- if (ssoAppId.value) {
- const ssoRes = await ssoLogin({
- app_id: ssoAppId.value,
- username: '',
- password: ''
- })
- if (ssoRes.data.redirect_url) {
- ElMessage.success('登录成功,正在跳转...')
- window.location.href = ssoRes.data.redirect_url
- return
- }
- }
- ElMessage.success('登录成功')
- router.push('/dashboard/launchpad')
- } catch (e) {
- // handled
- } finally {
- loading.value = false
- }
- }
- const handleLogin = async () => {
- if (!loginForm.mobile || !loginForm.password) {
- ElMessage.warning('请输入手机号和密码')
- return
- }
-
- loading.value = true
- try {
- if (loginChallenge.value) {
- // OIDC Flow
- const res = await acceptLogin(loginChallenge.value, loginForm)
- if (res.data.redirect_to) {
- window.location.href = res.data.redirect_to
- } else {
- ElMessage.error('OIDC 响应异常')
- }
- } else if (ssoAppId.value) {
- // SSO Ticket Flow (Simple API)
- const res = await ssoLogin({
- app_id: ssoAppId.value,
- username: loginForm.mobile,
- password: loginForm.password
- })
- if (res.data.redirect_url) {
- ElMessage.success('登录成功,正在跳转...')
- window.location.href = res.data.redirect_url
- }
- } else {
- // Platform Login
- await authStore.login({
- mobile: loginForm.mobile,
- password: loginForm.password,
- remember_me: loginForm.remember_me
- })
- ElMessage.success('登录成功')
- router.push('/dashboard/launchpad')
- }
- } catch (error) {
- // Error handled in interceptor
- } finally {
- loading.value = false
- }
- }
- onMounted(async () => {
- // 0. Capture route params immediately to avoid async timing issues
- const challenge = route.query.login_challenge as string
- const appid = route.query.appid as string || route.query.app_id as string
-
- // Fetch Config
- try {
- const configRes = await getPublicConfigs()
- if (configRes.data) {
- const pcConfig = configRes.data.find((c: any) => c.key === 'sms_login_pc_enabled')
- if (pcConfig && pcConfig.value === 'true') {
- smsEnabled.value = true
- activeTab.value = 'sms'
- }
- }
- } catch(e) {
- console.error("Failed to fetch public config", e)
- }
- console.log('Login page mounted. Params:', { challenge, appid, fullQuery: route.query })
- // 1. Check System Initialization Status
- try {
- const statusRes = await getSystemStatus()
- if (!statusRes.data.initialized) {
- router.replace('/setup')
- return
- }
- } catch (e) {
- console.error("Failed to check system status", e)
- } finally {
- checkLoading.value = false
- }
- // If no context (neither OIDC challenge nor App SSO), we just stay here as Platform Login
- if (!challenge && !appid) {
- console.log('No login context, showing Platform Login')
- checkLoading.value = false
- return
- }
-
- if (challenge) {
- loginChallenge.value = challenge
- // Remove auto-accept skip logic from here since it's handled by AutoLogin.vue
- checkLoading.value = false
- } else if (appid) {
- // 3. Handle App SSO
- try {
- const appInfo = await getAppPublicInfo(appid)
-
- // Auto-Login Check (Only if we have a token/session)
- // Since `ssoLogin` endpoint now supports session-based auth, we can try calling it without password
- // But we need to know if we are logged in first.
- // Let's rely on `authStore.checkAuth()` or similar if available, or just try `ssoLogin` and catch 401.
-
- if (appInfo.data.protocol_type === 'OIDC') {
- // OIDC Logic
- // Redirect to Hydra Auth Endpoint via Nginx proxy (/hydra prefix)
- // We need to parse redirect_uris to get the first one
- let redirectUri = ''
- try {
- const uris = JSON.parse(appInfo.data.redirect_uris || '[]')
- if (uris.length > 0) redirectUri = uris[0]
- } catch(e) {}
-
- if (redirectUri) {
- const authUrl = `/hydra/oauth2/auth?client_id=${appid}&response_type=code&scope=openid offline profile&redirect_uri=${encodeURIComponent(redirectUri)}&state=init_sso`
- window.location.href = authUrl
- return
- } else {
- ElMessage.error('该应用未配置回调地址,无法跳转')
- }
- } else {
- // Simple API
- ssoAppId.value = appid
- ssoAppName.value = appInfo.data.app_name
-
- // Try Auto-Login (SSO)
- try {
- // Attempt SSO login without credentials (relying on cookie)
- const res = await ssoLogin({
- app_id: appid,
- username: '',
- password: ''
- }, { skipGlobalErrorHandler: true }) // Skip global 401 redirect/error
- if (res.data.redirect_url) {
- ElMessage.success(`检测到已登录,正在跳转到 ${appInfo.data.app_name}...`)
- window.location.href = res.data.redirect_url
- return
- }
- } catch (e) {
- // 401 means not logged in, show login form.
- console.log("Auto-login failed, showing login form")
- }
- }
- } catch (e) {
- ElMessage.error('无效的应用ID')
- }
- }
- })
- </script>
- <style scoped>
- .login-container {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100vh;
- background-color: #f0f2f5;
- }
- .login-card {
- width: 400px;
- }
- .card-header {
- text-align: center;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- }
- .logo-img {
- height: 60px;
- object-fit: contain;
- }
- </style>
|