300 lines
6.8 KiB
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>
|