Ver Fonte

update:login.componet and resigter.component

0235701 há 2 dias atrás
pai
commit
45ea96e764

+ 477 - 0
ai-assisant/src/modules/crm/mobile/page-crm-home/login.component.ts

@@ -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)">&times;</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')!; }
+}

+ 36 - 5
ai-assisant/src/modules/crm/mobile/page-crm-home/page-crm-home.html

@@ -20,13 +20,21 @@
                     <div class="logo-subtext">酒店销售智能平台</div>
                 </div>
             </div>
+
             <div class="user-actions">
-                <div class="action-btn" (click)="toggleMessagePopup()">
-                    <i class="fas fa-bell"></i>
-                    <div class="badge" *ngIf="unreadMessagesCount > 0">{{ unreadMessagesCount }}</div>
+                <div *ngIf="!isLoggedIn">
+                    <button class="action-btn" (click)="openLoginModal()">
+                        <i class="fas fa-sign-in-alt"></i> 登录
+                    </button>
                 </div>
-                <div class="action-btn" (click)="toggleProfilePopup()">
-                    <i class="fas fa-user"></i>
+                <div *ngIf="isLoggedIn">
+                    <div class="action-btn" (click)="toggleMessagePopup()">
+                        <i class="fas fa-bell"></i>
+                        <div class="badge" *ngIf="unreadMessagesCount > 0">{{ unreadMessagesCount }}</div>
+                    </div>
+                    <div class="action-btn" (click)="toggleProfilePopup()">
+                        <i class="fas fa-user"></i>
+                    </div>
                 </div>
             </div>
         </div>
@@ -189,6 +197,11 @@
                             <span class="detail-label">训练次数:</span>
                             <span class="detail-value">{{ user.trainingCount }} 次</span>
                         </div>
+                        <div class="detail-item">
+                            <button class="btn-danger" (click)="logout()">
+                                <i class="fas fa-sign-out-alt"></i> 登出
+                            </button>
+                        </div>
                     </div>
                 </div>
             </div>
@@ -199,5 +212,23 @@
             <p>© 2023 AI实验室 - 酒店销售智能平台 | 赋能销售精英,创造无限可能</p>
         </div>
     </div>
+
+    <!-- 登录和注册弹窗 -->
+    <app-login-modal 
+         [visible]="showLoginModal"
+            (close)="closeModals()"
+            (switchToRegister)="openRegisterModal()"
+            (loginSuccess)="handleLoginSuccess()">
+    </app-login-modal>
+
+    <app-register-modal 
+        [visible]="showRegisterModal"
+        (close)="closeModals()"
+        (switchToLogin)="openLoginModal()"
+        (registerSuccess)="handleRegisterSuccess()">
+         <div style="background: red; height: 200px; width: 200px;" *ngIf="showRegisterModal">
+    注册组件已渲染
+  </div>
+    </app-register-modal>
 </body>
 </html>

+ 13 - 0
ai-assisant/src/modules/crm/mobile/page-crm-home/page-crm-home.scss

@@ -899,4 +899,17 @@
     margin-top: 8px;
     align-self: flex-end;
   }
+}
+app-login-modal {
+  display: block;
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  pointer-events: none; /* 允许点击穿透到下层 */
+  
+  .modal-overlay {
+    pointer-events: auto; /* 恢复弹窗区域的点击 */
+  }
 }

+ 157 - 8
ai-assisant/src/modules/crm/mobile/page-crm-home/page-crm-home.ts

@@ -1,18 +1,21 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
 import { RouterModule } from '@angular/router';
