134 lines
4.6 KiB
Vue
134 lines
4.6 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted, reactive, ref } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { apiErrorMessage } from '../api/http'
|
||
import { useAuthStore } from '../stores/auth'
|
||
import http from '../api/http'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const auth = useAuthStore()
|
||
const loading = ref(false)
|
||
const ssoLoading = ref(false)
|
||
const sso = reactive({ enabled: false, displayName: '学校统一身份认证' })
|
||
const form = reactive({
|
||
userName: String(route.query.userName ?? ''),
|
||
password: '',
|
||
})
|
||
|
||
async function submit() {
|
||
loading.value = true
|
||
try {
|
||
await auth.login(form.userName, form.password)
|
||
await router.replace(String(route.query.redirect ?? '/dashboard'))
|
||
} catch (error) {
|
||
ElMessage.error(apiErrorMessage(error))
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function startSso() {
|
||
ssoLoading.value = true
|
||
const apiBaseUrl = String(import.meta.env.VITE_API_BASE_URL ?? '/api').replace(/\/$/, '')
|
||
const redirect = String(route.query.redirect ?? '/dashboard')
|
||
window.location.assign(
|
||
`${apiBaseUrl}/auth/sso/login?returnUrl=${encodeURIComponent(redirect)}`,
|
||
)
|
||
}
|
||
|
||
const ssoErrors: Record<string, string> = {
|
||
authentication_failed: '统一身份认证未完成,请重新尝试。',
|
||
missing_subject: 'Keycloak 未返回用户唯一标识,请联系管理员检查客户端映射。',
|
||
account_disabled: '教务系统账号已停用或锁定,请联系管理员。',
|
||
account_link_failed: '统一身份账号绑定失败,请联系管理员。',
|
||
account_update_failed: '登录状态更新失败,请稍后重试。',
|
||
binding_expired: '账户绑定请求已失效,请重新使用统一身份认证登录。',
|
||
configuration_error: '统一身份认证回调地址配置不正确,请联系管理员。',
|
||
}
|
||
|
||
onMounted(async () => {
|
||
const ssoError = String(route.query.ssoError ?? '')
|
||
if (ssoError) ElMessage.error(ssoErrors[ssoError] ?? '统一身份认证失败,请重新尝试。')
|
||
try {
|
||
const { data } = await http.get('/auth/sso/settings')
|
||
sso.enabled = Boolean(data.enabled)
|
||
sso.displayName = String(data.displayName || sso.displayName)
|
||
} catch {
|
||
sso.enabled = false
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<main class="login-page">
|
||
<section class="login-story">
|
||
<div class="story-brand">
|
||
<div class="brand-mark large" aria-hidden="true">
|
||
<span v-for="index in 9" :key="index" />
|
||
</div>
|
||
<div>
|
||
<strong>明序教务</strong>
|
||
<small>MINGXU ACADEMIC SYSTEM</small>
|
||
</div>
|
||
</div>
|
||
<div class="story-copy">
|
||
<span class="eyebrow">大学教务管理平台</span>
|
||
<h1>让每一项教学安排,<br />都有清晰的来处与去向。</h1>
|
||
<p>从组织档案开始,逐步连接培养方案、教学任务、排课、选课与成绩。</p>
|
||
</div>
|
||
<div class="schedule-signature" aria-hidden="true">
|
||
<div v-for="item in 20" :key="item" :class="{ active: [3, 8, 9, 14, 18].includes(item) }">
|
||
<span v-if="[3, 8, 9, 14, 18].includes(item)">教学</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="login-panel">
|
||
<form class="login-form" @submit.prevent="submit">
|
||
<div class="form-intro">
|
||
<span>欢迎回来</span>
|
||
<h2>登录教务工作台</h2>
|
||
<p>学生使用学号、教师使用工号登录;管理人员使用学校分配的账号。</p>
|
||
</div>
|
||
<label>
|
||
<span>账号</span>
|
||
<el-input v-model="form.userName" size="large" autocomplete="username" />
|
||
</label>
|
||
<label>
|
||
<span>密码</span>
|
||
<el-input
|
||
v-model="form.password"
|
||
size="large"
|
||
type="password"
|
||
show-password
|
||
autocomplete="current-password"
|
||
@keyup.enter="submit"
|
||
/>
|
||
</label>
|
||
<el-button
|
||
class="login-submit"
|
||
type="primary"
|
||
size="large"
|
||
native-type="submit"
|
||
:loading="loading"
|
||
>
|
||
进入工作台
|
||
</el-button>
|
||
<div v-if="sso.enabled" class="login-divider"><span>或</span></div>
|
||
<el-button
|
||
v-if="sso.enabled"
|
||
class="sso-login-submit"
|
||
size="large"
|
||
:loading="ssoLoading"
|
||
@click="startSso"
|
||
>
|
||
使用{{ sso.displayName }}登录
|
||
</el-button>
|
||
<router-link class="public-timetable-link" to="/timetable">无需登录,查询班级课表 →</router-link>
|
||
<router-link class="account-activation-link" to="/activate">学生首次登录?自助激活账号 →</router-link>
|
||
</form>
|
||
</section>
|
||
</main>
|
||
</template>
|