mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-24 03:57:27 +08:00
feat: email template management with DB override, modern mail redesign
This commit is contained in:
@@ -147,7 +147,6 @@ class ConfigController extends Controller
|
||||
'server_ws_url' => admin_setting('server_ws_url', ''),
|
||||
],
|
||||
'email' => [
|
||||
'email_template' => admin_setting('email_template', 'default'),
|
||||
'email_host' => admin_setting('email_host'),
|
||||
'email_port' => admin_setting('email_port'),
|
||||
'email_username' => admin_setting('email_username'),
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\MailTemplate;
|
||||
use App\Services\MailService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MailTemplateController extends Controller
|
||||
{
|
||||
public function list()
|
||||
{
|
||||
$dbTemplates = MailTemplate::all()->keyBy('name');
|
||||
|
||||
$result = [];
|
||||
foreach (MailTemplate::TEMPLATES as $name => $meta) {
|
||||
$db = $dbTemplates->get($name);
|
||||
$result[] = [
|
||||
'name' => $name,
|
||||
'label' => $meta['label'],
|
||||
'customized' => $db !== null,
|
||||
'subject' => $db?->subject,
|
||||
'updated_at' => $db?->updated_at?->timestamp,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
public function get(Request $request)
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$meta = MailTemplate::getMeta($name);
|
||||
if (!$meta) {
|
||||
return $this->fail([404, '模板不存在']);
|
||||
}
|
||||
|
||||
$db = MailTemplate::where('name', $name)->first();
|
||||
|
||||
return $this->success([
|
||||
'name' => $name,
|
||||
'label' => $meta['label'],
|
||||
'required_vars' => $meta['required_vars'],
|
||||
'optional_vars' => $meta['optional_vars'],
|
||||
'customized' => $db !== null,
|
||||
'subject' => $db?->subject ?? $this->getDefaultSubject($name),
|
||||
'content' => $db?->content ?? $this->getDefaultContent($name),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
$params = $request->validate([
|
||||
'name' => 'required|string',
|
||||
'subject' => 'required|string|max:255',
|
||||
'content' => 'required|string',
|
||||
]);
|
||||
|
||||
$meta = MailTemplate::getMeta($params['name']);
|
||||
if (!$meta) {
|
||||
return $this->fail([404, '模板不存在']);
|
||||
}
|
||||
|
||||
$errors = MailTemplate::validateContent($params['name'], $params['content']);
|
||||
if (!empty($errors)) {
|
||||
return $this->fail([422, implode('; ', $errors)]);
|
||||
}
|
||||
|
||||
MailTemplate::updateOrCreate(
|
||||
['name' => $params['name']],
|
||||
['subject' => $params['subject'], 'content' => $params['content']]
|
||||
);
|
||||
Cache::forget("mail_template:{$params['name']}");
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function reset(Request $request)
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$meta = MailTemplate::getMeta($name);
|
||||
if (!$meta) {
|
||||
return $this->fail([404, '模板不存在']);
|
||||
}
|
||||
|
||||
MailTemplate::where('name', $name)->delete();
|
||||
Cache::forget("mail_template:{$name}");
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function test(Request $request)
|
||||
{
|
||||
$name = $request->input('name');
|
||||
$meta = MailTemplate::getMeta($name);
|
||||
if (!$meta) {
|
||||
return $this->fail([404, '模板不存在']);
|
||||
}
|
||||
|
||||
$email = $request->input('email', $request->user()->email);
|
||||
$testVars = $this->getTestVars($name);
|
||||
|
||||
try {
|
||||
$log = MailService::sendEmail([
|
||||
'email' => $email,
|
||||
'subject' => $this->getTestSubject($name),
|
||||
'template_name' => $name,
|
||||
'template_value' => $testVars,
|
||||
]);
|
||||
|
||||
if ($log['error']) {
|
||||
return $this->fail([500, '发送失败: ' . $log['error']]);
|
||||
}
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return $this->fail([500, '发送失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function getTestSubject(string $name): string
|
||||
{
|
||||
$appName = admin_setting('app_name', 'XBoard');
|
||||
return match ($name) {
|
||||
'verify' => "{$appName} - 验证码测试",
|
||||
'notify' => "{$appName} - 通知测试",
|
||||
'remindExpire' => "{$appName} - 到期提醒测试",
|
||||
'remindTraffic' => "{$appName} - 流量提醒测试",
|
||||
'mailLogin' => "{$appName} - 登录链接测试",
|
||||
default => "{$appName} - 邮件测试",
|
||||
};
|
||||
}
|
||||
|
||||
private function getTestVars(string $name): array
|
||||
{
|
||||
$appName = admin_setting('app_name', 'XBoard');
|
||||
$appUrl = admin_setting('app_url', 'https://example.com');
|
||||
|
||||
return match ($name) {
|
||||
'verify' => [
|
||||
'name' => $appName,
|
||||
'code' => '123456',
|
||||
'url' => $appUrl,
|
||||
],
|
||||
'notify' => [
|
||||
'name' => $appName,
|
||||
'content' => '这是一封测试通知邮件。',
|
||||
'url' => $appUrl,
|
||||
],
|
||||
'remindExpire' => [
|
||||
'name' => $appName,
|
||||
'url' => $appUrl,
|
||||
],
|
||||
'remindTraffic' => [
|
||||
'name' => $appName,
|
||||
'url' => $appUrl,
|
||||
],
|
||||
'mailLogin' => [
|
||||
'name' => $appName,
|
||||
'link' => $appUrl . '/login?token=test-token',
|
||||
'url' => $appUrl,
|
||||
],
|
||||
default => ['name' => $appName, 'url' => $appUrl],
|
||||
};
|
||||
}
|
||||
|
||||
private function getDefaultSubject(string $name): string
|
||||
{
|
||||
$appName = admin_setting('app_name', 'XBoard');
|
||||
return match ($name) {
|
||||
'verify' => "{$appName} - 邮箱验证码",
|
||||
'notify' => "{$appName} - 站点通知",
|
||||
'remindExpire' => "{$appName} - 服务即将到期",
|
||||
'remindTraffic' => "{$appName} - 流量使用提醒",
|
||||
'mailLogin' => "{$appName} - 邮件登录",
|
||||
default => "{$appName}",
|
||||
};
|
||||
}
|
||||
|
||||
private function getDefaultContent(string $name): string
|
||||
{
|
||||
$theme = 'default';
|
||||
$viewName = "mail.{$theme}.{$name}";
|
||||
|
||||
try {
|
||||
$viewPath = resource_path("views/mail/{$theme}/{$name}.blade.php");
|
||||
if (file_exists($viewPath)) {
|
||||
$blade = file_get_contents($viewPath);
|
||||
return self::bladeToPlaceholder($blade);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return self::hardcodedDefault($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Blade syntax to {{placeholder}} syntax for editing.
|
||||
*/
|
||||
private static function bladeToPlaceholder(string $blade): string
|
||||
{
|
||||
// {{$var}} → {{var}}
|
||||
$result = preg_replace('/\{\{\s*\$([a-zA-Z_]+)\s*\}\}/', '{{$1}}', $blade);
|
||||
// {!! nl2br($var) !!} → {{var}}
|
||||
$result = preg_replace('/\{!!\s*nl2br\(\$([a-zA-Z_]+)\)\s*!!\}/', '{{$1}}', $result);
|
||||
// {!! $var !!} → {{var}}
|
||||
$result = preg_replace('/\{!!\s*\$([a-zA-Z_]+)\s*!!\}/', '{{$1}}', $result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function hardcodedDefault(string $name): string
|
||||
{
|
||||
$layout = fn($title, $body) => <<<HTML
|
||||
<div style="background: #eee">
|
||||
<table width="600" border="0" align="center" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background:#fff">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td valign="middle" style="padding-left:30px;background-color:#415A94;color:#fff;padding:20px 40px;font-size: 21px;">{{name}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="padding:40px 40px 0 40px;display:table-cell">
|
||||
<td style="font-size:24px;line-height:1.5;color:#000;margin-top:40px">{$title}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:14px;color:#333;padding:24px 40px 0 40px">
|
||||
尊敬的用户您好!<br /><br />{$body}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:20px 40px;font-size:12px;color:#999;line-height:20px;background:#f7f7f7"><a href="{{url}}" style="font-size:14px;color:#929292">返回{{name}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
return match ($name) {
|
||||
'verify' => $layout('邮箱验证码', '您的验证码是:{{code}},请在 5 分钟内进行验证。如果该验证码不为您本人申请,请无视。'),
|
||||
'notify' => $layout('网站通知', '{{content}}'),
|
||||
'remindExpire' => $layout('服务到期提醒', '您的服务即将在24小时内到期,如需继续使用请及时续费。'),
|
||||
'remindTraffic' => $layout('流量使用提醒', '您的流量使用已达到80%,请注意流量使用情况。'),
|
||||
'mailLogin' => $layout('登入到{{name}}', '您正在登入到{{name}}, 请在 5 分钟内点击下方链接进行登入。如果您未授权该登入请求,请无视。<a href="{{link}}">{{link}}</a>'),
|
||||
default => $layout('通知', '{{content}}'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,6 @@ class ConfigSave extends FormRequest
|
||||
'frontend_theme_color' => 'nullable|in:default,darkblue,black,green',
|
||||
'frontend_background_url' => 'nullable|url',
|
||||
// email
|
||||
'email_template' => '',
|
||||
'email_host' => '',
|
||||
'email_port' => '',
|
||||
'email_username' => '',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
namespace App\Http\Routes\V2;
|
||||
|
||||
use App\Http\Controllers\V2\Admin\ConfigController;
|
||||
use App\Http\Controllers\V2\Admin\MailTemplateController;
|
||||
use App\Http\Controllers\V2\Admin\PlanController;
|
||||
use App\Http\Controllers\V2\Admin\Server\GroupController;
|
||||
use App\Http\Controllers\V2\Admin\Server\RouteController;
|
||||
@@ -41,6 +42,17 @@ class AdminRoute
|
||||
$router->post('/testSendMail', [ConfigController::class, 'testSendMail']);
|
||||
});
|
||||
|
||||
// Mail Templates
|
||||
$router->group([
|
||||
'prefix' => 'mail/template'
|
||||
], function ($router) {
|
||||
$router->get('/list', [MailTemplateController::class, 'list']);
|
||||
$router->get('/get', [MailTemplateController::class, 'get']);
|
||||
$router->post('/save', [MailTemplateController::class, 'save']);
|
||||
$router->post('/reset', [MailTemplateController::class, 'reset']);
|
||||
$router->post('/test', [MailTemplateController::class, 'test']);
|
||||
});
|
||||
|
||||
// Plan
|
||||
$router->group([
|
||||
'prefix' => 'plan'
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MailTemplate extends Model
|
||||
{
|
||||
protected $table = 'v2_mail_templates';
|
||||
|
||||
protected $fillable = ['name', 'subject', 'content'];
|
||||
|
||||
/**
|
||||
* Template definitions: required/optional vars and default content.
|
||||
*/
|
||||
public const TEMPLATES = [
|
||||
'verify' => [
|
||||
'label' => '邮箱验证码',
|
||||
'required_vars' => ['code'],
|
||||
'optional_vars' => ['name', 'url'],
|
||||
],
|
||||
'notify' => [
|
||||
'label' => '站点通知',
|
||||
'required_vars' => ['content'],
|
||||
'optional_vars' => ['name', 'url'],
|
||||
],
|
||||
'remindExpire' => [
|
||||
'label' => '到期提醒',
|
||||
'required_vars' => [],
|
||||
'optional_vars' => ['name', 'url'],
|
||||
],
|
||||
'remindTraffic' => [
|
||||
'label' => '流量提醒',
|
||||
'required_vars' => [],
|
||||
'optional_vars' => ['name', 'url'],
|
||||
],
|
||||
'mailLogin' => [
|
||||
'label' => '邮件登录',
|
||||
'required_vars' => ['link'],
|
||||
'optional_vars' => ['name', 'url'],
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get template metadata (vars, label) for a given template name.
|
||||
*/
|
||||
public static function getMeta(string $name): ?array
|
||||
{
|
||||
return self::TEMPLATES[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all template names.
|
||||
*/
|
||||
public static function getNames(): array
|
||||
{
|
||||
return array_keys(self::TEMPLATES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that required placeholders are present in the content.
|
||||
*/
|
||||
public static function validateContent(string $name, string $content): array
|
||||
{
|
||||
$meta = self::getMeta($name);
|
||||
if (!$meta) {
|
||||
return ["Unknown template: {$name}"];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach ($meta['required_vars'] as $var) {
|
||||
if (strpos($content, '{{' . $var . '}}') === false) {
|
||||
$errors[] = "缺少必要占位符: {{{$var}}}";
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ class MailLinkService
|
||||
'subject' => __('Login to :name', [
|
||||
'name' => admin_setting('app_name', 'XBoard')
|
||||
]),
|
||||
'template_name' => 'login',
|
||||
'template_name' => 'mailLogin',
|
||||
'template_value' => [
|
||||
'name' => admin_setting('app_name', 'XBoard'),
|
||||
'link' => $link,
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Jobs\SendEmailJob;
|
||||
use App\Models\MailLog;
|
||||
use App\Models\MailTemplate;
|
||||
use App\Models\User;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -249,6 +250,7 @@ class MailService
|
||||
}
|
||||
$email = $params['email'];
|
||||
$subject = $params['subject'];
|
||||
$templateName = $params['template_name'];
|
||||
|
||||
$templateValue = $params['template_value'] ?? [];
|
||||
$vars = is_array($templateValue) ? ($templateValue['vars'] ?? []) : [];
|
||||
@@ -262,21 +264,44 @@ class MailService
|
||||
}
|
||||
}
|
||||
|
||||
// Mass mail default: treat admin content as plain text and escape.
|
||||
if ($contentMode === 'text' && is_array($templateValue) && isset($templateValue['content']) && is_string($templateValue['content'])) {
|
||||
$templateValue['content'] = e($templateValue['content']);
|
||||
}
|
||||
|
||||
$params['template_value'] = $templateValue;
|
||||
$params['template_name'] = 'mail.' . admin_setting('email_template', 'default') . '.' . $params['template_name'];
|
||||
|
||||
// Check for DB template override (cached to avoid per-email queries in bulk sends).
|
||||
// Cache 'none' sentinel for templates that don't exist in DB.
|
||||
$cacheKey = "mail_template:{$templateName}";
|
||||
$cached = Cache::get($cacheKey);
|
||||
if ($cached === null) {
|
||||
$dbTemplate = MailTemplate::where('name', $templateName)->first();
|
||||
Cache::put($cacheKey, $dbTemplate ?: 'none', 3600);
|
||||
} else {
|
||||
$dbTemplate = ($cached === 'none') ? null : $cached;
|
||||
}
|
||||
|
||||
try {
|
||||
Mail::send(
|
||||
$params['template_name'],
|
||||
$params['template_value'],
|
||||
function ($message) use ($email, $subject) {
|
||||
if ($dbTemplate) {
|
||||
$renderVars = self::buildSafeVars($templateValue);
|
||||
$renderedSubject = self::renderPlaceholders($dbTemplate->subject, $renderVars);
|
||||
$renderedContent = self::renderPlaceholders($dbTemplate->content, $renderVars);
|
||||
$subject = $renderedSubject ?: $subject;
|
||||
|
||||
Mail::html($renderedContent, function ($message) use ($email, $subject) {
|
||||
$message->to($email)->subject($subject);
|
||||
}
|
||||
);
|
||||
});
|
||||
$params['template_name'] = 'db:' . $templateName;
|
||||
} else {
|
||||
$params['template_name'] = 'mail.default.' . $templateName;
|
||||
Mail::send(
|
||||
$params['template_name'],
|
||||
$params['template_value'],
|
||||
function ($message) use ($email, $subject) {
|
||||
$message->to($email)->subject($subject);
|
||||
}
|
||||
);
|
||||
}
|
||||
$error = null;
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
@@ -292,4 +317,26 @@ class MailService
|
||||
MailLog::create($log);
|
||||
return $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build HTML-escaped vars for DB template rendering.
|
||||
*/
|
||||
private static function buildSafeVars(array $templateValue): array
|
||||
{
|
||||
$safe = [];
|
||||
foreach ($templateValue as $key => $value) {
|
||||
if (is_scalar($value)) {
|
||||
$safe[$key] = e((string) $value);
|
||||
}
|
||||
}
|
||||
// 'content' may be pre-escaped text or admin-authored HTML.
|
||||
// For text mode, apply nl2br so line breaks survive in DB templates
|
||||
// (Blade templates handle this with {!! nl2br($content) !!}).
|
||||
if (isset($templateValue['content'])) {
|
||||
$content = (string) $templateValue['content'];
|
||||
$contentMode = $templateValue['content_mode'] ?? null;
|
||||
$safe['content'] = ($contentMode === 'text') ? nl2br($content) : $content;
|
||||
}
|
||||
return $safe;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('v2_mail_templates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 64)->unique();
|
||||
$table->string('subject', 255);
|
||||
$table->longText('content');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('v2_mail_templates');
|
||||
}
|
||||
};
|
||||
+1
-1
Submodule public/assets/admin updated: 37c135bdb8...f03d07a879
@@ -1,43 +1,37 @@
|
||||
<div style="background: #eee">
|
||||
<table width="600" border="0" align="center" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background:#fff">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td valign="middle" style="padding-left:30px;background-color:#415A94;color:#fff;padding:20px 40px;font-size: 21px;">{{$name}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="padding:40px 40px 0 40px;display:table-cell">
|
||||
<td style="font-size:24px;line-height:1.5;color:#000;margin-top:40px">登入到{{$name}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:14px;color:#333;padding:24px 40px 0 40px">
|
||||
尊敬的用户您好!
|
||||
<br />
|
||||
<br />
|
||||
您正在登入到{{$name}}, 请在 5 分钟内点击下方链接进行登入。如果您未授权该登入请求,请无视。
|
||||
<a href="{{$link}}">{{$link}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="padding:40px;display:table-cell">
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:20px 40px;font-size:12px;color:#999;line-height:20px;background:#f7f7f7"><a href="{{$url}}" style="font-size:14px;color:#929292">返回{{$name}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>邮箱登录</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;padding:40px 20px;">
|
||||
<tr><td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;">
|
||||
<!-- Logo -->
|
||||
<tr><td style="padding-bottom:24px;text-align:center;">
|
||||
<span style="font-size:20px;font-weight:700;color:#18181b;">{{$name}}</span>
|
||||
</td></tr>
|
||||
<!-- Card -->
|
||||
<tr><td style="background:#ffffff;border-radius:12px;border:1px solid #e4e4e7;padding:40px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr><td style="font-size:22px;font-weight:700;color:#18181b;padding-bottom:8px;">登录确认</td></tr>
|
||||
<tr><td style="font-size:15px;color:#52525b;line-height:1.7;padding-bottom:28px;">点击下方按钮登录到 {{$name}},链接有效期 5 分钟。如非本人操作,请忽略此邮件。</td></tr>
|
||||
<tr><td align="center" style="padding-bottom:28px;">
|
||||
<a href="{{$link}}" style="display:inline-block;background:#18181b;color:#ffffff;font-size:14px;font-weight:600;text-decoration:none;padding:14px 36px;border-radius:8px;">确认登录</a>
|
||||
</td></tr>
|
||||
<tr><td style="font-size:13px;color:#a1a1aa;line-height:1.5;">如果按钮无法点击,请复制以下链接到浏览器中打开:</td></tr>
|
||||
<tr><td style="font-size:13px;color:#71717a;line-height:1.5;word-break:break-all;padding-top:8px;">{{$link}}</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- Footer -->
|
||||
<tr><td style="padding-top:24px;text-align:center;">
|
||||
<a href="{{$url}}" style="font-size:13px;color:#a1a1aa;text-decoration:none;">{{$url}}</a>
|
||||
<p style="font-size:12px;color:#d4d4d8;margin:8px 0 0;">此邮件由系统自动发送,请勿直接回复。</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
<div style="background: #eee">
|
||||
<table width="600" border="0" align="center" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background:#fff">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td valign="middle" style="padding-left:30px;background-color:#415A94;color:#fff;padding:20px 40px;font-size: 21px;">{{$name}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="padding:40px 40px 0 40px;display:table-cell">
|
||||
<td style="font-size:24px;line-height:1.5;color:#000;margin-top:40px">网站通知</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:14px;color:#333;padding:24px 40px 0 40px">
|
||||
尊敬的用户您好!
|
||||
<br />
|
||||
<br />
|
||||
{!! nl2br($content) !!}
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="padding:40px;display:table-cell">
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:20px 40px;font-size:12px;color:#999;line-height:20px;background:#f7f7f7"><a href="{{$url}}" style="font-size:14px;color:#929292">返回{{$name}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>网站通知</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;padding:40px 20px;">
|
||||
<tr><td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;">
|
||||
<!-- Logo -->
|
||||
<tr><td style="padding-bottom:24px;text-align:center;">
|
||||
<span style="font-size:20px;font-weight:700;color:#18181b;">{{$name}}</span>
|
||||
</td></tr>
|
||||
<!-- Card -->
|
||||
<tr><td style="background:#ffffff;border-radius:12px;border:1px solid #e4e4e7;padding:40px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr><td style="font-size:22px;font-weight:700;color:#18181b;padding-bottom:8px;">网站通知</td></tr>
|
||||
<tr><td style="font-size:15px;color:#52525b;line-height:1.7;padding-bottom:28px;">{!! nl2br($content) !!}</td></tr>
|
||||
<tr><td align="center">
|
||||
<a href="{{$url}}" style="display:inline-block;background:#18181b;color:#ffffff;font-size:14px;font-weight:600;text-decoration:none;padding:12px 28px;border-radius:8px;">前往查看</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- Footer -->
|
||||
<tr><td style="padding-top:24px;text-align:center;">
|
||||
<a href="{{$url}}" style="font-size:13px;color:#a1a1aa;text-decoration:none;">{{$url}}</a>
|
||||
<p style="font-size:12px;color:#d4d4d8;margin:8px 0 0;">此邮件由系统自动发送,请勿直接回复。</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
<div style="background: #eee">
|
||||
<table width="600" border="0" align="center" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background:#fff">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td valign="middle" style="padding-left:30px;background-color:#415A94;color:#fff;padding:20px 40px;font-size: 21px;">{{$name}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="padding:40px 40px 0 40px;display:table-cell">
|
||||
<td style="font-size:24px;line-height:1.5;color:#000;margin-top:40px">到期通知</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:14px;color:#333;padding:24px 40px 0 40px">
|
||||
尊敬的用户您好!
|
||||
<br />
|
||||
<br />
|
||||
你的服务将在24小时内到期。为了不造成使用上的影响请尽快续费。如果你已续费请忽略此邮件。
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="padding:40px;display:table-cell">
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:20px 40px;font-size:12px;color:#999;line-height:20px;background:#f7f7f7"><a href="{{$url}}" style="font-size:14px;color:#929292">返回{{$name}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>到期提醒</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;padding:40px 20px;">
|
||||
<tr><td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;">
|
||||
<!-- Logo -->
|
||||
<tr><td style="padding-bottom:24px;text-align:center;">
|
||||
<span style="font-size:20px;font-weight:700;color:#18181b;">{{$name}}</span>
|
||||
</td></tr>
|
||||
<!-- Card -->
|
||||
<tr><td style="background:#ffffff;border-radius:12px;border:1px solid #e4e4e7;padding:40px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr><td style="font-size:22px;font-weight:700;color:#18181b;padding-bottom:8px;">订阅即将到期</td></tr>
|
||||
<tr><td style="font-size:15px;color:#52525b;line-height:1.7;padding-bottom:12px;">您的订阅服务将在 <strong style="color:#18181b;">24 小时</strong>内到期。</td></tr>
|
||||
<tr><td style="font-size:15px;color:#52525b;line-height:1.7;padding-bottom:28px;">为避免服务中断,请及时续费。如您已完成续费,请忽略此提醒。</td></tr>
|
||||
<tr><td align="center">
|
||||
<a href="{{$url}}" style="display:inline-block;background:#18181b;color:#ffffff;font-size:14px;font-weight:600;text-decoration:none;padding:12px 28px;border-radius:8px;">立即续费</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- Footer -->
|
||||
<tr><td style="padding-top:24px;text-align:center;">
|
||||
<a href="{{$url}}" style="font-size:13px;color:#a1a1aa;text-decoration:none;">{{$url}}</a>
|
||||
<p style="font-size:12px;color:#d4d4d8;margin:8px 0 0;">此邮件由系统自动发送,请勿直接回复。</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
<div style="background: #eee">
|
||||
<table width="600" border="0" align="center" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background:#fff">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td valign="middle" style="padding-left:30px;background-color:#415A94;color:#fff;padding:20px 40px;font-size: 21px;">{{$name}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="padding:40px 40px 0 40px;display:table-cell">
|
||||
<td style="font-size:24px;line-height:1.5;color:#000;margin-top:40px">流量通知</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:14px;color:#333;padding:24px 40px 0 40px">
|
||||
尊敬的用户您好!
|
||||
<br />
|
||||
<br />
|
||||
你的流量已经使用80%。为了不造成使用上的影响请合理安排流量的使用。
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="padding:40px;display:table-cell">
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:20px 40px;font-size:12px;color:#999;line-height:20px;background:#f7f7f7"><a href="{{$url}}" style="font-size:14px;color:#929292">返回{{$name}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>流量提醒</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;padding:40px 20px;">
|
||||
<tr><td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;">
|
||||
<!-- Logo -->
|
||||
<tr><td style="padding-bottom:24px;text-align:center;">
|
||||
<span style="font-size:20px;font-weight:700;color:#18181b;">{{$name}}</span>
|
||||
</td></tr>
|
||||
<!-- Card -->
|
||||
<tr><td style="background:#ffffff;border-radius:12px;border:1px solid #e4e4e7;padding:40px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr><td style="font-size:22px;font-weight:700;color:#18181b;padding-bottom:8px;">流量使用提醒</td></tr>
|
||||
<tr><td style="font-size:15px;color:#52525b;line-height:1.7;padding-bottom:12px;">您本月的套餐流量已使用 <strong style="color:#18181b;">80%</strong>。</td></tr>
|
||||
<tr><td style="font-size:15px;color:#52525b;line-height:1.7;padding-bottom:28px;">请合理安排使用,避免提前耗尽。如需更多流量,可前往面板升级套餐。</td></tr>
|
||||
<tr><td align="center">
|
||||
<a href="{{$url}}" style="display:inline-block;background:#18181b;color:#ffffff;font-size:14px;font-weight:600;text-decoration:none;padding:12px 28px;border-radius:8px;">查看用量</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- Footer -->
|
||||
<tr><td style="padding-top:24px;text-align:center;">
|
||||
<a href="{{$url}}" style="font-size:13px;color:#a1a1aa;text-decoration:none;">{{$url}}</a>
|
||||
<p style="font-size:12px;color:#d4d4d8;margin:8px 0 0;">此邮件由系统自动发送,请勿直接回复。</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
<div style="background: #eee">
|
||||
<table width="600" border="0" align="center" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<div style="background:#fff">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td valign="middle" style="padding-left:30px;background-color:#415A94;color:#fff;padding:20px 40px;font-size: 21px;">{{$name}}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style="padding:40px 40px 0 40px;display:table-cell">
|
||||
<td style="font-size:24px;line-height:1.5;color:#000;margin-top:40px">邮箱验证码</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-size:14px;color:#333;padding:24px 40px 0 40px">
|
||||
尊敬的用户您好!
|
||||
<br />
|
||||
<br />
|
||||
您的验证码是:{{$code}},请在 5 分钟内进行验证。如果该验证码不为您本人申请,请无视。
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="padding:40px;display:table-cell">
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:20px 40px;font-size:12px;color:#999;line-height:20px;background:#f7f7f7"><a href="{{$url}}" style="font-size:14px;color:#929292">返回{{$name}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>邮箱验证码</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:#f4f4f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f5;padding:40px 20px;">
|
||||
<tr><td align="center">
|
||||
<table width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;">
|
||||
<!-- Logo -->
|
||||
<tr><td style="padding-bottom:24px;text-align:center;">
|
||||
<span style="font-size:20px;font-weight:700;color:#18181b;">{{$name}}</span>
|
||||
</td></tr>
|
||||
<!-- Card -->
|
||||
<tr><td style="background:#ffffff;border-radius:12px;border:1px solid #e4e4e7;padding:40px;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr><td style="font-size:22px;font-weight:700;color:#18181b;padding-bottom:8px;">邮箱验证码</td></tr>
|
||||
<tr><td style="font-size:15px;color:#52525b;line-height:1.6;padding-bottom:28px;">请使用以下验证码完成验证,有效期 5 分钟。如非本人操作,请忽略此邮件。</td></tr>
|
||||
<tr><td align="center" style="padding-bottom:28px;">
|
||||
<div style="display:inline-block;background:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:16px 40px;font-size:32px;font-weight:700;letter-spacing:6px;color:#18181b;font-family:'Courier New',Courier,monospace;">{{$code}}</div>
|
||||
</td></tr>
|
||||
<tr><td style="font-size:13px;color:#a1a1aa;line-height:1.5;">如果您没有请求此验证码,无需进行任何操作。</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<!-- Footer -->
|
||||
<tr><td style="padding-top:24px;text-align:center;">
|
||||
<a href="{{$url}}" style="font-size:13px;color:#a1a1aa;text-decoration:none;">{{$url}}</a>
|
||||
<p style="font-size:12px;color:#d4d4d8;margin:8px 0 0;">此邮件由系统自动发送,请勿直接回复。</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user