2.加入signup页面和verify-email验证功能

Article detail

前端

2026/4/20 · 73 分钟阅读

2.加入signup页面和verify-email验证功能

在app文件中输入

ng g c signup
ng g c login

signup.css

/* Page frame */

.signup-page {

  background: #141414;

  color: #fff;

  min-height: 100vh;

  min-height: 100dvh;

  width: 100vw;

  position: relative;

  overflow: hidden;

  font-family: 'Helvetica Neue', Arial, sans-serif;

}

  

/* Background collage + dark overlay */

.bg {

  position: absolute;

  inset: 0;

  background-image:

    linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0.3) 40%, rgba(0,0,0,0.9)),

     url('/landing.jpg');

  background-size: cover;

  background-position: center;

  filter: brightness(0.9);

  z-index: 1;

}

  

/* PulseScreen logo */

.logo {

  position: absolute;

  left: 32px;

  top: 18px;

  z-index: 2;

  display: flex;

  align-items: center;

  gap: 12px;

  font-size: 24px;

  font-weight: 700;

  letter-spacing: -0.5px;

  flex-shrink: 0;

}

  

.logo-icon {

  font-size: 32px;

  width: 32px;

  height: 32px;

  color: #e50914;

}

  

.logo-text {

  color: #e50914;

  font-weight: 900;

  letter-spacing: -0.5px;

}

  

/* The card */

.signup-card {

  position: relative;

  z-index: 2;

  margin: 0 auto;

  margin-top: clamp(64px, 12vh, 120px);

  width: min(92vw, 440px);

  padding: 36px 40px 28px;

  background: rgba(0, 0, 0, 0.75) !important;

  border: 1px solid rgba(255,255,255,0.08);

  border-radius: 6px;

  box-shadow: 0 8px 32px rgba(0,0,0,0.6);

  

  h1 {

    margin: 0 0 12px 0;

    font-size: 32px;

    font-weight: 700;

  }

  

  h2 {

    margin: 12px 0 8px 0;

    font-size: 24px;

    font-weight: 600;

    text-align: center;

  }

  

  .subtitle {

    margin: 0 0 24px 0;

    color: #b3b3b3;

    font-size: 16px;

    line-height: 1.4;

  }

}

  

/* Form */

.form {

  display: flex;

  flex-direction: column;

}

  

.field {

  width: 100%;

  margin-bottom: 14px;

  

  .mat-mdc-input-element {

    color: #fff;

  }

  .mat-mdc-form-field-icon-prefix .mat-icon {

    color: rgba(255,255,255,0.7);

    margin-right: 8px;

  }

  .mat-mdc-form-field-icon-suffix .mat-icon {

    color: rgba(255,255,255,0.8);

  }

}

  

/* Outline field borders */

:host ::ng-deep .signup-card .mdc-text-field--outlined .mdc-notched-outline__leading,

:host ::ng-deep .signup-card .mdc-text-field--outlined .mdc-notched-outline__notch,

:host ::ng-deep .signup-card .mdc-text-field--outlined .mdc-notched-outline__trailing {

  border-color: rgba(255,255,255,0.35);

}

:host ::ng-deep .signup-card .mdc-text-field--focused .mdc-notched-outline__leading,

:host ::ng-deep .signup-card .mdc-text-field--focused .mdc-notched-outline__notch,

:host ::ng-deep .signup-card .mdc-text-field--focused .mdc-notched-outline__trailing {

  border-color: #fff;

}

:host ::ng-deep .signup-card .mdc-text-field .mdc-floating-label,

:host ::ng-deep .signup-card .mat-mdc-form-field-hint-wrapper,

:host ::ng-deep .signup-card .mat-mdc-form-field-subscript-wrapper {

  color: #b3b3b3 !important;

}

  

/* Error message */