-import { Component, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { LoginModalComponent } from './login.component';
+import { RegisterModalComponent } from './register.component';
 import { 
   faBrain, faBell, faUser, faRobot, faComments, 
   faDatabase, faUserTie, faTimes, faHistory, faStar, 
   faChartLine, faFileAlt, faBullhorn, faUserCircle,
   faFire, faTrophy, faCheck
 } from '@fortawesome/free-solid-svg-icons';
+import { CloudUser } from '../../../../lib/ncloud';
 
 @Component({
-  imports: [RouterModule, CommonModule, FontAwesomeModule],
+  imports: [RouterModule, CommonModule, FontAwesomeModule, LoginModalComponent, RegisterModalComponent],
   templateUrl: './page-crm-home.html',
-  styleUrl: './page-crm-home.scss',
+  styleUrls: ['./page-crm-home.scss'],
   selector: 'page-crm-home',
   standalone: true,
 })
@@ -25,6 +28,16 @@ export class PageCrmHome implements OnInit {
     faFire, faTrophy, faCheck
   };
 
+  // 弹窗控制变量
+  showLoginModal: boolean = false;
+  showRegisterModal: boolean = false;
+  loginError: string = '';
+  registerError: string = '';
+
+  // 当前用户信息
+  currentUser: any = null;
+  isLoggedIn: boolean = false;
+
   // 弹窗状态
   showMessagePopup: boolean = false;
   showProfilePopup: boolean = false;
@@ -130,10 +143,35 @@ export class PageCrmHome implements OnInit {
     "您不是一个人在战斗,AI是您最强大的后援!"
   ];
 
+  // 使用ViewChild直接引用模态框组件
+  @ViewChild(LoginModalComponent) loginModal!: LoginModalComponent;
+  @ViewChild(RegisterModalComponent) registerModal!: RegisterModalComponent;
+
+  // 跟踪打开的模态框数量
+  private modalOpenCount = 0;
+  // 记录模态框关闭原因
+  private modalCloseReason: string = '';
+
   ngOnInit(): void {
     this.updateUnreadCount();
     this.updateMotivationalText();
     
+    // 检查用户是否已登录
+    const cloudUser = new CloudUser();
+    cloudUser.current().then(currentUser => {
+      if (currentUser && currentUser.sessionToken) {
+        this.isLoggedIn = true;
+        this.currentUser = {
+          name: currentUser.get('username') || '用户',
+          role: currentUser.get('role') || '销售',
+          age: currentUser.get('age') || 30,
+          birthDate: currentUser.get('birthDate') || '1990-01-01',
+          trainingCount: currentUser.get('trainingCount') || 0
+        };
+        this.user = this.currentUser;
+      }
+    });
+    
     // 每10秒更新一次激励语句
     setInterval(() => this.updateMotivationalText(), 10000);
   }
@@ -143,6 +181,21 @@ export class PageCrmHome implements OnInit {
     this.motivationalText = this.motivationalPhrases[randomIndex];
   }
 
+  async logout(): Promise<void> {
+    const cloudUser = new CloudUser();
+    await cloudUser.logout();
+    this.isLoggedIn = false;
+    this.currentUser = null;
+    this.user = {
+      name: '访客',
+      role: '未登录用户',
+      age: 0,
+      birthDate: '',
+      trainingCount: 0
+    };
+    this.toggleProfilePopup();
+  }
+
   toggleMessagePopup(): void {
     this.showMessagePopup = !this.showMessagePopup;
     if (this.showMessagePopup) {
@@ -156,10 +209,7 @@ export class PageCrmHome implements OnInit {
 
   showMessageDetail(message: any, event?: Event): void {
     if (event) event.stopPropagation();
-    
     this.selectedMessage = message;
-    
-    // 如果消息未读,则标记为已读
     if (message.unread) {
       message.unread = false;
       this.updateUnreadCount();
@@ -196,7 +246,106 @@ export class PageCrmHome implements OnInit {
 
   navigateTo(featureId: string): void {
     console.log('导航至:', featureId);
-    // 实际项目中这里会有路由导航逻辑
-    // this.router.navigate([`/${featureId}`]);
+  }
+
+  // 改进的模态框控制方法
+  openLoginModal(): void {
+    // 确保其他模态框已关闭
+    this.showRegisterModal = false;
+    
+    // 重置登录表单状态
+    if (this.loginModal) {
+      this.loginModal.resetForm();
+    }
+    
+    this.showLoginModal = true;
+    this.modalOpenCount++;
+    this.modalCloseReason = '';
+    document.body.classList.add('modal-open');
+  }
+
+  openRegisterModal(): void {
+    console.log('接收到注册模态框打开请求'); // 添加日志
+    // 确保其他模态框已关闭
+    this.showLoginModal = false;
+        this.showRegisterModal = true;
+
+    // 重置注册表单状态
+    if (this.registerModal) {
+      this.registerModal.resetForm();
+    }
+    console.log('打开注册模态框后状态:', {
+    showLoginModal: this.showLoginModal,
+    showRegisterModal: this.showRegisterModal
+  });
+    this.modalOpenCount++;
+    this.modalCloseReason = '';
+    document.body.classList.add('modal-open');
+  }
+
+  closeModals(reason: string = 'manual'): void {
+    this.modalCloseReason = reason;
+    
+    // 逐步关闭模态框,避免状态冲突
+    if (this.showRegisterModal) {
+      this.showRegisterModal = false;
+      this.registerError = '';
+    }
+    
+    if (this.showLoginModal) {
+      this.showLoginModal = false;
+      this.loginError = '';
+    }
+    
+    this.modalOpenCount--;
+    
+    if (this.modalOpenCount <= 0) {
+      this.modalOpenCount = 0;
+      document.body.classList.remove('modal-open');
+      
+      // 确保页面滚动位置恢复
+      setTimeout(() => {
+        window.scrollTo(0, 0);
+      }, 100);
+    }
+  }
+
+  // 登录/注册成功处理
+  async handleLoginSuccess(): Promise<void> {
+    this.closeModals('login-success');
+    
+    // 延迟获取用户信息,确保会话已建立
+    setTimeout(async () => {
+      const cloudUser = new CloudUser();
+      const currentUser = await cloudUser.current();
+      if (currentUser) {
+        this.isLoggedIn = true;
+        this.currentUser = {
+          name: currentUser.get('username') || '用户',
+          role: currentUser.get('role') || '销售',
+          age: currentUser.get('age') || 30,
+          birthDate: currentUser.get('birthDate') || '1990-01-01',
+          trainingCount: currentUser.get('trainingCount') || 0
+        };
+        this.user = this.currentUser;
+        
+        // 登录成功提示
+        console.log('登录成功:', this.currentUser);
+      }
+    }, 300);
+  }
+
+  handleRegisterSuccess(): void {
+    this.closeModals('register-success');
+    
+    // 注册成功后自动切换到登录页面
+    setTimeout(() => {
+      this.openLoginModal();
+      
+      // 显示注册成功提示
+      if (this.loginModal) {
+        this.loginModal.showSuccessMessage('注册成功,请登录');
+      }
+    }, 300);
   }
 }

+ 450 - 0
ai-assisant/src/modules/crm/mobile/page-crm-home/register.component.ts

@@ -0,0 +1,450 @@
+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-register-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)">&times;</button>
+          </div>
+          
+          <form [formGroup]="registerForm" (ngSubmit)="onRegister($event)">
+            <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="请输入密码(至少6位)"
+                [ngClass]="{ 'error-border': password.touched && password.invalid }">
+              <div class="error-message" *ngIf="password.touched && password.hasError('required')">
+                密码不能为空
+              </div>
+              <div class="error-message" *ngIf="password.touched && password.hasError('minlength')">
+                密码长度至少6位
+              </div>
+            </div>
+            
+            <div class="form-group">
+              <label for="confirmPassword">确认密码</label>
+              <input 
+                type="password" 
+                id="confirmPassword" 
+                formControlName="confirmPassword" 
+                placeholder="请再次输入密码"
+                [ngClass]="{ 'error-border': confirmPassword.touched && (confirmPassword.invalid || registerForm.hasError('passwordMismatch')) }">
+              <div class="error-message" *ngIf="confirmPassword.touched && confirmPassword.hasError('required')">
+                请确认密码
+              </div>
+              <div class="error-message" *ngIf="confirmPassword.touched && registerForm.hasError('passwordMismatch')">
+                两次输入的密码不一致
+              </div>
+            </div>
+            
+            <button 
+              type="submit" 
+              class="submit-btn" 
+              [disabled]="registerForm.invalid || isLoading">
+              {{ isLoading ? '注册中...' : '立即注册' }}
+            </button>
+            
+            <div class="switch-link">
+              已有账号? 
+              <button type="button" class="login-link" (click)="onSwitchToLogin($event)">
+                立即登录
+              </button>
+            </div>
+          </form>
+          
+          <div class="global-error-message" *ngIf="errorMessage">
+            {{ 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;
+    }
+    
+    .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;
+    }
+    
+    .login-link {
+      background: none;
+      border: none;
+      color: #007bff;
+      cursor: pointer;
+      text-decoration: underline;
+      font-size: 0.9rem;
+    }
+    
+    .login-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 RegisterModalComponent implements OnChanges, AfterViewInit, OnDestroy {
+  @Input() visible: boolean = false;
+  @Output() close = new EventEmitter<void>();
+  @Output() switchToLogin = new EventEmitter<void>();
+  @Output() registerSuccess = new EventEmitter<void>();
+
+  registerForm: FormGroup;
+  errorMessage: string = '';
+  isLoading: boolean = false;
+  private modalElement!: HTMLElement;
+  private scrollTop: number = 0;
+
+  constructor(private fb: FormBuilder, private elementRef: ElementRef) {
+    this.registerForm = this.fb.group({
+      username: ['', Validators.required],
+      password: ['', [Validators.required, Validators.minLength(6)]],
+      confirmPassword: ['', Validators.required]
+    }, { validator: this.passwordMatchValidator });
+  }
+
+  ngAfterViewInit() {
+    this.modalElement = this.elementRef.nativeElement.querySelector('.modal-container');
+    if (this.visible) {
+      this.focusModal();
+    }
+  }
+
+  ngOnChanges() {
+    if (this.visible) {
+      this.disableBodyScroll();
+      this.resetForm();
+      setTimeout(() => this.focusModal(), 100);
+    } else {
+      this.enableBodyScroll();
+    }
+  }
+
+  ngOnDestroy() {
+    this.enableBodyScroll();
+  }
+
+  // 重置表单状态
+  resetForm() {
+    this.registerForm.reset();
+    this.errorMessage = '';
+    this.isLoading = false;
+  }
+
+  passwordMatchValidator(form: FormGroup) {
+    const password = form.get('password')?.value;
+    const confirmPassword = form.get('confirmPassword')?.value;
+    return password === confirmPassword ? null : { passwordMismatch: true };
+  }
+
+  private focusModal() {
+    if (this.modalElement) {
+      this.modalElement.focus();
+      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();
+  if (this.modalElement) { // 增加非空判断,避免 modalElement 为 null 时报错
+    this.modalElement.style.transform = 'scale(0.95)';
+    this.modalElement.style.opacity = '0';
+  }
+  setTimeout(() => {
+    this.visible = false;
+    this.close.emit();
+  }, 200);
+}
+
+
+  onSwitchToLogin(event: Event) {
+    event.stopPropagation();
+    
+    // 添加切换动画
+    this.modalElement.style.transform = 'scale(0.95)';
+    this.modalElement.style.opacity = '0';
+    
+    setTimeout(() => {
+      this.visible = false;
+      this.switchToLogin.emit();
+    }, 200);
+  }
+
+  async onRegister(event: Event) {
+    event.preventDefault();
+    
+    if (this.registerForm.invalid || this.isLoading) {
+      return;
+    }
+
+    this.isLoading = true;
+    this.errorMessage = '';
+
+    try {
+      const { username, password } = this.registerForm.value;
+      const cloudUser = new CloudUser();
+      
+      // 调用后端注册接口
+      const user = await cloudUser.signUp(username, password, {
+  role: 'sales',
+  trainingCount: 0,
+});
+
+      if (user) {
+        this.registerSuccess.emit();
+        this.visible = false;
+      } else {
+        this.errorMessage = '注册失败,请稍后重试';
+      }
+    } catch (error: any) {
+      // 处理常见错误(如用户名已存在)
+      if (error.message.includes('用户名已存在')) {
+        this.errorMessage = '该用户名已被注册,请更换其他用户名';
+      } else {
+        this.errorMessage = error?.message || '注册失败,请检查网络连接';
+      }
+      console.error('注册错误:', error);
+    } finally {
+      this.isLoading = false;
+    }
+  }
+
+  get username() { return this.registerForm.get('username')!; }
+  get password() { return this.registerForm.get('password')!; }
+  get confirmPassword() { return this.registerForm.get('confirmPassword')!; }
+}

+ 4 - 6
ai-assisant/src/modules/crm/mobile/page-crm-training/page-crm-training.scss

@@ -515,19 +515,17 @@
 
 /* 弹窗样式 */
 .modal-overlay {
-  position: fixed;
+   position: fixed;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
-  background-color: rgba(0, 0, 0, 0.5);
+  background: rgba(0, 0, 0, 0.6); /* 加深背景透明度,减少透底 */
+  z-index: 9999; /* 提高层级,确保覆盖所有主页面元素 */
   display: flex;
   align-items: center;
   justify-content: center;
-  z-index: 1000;
-  opacity: 0;
-  pointer-events: none;
-  transition: opacity 0.3s ease;
+  backdrop-filter: blur(3px); /* 可选:添加背景模糊,增强层次感 */
 }
 
 .modal-overlay.active {