Files
usernode/web/src/views/LoginView.vue
T

236 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import * as api from '@/api'
import { useAuthStore } from '@/stores/auth'
import type { Captcha } from '@/api/types'
const router = useRouter()
const auth = useAuthStore()
const tab = ref<'admin' | 'user'>('admin')
const loading = ref(false)
// 管理员登录
const adminForm = reactive({ username: '', password: '' })
// 外部用户 OTP 登录
const userForm = reactive({ username: '', code: '', captcha_id: '', captcha_code: '' })
const captcha = ref<Captcha | null>(null)
const captchaLoading = ref(false)
const cooldown = ref(0) // OTP 发送冷却倒计时(秒)
let cooldownTimer: number | undefined
async function loadCaptcha() {
captchaLoading.value = true
try {
captcha.value = await api.getCaptcha()
} catch {
captcha.value = null
} finally {
captchaLoading.value = false
}
}
function startCooldown(secs: number) {
cooldown.value = secs
cooldownTimer = window.setInterval(() => {
cooldown.value -= 1
if (cooldown.value <= 0) {
window.clearInterval(cooldownTimer)
cooldown.value = 0
}
}, 1000)
}
async function onAdminLogin() {
if (!adminForm.username || !adminForm.password) {
ElMessage.warning('请输入用户名和密码')
return
}
loading.value = true
try {
await api.adminLogin(adminForm)
await auth.fetchMe()
router.push('/admin/dashboard')
} catch {
// 错误已由拦截器提示
} finally {
loading.value = false
}
}
async function onSendOtp() {
if (!userForm.username || !userForm.captcha_code) {
ElMessage.warning('请输入用户名和图形验证码')
return
}
if (!captcha.value) {
await loadCaptcha()
if (!captcha.value) return
}
try {
await api.otpSend({
username: userForm.username,
captcha_id: captcha.value.captcha_id,
captcha_code: userForm.captcha_code,
})
ElMessage.success('验证码已发送(邮件或 CLIusernode user otp')
startCooldown(60)
// 验证码一次性,重新生成
userForm.captcha_code = ''
await loadCaptcha()
} catch {
// 验证码错误等已提示;重新生成一次
await loadCaptcha()
}
}
async function onUserLogin() {
if (!userForm.username || !userForm.code) {
ElMessage.warning('请输入用户名和 OTP 验证码')
return
}
loading.value = true
try {
await api.otpLogin({ username: userForm.username, code: userForm.code })
await auth.fetchMe()
router.push('/me/profile')
} catch {
// 已提示
} finally {
loading.value = false
}
}
function onTabChange() {
if (tab.value === 'user' && !captcha.value) {
loadCaptcha()
}
}
onMounted(() => {
if (tab.value === 'user') loadCaptcha()
})
</script>
<template>
<div class="login-page">
<el-card class="login-card" shadow="always">
<div class="brand">
<el-icon :size="30"><Monitor /></el-icon>
<h2>服务器用户管理节点</h2>
</div>
<el-tabs v-model="tab" stretch @tab-change="onTabChange">
<!-- 管理员登录 -->
<el-tab-pane label="管理员登录" name="admin">
<el-form label-position="top" @submit.prevent="onAdminLogin">
<el-form-item label="用户名">
<el-input v-model="adminForm.username" placeholder="管理员用户名" autocomplete="username" />
</el-form-item>
<el-form-item label="密码">
<el-input
v-model="adminForm.password"
type="password"
placeholder="密码"
show-password
autocomplete="current-password"
@keyup.enter="onAdminLogin"
/>
</el-form-item>
<el-button type="primary" class="submit" :loading="loading" @click="onAdminLogin">登录</el-button>
</el-form>
</el-tab-pane>
<!-- 外部用户 OTP 登录 -->
<el-tab-pane label="外部用户登录" name="user">
<el-form label-position="top" @submit.prevent="onUserLogin">
<el-form-item label="用户名">
<el-input v-model="userForm.username" placeholder="用户名(可带或不带 ext_ 前缀)" autocomplete="username" />
</el-form-item>
<el-form-item label="图形验证码">
<div class="captcha-row">
<el-input v-model="userForm.captcha_code" placeholder="4 位验证码" maxlength="4" @keyup.enter="onSendOtp" />
<el-image
v-if="captcha"
:src="captcha.image"
fit="contain"
class="captcha-img"
:title="'点击刷新'"
@click="loadCaptcha"
/>
<el-button v-else :loading="captchaLoading" @click="loadCaptcha">获取</el-button>
</div>
</el-form-item>
<el-form-item label="登录验证码(OTP">
<el-input
v-model="userForm.code"
placeholder="6 位 OTP(邮件或 CLI 获取)"
maxlength="6"
@keyup.enter="onUserLogin"
/>
</el-form-item>
<el-button type="primary" class="submit" :loading="loading" @click="onSendOtp" :disabled="cooldown > 0">
{{ cooldown > 0 ? `重新发送(${cooldown}s` : '发送验证码' }}
</el-button>
<el-button type="success" class="submit" :loading="loading" @click="onUserLogin">登录</el-button>
</el-form>
</el-tab-pane>
</el-tabs>
<div class="foot">
<router-link to="/apply">申请新账号</router-link>
</div>
</el-card>
</div>
</template>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1f2d3d 0%, #2c3e50 100%);
}
.login-card {
width: 400px;
border-radius: 8px;
}
.brand {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 8px;
}
.brand h2 {
margin: 0;
font-size: 18px;
}
.captcha-row {
display: flex;
gap: 8px;
width: 100%;
}
.captcha-img {
width: 110px;
height: 32px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
cursor: pointer;
flex-shrink: 0;
}
.submit {
width: 100%;
margin-top: 4px;
}
.foot {
margin-top: 16px;
text-align: center;
font-size: 13px;
}
</style>