Laravel: Phase 1综合练习—ShopMetrics着陆页
最后更新:2026-08-26
Phase 1 综合练习是"验收大会"——把前 6 课学的知识全部串起来,交付一个真实可运行的着陆页。
1. 你将学到
- 设计完整的路由蓝图
- 构建可复用的 Blade 布局与组件
- 创建控制器处理页面逻辑
- 集成 Tailwind CSS 与 Alpine.js 交互
- 环境配置与数据库连接
2. Bob 的 Phase 1 验收故事
(1) 痛点:7 课学完还是写不出一个完整页面
Alice 学完了前 6 课,但单独看每个知识点都懂,组合在一起就懵了——路由怎么连控制器?控制器怎么传数据到 Blade?布局继承和组件怎么选?Bob 说:"这就是学游泳和下水的区别——你看了 7 节理论课,但还没游过 25 米。"
(2) 综合练习的解法
本课把路由、视图、控制器、Blade 组件、环境配置全部串起来,从零搭建一个完整可运行的 ShopMetrics 着陆页——这就是你的第一次"下水"。
BASH
# After this lesson, you will have:
# - A landing page with hero section
# - A pricing page with plan cards
# - A dashboard layout with sidebar
# - Reusable Blade components
# - Working database connection
(3) 收益
完成综合练习后,Alice 独立完成了 ShopMetrics 着陆页,5 个页面全部通过验收。Bob 说:"从'看懂了'到'写出来了',差距就是一个综合练习。"
3. 路由蓝图设计
(1) 页面规划
graph LR
A["/ (Home)"] --> B["/pricing (Pricing)"]
A --> C["/about (About)"]
A --> D["/contact (Contact)"]
A --> E["/dashboard (Dashboard)"]
E --> F["/dashboard/shops"]
E --> G["/dashboard/analytics"]
(2) 路由定义
PHP
// routes/web.php
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/pricing', [PricingController::class, 'index'])->name('pricing');
Route::get('/about', [AboutController::class, 'index'])->name('about');
Route::get('/contact', [ContactController::class, 'create'])->name('contact.create');
Route::post('/contact', [ContactController::class, 'store'])->name('contact.store');
Route::middleware('auth')->prefix('dashboard')->name('dashboard.')->group(function () {
Route::get('/', [DashboardController::class, 'index'])->name('index');
Route::resource('shops', Dashboard\ShopController::class);
});
| 页面 | 路由 | 控制器 | 中间件 |
|---|---|---|---|
| 首页 | GET / | HomeController | 无 |
| 定价 | GET /pricing | PricingController | 无 |
| 关于 | GET /about | AboutController | 无 |
| 联系 | GET+POST /contact | ContactController | 无 |
| 仪表盘 | GET /dashboard | DashboardController | auth |
▶ 示例:创建所有控制器
BASH
php artisan make:controller HomeController
php artisan make:controller PricingController
php artisan make:controller AboutController
php artisan make:controller ContactController
php artisan make:controller Dashboard/DashboardController
php artisan make:controller Dashboard/ShopController --resource
输出:
TEXT
📖 仅展示
# 命令执行成功
4. 布局与组件设计
(1) 布局层级
TEXT
📖 仅展示
layouts/app.blade.php ← Main layout (navbar + footer)
├── layouts/dashboard.blade.php ← Dashboard layout (sidebar + content)
│ └── dashboard/analytics.blade.php
└── home/index.blade.php ← Landing page
(2) 组件清单
| 组件 | 类型 | 文件 | 用途 |
|---|---|---|---|
<x-navbar> |
Anonymous | components/navbar.blade.php | 全站导航栏 |
<x-footer> |
Anonymous | components/footer.blade.php | 全站页脚 |
<x-hero> |
Anonymous | components/hero.blade.php | 首页 Hero 区域 |
<x-plan-card> |
Anonymous | components/plan-card.blade.php | 定价卡片 |
<x-stat-card> |
Anonymous | components/stat-card.blade.php | 仪表盘统计卡片 |
<x-sidebar> |
Anonymous | components/sidebar.blade.php | 仪表盘侧边栏 |
▶ 示例:创建 Blade 组件
BASH
# Create component files manually
mkdir -p resources/views/components
touch resources/views/components/navbar.blade.php
touch resources/views/components/footer.blade.php
touch resources/views/components/hero.blade.php
touch resources/views/components/plan-card.blade.php
touch resources/views/components/stat-card.blade.php
touch resources/views/components/sidebar.blade.php
输出:
TEXT
📖 仅展示
# 命令执行成功
5. 核心布局与组件实现
(1) 主布局
HTML
<!-- resources/views/layouts/app.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'ShopMetrics') — E-commerce Analytics</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50 text-gray-900">
<x-navbar />
<main>
@yield('content')
</main>
<x-footer />
</body>
</html>
(2) 导航栏组件
HTML
<!-- resources/views/components/navbar.blade.php -->
<nav class="bg-white shadow-sm" x-data="{ open: false }">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<a href="{{ route('home') }}" class="flex items-center font-bold text-xl text-blue-600">
ShopMetrics
</a>
<div class="hidden md:flex items-center space-x-6">
<a href="{{ route('pricing') }}" class="text-gray-600 hover:text-gray-900">Pricing</a>
<a href="{{ route('about') }}" class="text-gray-600 hover:text-gray-900">About</a>
@auth
<a href="{{ route('dashboard.index') }}" class="btn-primary">Dashboard</a>
@else
<a href="{{ route('login') }}" class="text-gray-600 hover:text-gray-900">Login</a>
<a href="{{ route('register') }}" class="btn-primary">Sign Up</a>
@endauth
</div>
<button @click="open = !open" class="md:hidden">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path x-show="!open" d="M4 6h16M4 12h16M4 18h16"/>
<path x-show="open" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div x-show="open" class="md:hidden py-2 space-y-2">
<a href="{{ route('pricing') }}" class="block py-2 text-gray-600">Pricing</a>
@auth
<a href="{{ route('dashboard.index') }}" class="block py-2 btn-primary">Dashboard</a>
@else
<a href="{{ route('login') }}" class="block py-2 text-gray-600">Login</a>
<a href="{{ route('register') }}" class="block py-2 btn-primary">Sign Up</a>
@endauth
</div>
</div>
</nav>
(3) Hero 组件
HTML
<!-- resources/views/components/hero.blade.php -->
@props(['title', 'subtitle', 'cta_text' => 'Get Started', 'cta_route' => 'register'])
<section class="bg-gradient-to-r from-blue-600 to-indigo-700 text-white py-20">
<div class="max-w-7xl mx-auto px-4 text-center">
<h1 class="text-4xl md:text-6xl font-bold mb-6">{{ $title }}</h1>
<p class="text-xl md:text-2xl mb-8 text-blue-100">{{ $subtitle }}</p>
<a href="{{ route($cta_route) }}" class="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-blue-50 transition">
{{ $cta_text }}
</a>
</div>
</section>
▶ 示例:定价卡片组件
HTML
<!-- resources/views/components/plan-card.blade.php -->
@props(['plan', 'popular' => false])
<div class="bg-white rounded-xl shadow-lg p-8 {{ $popular ? 'ring-2 ring-blue-500 relative' : '' }}">
@if($popular)
<span class="absolute -top-3 left-1/2 -translate-x-1/2 bg-blue-500 text-white px-4 py-1 rounded-full text-sm font-semibold">
Most Popular
</span>
@endif
<h3 class="text-xl font-bold mb-2">{{ $plan['name'] }}</h3>
<div class="mb-6">
<span class="text-4xl font-bold">${{ $plan['price'] }}</span>
<span class="text-gray-500">/month</span>
</div>
<ul class="space-y-3 mb-8">
@foreach($plan['features'] as $feature)
<li class="flex items-center">
<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/>
</svg>
{{ $feature }}
</li>
@endforeach
</ul>
<a href="{{ route('register') }}" class="{{ $popular ? 'btn-primary' : 'btn-secondary' }} w-full text-center block">
Choose {{ $plan['name'] }}
</a>
</div>
输出:
TEXT
📖 仅展示
// 执行成功
6. 控制器实现
▶ 示例:HomeController 和 PricingController
PHP
// app/Http/Controllers/HomeController.php
class HomeController extends Controller
{
public function index(): View
{
$features = [
['icon' => 'chart', 'title' => 'Real-time Analytics', 'desc' => 'Track sales, orders and revenue in real-time.'],
['icon' => 'users', 'title' => 'Multi-tenant', 'desc' => 'Separate data for each e-commerce store.'],
['icon' => 'bell', 'title' => 'Smart Alerts', 'desc' => 'Get notified when key metrics change.'],
];
return view('home.index', compact('features'));
}
}
// app/Http/Controllers/PricingController.php
class PricingController extends Controller
{
public function index(): View
{
$plans = [
['name' => 'Starter', 'price' => 29, 'features' => ['5 Shops', '1K Orders/month', 'Basic Analytics', 'Email Support']],
['name' => 'Pro', 'price' => 79, 'features' => ['25 Shops', '10K Orders/month', 'Advanced Analytics', 'Priority Support', 'API Access']],
['name' => 'Enterprise', 'price' => 199, 'features' => ['Unlimited Shops', 'Unlimited Orders', 'Custom Analytics', 'Dedicated Support', 'API + Webhooks', 'SSO']],
];
return view('pricing.index', compact('plans'));
}
}
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例:DashboardController
PHP
// app/Http/Controllers/Dashboard/DashboardController.php
class DashboardController extends Controller
{
public function index(): View
{
$user = auth()->user();
$tenant = $user->tenant;
$stats = [
['label' => 'Total Revenue', 'value' => '$' . number_format($tenant->revenue ?? 0), 'trend' => 12.5],
['label' => 'Active Shops', 'value' => $tenant->shops()->count(), 'trend' => 3.2],
['label' => 'Orders Today', 'value' => $tenant->orders()->today()->count(), 'trend' => -2.1],
];
return view('dashboard.index', compact('tenant', 'stats'));
}
}
输出:
TEXT
📖 仅展示
// 执行成功
7. 环境配置与数据库
BASH
# .env — Configure MySQL for ShopMetrics
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=shopmetrics
DB_USERNAME=root
DB_PASSWORD=secret
# Generate app key and run migrations
php artisan key:generate
php artisan migrate
# Install Breeze for auth scaffolding
composer require laravel/breeze --dev
php artisan breeze:install blade
npm install && npm run build
| 步骤 | 命令 | 作用 |
|---|---|---|
| 1 | key:generate |
生成 APP_KEY |
| 2 | migrate |
创建数据表 |
| 3 | breeze:install |
生成登录/注册页面 |
| 4 | npm run build |
编译前端资产 |
8. 综合示例:ShopMetrics 着陆页首页
HTML
<!-- ============================================
Comprehensive: ShopMetrics Landing Page
Covers: layout, components, routes, data passing
============================================ -->
<!-- resources/views/home/index.blade.php -->
@extends('layouts.app')
@section('title', 'ShopMetrics — E-commerce Analytics Platform')
@section('content')
<!-- Hero Section -->
<x-hero
title="Track Your E-commerce Performance"
subtitle="Real-time analytics for multi-tenant e-commerce platforms. Monitor revenue, orders, and customer insights — all in one dashboard."
cta_text="Start Free Trial"
cta_route="register"
/>
<!-- Features Section -->
<section class="py-20 bg-white">
<div class="max-w-7xl mx-auto px-4">
<h2 class="text-3xl font-bold text-center mb-12">Why ShopMetrics?</h2>
<div class="grid md:grid-cols-3 gap-8">
@foreach($features as $feature)
<div class="text-center p-6">
<div class="w-12 h-12 bg-blue-100 rounded-lg mx-auto mb-4 flex items-center justify-center">
<span class="text-blue-600 text-2xl">
@if($feature['icon'] === 'chart')📈@elseif($feature['icon'] === 'users')👥@else🔔@endif
</span>
</div>
<h3 class="text-xl font-semibold mb-2">{{ $feature['title'] }}</h3>
<p class="text-gray-600">{{ $feature['desc'] }}</p>
</div>
@endforeach
</div>
</div>
</section>
<!-- CTA Section -->
<section class="py-16 bg-gray-100">
<div class="max-w-4xl mx-auto text-center px-4">
<h2 class="text-3xl font-bold mb-4">Ready to grow your business?</h2>
<p class="text-gray-600 mb-8">Join 500+ merchants who trust ShopMetrics for their analytics.</p>
<div class="flex gap-4 justify-center">
<a href="{{ route('register') }}" class="btn-primary">Start Free Trial</a>
<a href="{{ route('pricing') }}" class="btn-secondary">View Pricing</a>
</div>
</div>
</section>
@endsection
❓ 常见问题
Q 着陆页需要认证吗?
A 首页、定价、关于页不需要认证,任何人可访问;仪表盘需要 auth 中间件保护。Breeze 已自动处理 login/register 路由。
Q 布局继承和组件怎么选?
A 页面级别的结构(navbar+footer 包裹)用布局继承;可复用的 UI 片段(卡片、按钮、表单)用 Blade 组件。布局定义"骨架",组件填充"血肉"。
Q Tailwind CSS 不生效怎么办?
A 确保运行了
npm run dev(开发模式热更新)或 npm run build(生产编译),检查 tailwind.config.js 的 content 配置是否包含所有 Blade 文件路径。Q Alpine.js 的 x-show 和 v-show 一样吗?
A 功能类似,但 Alpine.js 是轻量级(15KB)的独立库,不需要构建步骤;Vue 的 v-show 需要 Vue 实例。简单交互用 Alpine.js,复杂单页应用用 Vue。
Q Breeze 和 Jetstream 有什么区别?
A Breeze 是轻量认证脚手架(Blade/Inertia/Livewire 三选一),代码少易理解;Jetstream 是功能更全的方案(含团队管理、2FA),但更复杂。入门推荐 Breeze。
Q 多个控制器共享数据怎么办?
A 用 View Composer 自动注入共享数据(如导航栏的店铺列表),或创建一个 Service 类在控制器间共享逻辑。不要在基类控制器中共享数据。
📖 小节
- 路由蓝图先规划页面,再定义控制器和中间件
- 布局继承处理页面级结构,Blade 组件处理可复用 UI 片段
- 控制器方法保持精简,复杂逻辑抽取到 Service 类
- Tailwind CSS 通过 Vite 编译,Alpine.js 处理简单交互
- Breeze 提供开箱即用的认证页面
- 环境配置用 .env,开发环境用 debug 模式
📝 作业
-
基础题(⭐):完成 ShopMetrics 着陆页的 5 个页面(首页/定价/关于/联系/仪表盘),确保所有路由可访问、布局继承正常、导航栏链接正确。
-
进阶题(⭐⭐):为定价页面添加月付/年付切换功能(使用 Alpine.js),年付价格打 8 折,切换时卡片价格动画过渡。
-
挑战题(⭐⭐⭐):实现仪表盘侧边栏的动态菜单——根据用户角色(admin/tenant_owner/analyst)显示不同的菜单项,使用 View Composer 从数据库读取权限配置。