17 - 电商平台项目
📖 项目概述
这是一个完整的电商平台项目,集成了Angular的所有核心功能,包括:
- 用户认证和授权
- 商品展示和搜索
- 购物车管理
- 订单处理
- 用户管理
- 响应式设计
🎯 项目目标
通过完成这个项目,您将:
- 整合所有学过的Angular知识
- 掌握大型应用的架构设计
- 学会处理复杂的业务逻辑
- 实现完整的用户流程
- 应用最佳实践和设计模式
🏗️ 项目架构
src/
├── app/
│ ├── core/ # 核心模块
│ │ ├── services/ # 核心服务
│ │ ├── guards/ # 路由守卫
│ │ └── interceptors/ # HTTP拦截器
│ ├── shared/ # 共享模块
│ │ ├── components/ # 共享组件
│ │ ├── pipes/ # 共享管道
│ │ └── directives/ # 共享指令
│ ├── features/ # 功能模块
│ │ ├── auth/ # 认证模块
│ │ ├── products/ # 商品模块
│ │ ├── cart/ # 购物车模块
│ │ ├── orders/ # 订单模块
│ │ └── admin/ # 管理模块
│ ├── layouts/ # 布局组件
│ └── app-routing.module.ts # 路由配置
├── assets/ # 静态资源
├── environments/ # 环境配置
└── styles/ # 全局样式
🚀 快速开始
1. 创建项目
bash
# 创建新项目
ng new ecommerce-platform --routing --style=scss
# 进入项目目录
cd ecommerce-platform
# 安装依赖
npm install @angular/material @angular/cdk
npm install @angular/flex-layout
npm install ngx-toastr
2. 项目结构设置
bash
# 创建目录结构
mkdir -p src/app/core/services
mkdir -p src/app/core/guards
mkdir -p src/app/core/interceptors
mkdir -p src/app/shared/components
mkdir -p src/app/shared/pipes
mkdir -p src/app/shared/directives
mkdir -p src/app/features/auth
mkdir -p src/app/features/products
mkdir -p src/app/features/cart
mkdir -p src/app/features/orders
mkdir -p src/app/features/admin
mkdir -p src/app/layouts
📋 功能模块
1. 认证模块 (Auth)
typescript
// features/auth/auth.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { AuthRoutingModule } from './auth-routing.module';
import { LoginComponent } from './components/login/login.component';
import { RegisterComponent } from './components/register/register.component';
import { ForgotPasswordComponent } from './components/forgot-password/forgot-password.component';
import { AuthService } from './services/auth.service';
import { AuthGuard } from './guards/auth.guard';
@NgModule({
declarations: [
LoginComponent,
RegisterComponent,
ForgotPasswordComponent
],
imports: [
CommonModule,
ReactiveFormsModule,
AuthRoutingModule
],
providers: [
AuthService,
AuthGuard
]
})
export class AuthModule { }
2. 商品模块 (Products)
typescript
// features/products/products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ProductsRoutingModule } from './products-routing.module';
import { ProductListComponent } from './components/product-list/product-list.component';
import { ProductDetailComponent } from './components/product-detail/product-detail.component';
import { ProductSearchComponent } from './components/product-search/product-search.component';
import { ProductService } from './services/product.service';
@NgModule({
declarations: [
ProductListComponent,
ProductDetailComponent,
ProductSearchComponent
],
imports: [
CommonModule,
FormsModule,
ProductsRoutingModule
],
providers: [
ProductService
]
})
export class ProductsModule { }
3. 购物车模块 (Cart)
typescript
// features/cart/cart.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CartRoutingModule } from './cart-routing.module';
import { CartComponent } from './components/cart/cart.component';
import { CartItemComponent } from './components/cart-item/cart-item.component';
import { CartService } from './services/cart.service';
@NgModule({
declarations: [
CartComponent,
CartItemComponent
],
imports: [
CommonModule,
CartRoutingModule
],
providers: [
CartService
]
})
export class CartModule { }
🔧 核心服务
1. 认证服务
typescript
// core/services/auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
export interface User {
id: number;
email: string;
name: string;
role: string;
avatar?: string;
}
export interface LoginRequest {
email: string;
password: string;
}
export interface RegisterRequest {
name: string;
email: string;
password: string;
confirmPassword: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private currentUserSubject = new BehaviorSubject<User | null>(null);
public currentUser$ = this.currentUserSubject.asObservable();
private readonly API_URL = '/api/auth';
constructor(private http: HttpClient) {
this.loadUserFromStorage();
}
login(credentials: LoginRequest): Observable<User> {
return this.http.post<{ user: User; token: string }>(`${this.API_URL}/login`, credentials)
.pipe(
tap(response => {
localStorage.setItem('token', response.token);
this.currentUserSubject.next(response.user);
}),
map(response => response.user)
);
}
register(userData: RegisterRequest): Observable<User> {
return this.http.post<{ user: User; token: string }>(`${this.API_URL}/register`, userData)
.pipe(
tap(response => {
localStorage.setItem('token', response.token);
this.currentUserSubject.next(response.user);
}),
map(response => response.user)
);
}
logout(): void {
localStorage.removeItem('token');
this.currentUserSubject.next(null);
}
getCurrentUser(): User | null {
return this.currentUserSubject.value;
}
isAuthenticated(): boolean {
return !!this.getCurrentUser();
}
isAdmin(): boolean {
const user = this.getCurrentUser();
return user?.role === 'admin';
}
private loadUserFromStorage(): void {
const token = localStorage.getItem('token');
if (token) {
// 验证token并获取用户信息
this.http.get<User>(`${this.API_URL}/me`).subscribe(
user => this.currentUserSubject.next(user),
() => this.logout()
);
}
}
}
2. 商品服务
typescript
// features/products/services/product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Product {
id: number;
name: string;
description: string;
price: number;
originalPrice?: number;
image: string;
images: string[];
category: string;
brand: string;
stock: number;
rating: number;
reviewCount: number;
features: string[];
specifications: { [key: string]: string };
}
export interface ProductFilters {
category?: string;
brand?: string;
minPrice?: number;
maxPrice?: number;
rating?: number;
search?: string;
}
export interface ProductListResponse {
products: Product[];
total: number;
page: number;
limit: number;
totalPages: number;
}
@Injectable({
providedIn: 'root'
})
export class ProductService {
private readonly API_URL = '/api/products';
private searchResultsSubject = new BehaviorSubject<Product[]>([]);
public searchResults$ = this.searchResultsSubject.asObservable();
constructor(private http: HttpClient) {}
getProducts(
page: number = 1,
limit: number = 12,
filters?: ProductFilters
): Observable<ProductListResponse> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString());
if (filters) {
if (filters.category) params = params.set('category', filters.category);
if (filters.brand) params = params.set('brand', filters.brand);
if (filters.minPrice) params = params.set('minPrice', filters.minPrice.toString());
if (filters.maxPrice) params = params.set('maxPrice', filters.maxPrice.toString());
if (filters.rating) params = params.set('rating', filters.rating.toString());
if (filters.search) params = params.set('search', filters.search);
}
return this.http.get<ProductListResponse>(this.API_URL, { params });
}
getProductById(id: number): Observable<Product> {
return this.http.get<Product>(`${this.API_URL}/${id}`);
}
getFeaturedProducts(): Observable<Product[]> {
return this.http.get<Product[]>(`${this.API_URL}/featured`);
}
getCategories(): Observable<string[]> {
return this.http.get<string[]>(`${this.API_URL}/categories`);
}
getBrands(): Observable<string[]> {
return this.http.get<string[]>(`${this.API_URL}/brands`);
}
searchProducts(query: string): Observable<Product[]> {
return this.http.get<Product[]>(`${this.API_URL}/search`, {
params: { q: query }
}).pipe(
map(products => {
this.searchResultsSubject.next(products);
return products;
})
);
}
getRelatedProducts(productId: number): Observable<Product[]> {
return this.http.get<Product[]>(`${this.API_URL}/${productId}/related`);
}
}
3. 购物车服务
typescript
// features/cart/services/cart.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Product } from '../../products/services/product.service';
export interface CartItem {
product: Product;
quantity: number;
selected: boolean;
}
export interface CartSummary {
totalItems: number;
totalPrice: number;
selectedItems: number;
selectedPrice: number;
}
@Injectable({
providedIn: 'root'
})
export class CartService {
private cartItemsSubject = new BehaviorSubject<CartItem[]>([]);
public cartItems$ = this.cartItemsSubject.asObservable();
public cartSummary$ = this.cartItems$.pipe(
map(items => this.calculateSummary(items))
);
constructor() {
this.loadCartFromStorage();
}
addToCart(product: Product, quantity: number = 1): void {
const currentItems = this.cartItemsSubject.value;
const existingItem = currentItems.find(item => item.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
currentItems.push({
product,
quantity,
selected: true
});
}
this.updateCart(currentItems);
}
removeFromCart(productId: number): void {
const currentItems = this.cartItemsSubject.value;
const updatedItems = currentItems.filter(item => item.product.id !== productId);
this.updateCart(updatedItems);
}
updateQuantity(productId: number, quantity: number): void {
const currentItems = this.cartItemsSubject.value;
const item = currentItems.find(item => item.product.id === productId);
if (item) {
if (quantity <= 0) {
this.removeFromCart(productId);
} else {
item.quantity = quantity;
this.updateCart(currentItems);
}
}
}
toggleSelection(productId: number): void {
const currentItems = this.cartItemsSubject.value;
const item = currentItems.find(item => item.product.id === productId);
if (item) {
item.selected = !item.selected;
this.updateCart(currentItems);
}
}
selectAll(): void {
const currentItems = this.cartItemsSubject.value;
currentItems.forEach(item => item.selected = true);
this.updateCart(currentItems);
}
deselectAll(): void {
const currentItems = this.cartItemsSubject.value;
currentItems.forEach(item => item.selected = false);
this.updateCart(currentItems);
}
clearCart(): void {
this.updateCart([]);
}
getCartItems(): CartItem[] {
return this.cartItemsSubject.value;
}
getCartSummary(): CartSummary {
return this.calculateSummary(this.cartItemsSubject.value);
}
private updateCart(items: CartItem[]): void {
this.cartItemsSubject.next(items);
this.saveCartToStorage(items);
}
private calculateSummary(items: CartItem[]): CartSummary {
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce((sum, item) => sum + (item.product.price * item.quantity), 0);
const selectedItems = items
.filter(item => item.selected)
.reduce((sum, item) => sum + item.quantity, 0);
const selectedPrice = items
.filter(item => item.selected)
.reduce((sum, item) => sum + (item.product.price * item.quantity), 0);
return {
totalItems,
totalPrice,
selectedItems,
selectedPrice
};
}
private loadCartFromStorage(): void {
const savedCart = localStorage.getItem('cart');
if (savedCart) {
try {
const cartItems = JSON.parse(savedCart);
this.cartItemsSubject.next(cartItems);
} catch (error) {
console.error('Error loading cart from storage:', error);
}
}
}
private saveCartToStorage(items: CartItem[]): void {
localStorage.setItem('cart', JSON.stringify(items));
}
}
🎨 组件实现
1. 商品列表组件
typescript
// features/products/components/product-list/product-list.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { Product, ProductFilters, ProductListResponse } from '../../services/product.service';
@Component({
selector: 'app-product-list',
templateUrl: './product-list.component.html',
styleUrls: ['./product-list.component.scss']
})
export class ProductListComponent implements OnInit, OnDestroy {
products: Product[] = [];
loading = false;
error: string | null = null;
// 分页
currentPage = 1;
pageSize = 12;
totalItems = 0;
totalPages = 0;
// 筛选
filters: ProductFilters = {};
categories: string[] = [];
brands: string[] = [];
// 搜索
searchTerm = '';
private searchSubject = new Subject<string>();
// 订阅管理
private subscriptions: Subscription[] = [];
constructor(
private productService: ProductService
) {}
ngOnInit() {
this.loadProducts();
this.loadCategories();
this.loadBrands();
this.setupSearch();
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
loadProducts() {
this.loading = true;
this.error = null;
this.subscriptions.push(
this.productService.getProducts(this.currentPage, this.pageSize, this.filters)
.subscribe({
next: (response: ProductListResponse) => {
this.products = response.products;
this.totalItems = response.total;
this.totalPages = response.totalPages;
this.loading = false;
},
error: (error) => {
this.error = '加载商品失败';
this.loading = false;
console.error('Error loading products:', error);
}
})
);
}
onPageChange(page: number) {
this.currentPage = page;
this.loadProducts();
}
onFilterChange() {
this.currentPage = 1;
this.loadProducts();
}
onSearch(searchTerm: string) {
this.searchSubject.next(searchTerm);
}
clearFilters() {
this.filters = {};
this.searchTerm = '';
this.currentPage = 1;
this.loadProducts();
}
private loadCategories() {
this.subscriptions.push(
this.productService.getCategories().subscribe(
categories => this.categories = categories
)
);
}
private loadBrands() {
this.subscriptions.push(
this.productService.getBrands().subscribe(
brands => this.brands = brands
)
);
}
private setupSearch() {
this.subscriptions.push(
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(searchTerm => {
this.searchTerm = searchTerm;
this.filters.search = searchTerm;
this.currentPage = 1;
this.loadProducts();
return [];
})
).subscribe()
);
}
}
2. 购物车组件
typescript
// features/cart/components/cart/cart.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { CartItem, CartSummary } from '../../services/cart.service';
import { OrderService } from '../../../orders/services/order.service';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss']
})
export class CartComponent implements OnInit, OnDestroy {
cartItems: CartItem[] = [];
cartSummary: CartSummary = {
totalItems: 0,
totalPrice: 0,
selectedItems: 0,
selectedPrice: 0
};
loading = false;
private subscriptions: Subscription[] = [];
constructor(
private cartService: CartService,
private orderService: OrderService
) {}
ngOnInit() {
this.subscribeToCart();
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
updateQuantity(productId: number, quantity: number) {
this.cartService.updateQuantity(productId, quantity);
}
removeItem(productId: number) {
this.cartService.removeFromCart(productId);
}
toggleSelection(productId: number) {
this.cartService.toggleSelection(productId);
}
selectAll() {
this.cartService.selectAll();
}
deselectAll() {
this.cartService.deselectAll();
}
clearCart() {
if (confirm('确定要清空购物车吗?')) {
this.cartService.clearCart();
}
}
checkout() {
const selectedItems = this.cartItems.filter(item => item.selected);
if (selectedItems.length === 0) {
alert('请选择要购买的商品');
return;
}
this.loading = true;
this.subscriptions.push(
this.orderService.createOrder(selectedItems).subscribe({
next: (order) => {
this.loading = false;
// 移除已购买的商品
selectedItems.forEach(item => {
this.cartService.removeFromCart(item.product.id);
});
// 跳转到订单确认页面
this.router.navigate(['/orders', order.id]);
},
error: (error) => {
this.loading = false;
console.error('Checkout error:', error);
}
})
);
}
private subscribeToCart() {
this.subscriptions.push(
this.cartService.cartItems$.subscribe(items => {
this.cartItems = items;
})
);
this.subscriptions.push(
this.cartService.cartSummary$.subscribe(summary => {
this.cartSummary = summary;
})
);
}
}
🛡️ 路由守卫
1. 认证守卫
typescript
// core/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
if (this.authService.isAuthenticated()) {
return true;
} else {
this.router.navigate(['/auth/login'], {
queryParams: { returnUrl: state.url }
});
return false;
}
}
}
2. 管理员守卫
typescript
// core/guards/admin.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
@Injectable({
providedIn: 'root'
})
export class AdminGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(): boolean {
if (this.authService.isAdmin()) {
return true;
} else {
this.router.navigate(['/']);
return false;
}
}
}
🎨 样式和主题
1. 全局样式
scss
// styles.scss
@import '~@angular/material/theming';
@include mat-core();
// 定义主题颜色
$primary: mat-palette($mat-blue);
$accent: mat-palette($mat-orange);
$warn: mat-palette($mat-red);
// 创建主题
$theme: mat-light-theme($primary, $accent, $warn);
@include angular-material-theme($theme);
// 全局样式
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
line-height: 1.6;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
// 响应式设计
@media (max-width: 768px) {
.container {
padding: 0 10px;
}
}
2. 组件样式
scss
// product-card.component.scss
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.product-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.product-info {
padding: 16px;
.product-name {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #333;
}
.product-price {
font-size: 18px;
font-weight: 600;
color: #e74c3c;
margin-bottom: 8px;
.original-price {
font-size: 14px;
color: #999;
text-decoration: line-through;
margin-left: 8px;
}
}
.product-rating {
display: flex;
align-items: center;
margin-bottom: 12px;
.stars {
color: #ffc107;
margin-right: 8px;
}
.rating-text {
font-size: 14px;
color: #666;
}
}
}
.product-actions {
padding: 0 16px 16px;
display: flex;
gap: 8px;
.add-to-cart-btn {
flex: 1;
background: #3498db;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s ease;
&:hover {
background: #2980b9;
}
}
.wishlist-btn {
background: transparent;
border: 1px solid #ddd;
padding: 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: #f8f9fa;
border-color: #ccc;
}
}
}
}
🚀 部署配置
1. 生产环境配置
typescript
// environments/environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.ecommerce-platform.com',
appName: 'Ecommerce Platform',
version: '1.0.0'
};
2. 构建配置
json
// angular.json
{
"projects": {
"ecommerce-platform": {
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/ecommerce-platform",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
}
}
}
}
📱 响应式设计
1. 响应式布局
scss
// responsive-layout.scss
.responsive-grid {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
@media (max-width: 768px) {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
@media (max-width: 480px) {
grid-template-columns: 1fr;
gap: 10px;
}
}
.sidebar-layout {
display: grid;
grid-template-columns: 250px 1fr;
gap: 20px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
.sidebar {
order: 2;
}
.main-content {
order: 1;
}
}
}
🎮 实践练习
练习1:实现用户认证流程
- 创建登录和注册组件
- 实现表单验证
- 集成认证服务
- 添加路由守卫
- 实现用户状态管理
练习2:构建商品展示系统
- 创建商品列表组件
- 实现搜索和筛选功能
- 添加分页功能
- 创建商品详情页面
- 实现相关商品推荐
练习3:开发购物车功能
- 实现购物车服务
- 创建购物车组件
- 添加数量调整功能
- 实现商品选择功能
- 集成结算流程
📊 项目评估
功能完整性 (40%)
- [ ] 用户认证和授权
- [ ] 商品展示和搜索
- [ ] 购物车管理
- [ ] 订单处理
- [ ] 用户管理
- [ ] 响应式设计
代码质量 (30%)
- [ ] 代码结构和组织
- [ ] 错误处理
- [ ] 性能优化
- [ ] 测试覆盖
- [ ] 代码注释
用户体验 (20%)
- [ ] 界面设计
- [ ] 交互流畅性
- [ ] 加载性能
- [ ] 错误提示
- [ ] 移动端适配
技术实现 (10%)
- [ ] Angular最佳实践
- [ ] 架构设计
- [ ] 安全性
- [ ] 可维护性
✅ 项目完成检查
完成项目后,请确认:
- [ ] 所有功能模块正常工作
- [ ] 代码结构清晰合理
- [ ] 响应式设计完善
- [ ] 性能优化到位
- [ ] 错误处理完善
- [ ] 用户体验良好
🎉 恭喜
您已经完成了一个完整的Angular电商平台项目!这个项目展示了:
- Angular框架的全面应用
- 大型应用的架构设计
- 复杂业务逻辑的处理
- 最佳实践的运用
- 完整的开发流程
通过这个项目,您已经具备了使用Angular开发企业级应用的能力!