4.

Article detail

学习笔记

2026/4/20 · 50 分钟阅读

4.

创建login

ng g c login

login.css

/* Page frame */

.auth-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 {

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

  -webkit-background-clip: text;

  -webkit-text-fill-color: transparent;

  background-clip: text;

}

  

/* The card */

.auth-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 to match screenshot */

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

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

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

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

}

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

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

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

  border-color: #fff;

}

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

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

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

  color: #b3b3b3 !important;

}

  

/* Submit Button */

.sign-in-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;

  }

}

  

.or-wrap {

  display: grid;

  grid-template-columns: 1fr auto 1fr;

  align-items: center;

  gap: 12px;

  margin: 14px 0 8px;

  

  mat-divider {

    border-top-color: rgba(255,255,255,0.2);

  }

  span { color: #b3b3b3; letter-spacing: 0.06em; }

}

  

.code-btn {

  width: 100%;

  height: 46px;

  margin-top: 6px;

  font-weight: 700;

  background: rgba(109,109,110,0.7) !important;

  color: #fff !important;

  border-radius: 4px;

  

  &:hover { background: #333 !important; }

}

  

/* Error message */

.error-message {

  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;

  text-align: center;

}

  

.link-btn.resend,

.link-btn.forgot {

  margin-top: 10px;

  background: transparent;

  border: 0;

  color: #b3b3b3;

  text-align: center;

  width: 100%;

  cursor: pointer;

  font-size: 14px;

  

  &:hover { color: #fff; text-decoration: underline; }

}

  

.link-btn.resend {

  color: #e50914;

  font-weight: 600;

  

  &:hover { color: #f40612; }

}

  

/* Options row */

.options {

  margin-top: 6px;

}

  

:host ::ng-deep .options .mat-mdc-checkbox {

  --mdc-checkbox-selected-icon-color: #e50914;

  --mdc-checkbox-unselected-icon-color: rgba(255, 255, 255, 0.7);

  --mdc-checkbox-selected-checkmark-color: #fff;

  --mdc-checkbox-unselected-hover-icon-color: #fff;

}

  

:host ::ng-deep .options .mdc-label {

  color: #b3b3b3 !important;

  font-size: 14px;

}

  

:host ::ng-deep .options .mdc-form-field {

  color: #b3b3b3 !important;

}

  

/* Sign up */

.signup {

  margin-top: 18px;

  text-align: center;

  color: #b3b3b3;

  .link { color: #fff; text-decoration: none; margin-left: 6px; }

  .link:hover { text-decoration: underline; }

}

  

/* reCAPTCHA note */

.captcha-note {

  margin-top: 14px;

  color: #8c8c8c;

  font-size: 0.92rem;

  

  .link { color: #0aa6ff; text-decoration: none; }

  .link:hover { text-decoration: underline; }

}

  

/* Mobile spacing tweaks */

@media (max-width: 600px) {

  .auth-card { padding: 28px 24px 24px; }

}

login.html

<div class="auth-page">

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

  

  <div class="logo">

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

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

  </div>

  

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

    <h1>Sign In</h1>

    <p class="subtitle">Welcome back! Please sign in to your account.</p>

  

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

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

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

        <input matInput formControlName="email" type="email" id="email" name="email">

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

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

          Email is required

        </mat-error>

        <mat-error *ngIf="loginForm.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'" id="password" name="password">

        <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="loginForm.get('password')?.hasError('required')">

          Password is required

        </mat-error>

      </mat-form-field>

  

      <button mat-raised-button color="primary" type="submit" [disabled]="loginForm.invalid">{{loading ? 'Signing In...' : 'Sign In'}}</button>

        <button class="link-btn resend" *ngIf="showResendEmail" type="button" (click)="resendVerificationEmail()">Resend Verification Email</button>

          <button  class="link-btn forgot" type="button" (click)="forgot()">Forgot Password?</button>

  

                  <div class="singup">

                    <span>New to Pudplanet? <a routerLink="/signup">Sign up now.</a></span>

                  </div>

    </form>

  

    <p class="signup-link">

      New to Pudplanet? <a routerLink="/signup">Sign up now.</a>

    </p>

  </mat-card>

</div>

login.ts

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

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

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

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

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

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

  
  

@Component({

  selector: 'app-login',

  standalone: false,

  templateUrl: './login.html',

  styleUrls: ['./login.css']

})

export class Login {

  showResendEmail = false;

  userEmail = '';

  loading = false;

  hidePassword = true;

  loginForm: FormGroup;

  

  constructor(

    private fb: FormBuilder,

    private authService: AuthService,

    private router: Router,

    private notification: NotificationService,

    private errorHandlerService: ErrorHandlerService

  ) {

    this.loginForm = this.fb.group({

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

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

    });

  }

  

  ngOnInit(): void {

    if (this.authService.isLoggedIn()) {

      this.authService.redirectBaseOnRole();

    }

  }

  

  submit() {

    console.log('submit called');

    this.loading = true;

    const formData = this.loginForm.value;

    console.log('formData:', formData);

    const authdata = {

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

      password: formData.password

    };

    console.log('authdata:', authdata);

    console.log('before login request');

    this.authService.login(authdata).subscribe({

      next: (response: any) => {

        console.log('login request success');

        this.loading = false;

        console.debug('[Login] 登录成功,响应:', response);

        if (!response || !response.token) {

          console.error('[Login] 响应无 token:', response);

        }

        this.authService.handleAuthSuccess(response);

        // 登录成功后直接根据角色跳转,避免 currentUser 异步问题

        if (response.role === 'ADMIN') {

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

        } else {

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

        }

      },

      error: (err: any) => {

        console.log('login request error', err);

        this.loading = false;

        const errorMsg = err?.error?.message || err?.error?.err || 'Login failed. Please check your credentials and try again.';

        if (err.status === 403 && errorMsg.toLowerCase().includes('verified')) {

          this.showResendEmail = true;

          this.userEmail = this.loginForm.value.email;

        } else {

          this.showResendEmail = false;

        }

        this.notification.error(errorMsg);

        console.error('[Login] 登录失败:', err);

        debugger;

      },

      complete: () => {

        console.debug('[Login] 登录流程 complete');

      }

    });

  }

  

  resendVerificationEmail() {

    if (!this.userEmail) {

      this.notification.error('No email available to resend verification link.');

      return;

    }

    this.showResendEmail = false;

    this.loading = true;

    this.authService.resendVerificationEmail(this.userEmail).subscribe({

      next: (response: any) => {

        this.loading = false;

            this.notification.success(response?.message || 'Verification email resent! Please check your inbox.');

      },

      error: (err: any) => {

        this.loading = false;

        this.errorHandlerService.handle(err, 'Failed to resend verification email. Please try again later.');

      }

    });

  }

  

  forgot() {

    this.router.navigate(['/forgot-password']);

  }

}

app-routing-module.ts

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

import { RouterModule, Routes } from '@angular/router';

import { Landing } from './landing/landing';

import { Signup } from './signup/signup';

import { Login } from './login/login';

import { VerifyEmail } from './verify-email/verify-email';

import { Home } from './user/home/home';

import { authGuard } from './shared/guards/auth-guard';

import { adminGuard } from './shared/guards/admin-guard';

import { ForgotPassword } from './forgot-password/forgot-password';

  

const routes: Routes = [

  { path: '', component: Landing },

  { path: 'signup', component: Signup },

  { path: 'login', component: Login },

  { path: 'verify-email', component: VerifyEmail },

  { path: 'home', component: Home, canActivate: [authGuard] },

  { path: '**', redirectTo: '', pathMatch: 'full' },

  { path: 'forgot-password', component: ForgotPassword },

  { path: 'admin', loadChildren: () => import('./admin/admin-module').then(m => m.AdminModule), canActivate: [adminGuard] },

];

  

@NgModule({

  imports: [RouterModule.forRoot(routes)],

  exports: [RouterModule]

})

export class AppRoutingModule { }

创建user

mkdir user

cd user
ng g c home

评论

动作测试