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
评论