|
|
@@ -0,0 +1,304 @@
|
|
|
+<template>
|
|
|
+ <div v-if="visible" class="modal-overlay" @click.self="handleCancel">
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h2>登录</h2>
|
|
|
+ </div>
|
|
|
+ <div class="modal-body">
|
|
|
+ <div v-if="error" class="error-message">{{ error }}</div>
|
|
|
+ <form @submit.prevent="handleSubmit">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="username">账号</label>
|
|
|
+ <input
|
|
|
+ id="username"
|
|
|
+ v-model="formData.username"
|
|
|
+ type="text"
|
|
|
+ placeholder="请输入账号"
|
|
|
+ required
|
|
|
+ autocomplete="username"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="password">密码</label>
|
|
|
+ <div class="password-input-wrapper">
|
|
|
+ <input
|
|
|
+ id="password"
|
|
|
+ v-model="formData.password"
|
|
|
+ :type="showPassword ? 'text' : 'password'"
|
|
|
+ placeholder="请输入密码"
|
|
|
+ required
|
|
|
+ autocomplete="current-password"
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="password-toggle"
|
|
|
+ @click="togglePasswordVisibility"
|
|
|
+ :title="showPassword ? '隐藏密码' : '显示密码'"
|
|
|
+ >
|
|
|
+ <svg
|
|
|
+ v-if="!showPassword"
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ width="20"
|
|
|
+ height="20"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ fill="none"
|
|
|
+ stroke="currentColor"
|
|
|
+ stroke-width="2"
|
|
|
+ stroke-linecap="round"
|
|
|
+ stroke-linejoin="round"
|
|
|
+ >
|
|
|
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
|
|
+ <circle cx="12" cy="12" r="3"></circle>
|
|
|
+ </svg>
|
|
|
+ <svg
|
|
|
+ v-else
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ width="20"
|
|
|
+ height="20"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ fill="none"
|
|
|
+ stroke="currentColor"
|
|
|
+ stroke-width="2"
|
|
|
+ stroke-linecap="round"
|
|
|
+ stroke-linejoin="round"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"
|
|
|
+ ></path>
|
|
|
+ <line x1="1" y1="1" x2="23" y2="23"></line>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="form-actions">
|
|
|
+ <button type="submit" class="btn-primary" :disabled="loading">
|
|
|
+ {{ loading ? '登录中...' : '登录' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive } from 'vue'
|
|
|
+import { login } from '../api/auth'
|
|
|
+import { setToken } from '../utils/auth'
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ visible: boolean
|
|
|
+}
|
|
|
+
|
|
|
+interface Emits {
|
|
|
+ (e: 'success'): void
|
|
|
+ (e: 'cancel'): void
|
|
|
+}
|
|
|
+
|
|
|
+defineProps<Props>()
|
|
|
+const emit = defineEmits<Emits>()
|
|
|
+
|
|
|
+const formData = reactive({
|
|
|
+ username: '',
|
|
|
+ password: ''
|
|
|
+})
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const error = ref('')
|
|
|
+const showPassword = ref(false)
|
|
|
+
|
|
|
+const togglePasswordVisibility = () => {
|
|
|
+ showPassword.value = !showPassword.value
|
|
|
+}
|
|
|
+
|
|
|
+const handleSubmit = async () => {
|
|
|
+ if (!formData.username || !formData.password) {
|
|
|
+ error.value = '请输入账号和密码'
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ loading.value = true
|
|
|
+ error.value = ''
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await login({
|
|
|
+ username: formData.username,
|
|
|
+ password: formData.password
|
|
|
+ })
|
|
|
+
|
|
|
+ setToken(response.token)
|
|
|
+ emit('success')
|
|
|
+ } catch (err: any) {
|
|
|
+ error.value = err.message || '登录失败,请稍后重试'
|
|
|
+ console.error('Login failed:', err)
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleCancel = () => {
|
|
|
+ if (!loading.value) {
|
|
|
+ emit('cancel')
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.modal-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.9);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-content {
|
|
|
+ background: #1a1a1a;
|
|
|
+ border: 2px solid #00ff88;
|
|
|
+ border-radius: 8px;
|
|
|
+ width: 90%;
|
|
|
+ max-width: 400px;
|
|
|
+ box-shadow: 0 0 30px rgba(0, 255, 136, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.modal-header {
|
|
|
+ padding: 20px;
|
|
|
+ border-bottom: 1px solid #333;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-header h2 {
|
|
|
+ margin: 0;
|
|
|
+ color: #00ff88;
|
|
|
+ font-size: 24px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-body {
|
|
|
+ padding: 30px 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-message {
|
|
|
+ background: rgba(255, 68, 68, 0.15);
|
|
|
+ border: 1px solid #ff4444;
|
|
|
+ color: #ff6666;
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 14px;
|
|
|
+ animation: shake 0.5s;
|
|
|
+ box-shadow: 0 0 15px rgba(255, 68, 68, 0.2);
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes shake {
|
|
|
+ 0%,
|
|
|
+ 100% {
|
|
|
+ transform: translateX(0);
|
|
|
+ }
|
|
|
+ 10%,
|
|
|
+ 30%,
|
|
|
+ 50%,
|
|
|
+ 70%,
|
|
|
+ 90% {
|
|
|
+ transform: translateX(-5px);
|
|
|
+ }
|
|
|
+ 20%,
|
|
|
+ 40%,
|
|
|
+ 60%,
|
|
|
+ 80% {
|
|
|
+ transform: translateX(5px);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.form-group {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group label {
|
|
|
+ display: block;
|
|
|
+ color: #00ff88;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px;
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+ border: 1px solid #333;
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.password-input-wrapper {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.password-input-wrapper input {
|
|
|
+ padding-right: 45px;
|
|
|
+}
|
|
|
+
|
|
|
+.password-toggle {
|
|
|
+ position: absolute;
|
|
|
+ right: 10px;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: #666;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 5px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.password-toggle:hover {
|
|
|
+ color: #00ff88;
|
|
|
+}
|
|
|
+
|
|
|
+.form-group input:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #00ff88;
|
|
|
+ background: rgba(0, 255, 136, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.form-group input::placeholder {
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.form-actions {
|
|
|
+ margin-top: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary {
|
|
|
+ width: 100%;
|
|
|
+ padding: 12px;
|
|
|
+ background: #00ff88;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ color: #000;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: bold;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary:hover:not(:disabled) {
|
|
|
+ background: #00dd77;
|
|
|
+ box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary:disabled {
|
|
|
+ opacity: 0.6;
|
|
|
+ cursor: not-allowed;
|
|
|
+}
|
|
|
+</style>
|