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'],
});
}
}
评论