|
@@ -0,0 +1,477 @@
|
|
|
+import { Component, Output, EventEmitter, Input, OnChanges, OnDestroy, AfterViewInit, ElementRef, HostListener } from '@angular/core';
|
|
|
+import { CommonModule } from '@angular/common';
|
|
|
+import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
|
+import { CloudUser } from '../../../../lib/ncloud';
|
|
|
+
|
|
|
+@Component({
|
|
|
+ selector: 'app-login-modal',
|
|
|
+ standalone: true,
|
|
|
+ imports: [CommonModule, ReactiveFormsModule],
|
|
|
+ template: `
|
|
|
+ <div class="modal-overlay" *ngIf="visible" (click)="onClose($event)">
|
|
|
+ <div class="overlay-backdrop" (click)="onClose($event)"></div>
|
|
|
+
|
|
|
+ <div class="modal-container" tabindex="-1" (click)="$event.stopPropagation()">
|
|
|
+ <div class="modal-content">
|
|
|
+ <div class="modal-header">
|
|
|
+ <h2>用户登录</h2>
|
|
|
+ <button class="close-btn" (click)="onClose($event)">×</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 成功消息提示 -->
|
|
|
+ <div class="success-message" *ngIf="successMessage">
|
|
|
+ <div class="success-icon">
|
|
|
+ <i class="fas fa-check-circle"></i>
|
|
|
+ </div>
|
|
|
+ <div class="success-text">{{ successMessage }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <form [formGroup]="loginForm" (ngSubmit)="onLogin($event)" *ngIf="!successMessage">
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="username">用户名</label>
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="username"
|
|
|
+ formControlName="username"
|
|
|
+ placeholder="请输入用户名"
|
|
|
+ [ngClass]="{ 'error-border': username.touched && username.invalid }">
|
|
|
+ <div class="error-message" *ngIf="username.touched && username.hasError('required')">
|
|
|
+ 用户名不能为空
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="form-group">
|
|
|
+ <label for="password">密码</label>
|
|
|
+ <input
|
|
|
+ type="password"
|
|
|
+ id="password"
|
|
|
+ formControlName="password"
|
|
|
+ placeholder="请输入密码"
|
|
|
+ [ngClass]="{ 'error-border': password.touched && password.invalid }">
|
|
|
+ <div class="error-message" *ngIf="password.touched && password.hasError('required')">
|
|
|
+ 密码不能为空
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ class="submit-btn"
|
|
|
+ [disabled]="loginForm.invalid || isLoading">
|
|
|
+ {{ isLoading ? '登录中...' : '立即登录' }}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div class="switch-link">
|
|
|
+ 还没有账号?
|
|
|
+ <button type="button" class="register-link" (click)="onSwitchToRegister($event)">
|
|
|
+ 立即注册
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ <div class="global-error-message" *ngIf="errorMessage && !successMessage">
|
|
|
+ {{ errorMessage }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `,
|
|
|
+ styles: [`
|
|
|
+ .modal-overlay {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ z-index: 1000;
|
|
|
+ overflow-y: auto;
|
|
|
+ pointer-events: all;
|
|
|
+ user-select: none;
|
|
|
+ opacity: 0;
|
|
|
+ animation: fadeInOverlay 0.3s forwards;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes fadeInOverlay {
|
|
|
+ to { opacity: 1; }
|
|
|
+ }
|
|
|
+
|
|
|
+ .overlay-backdrop {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: rgba(0, 0, 0, 0.5);
|
|
|
+ z-index: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-container {
|
|
|
+ position: relative;
|
|
|
+ z-index: 2;
|
|
|
+ width: 90%;
|
|
|
+ max-width: 400px;
|
|
|
+ background-color: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
|
+ margin: 20px;
|
|
|
+ outline: none;
|
|
|
+ pointer-events: all;
|
|
|
+ transform: scale(0.95);
|
|
|
+ opacity: 0;
|
|
|
+ animation: fadeInModal 0.3s forwards;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes fadeInModal {
|
|
|
+ to {
|
|
|
+ transform: scale(1);
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-header {
|
|
|
+ padding: 20px;
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-header h2 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 1.2rem;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ font-size: 1.5rem;
|
|
|
+ color: #999;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: color 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close-btn:hover {
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-content {
|
|
|
+ padding: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .success-message {
|
|
|
+ background-color: #e8f5e9;
|
|
|
+ color: #2e7d32;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ .success-icon {
|
|
|
+ font-size: 1.5rem;
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .success-text {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group label {
|
|
|
+ display: block;
|
|
|
+ margin-bottom: 5px;
|
|
|
+ font-size: 0.9rem;
|
|
|
+ color: #555;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group input {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ font-size: 1rem;
|
|
|
+ transition: border-color 0.2s;
|
|
|
+ }
|
|
|
+
|
|
|
+ .form-group input:focus {
|
|
|
+ outline: none;
|
|
|
+ border-color: #007bff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .error-border {
|
|
|
+ border-color: #dc3545 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .error-message {
|
|
|
+ color: #dc3545;
|
|
|
+ font-size: 0.8rem;
|
|
|
+ margin-top: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .submit-btn {
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #007bff;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 1rem;
|
|
|
+ transition: background-color 0.2s;
|
|
|
+ margin-top: 10px;
|
|
|
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .submit-btn:hover {
|
|
|
+ background-color: #0069d9;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ .submit-btn:disabled {
|
|
|
+ background-color: #6c757d;
|
|
|
+ cursor: not-allowed;
|
|
|
+ box-shadow: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .switch-link {
|
|
|
+ text-align: center;
|
|
|
+ margin-top: 15px;
|
|
|
+ font-size: 0.9rem;
|
|
|
+ color: #555;
|
|
|
+ }
|
|
|
+
|
|
|
+ .register-link {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: #007bff;
|
|
|
+ cursor: pointer;
|
|
|
+ text-decoration: underline;
|
|
|
+ font-size: 0.9rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .register-link:hover {
|
|
|
+ color: #0056b3;
|
|
|
+ }
|
|
|
+
|
|
|
+ .global-error-message {
|
|
|
+ color: #dc3545;
|
|
|
+ font-size: 0.9rem;
|
|
|
+ text-align: center;
|
|
|
+ margin-top: 15px;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f8d7da;
|
|
|
+ border-radius: 4px;
|
|
|
+ opacity: 0;
|
|
|
+ animation: fadeInError 0.3s forwards;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes fadeInError {
|
|
|
+ to { opacity: 1; }
|
|
|
+ }
|
|
|
+ `]
|
|
|
+})
|
|
|
+export class LoginModalComponent implements OnChanges, AfterViewInit, OnDestroy {
|
|
|
+ @Input() visible: boolean = false;
|
|
|
+ @Output() close = new EventEmitter<void>();
|
|
|
+ @Output() switchToRegister = new EventEmitter<void>();
|
|
|
+ @Output() loginSuccess = new EventEmitter<void>();
|
|
|
+
|
|
|
+ loginForm: FormGroup;
|
|
|
+ errorMessage: string = '';
|
|
|
+ successMessage: string = '';
|
|
|
+ isLoading: boolean = false;
|
|
|
+ private modalElement!: HTMLElement;
|
|
|
+ private scrollTop: number = 0;
|
|
|
+ private successMessageTimeout: any;
|
|
|
+
|
|
|
+ constructor(private fb: FormBuilder, private elementRef: ElementRef) {
|
|
|
+ this.loginForm = this.fb.group({
|
|
|
+ username: ['', Validators.required],
|
|
|
+ password: ['', Validators.required]
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ ngAfterViewInit() {
|
|
|
+ this.modalElement = this.elementRef.nativeElement.querySelector('.modal-container');
|
|
|
+ if (this.visible) {
|
|
|
+ this.focusModal();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ngOnChanges() {
|
|
|
+ if (this.visible) {
|
|
|
+ this.disableBodyScroll();
|
|
|
+
|
|
|
+ // 重置表单状态
|
|
|
+ this.resetForm();
|
|
|
+
|
|
|
+ // 延迟聚焦以确保DOM已渲染
|
|
|
+ setTimeout(() => this.focusModal(), 100);
|
|
|
+ } else {
|
|
|
+ this.enableBodyScroll();
|
|
|
+ this.clearSuccessMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ngOnDestroy() {
|
|
|
+ this.enableBodyScroll();
|
|
|
+ this.clearSuccessMessage();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置表单状态
|
|
|
+ resetForm() {
|
|
|
+ this.loginForm.reset();
|
|
|
+ this.errorMessage = '';
|
|
|
+ this.successMessage = '';
|
|
|
+ this.isLoading = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示成功消息
|
|
|
+ showSuccessMessage(message: string) {
|
|
|
+ this.clearSuccessMessage();
|
|
|
+ this.successMessage = message;
|
|
|
+
|
|
|
+ // 3秒后自动清除成功消息
|
|
|
+ this.successMessageTimeout = setTimeout(() => {
|
|
|
+ this.successMessage = '';
|
|
|
+ }, 3000);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除成功消息
|
|
|
+ clearSuccessMessage() {
|
|
|
+ if (this.successMessageTimeout) {
|
|
|
+ clearTimeout(this.successMessageTimeout);
|
|
|
+ this.successMessageTimeout = null;
|
|
|
+ }
|
|
|
+ this.successMessage = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ private focusModal() {
|
|
|
+ if (this.modalElement) {
|
|
|
+ this.modalElement.focus();
|
|
|
+
|
|
|
+ // 如果没有成功消息,聚焦第一个输入框
|
|
|
+ if (!this.successMessage) {
|
|
|
+ const firstInput = this.modalElement.querySelector('input');
|
|
|
+ if (firstInput) {
|
|
|
+ (firstInput as HTMLElement).focus();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @HostListener('keydown.tab', ['$event'])
|
|
|
+ onTabKey(event: Event) {
|
|
|
+ const keyboardEvent = event as KeyboardEvent;
|
|
|
+
|
|
|
+ if (!this.modalElement) return;
|
|
|
+
|
|
|
+ const focusableElements = this.modalElement.querySelectorAll(
|
|
|
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
|
+ );
|
|
|
+
|
|
|
+ if (focusableElements.length === 0) return;
|
|
|
+
|
|
|
+ const firstElement = focusableElements[0] as HTMLElement;
|
|
|
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
|
|
|
+
|
|
|
+ if (keyboardEvent.shiftKey) {
|
|
|
+ if (document.activeElement === firstElement) {
|
|
|
+ lastElement.focus();
|
|
|
+ keyboardEvent.preventDefault();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (document.activeElement === lastElement) {
|
|
|
+ firstElement.focus();
|
|
|
+ keyboardEvent.preventDefault();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private disableBodyScroll() {
|
|
|
+ this.scrollTop = window.scrollY;
|
|
|
+ document.body.style.overflow = 'hidden';
|
|
|
+ document.body.style.position = 'fixed';
|
|
|
+ document.body.style.top = `-${this.scrollTop}px`;
|
|
|
+ document.body.style.width = '100%';
|
|
|
+ }
|
|
|
+
|
|
|
+ private enableBodyScroll() {
|
|
|
+ document.body.style.overflow = '';
|
|
|
+ document.body.style.position = '';
|
|
|
+ document.body.style.top = '';
|
|
|
+ document.body.style.width = '';
|
|
|
+ window.scrollTo(0, this.scrollTop);
|
|
|
+ }
|
|
|
+
|
|
|
+ onClose(event: Event) {
|
|
|
+ event.stopPropagation();
|
|
|
+
|
|
|
+ // 添加关闭动画
|
|
|
+ this.modalElement.style.transform = 'scale(0.95)';
|
|
|
+ this.modalElement.style.opacity = '0';
|
|
|
+
|
|
|
+ // 延迟关闭以完成动画
|
|
|
+ setTimeout(() => {
|
|
|
+ this.visible = false;
|
|
|
+ this.close.emit();
|
|
|
+ }, 200);
|
|
|
+ }
|
|
|
+
|
|
|
+ onSwitchToRegister(event: Event) {
|
|
|
+ event.stopPropagation();
|
|
|
+ console.log('触发了注册切换事件'); // 添加日志
|
|
|
+
|
|
|
+ if (this.modalElement) {
|
|
|
+ this.modalElement.style.transform = 'scale(0.95)';
|
|
|
+ this.modalElement.style.opacity = '0';
|
|
|
+ }
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ console.log('准备切换到注册页面'); // 添加日志
|
|
|
+ this.visible = false;
|
|
|
+ this.switchToRegister.emit();
|
|
|
+ }, 200);
|
|
|
+}
|
|
|
+
|
|
|
+ async onLogin(event: Event) {
|
|
|
+ event.preventDefault();
|
|
|
+
|
|
|
+ if (this.loginForm.invalid || this.isLoading) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isLoading = true;
|
|
|
+ this.errorMessage = '';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const { username, password } = this.loginForm.value;
|
|
|
+ const cloudUser = new CloudUser();
|
|
|
+ const user = await cloudUser.login(username, password);
|
|
|
+
|
|
|
+ if (user) {
|
|
|
+ this.loginSuccess.emit();
|
|
|
+ this.visible = false;
|
|
|
+ } else {
|
|
|
+ this.errorMessage = '用户名或密码错误';
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ this.errorMessage = error?.message || '登录失败,请检查网络连接';
|
|
|
+ console.error('登录错误:', error);
|
|
|
+ } finally {
|
|
|
+ this.isLoading = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ get username() { return this.loginForm.get('username')!; }
|
|
|
+ get password() { return this.loginForm.get('password')!; }
|
|
|
+}
|