Laravel 13 提供了强大的邮件模板系统,本文介绍如何创建和管理专业的邮件模板。

邮件模板配置

邮件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

return [
'default' => env('MAIL_MAILER', 'smtp'),

'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
],

'ses' => [
'transport' => 'ses',
],

'postmark' => [
'transport' => 'postmark',
],

'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],

'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],

'array' => [
'transport' => 'array',
],
],

'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
];

邮件模板系统

基础邮件模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

abstract class BaseMailable extends Mailable
{
use Queueable, SerializesModels;

protected string $theme = 'default';
protected array $previewText = [];

public function envelope(): Envelope
{
return new Envelope(
subject: $this->getSubject(),
from: $this->getFrom(),
replyTo: $this->getReplyTo(),
tags: $this->getTags(),
metadata: $this->getMetadata(),
);
}

protected function getSubject(): string
{
return '';
}

protected function getFrom(): array
{
return [
'address' => config('mail.from.address'),
'name' => config('mail.from.name'),
];
}

protected function getReplyTo(): array
{
return [];
}

protected function getTags(): array
{
return [];
}

protected function getMetadata(): array
{
return [];
}

public function content(): Content
{
return new Content(
view: $this->getView(),
with: array_merge($this->getViewData(), [
'theme' => $this->theme,
'previewText' => $this->previewText,
]),
);
}

abstract protected function getView(): string;

protected function getViewData(): array
{
return [];
}
}

欢迎邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php

namespace App\Mail\Auth;

use App\Mail\BaseMailable;
use App\Models\User;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;

class WelcomeEmail extends BaseMailable
{
protected string $theme = 'welcome';

public function __construct(
protected User $user
) {}

protected function getSubject(): string
{
return "Welcome to " . config('app.name') . "!";
}

protected function getTags(): array
{
return ['welcome', 'auth'];
}

protected function getMetadata(): array
{
return [
'user_id' => $this->user->id,
];
}

protected function getView(): string
{
return 'emails.auth.welcome';
}

protected function getViewData(): array
{
return [
'user' => $this->user,
'loginUrl' => route('login'),
'dashboardUrl' => route('dashboard'),
'supportEmail' => config('mail.support.address'),
];
}
}

订单确认邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php

namespace App\Mail\Orders;

use App\Mail\BaseMailable;
use App\Models\Order;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;

class OrderConfirmationEmail extends BaseMailable
{
protected string $theme = 'order';

public function __construct(
protected Order $order
) {}

protected function getSubject(): string
{
return "Order Confirmation #{$this->order->order_number}";
}

protected function getTags(): array
{
return ['order', 'confirmation'];
}

protected function getMetadata(): array
{
return [
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
];
}

protected function getView(): string
{
return 'emails.orders.confirmation';
}

protected function getViewData(): array
{
return [
'order' => $this->order,
'items' => $this->order->items,
'shipping' => $this->order->shippingAddress,
'billing' => $this->order->billingAddress,
'trackingUrl' => $this->order->tracking_url ?? null,
];
}

public function attachments(): array
{
$attachments = [];

if ($this->order->invoice_path) {
$attachments[] = Attachment::fromStorage($this->order->invoice_path)
->as("invoice-{$this->order->order_number}.pdf")
->withMime('application/pdf');
}

return $attachments;
}
}

邮件模板视图

