news-classifier/client/src/views/auth/LoginView.vue

300 lines
6.8 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { userApi } from '@/api/user'
import type { LoginRequest } from '@/types/api'
const router = useRouter()
const userStore = useUserStore()
// 表单数据
const form = ref<LoginRequest>({
username: '',
password: ''
})
// 加载状态
const loading = ref(false)
// 错误信息
const errorMessage = ref('')
/**
* 处理登录提交
*/
async function handleSubmit() {
// 表单验证
if (!form.value.username.trim()) {
errorMessage.value = '请输入用户名'
return
}
if (!form.value.password.trim()) {
errorMessage.value = '请输入密码'
return
}
loading.value = true
errorMessage.value = ''
try {
// 调用登录 API
const token = await userApi.login(form.value)
// 保存 token
userStore.setToken(token)
// 获取用户信息
const userInfo = await userApi.getUserInfo()
userStore.setUserInfo(userInfo)
// 跳转到首页
router.push('/home')
} catch (error: any) {
errorMessage.value = error.message || '登录失败,请检查用户名和密码'
} finally {
loading.value = false
}
}
/**
* 跳转到注册页面
*/
function goToRegister() {
router.push('/register')
}
</script>
<template>
<div class="login-container">
<div class="login-card">
<!-- Logo 和标题 -->
<div class="login-header">
<div class="logo">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/>
<path d="M18 14h-8"/>
<path d="M15 18h-5"/>
<path d="M10 6h8v4h-8V6Z"/>
</svg>
</div>
<h1 class="title">News Classifier</h1>
<p class="subtitle">智能新闻分类管理系统</p>
</div>
<!-- 登录表单 -->
<form class="login-form" @submit.prevent="handleSubmit">
<!-- 错误提示 -->
<div v-if="errorMessage" class="error-message">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<span>{{ errorMessage }}</span>
</div>
<!-- 用户名输入框 -->
<div class="form-group">
<label for="username">用户名</label>
<input
id="username"
v-model="form.username"
type="text"
placeholder="请输入用户名"
autocomplete="off"
:disabled="loading"
/>
</div>
<!-- 密码输入框 -->
<div class="form-group">
<label for="password">密码</label>
<input
id="password"
v-model="form.password"
type="password"
placeholder="请输入密码"
autocomplete="off"
:disabled="loading"
/>
</div>
<!-- 登录按钮 -->
<button type="submit" class="login-btn" :disabled="loading">
<span v-if="loading" class="spinner"></span>
<span>{{ loading ? '登录中...' : '登录' }}</span>
</button>
</form>
<!-- 注册链接 -->
<div class="register-link">
<span>还没有账号?</span>
<a @click="goToRegister">立即注册</a>
</div>
</div>
</div>
</template>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--accent)) 100%);
padding: 1rem;
}
.login-card {
width: 100%;
max-width: 400px;
background: hsl(var(--card));
border-radius: calc(var(--radius) + 4px);
padding: 2.5rem;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--accent)) 100%);
border-radius: calc(var(--radius) + 4px);
color: hsl(var(--primary-foreground));
margin-bottom: 1rem;
}
.title {
font-size: 1.75rem;
font-weight: 700;
color: hsl(var(--foreground));
margin: 0 0 0.5rem 0;
}
.subtitle {
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
margin: 0;
}
.login-form {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.error-message {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: hsl(var(--destructive) / 0.1);
border: 1px solid hsl(var(--destructive) / 0.2);
border-radius: var(--radius);
color: hsl(var(--destructive));
font-size: 0.875rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
}
.form-group input {
padding: 0.75rem 1rem;
border: 1px solid #d1d5db;
border-radius: var(--radius);
background: hsl(var(--background));
color: hsl(var(--foreground));
font-size: 0.875rem;
transition: border-color 0.2s, box-shadow 0.2s;
/* 增强默认边框可见性 */
border-color: #d1d5db !important;
}
.form-group input:focus {
outline: none;
/* 增强聚焦时的边框效果 */
border-color: #3b82f6 !important;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
.form-group input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.login-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border: none;
border-radius: var(--radius);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.login-btn:hover:not(:disabled) {
background: hsl(var(--primary) / 0.9);
}
.login-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid hsl(var(--primary-foreground));
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.register-link {
text-align: center;
margin-top: 1.5rem;
font-size: 0.875rem;
color: hsl(var(--muted-foreground));
}
.register-link a {
color: hsl(var(--primary));
cursor: pointer;
text-decoration: none;
font-weight: 500;
}
.register-link a:hover {
text-decoration: underline;
}
</style>