.error-message {

  display: flex;

  align-items: center;

  gap: 8px;

  padding: 12px;

  margin-bottom: 12px;

  background: rgba(229, 9, 20, 0.1);

  border: 1px solid rgba(229, 9, 20, 0.3);

  border-radius: 4px;

  color: #ff6b6b;

  font-size: 14px;

  

  mat-icon {

    font-size: 20px;

    width: 20px;

    height: 20px;

  }

}

  

/* Submit Button */

.sign-up-btn {

  width: 100%;

  height: 48px;

  margin-top: 10px;

  font-weight: 700;

  background: #e50914 !important;

  color: #fff !important;

  border-radius: 4px;

  transition: all 0.2s ease;

  

  &:hover:not(:disabled) {

    background: #f40612 !important;

  }

  

  &:disabled {

    opacity: 0.6;

    cursor: not-allowed;

  }

}

  

/* Sign in link */

.signin {

  margin-top: 20px;

  text-align: center;

  color: #b3b3b3;

  

  .link {

    color: #fff;

    text-decoration: none;

    margin-left: 6px;

    font-size: 14px;

  

    &:hover {

      text-decoration: underline;

    }

  }

}

  

/* Mobile spacing tweaks */

@media (max-width: 600px) {

  .signup-card {

    padding: 28px 24px 24px;

  }

}

signup.ts

import { Component, OnInit } from '@angular/core';

import { ErrorHandlerService } from '../shared/services/error-handler-service';

import { AuthService } from '../shared/services/auth-service';

import { NotificationService } from '../shared/services/notification-service';

import { FormGroup, FormBuilder, Validators } from '@angular/forms';

import { ActivatedRoute, Router } from '@angular/router';

  

@Component({

  selector: 'app-signup',

  standalone: false,

  templateUrl: './signup.html',

  styleUrls: ['./signup.css'],

})

export class Signup implements OnInit {

  hidePassword = true;

  hideConfirmPassword = true;

  signupForm!: FormGroup;

  loading = false;

  

  constructor(

    private fb: FormBuilder,

    private authService: AuthService,

    private router: Router,

    private route: ActivatedRoute,

    private notification: NotificationService,

    private errorHandlerService: ErrorHandlerService,

  ){

    this.signupForm = this.fb.group({

      fullName: ['', [Validators.required, Validators.minLength(2)]],

      email: ['', [Validators.required, Validators.email]],

      password: ['', [Validators.required, Validators.minLength(6)]],

      confirmPassword: ['', [Validators.required, this.authService.passwrodMatchValidator('password')]]

    });

  }

  

  ngOnInit(): void {

    const email = this.route.snapshot.queryParamMap.get('email');

    if (email) {

      this.signupForm.patchValue({ email: email });

      console.log('prefill signup email:', email);

    }

  }

  

  submit(){

    this.loading = true;

    const formData = this.signupForm.value;

    const data = {

      fullName: formData.fullName,

      email: formData.email?.trim().toLowerCase(),

      password: formData.password

    };

  

    this.authService.signup(data).subscribe({

      next: (response: any) => {

        this.loading = false;

        this.notification.sccess(response?.message || 'Signup successful! Please check your email to verify your account.');

        this.router.navigate(['/login']);

      },

      error: (err) => {

        this.loading = false;

        this.errorHandlerService.handle(err, 'Signup failed. Please try again later.');

      }

    });

}

  

  }

signup.html