布局模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<!-- resources/views/emails/layouts/default.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{ $subject ?? '' }}</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #f4f4f4;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
}
.email-header {
background-color: #4f46e5;
padding: 30px;
text-align: center;
}
.email-header h1 {
color: #ffffff;
margin: 0;
font-size: 24px;
}
.email-body {
padding: 40px 30px;
}
.email-footer {
background-color: #f9fafb;
padding: 30px;
text-align: center;
color: #6b7280;
font-size: 14px;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #4f46e5;
color: #ffffff;
text-decoration: none;
border-radius: 6px;
font-weight: 600;
}
.button:hover {
background-color: #4338ca;
}
.divider {
border-top: 1px solid #e5e7eb;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="email-container">
<div class="email-header">
<h1>{{ config('app.name') }}</h1>
</div>

<div class="email-body">
@if(!empty($previewText))
<div style="display: none; max-height: 0; overflow: hidden;">
@foreach($previewText as $text)
{{ $text }}
@endforeach
</div>
@endif

@yield('content')
</div>

<div class="email-footer">
<p>
&copy; {{ date('Y') }} {{ config('app.name') }}. All rights reserved.
</p>
<p>
<a href="{{ route('privacy') }}">Privacy Policy</a> |
<a href="{{ route('terms') }}">Terms of Service</a>
</p>
<p>
If you no longer wish to receive these emails, you can
<a href="{{ route('unsubscribe', ['token' => $unsubscribeToken ?? '']) }}">unsubscribe</a>.
</p>
</div>
</div>
</body>
</html>

欢迎邮件模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!-- resources/views/emails/auth/welcome.blade.php -->
@extends('emails.layouts.default')

@section('content')
<h2 style="margin: 0 0 20px; color: #111827; font-size: 22px;">
Welcome, {{ $user->name }}!
</h2>

<p style="margin: 0 0 20px; color: #4b5563; line-height: 1.6;">
Thank you for joining {{ config('app.name') }}. We're excited to have you on board!
</p>

<p style="margin: 0 0 30px; color: #4b5563; line-height: 1.6;">
Here are some quick links to help you get started:
</p>

<div style="margin: 0 0 30px;">
<a href="{{ $dashboardUrl }}" class="button">
Go to Dashboard
</a>
</div>

<div class="divider"></div>

<h3 style="margin: 20px 0 10px; color: #111827; font-size: 18px;">
What's Next?
</h3>

<ul style="margin: 0; padding-left: 20px; color: #4b5563;">
<li style="margin-bottom: 10px;">Complete your profile</li>
<li style="margin-bottom: 10px;">Explore our features</li>
<li style="margin-bottom: 10px;">Connect with others</li>
</ul>

<div class="divider"></div>

<p style="margin: 20px 0 0; color: #6b7280; font-size: 14px;">
If you have any questions, feel free to reach out to us at
<a href="mailto:{{ $supportEmail }}">{{ $supportEmail }}</a>.
</p>
@endsection

订单确认邮件模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<!-- resources/views/emails/orders/confirmation.blade.php -->
@extends('emails.layouts.default')

@section('content')
<h2 style="margin: 0 0 10px; color: #111827; font-size: 22px;">
Order Confirmed!
</h2>

<p style="margin: 0 0 20px; color: #4b5563;">
Order #{{ $order->order_number }} • {{ $order->created_at->format('F j, Y') }}
</p>

<div style="background-color: #f9fafb; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid #e5e7eb;">
<th style="text-align: left; padding: 10px 0; color: #6b7280; font-weight: 500;">Item</th>
<th style="text-align: center; padding: 10px 0; color: #6b7280; font-weight: 500;">Qty</th>
<th style="text-align: right; padding: 10px 0; color: #6b7280; font-weight: 500;">Price</th>
</tr>
</thead>
<tbody>
@foreach($items as $item)
<tr style="border-bottom: 1px solid #e5e7eb;">
<td style="padding: 15px 0;">
<strong style="color: #111827;">{{ $item->name }}</strong>
@if($item->variant)
<br><span style="color: #6b7280; font-size: 14px;">{{ $item->variant }}</span>
@endif
</td>
<td style="text-align: center; padding: 15px 0; color: #4b5563;">
{{ $item->quantity }}
</td>
<td style="text-align: right; padding: 15px 0; color: #111827;">
${{ number_format($item->total, 2) }}
</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="2" style="text-align: right; padding: 15px 0; color: #6b7280;">Subtotal:</td>
<td style="text-align: right; padding: 15px 0; color: #111827;">${{ number_format($order->subtotal, 2) }}</td>
</tr>
<tr>
<td colspan="2" style="text-align: right; padding: 5px 0; color: #6b7280;">Shipping:</td>
<td style="text-align: right; padding: 5px 0; color: #111827;">${{ number_format($order->shipping, 2) }}</td>
</tr>
<tr>
<td colspan="2" style="text-align: right; padding: 15px 0; color: #111827; font-weight: 600;">Total:</td>
<td style="text-align: right; padding: 15px 0; color: #111827; font-weight: 600;">${{ number_format($order->total, 2) }}</td>
</tr>
</tfoot>
</table>
</div>

<div style="display: flex; gap: 20px; margin-bottom: 20px;">
<div style="flex: 1;">
<h4 style="margin: 0 0 10px; color: #111827; font-size: 14px;">Shipping Address</h4>
<p style="margin: 0; color: #6b7280; font-size: 14px; line-height: 1.6;">
{{ $shipping->name }}<br>
{{ $shipping->address_line_1 }}<br>
@if($shipping->address_line_2)
{{ $shipping->address_line_2 }}<br>
@endif
{{ $shipping->city }}, {{ $shipping->state }} {{ $shipping->zip }}
</p>
</div>
</div>

@if($trackingUrl)
<div style="margin: 30px 0;">
<a href="{{ $trackingUrl }}" class="button">
Track Your Order
</a>
</div>
@endif

<p style="margin: 20px 0 0; color: #6b7280; font-size: 14px;">
If you have any questions about your order, please contact our support team.
</p>
@endsection

邮件模板组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?php

namespace App\Services\Mail;

class EmailComponent
{
public static function button(string $text, string $url, string $color = '#4f46e5'): string
{
return <<<HTML
<a href="{$url}" style="display: inline-block; padding: 12px 24px; background-color: {$color}; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 600;">
{$text}
</a>
HTML;
}

public static function divider(): string
{
return '<div style="border-top: 1px solid #e5e7eb; margin: 20px 0;"></div>';
}

public static function spacer(int $height = 20): string
{
return "<div style=\"height: {$height}px;\"></div>";
}

public static function alert(string $message, string $type = 'info'): string
{
$colors = [
'info' => '#3b82f6',
'success' => '#10b981',
'warning' => '#f59e0b',
'error' => '#ef4444',
];

$color = $colors[$type] ?? $colors['info'];

return <<<HTML
<div style="padding: 16px; background-color: {$color}15; border-left: 4px solid {$color}; margin: 20px 0;">
<p style="margin: 0; color: {$color};">{$message}</p>
</div>
HTML;
}

public static function table(array $headers, array $rows): string
{
$html = '<table style="width: 100%; border-collapse: collapse;">';

$html .= '<thead><tr>';
foreach ($headers as $header) {
$html .= "<th style=\"text-align: left; padding: 10px; border-bottom: 2px solid #e5e7eb; color: #6b7280;\">{$header}</th>";
}
$html .= '</tr></thead>';

$html .= '<tbody>';
foreach ($rows as $row) {
$html .= '<tr>';
foreach ($row as $cell) {
$html .= "<td style=\"padding: 10px; border-bottom: 1px solid #e5e7eb;\">{$cell}</td>";
}
$html .= '</tr>';
}
$html .= '</tbody></table>';

return $html;
}
}

邮件预览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

namespace App\Http\Controllers;

use App\Mail\Auth\WelcomeEmail;
use App\Mail\Orders\OrderConfirmationEmail;
use App\Models\Order;
use App\Models\User;
use Illuminate\Http\Request;

class MailPreviewController extends Controller
{
public function welcome()
{
$user = User::first() ?? new User([
'name' => 'John Doe',
'email' => 'john@example.com',
]);

return (new WelcomeEmail($user))->render();
}

public function orderConfirmation()
{
$order = Order::first();

if (!$order) {
return 'No orders found for preview';
}

return (new OrderConfirmationEmail($order))->render();
}
}

邮件模板命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class MakeMailTemplateCommand extends Command
{
protected $signature = 'make:mail-template {name} {--layout=default}';
protected $description = 'Create a new email template';

public function handle(): int
{
$name = $this->argument('name');
$layout = $this->option('layout');

$path = resource_path("views/emails/{$name}.blade.php");

if (File::exists($path)) {
$this->error("Template {$name} already exists!");
return self::FAILURE;
}

$content = $this->getTemplateContent($layout);

File::ensureDirectoryExists(dirname($path));
File::put($path, $content);

$this->info("Email template created: {$path}");

return self::SUCCESS;
}

protected function getTemplateContent(string $layout): string
{
return <<<BLADE
@extends('emails.layouts.{$layout}')

@section('content')
<h2 style="margin: 0 0 20px; color: #111827; font-size: 22px;">
Email Title
</h2>

<p style="margin: 0 0 20px; color: #4b5563; line-height: 1.6;">
Email content goes here.
</p>
@endsection
BLADE;
}
}

总结

Laravel 13 的邮件模板系统提供了灵活的布局继承、组件复用和样式管理功能。通过合理的模板组织,可以创建专业、美观且易于维护的邮件模板。