|
|
@@ -0,0 +1,224 @@
|
|
|
+<template>
|
|
|
+ <div class="login-container" style="--wails-draggable:drag">
|
|
|
+ <div class="login-box">
|
|
|
+ <h2 class="title">用户登录</h2>
|
|
|
+
|
|
|
+ <form @submit.prevent="handleSubmit">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="username">用户名</label>
|
|
|
+ <input
|
|
|
+ v-model="formData.username"
|
|
|
+ type="text"
|
|
|
+ id="username"
|
|
|
+ required
|
|
|
+ :disabled="isLoading"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="password">密码</label>
|
|
|
+ <input
|
|
|
+ v-model="formData.password"
|
|
|
+ type="password"
|
|
|
+ id="password"
|
|
|
+ required
|
|
|
+ :disabled="isLoading"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="remember-me">
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ id="remember"
|
|
|
+ v-model="formData.rememberMe"
|
|
|
+ />
|
|
|
+ <label for="remember">记住我</label>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ class="submit-btn"
|
|
|
+ :disabled="isLoading"
|
|
|
+ >
|
|
|
+ {{ isLoading ? '登录中...' : '立即登录' }}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div v-if="errorMessage" class="error-message">
|
|
|
+ {{ errorMessage }}
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive } from 'vue'
|
|
|
+import axios from 'axios'
|
|
|
+import type { AxiosError } from 'axios'
|
|
|
+
|
|
|
+interface LoginForm {
|
|
|
+ username: string
|
|
|
+ password: string
|
|
|
+ rememberMe: boolean
|
|
|
+}
|
|
|
+
|
|
|
+interface LoginResponse {
|
|
|
+ token: string
|
|
|
+ userInfo: {
|
|
|
+ id: number
|
|
|
+ username: string
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+// 表单数据
|
|
|
+const formData = reactive<LoginForm>({
|
|
|
+ username: '',
|
|
|
+ password: '',
|
|
|
+ rememberMe: false
|
|
|
+})
|
|
|
+
|
|
|
+// 状态管理
|
|
|
+const isLoading = ref(false)
|
|
|
+const errorMessage = ref('')
|
|
|
+
|
|
|
+// 创建 axios 实例
|
|
|
+const api = axios.create({
|
|
|
+ baseURL: import.meta.env.VITE_API_BASE_URL,
|
|
|
+ timeout: 5000
|
|
|
+})
|
|
|
+
|
|
|
+const handleSubmit = async () => {
|
|
|
+ if (!formData.username || !formData.password) {
|
|
|
+ errorMessage.value = '请输入用户名和密码'
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ isLoading.value = true
|
|
|
+ errorMessage.value = ''
|
|
|
+
|
|
|
+ const response = await api.post<LoginResponse>('/login', {
|
|
|
+ username: formData.username,
|
|
|
+ password: formData.password
|
|
|
+ })
|
|
|
+
|
|
|
+ // 处理登录成功
|
|
|
+ handleLoginSuccess(response.data)
|
|
|
+ } catch (error) {
|
|
|
+ handleLoginError(error as AxiosError)
|
|
|
+ } finally {
|
|
|
+ isLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleLoginSuccess = (data: LoginResponse) => {
|
|
|
+ // 存储 token
|
|
|
+ localStorage.setItem('token', data.token)
|
|
|
+
|
|
|
+ // 如果选择记住我,存储用户名
|
|
|
+ if (formData.rememberMe) {
|
|
|
+ localStorage.setItem('username', formData.username)
|
|
|
+ } else {
|
|
|
+ localStorage.removeItem('username')
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+const handleLoginError = (error: AxiosError) => {
|
|
|
+ if (error.response) {
|
|
|
+ switch (error.response.status) {
|
|
|
+ case 401:
|
|
|
+ errorMessage.value = '用户名或密码错误'
|
|
|
+ break
|
|
|
+ case 500:
|
|
|
+ errorMessage.value = '服务器错误,请稍后再试'
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ errorMessage.value = '登录失败,请重试'
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ errorMessage.value = '网络连接异常,请检查网络'
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.login-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f0f2f5;
|
|
|
+}
|
|
|
+
|
|
|
+.login-box {
|
|
|
+ background: white;
|
|
|
+ padding: 2rem;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+ width: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+.title {
|
|
|
+ text-align: center;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 1.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group {
|
|
|
+ margin-bottom: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+input[type="text"],
|
|
|
+input[type="password"] {
|
|
|
+ width: 100%;
|
|
|
+ padding: 0.8rem;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 1rem;
|
|
|
+}
|
|
|
+
|
|
|
+.remember-me {
|
|
|
+ margin: 1rem 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.remember-me input {
|
|
|
+ margin-right: 0.5rem;
|
|
|
+}
|
|
|
+
|
|
|
+.submit-btn {
|
|
|
+ width: 100%;
|
|
|
+ padding: 0.8rem;
|
|
|
+ background: #1890ff;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 1rem;
|
|
|
+ transition: background 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.submit-btn:hover {
|
|
|
+ background: #40a9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.submit-btn:disabled {
|
|
|
+ background: #8fc7ff;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+
|
|
|
+.error-message {
|
|
|
+ color: #ff4d4f;
|
|
|
+ margin-top: 1rem;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+</style>
|