<div class="signup-page">

  <div class="bg"></div>

  

  <div class="logo">

    <mat-icon class="logo-icon">movie</mat-icon>

    <span class="logo-text">Pudplanet</span>

  </div>

  <mat-card class="signup-card" appearance="outlined">

    <h1>Sign Up</h1>

    <p class="subtitle">Create your account to get started</p>

    <form [formGroup]="signupForm" (ngSubmit)="submit()" class="form">

      <mat-form-field appearance="outline" class="field">

        <mat-label>Full Name</mat-label>

        <input matInput formControlName="fullName" >

        <mat-icon matPrefix>person</mat-icon>

        <mat-error *ngIf="signupForm['get']('fullName')?.hasError('required')">

          Full name is required

        </mat-error>

        <mat-error *ngIf="signupForm['get']('fullName')?.hasError('minlength')">

          Full name must be at least 2 characters long

        </mat-error>

      </mat-form-field>

  

      <mat-form-field appearance="outline" class="field">

        <mat-label>Email</mat-label>

        <input matInput formControlName="email" >

        <mat-icon matPrefix>email</mat-icon>

        <mat-error *ngIf="signupForm['get']('email')?.hasError('required')">

          Email is required

        </mat-error>

        <mat-error *ngIf="signupForm['get']('email')?.hasError('email')">

          Please enter a valid email address

        </mat-error>

      </mat-form-field>

  

      <mat-form-field appearance="outline" class="field">

        <mat-label>Password</mat-label>

        <input matInput formControlName="password" [type]="hidePassword? 'password' : 'text'" >

        <mat-icon matPrefix>lock</mat-icon>

        <button mat-icon-button matSuffix type="button" (click)="hidePassword = !hidePassword" tabindex="-1">

          <mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>

        </button>

        <mat-error *ngIf="signupForm['get']('password')?.hasError('required')">

          Password is required

        </mat-error>

        <mat-error *ngIf="signupForm['get']('password')?.hasError('minlength')">

          Password must be at least 6 characters

        </mat-error>

      </mat-form-field>

  

      <mat-form-field appearance="outline" class="field">

        <mat-label>Confirm Password</mat-label>

        <input matInput formControlName="confirmPassword" [type]="hideConfirmPassword? 'password' : 'text'" >

        <mat-icon matPrefix>lock</mat-icon>

        <button mat-icon-button matSuffix type="button" (click)="hideConfirmPassword = !hideConfirmPassword" tabindex="-1">

          <mat-icon>{{ hideConfirmPassword ? 'visibility_off' : 'visibility' }}</mat-icon>

        </button>

        <mat-error *ngIf="signupForm['get']('confirmPassword')?.hasError('required')">

          Confirm password is required

        </mat-error>

        <mat-error *ngIf="signupForm['get']('confirmPassword')?.hasError('passwordMismatch')">

          Passwords do not match

        </mat-error>

      </mat-form-field>

  

      <button mat-raised-button color="primary" class="sign-up-btn" type="submit" [disabled]="signupForm.invalid">

        Sign Up

      </button>

    </form>

    <div class="signin">

      Already have an account?

      <a class="link" routerLink="/login">Sign In</a>

    </div>

  </mat-card>

</div>

然后再src下面开启终端

ng g environments

和然后再分别再两个文件中输入 environment.development.ts

//连接后端接口的环境配置文件

export const environment = {

  production: false,

  apiUrl: 'http://localhost:8080/api' // 替换为你的后端API地址

};

environment.ts

//连接后端接口的环境配置文件

export const environment = {

  production: true,

  apiUrl: 'http://localhost:8080/api' // 替换为你的后端API地址

};

然后app再终端

ng g c verify-email

verify-email.html

<div class="auth-page">

  <div class="bg"></div>

  <div class="logo">

    <div class="logo-icon">P</div>

    <div class="logo-text">PulseScreen</div>

  </div>

  <div class="auth-card" appearance="outlined">

    <div *ngIf="loading" class="loading-container">

      <mat-spinner diameter="50"></mat-spinner>

      <h2>Verifying Email...</h2>

    </div>

  

    <div *ngIf="!loading && success" class="success-container">

      <mat-icon class="success-icon">check_circle</mat-icon>

      <h1>Email Verified!</h1>

      <p class="message">{{ message }}</p>

      <button mat-flat-button class="action-btn" routerLink="/login">Go to Login</button>

    </div>

  

    <div *ngIf="!loading && !success" class="error-container">

      <mat-icon class="error-icon">error_outline</mat-icon>

      <h1>Verification Failed</h1>

      <p class="message">{{ message }}</p>

      <div class="actions">

        <button mat-flat-button class="action-btn" routerLink="/login">Go to Login</button>

        <p class="resend-info">

          Didn't receive the verification email? <a routerLink="/resend-verification">Resend it</a>

        </p>

      </div>

    </div>

  </div>

</div>

verify-email.css

.auth-page {

  background: #141414;

  color: #fff;

  min-height: 100vh;

  width: 100vw;

  position: relative;

  overflow: hidden;

  font-family: 'Helvetica Neue', Arial, sans-serif;

  display: flex;

  align-items: center;

  justify-content: center;

}

  

.bg {

  position: absolute;

  inset: 0;

  background-image:

    linear-gradient(to top, rgba(0,0,0,0.9), rgba(0,0,0,0.3) 40%, rgba(0,0,0,0.9)),

    url('/landing.jpg');

  background-size: cover;

  background-position: center;

  filter: brightness(0.9);

  z-index: 1;

}

  

/* PulseScreen logo */

.logo {

  position: absolute;

  left: 32px;

  top: 18px;

  z-index: 2;

  display: flex;

  align-items: center;

  gap: 12px;

  font-size: 24px;

  font-weight: 700;

  letter-spacing: -0.5px;

  flex-shrink: 0;

}

  

.logo-icon {

  font-size: 32px;

  width: 32px;

  height: 32px;

  color: #e50914;

}

  

.logo-text {

  background: linear-gradient(135deg, #e50914 0%, #ff6b6b 100%);

  -webkit-background-clip: text;

  -webkit-text-fill-color: transparent;

  background-clip: text;

}

  

.auth-card {

  position: relative;

  z-index: 2;

  width: min(92vw, 480px);

  padding: 48px 40px;

  background: rgba(0, 0, 0, 0.75) !important;

  border: 1px solid rgba(255,255,255,0.08);

  border-radius: 8px;

  box-shadow: 0 8px 32px rgba(0,0,0,0.6);

  text-align: center;

}

  

.loading-container,

.success-container,

.error-container {

  display: flex;

  flex-direction: column;

  align-items: center;

  gap: 20px;

}

  

.loading-container h2 {

  margin: 0;

  font-size: 24px;

  font-weight: 600;

  color: rgba(255, 255, 255, 0.9);

}

  

.success-icon {

  font-size: 80px;

  width: 80px;

  height: 80px;

  color: #4caf50;

}

  

.error-icon {

  font-size: 80px;

  width: 80px;

  height: 80px;

  color: #e50914;

}

  

h1 {

  margin: 0;

  font-size: 32px;

  font-weight: 700;

  color: #fff;

}

  

.message {

  margin: 0;

  font-size: 16px;

  color: rgba(255, 255, 255, 0.8);

  line-height: 1.6;

}

  

.actions {

  display: flex;

  flex-direction: column;

  gap: 12px;

  width: 100%;

  max-width: 300px;

  margin-top: 8px;

}

  

.action-btn {

  width: 100%;

  height: 48px;

  font-weight: 700;

  background: #e50914 !important;

  color: #fff !important;

  border-radius: 4px;

  transition: all 0.3s ease;

}

  

.action-btn:hover {

  background: #f40612 !important;

  transform: translateY(-1px);

}

  

.secondary-btn {

  width: 100%;

  height: 48px;

  font-weight: 600;

  color: #fff !important;

  border-color: rgba(255, 255, 255, 0.3) !important;

  border-radius: 4px;

  transition: all 0.3s ease;

}

  

.secondary-btn:hover {

  background: rgba(255, 255, 255, 0.1) !important;

  border-color: rgba(255, 255, 255, 0.5) !important;

}

  

@media (max-width: 600px) {

  .auth-card {

    padding: 36px 24px;

  }

  

  h1 {

    font-size: 26px;

  }

  

  .success-icon,

  .error-icon {

    font-size: 60px;

    width: 60px;

    height: 60px;

  }

}

verify-email.ts

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';

import { ActivatedRoute } from '@angular/router';

import { AuthService } from '../shared/services/auth-service';

  

@Component({

  selector: 'app-verify-email',

  standalone: false,

  templateUrl: './verify-email.html',

  styleUrls: ['./verify-email.css'],

})

export class VerifyEmail implements OnInit {

  loading: boolean = true;

  success: boolean = false;

  message: string = '';

  

  constructor(

    private route: ActivatedRoute,

    private authService: AuthService,

    private cdr: ChangeDetectorRef

  ){

  }

  

  ngOnInit(): void {

    const token = this.route.snapshot.queryParamMap.get('token');

    console.log('[VerifyEmail] token:', token);

  

    if (!token) {

      this.loading = false;

      this.success = false;

      this.message = 'Invalid verification link. No token provided.';

      console.log('[VerifyEmail] No token provided');

      return;

    }

  

    this.authService.verifyEmail(token).subscribe({

      next: (response: any) => {

        this.loading = false;

        this.success = true;

        this.message = response?.message || 'Email verified successfully! You can now log in.';

        this.cdr.detectChanges();

        console.log('[VerifyEmail] Success:', response);

      },

      error: (err) => {

        this.loading = false;

        this.success = false;

        this.message = err?.error?.message || err?.error?.err || 'Verification failed. The link may be invalid or expired.';

        this.cdr.detectChanges();

        console.log('[VerifyEmail] Error:', err);

      }

    });

  }

}

然后再终端去到shared文件

ng g s auth-servie
ng g s error-handler
ng g s notification-service

然后再auth-service.ts

import { Injectable } from '@angular/core';

import { environment } from '../../../environments/environment.development';

import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';

import { ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';

  

@Injectable({

  providedIn: 'root',

})

export class AuthService {

  private apiUrl = environment.apiUrl + '/auth'; // 后端认证API的基础URL

  

  constructor(private http: HttpClient) {}

  

  passwrodMatchValidator(passwrodControlName:string): ValidatorFn {

    return (confirmControl: AbstractControl): ValidationErrors | null => {

      if(!confirmControl.parent) return null; // 确保父控件存在

      const password = confirmControl.parent.get(passwrodControlName)?.value; // 获取密码控件的值

      const confirmPassword = confirmControl.value; // 获取确认密码控件的值

      if (password !== confirmPassword) {

        return { passwordMismatch: true }; // 返回验证错误对象

      }

      return null; // 验证通过,返回null

    };

  }

  

  signup(signupData: any): Observable<any> {

    return this.http.post<any>(`${this.apiUrl}/signup`, signupData);

  }

  

  verifyEmail(token: string) {

    return this.http.get(`${this.apiUrl}/verify-email?token=${token}`);

  }

  
  

}

error-handler-service.ts

import { Injectable } from '@angular/core';

import { NotificationService } from './notification-service';

  

@Injectable({

  providedIn: 'root',

})

export class ErrorHandlerService {

  constructor(private notificationService: NotificationService) {}

  

  handle(err:any, fallbackMessage: string) {

    const errorMsg = err.console.error.error || fallbackMessage;

    this.notificationService.error(errorMsg);

    console.error('API Error:', err);

  }

}

notification-service.ts

import { Injectable } from '@angular/core';

import { MatSnackBar } from '@angular/material/snack-bar';

  

@Injectable({

  providedIn: 'root',

})

export class NotificationService {

  constructor(private snackBar: MatSnackBar) {}

  

  sccess(message: string, duration: number = 3000) {

    this.snackBar.open(message, 'Close', {

      duration: duration,

      horizontalPosition: 'center',

      verticalPosition: 'top',

      panelClass: ['notification-success'],

    });

  }

  

  error(message: string, duration: number = 5000) {

    this.snackBar.open(message, 'Close', {

      duration: duration,

      horizontalPosition: 'center',

      verticalPosition: 'top',

      panelClass: ['notification-error'],

    });

  }

}

评论

动作测试