bianjunhui 22 godzin temu
rodzic
commit
43c0e529ab

+ 1 - 0
bootstrap/providers.php

@@ -12,6 +12,7 @@ return [
     Webkul\Admin\Providers\AdminServiceProvider::class,
     Webkul\Admin\Providers\AdminServiceProvider::class,
     Longyi\Core\Providers\LongyiCoreServiceProvider::class,
     Longyi\Core\Providers\LongyiCoreServiceProvider::class,
     Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
     Longyi\DynamicMenu\Providers\DynamicMenuServiceProvider::class,
+    Longyi\Email\Providers\EmailServiceProvider::class,
     Longyi\RewardPoints\Providers\RewardPointsServiceProvider::class,
     Longyi\RewardPoints\Providers\RewardPointsServiceProvider::class,
     Webkul\Attribute\Providers\AttributeServiceProvider::class,
     Webkul\Attribute\Providers\AttributeServiceProvider::class,
     Webkul\BookingProduct\Providers\BookingProductServiceProvider::class,
     Webkul\BookingProduct\Providers\BookingProductServiceProvider::class,

+ 1 - 0
composer.json

@@ -67,6 +67,7 @@
             "Database\\Seeders\\": "database/seeders/",
             "Database\\Seeders\\": "database/seeders/",
             "Longyi\\Core\\": "packages/Longyi/Core/src/",
             "Longyi\\Core\\": "packages/Longyi/Core/src/",
             "Longyi\\DynamicMenu\\": "packages/Longyi/DynamicMenu/src/",
             "Longyi\\DynamicMenu\\": "packages/Longyi/DynamicMenu/src/",
+            "Longyi\\Email\\": "packages/Longyi/Email/src/",
             "Longyi\\RewardPoints\\": "packages/Longyi/RewardPoints/src/",
             "Longyi\\RewardPoints\\": "packages/Longyi/RewardPoints/src/",
             "Webkul\\Admin\\": "packages/Webkul/Admin/src",
             "Webkul\\Admin\\": "packages/Webkul/Admin/src",
             "Webkul\\Attribute\\": "packages/Webkul/Attribute/src",
             "Webkul\\Attribute\\": "packages/Webkul/Attribute/src",

+ 7 - 6
packages/Longyi/DynamicMenu/src/Models/MenuItem.php

@@ -7,18 +7,19 @@ use Illuminate\Support\Facades\Cache;
 class MenuItem extends Model
 class MenuItem extends Model
 {
 {
     protected $table = 'dynamic_menu_items';
     protected $table = 'dynamic_menu_items';
-    
+
     protected $fillable = [
     protected $fillable = [
         'name',
         'name',
         'key',
         'key',
         'route',
         'route',
+        'route_parameters',
         'icon',
         'icon',
         'parent_id',
         'parent_id',
         'sort_order',
         'sort_order',
         'status',
         'status',
         'created_by'
         'created_by'
     ];
     ];
-    
+
     protected $casts = [
     protected $casts = [
         'status' => 'boolean',
         'status' => 'boolean',
         'sort_order' => 'integer'
         'sort_order' => 'integer'
@@ -26,14 +27,14 @@ class MenuItem extends Model
     protected static function boot()
     protected static function boot()
     {
     {
         parent::boot();
         parent::boot();
-        
+
         // 当菜单项被创建、更新或删除时,清除缓存
         // 当菜单项被创建、更新或删除时,清除缓存
         static::saved(function () {
         static::saved(function () {
             Cache::forget('dynamic_menu_config');
             Cache::forget('dynamic_menu_config');
             Cache::forget('dynamic_menu_items');
             Cache::forget('dynamic_menu_items');
             \Log::info('菜单缓存已清除 (saved)');
             \Log::info('菜单缓存已清除 (saved)');
         });
         });
-        
+
         static::deleted(function () {
         static::deleted(function () {
             Cache::forget('dynamic_menu_config');
             Cache::forget('dynamic_menu_config');
             Cache::forget('dynamic_menu_items');
             Cache::forget('dynamic_menu_items');
@@ -48,7 +49,7 @@ class MenuItem extends Model
     {
     {
         return $this->belongsTo(MenuItem::class, 'parent_id');
         return $this->belongsTo(MenuItem::class, 'parent_id');
     }
     }
-    
+
     /**
     /**
      * 获取子菜单
      * 获取子菜单
      */
      */
@@ -56,4 +57,4 @@ class MenuItem extends Model
     {
     {
         return $this->hasMany(MenuItem::class, 'parent_id')->orderBy('sort_order');
         return $this->hasMany(MenuItem::class, 'parent_id')->orderBy('sort_order');
     }
     }
-}
+}

+ 34 - 0
packages/Longyi/Email/README.md

@@ -0,0 +1,34 @@
+### 依赖关系
+
+- **PHP**: ^8.1|^8.2
+- **Laravel**: ^10.0|^11.0
+- **Bagisto**: 2.x
+
+---
+
+## 安装步骤
+
+### 1. 确认插件已注册
+
+确保 `bootstrap/providers.php` 文件中已添加:
+
+Longyi\Email\Providers\EmailServiceProvider::class,
+
+### 2. 运行数据库迁移
+composer dump-autoload
+php artisan migrate
+### 3. 初始化默认设置
+php artisan email:init
+### 4. 清理缓存
+php artisan cache:clear      # 清除应用缓存
+php artisan config:clear     # 清除配置缓存
+php artisan view:clear       # 清除视图缓存
+php artisan route:clear      # 清除路由缓存
+
+## 技术支持
+
+如有问题或建议,请联系开发团队。
+
+**开发者**: Longyi Team  
+**邮箱**: dev@longyi.com  
+**许可证**: MIT License

+ 134 - 0
packages/Longyi/Email/src/Console/Commands/InitializeSettings.php

@@ -0,0 +1,134 @@
+<?php
+
+namespace Longyi\Email\Console\Commands;
+
+use Illuminate\Console\Command;
+use Longyi\DynamicMenu\Models\MenuItem;
+
+class InitializeSettings extends Command
+{
+    protected $signature = 'email:init';
+
+    protected $description = 'Initialize email module (create menu items)';
+
+    public function handle()
+    {
+        $this->info('Initializing email module...');
+
+        try {
+            // 添加邮件模块菜单项到动态菜单
+            $this->addMenuItems();
+
+            $this->info('✓ Email module initialized successfully!');
+            $this->info('You can now view email logs in admin panel.');
+
+            return 0;
+
+        } catch (\Exception $e) {
+            $this->error('Error initializing email module: ' . $e->getMessage());
+            $this->error($e->getTraceAsString());
+            return 1;
+        }
+    }
+
+    /**
+     * 添加邮件模块的菜单项到动态菜单
+     */
+    protected function addMenuItems()
+    {
+        $this->info('Adding email menu items to dynamic menu...');
+
+        // 检查是否已存在邮件日志菜单项
+        $existingMenu = MenuItem::where('key', 'email_logs')->first();
+
+        if ($existingMenu) {
+            $this->warn('Email logs menu item already exists in dynamic menu.');
+            return;
+        }
+
+        // 创建邮件日志菜单项
+        $parentItem = MenuItem::create([
+            'name' => '邮件管理',
+            'key' => 'email',
+            'route' => 'admin.configuration.index',
+            'route_parameters' => json_encode(['emails','configure']),
+            'icon' => 'icon-mail',
+            'parent_id' => null,
+            'sort_order' => 90,
+            'status' => 1,
+            'created_by' => 1,
+        ]);
+
+        // 创建子菜单项
+        $childItems = [
+            [
+                'name' => '邮件设置',
+                'key' => 'email.setting',
+                'route' => 'admin.configuration.index',
+                'route_parameters' => json_encode(['emails','configure']),
+                'icon' => 'icon-setting',
+                'sort_order' => 1,
+            ],
+            [
+                'name' => '邮件通知',
+                'key' => 'email.notifications',
+                'route' => 'admin.configuration.index',
+                'route_parameters' => json_encode(['emails','general']),
+                'icon' => 'icon-setting',
+                'sort_order' => 2,
+            ],
+            [
+                'name' => '邮件模版',
+                'key' => 'email.templates',
+                'route' => 'admin.marketing.communications.email_templates.index',
+                'route_parameters' => json_encode([]),
+                'icon' => 'icon-chart',
+                'sort_order' => 3,
+            ],
+            [
+                'name' => '邮件营销',
+                'key' => 'email.events',
+                'route' => 'admin.marketing.communications.events.index',
+                'route_parameters' => json_encode([]),
+                'icon' => 'icon-chart',
+                'sort_order' => 3,
+            ],
+            [
+                'name' => '邮件活动',
+                'key' => 'email.campaigns',
+                'route' => 'admin.marketing.communications.campaigns.index',
+                'route_parameters' => json_encode([]),
+                'icon' => 'icon-chart',
+                'sort_order' => 3,
+            ],
+            [
+                'name' => '邮件日志',
+                'key' => 'email.log',
+                'route' => 'admin.email.logs.index',
+                'route_parameters' => json_encode([]),
+                'icon' => 'icon-setting',
+                'sort_order' => 4,
+            ],
+        ];
+
+        foreach ($childItems as $item) {
+            MenuItem::create([
+                'name' => $item['name'],
+                'key' => $item['key'],
+                'route' => $item['route'],
+                'route_parameters' => $item['route_parameters'],
+                'icon' => $item['icon'],
+                'sort_order' => $item['sort_order'],
+                'parent_id' => $parentItem->id,
+                'status' => 1,
+                'created_by' => 1,
+            ]);
+        }
+
+
+        $this->info('✓ Email logs menu item added to dynamic menu successfully!');
+        $this->info('  - Menu Name: 邮件日志');
+        $this->info('  - Route: admin.email.logs.index');
+        $this->info('  - Icon: fas fa-envelope-open-text');
+    }
+}

+ 41 - 0
packages/Longyi/Email/src/Database/Migrations/2026_04_20_000001_create_email_logs_table.php

@@ -0,0 +1,41 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     */
+    public function up(): void
+    {
+        Schema::create('email_logs', function (Blueprint $table) {
+            $table->id();
+            $table->string('recipient_email')->comment('收件人邮箱');
+            $table->string('recipient_name')->nullable()->comment('收件人姓名');
+            $table->string('subject')->comment('邮件主题');
+            $table->longText('content')->nullable()->comment('邮件内容');
+            $table->string('template')->nullable()->comment('使用的模板');
+            $table->string('status')->default('pending')->comment('状态: pending, sent, failed');
+            $table->text('error_message')->nullable()->comment('错误信息');
+            $table->json('metadata')->nullable()->comment('元数据');
+            $table->timestamp('sent_at')->nullable()->comment('发送时间');
+            $table->timestamps();
+
+            // 索引优化查询性能
+            $table->index(['recipient_email', 'status']);
+            $table->index('created_at');
+            $table->index('status');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     */
+    public function down(): void
+    {
+        Schema::dropIfExists('email_logs');
+    }
+};

+ 111 - 0
packages/Longyi/Email/src/Http/Controllers/LogController.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace Longyi\Email\Http\Controllers;
+
+use Illuminate\Routing\Controller;
+use Illuminate\Http\Request;
+use Longyi\Email\Models\EmailLog;
+
+class LogController extends Controller
+{
+    /**
+     * 显示邮件日志列表
+     */
+    public function index(Request $request)
+    {
+        $query = EmailLog::query();
+
+        // 筛选:状态
+        if ($request->filled('status')) {
+            $query->where('status', $request->input('status'));
+        }
+
+        // 筛选:邮箱
+        if ($request->filled('email')) {
+            $query->where('recipient_email', 'like', '%' . $request->input('email') . '%');
+        }
+
+        // 筛选:主题
+        if ($request->filled('subject')) {
+            $query->where('subject', 'like', '%' . $request->input('subject') . '%');
+        }
+
+        // 筛选:日期范围
+        if ($request->filled('date_from')) {
+            $query->whereDate('created_at', '>=', $request->input('date_from'));
+        }
+
+        if ($request->filled('date_to')) {
+            $query->whereDate('created_at', '<=', $request->input('date_to'));
+        }
+
+        $logs = $query->orderBy('created_at', 'desc')->paginate(20);
+
+        // 统计数据
+        $stats = [
+            'total' => EmailLog::count(),
+            'sent' => EmailLog::successful()->count(),
+            'failed' => EmailLog::failed()->count(),
+            'pending' => EmailLog::pending()->count(),
+        ];
+
+        return view('email::admin.logs.index', compact('logs', 'stats'));
+    }
+
+    /**
+     * 查看邮件详情
+     */
+    public function show($id)
+    {
+        $log = EmailLog::findOrFail($id);
+
+        return view('email::admin.logs.show', compact('log'));
+    }
+
+    /**
+     * 删除单条日志
+     */
+    public function destroy($id)
+    {
+        $log = EmailLog::findOrFail($id);
+        $log->delete();
+
+        session()->flash('success', '日志删除成功');
+
+        return redirect()->back();
+    }
+
+    /**
+     * 批量删除日志
+     */
+    public function massDestroy(Request $request)
+    {
+        $ids = $request->input('ids', []);
+
+        if (empty($ids)) {
+            session()->flash('error', '请选择要删除的日志');
+            return redirect()->back();
+        }
+
+        $count = EmailLog::whereIn('id', $ids)->delete();
+
+        session()->flash('success', "成功删除 {$count} 条日志");
+
+        return redirect()->back();
+    }
+
+    /**
+     * 清理旧日志(保留最近30天)
+     */
+    public function cleanOldLogs()
+    {
+        $days = request()->input('days', 30);
+        $date = now()->subDays($days);
+
+        $count = EmailLog::where('created_at', '<', $date)->delete();
+
+        session()->flash('success', "已清理 {$count} 条 {$days} 天前的日志");
+
+        return redirect()->back();
+    }
+}

+ 65 - 0
packages/Longyi/Email/src/Models/EmailLog.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Longyi\Email\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Casts\AsArrayObject;
+
+class EmailLog extends Model
+{
+    protected $table = 'email_logs';
+
+    protected $fillable = [
+        'recipient_email',
+        'recipient_name',
+        'subject',
+        'content',
+        'template',
+        'status',
+        'error_message',
+        'metadata',
+        'sent_at',
+    ];
+
+    protected $casts = [
+        'metadata' => AsArrayObject::class,
+        'sent_at' => 'datetime',
+    ];
+
+    /**
+     * Scope: 成功的邮件
+     */
+    public function scopeSuccessful($query)
+    {
+        return $query->where('status', 'sent');
+    }
+
+    /**
+     * Scope: 失败的邮件
+     */
+    public function scopeFailed($query)
+    {
+        return $query->where('status', 'failed');
+    }
+
+    /**
+     * Scope: 待发送的邮件
+     */
+    public function scopePending($query)
+    {
+        return $query->where('status', 'pending');
+    }
+
+    /**
+     * 获取状态文本
+     */
+    public function getStatusTextAttribute(): string
+    {
+        return match($this->status) {
+            'sent' => '已发送',
+            'failed' => '发送失败',
+            'pending' => '待发送',
+            default => '未知',
+        };
+    }
+}

+ 44 - 0
packages/Longyi/Email/src/Providers/EmailServiceProvider.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Longyi\Email\Providers;
+
+use Illuminate\Support\ServiceProvider;
+
+class EmailServiceProvider extends ServiceProvider
+{
+    /**
+     * Bootstrap services.
+     */
+    public function boot(): void
+    {
+        // 加载路由
+        $this->loadRoutesFrom(__DIR__ . '/../Routes/admin-routes.php');
+
+        // 加载视图
+        $this->loadViewsFrom(__DIR__ . '/../Resources/views', 'email');
+
+        // 加载语言文件
+        $this->loadTranslationsFrom(__DIR__ . '/../Resources/lang', 'email');
+
+        // 加载迁移
+        $this->loadMigrationsFrom(__DIR__ . '/../Database/Migrations');
+
+        // 注册事件监听器
+        $this->app->register(EventServiceProvider::class);
+
+        // 注册命令
+        if ($this->app->runningInConsole()) {
+            $this->commands([
+                \Longyi\Email\Console\Commands\InitializeSettings::class,
+            ]);
+        }
+    }
+
+    /**
+     * Register services.
+     */
+    public function register(): void
+    {
+        //
+    }
+}

+ 125 - 0
packages/Longyi/Email/src/Providers/EventServiceProvider.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace Longyi\Email\Providers;
+
+use Illuminate\Support\Facades\Event;
+use Illuminate\Support\ServiceProvider;
+use Longyi\Email\Models\EmailLog;
+
+class EventServiceProvider extends ServiceProvider
+{
+    public function boot(): void
+    {
+        // 监听邮件发送前事件
+        Event::listen('illuminate.mail.sending', function ($event) {
+            try {
+                $message = $event->message;
+
+                // 获取收件人
+                $to = $message->getTo();
+                if (empty($to)) {
+                    return;
+                }
+
+                foreach ($to as $email => $name) {
+                    EmailLog::create([
+                        'recipient_email' => $email,
+                        'recipient_name' => is_string($name) ? $name : null,
+                        'subject' => $message->getSubject() ?? '',
+                        'content' => $message->getBody()?->toString() ?? '',
+                        'template' => $this->extractTemplateName($message),
+                        'status' => 'pending',
+                        'metadata' => [
+                            'from' => $this->formatAddresses($message->getFrom()),
+                            'cc' => $this->formatAddresses($message->getCc()),
+                            'bcc' => $this->formatAddresses($message->getBcc()),
+                            'reply_to' => $this->formatAddresses($message->getReplyTo()),
+                        ],
+                    ]);
+                }
+            } catch (\Exception $e) {
+                \Log::error('Failed to log email sending: ' . $e->getMessage());
+            }
+        });
+
+        // 监听邮件发送成功事件
+        Event::listen('illuminate.mail.sent', function ($event) {
+            try {
+                $message = $event->message;
+                $to = $message->getTo();
+
+                if (empty($to)) {
+                    return;
+                }
+
+                foreach ($to as $email => $name) {
+                    // 更新最近的一条 pending 日志为 sent
+                    EmailLog::where('recipient_email', $email)
+                        ->where('status', 'pending')
+                        ->orderBy('created_at', 'desc')
+                        ->limit(1)
+                        ->update([
+                            'status' => 'sent',
+                            'sent_at' => now(),
+                        ]);
+                }
+            } catch (\Exception $e) {
+                \Log::error('Failed to update email log: ' . $e->getMessage());
+            }
+        });
+
+        // 监听邮件发送失败(通过队列失败事件)
+        Event::listen('Illuminate\Queue\Events\JobFailed', function ($event) {
+            try {
+                $payload = $event->job->payload();
+
+                // 检查是否是邮件任务
+                if (isset($payload['data']['command']) &&
+                    str_contains($payload['data']['command'], 'SendQueuedMailable')) {
+
+                    // 这里可以尝试解析并标记失败的邮件
+                    \Log::info('Email job failed', [
+                        'exception' => $event->exception->getMessage(),
+                    ]);
+                }
+            } catch (\Exception $e) {
+                \Log::error('Failed to log email failure: ' . $e->getMessage());
+            }
+        });
+    }
+
+    /**
+     * 提取模板名称
+     */
+    protected function extractTemplateName($message): ?string
+    {
+        // 尝试从消息头或内容中提取模板信息
+        $headers = $message->getHeaders();
+
+        if ($headers->has('X-Template-Name')) {
+            return $headers->get('X-Template-Name')->getFieldBody();
+        }
+
+        return null;
+    }
+
+    /**
+     * 格式化地址数组
+     */
+    protected function formatAddresses($addresses): array
+    {
+        if (empty($addresses)) {
+            return [];
+        }
+
+        $result = [];
+        foreach ($addresses as $email => $name) {
+            $result[] = [
+                'email' => $email,
+                'name' => is_string($name) ? $name : null,
+            ];
+        }
+
+        return $result;
+    }
+}

+ 49 - 0
packages/Longyi/Email/src/Resources/lang/zh_CN/app.php

@@ -0,0 +1,49 @@
+<?php
+
+return [
+    'email' => [
+        'title' => '邮件管理',
+        'logs' => '邮件日志',
+
+        // 日志列表
+        'recipient_email' => '收件人邮箱',
+        'recipient_name' => '收件人姓名',
+        'subject' => '主题',
+        'status' => '状态',
+        'sent_at' => '发送时间',
+        'created_at' => '创建时间',
+        'view_details' => '查看详情',
+        'delete' => '删除',
+        'mass_delete' => '批量删除',
+        'clean_old_logs' => '清理旧日志',
+
+        // 状态
+        'status_sent' => '已发送',
+        'status_failed' => '发送失败',
+        'status_pending' => '待发送',
+
+        // 筛选
+        'filter_by_status' => '按状态筛选',
+        'filter_by_email' => '按邮箱筛选',
+        'filter_by_subject' => '按主题筛选',
+        'filter_by_date' => '按日期筛选',
+        'all_status' => '全部状态',
+
+        // 统计
+        'total_logs' => '总日志数',
+        'sent_count' => '已发送',
+        'failed_count' => '发送失败',
+        'pending_count' => '待发送',
+
+        // 详情
+        'email_content' => '邮件内容',
+        'error_message' => '错误信息',
+        'metadata' => '元数据',
+        'template_used' => '使用模板',
+
+        // 清理
+        'clean_old_logs_confirm' => '确定要清理旧日志吗?',
+        'keep_days' => '保留天数',
+        'clean' => '清理',
+    ],
+];

+ 190 - 0
packages/Longyi/Email/src/Resources/views/admin/logs/index.blade.php

@@ -0,0 +1,190 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('email::app.email.logs')
+    </x-slot:title>
+
+    <div class="flex items-center justify-between gap-4 max-sm:flex-wrap">
+        <p class="text-xl font-bold text-gray-800 dark:text-white">
+            @lang('email::app.email.logs')
+        </p>
+
+        <div class="flex items-center gap-x-2.5">
+            <!-- 清理旧日志按钮 -->
+            <form action="{{ route('admin.email.logs.clean_old') }}" method="POST" onsubmit="return confirm('@lang('email::app.email.clean_old_logs_confirm')')">
+                @csrf
+                <input type="hidden" name="days" value="30">
+                <button type="submit" class="secondary-button">
+                    @lang('email::app.email.clean_old_logs')
+                </button>
+            </form>
+        </div>
+    </div>
+
+    <!-- 统计卡片 -->
+    <div class="mt-4 grid grid-cols-4 gap-4">
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <p class="text-sm text-gray-500">@lang('email::app.email.total_logs')</p>
+            <p class="text-2xl font-bold">{{ number_format($stats['total']) }}</p>
+        </div>
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <p class="text-sm text-green-600">@lang('email::app.email.sent_count')</p>
+            <p class="text-2xl font-bold text-green-600">{{ number_format($stats['sent']) }}</p>
+        </div>
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <p class="text-sm text-red-600">@lang('email::app.email.failed_count')</p>
+            <p class="text-2xl font-bold text-red-600">{{ number_format($stats['failed']) }}</p>
+        </div>
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <p class="text-sm text-yellow-600">@lang('email::app.email.pending_count')</p>
+            <p class="text-2xl font-bold text-yellow-600">{{ number_format($stats['pending']) }}</p>
+        </div>
+    </div>
+
+    <!-- 筛选表单 -->
+    <div class="mt-4 rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+        <form method="GET" action="{{ route('admin.email.logs.index') }}" class="grid grid-cols-4 gap-4">
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-1">
+                    @lang('email::app.email.filter_by_status')
+                </label>
+                <select name="status" class="control">
+                    <option value="">@lang('email::app.email.all_status')</option>
+                    <option value="sent" {{ request('status') == 'sent' ? 'selected' : '' }}>@lang('email::app.email.status_sent')</option>
+                    <option value="failed" {{ request('status') == 'failed' ? 'selected' : '' }}>@lang('email::app.email.status_failed')</option>
+                    <option value="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>@lang('email::app.email.status_pending')</option>
+                </select>
+            </div>
+
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-1">
+                    @lang('email::app.email.filter_by_email')
+                </label>
+                <input type="text" name="email" value="{{ request('email') }}" placeholder="输入邮箱地址" class="control">
+            </div>
+
+            <div>
+                <label class="block text-sm font-medium text-gray-700 mb-1">
+                    @lang('email::app.email.filter_by_date')
+                </label>
+                <input type="date" name="date_from" value="{{ request('date_from') }}" class="control">
+            </div>
+
+            <div class="flex items-end">
+                <button type="submit" class="primary-button w-full">
+                    @lang('admin::app.common.filter')
+                </button>
+            </div>
+        </form>
+    </div>
+
+    <!-- 日志列表 -->
+    <div class="mt-4 rounded-lg bg-white shadow dark:bg-gray-900">
+        <form id="massDeleteForm" method="POST" action="{{ route('admin.email.logs.mass_delete') }}">
+            @csrf
+            @method('DELETE')
+
+            <div class="overflow-x-auto">
+                <table class="w-full">
+                    <thead>
+                    <tr class="border-b dark:border-gray-800">
+                        <th class="px-4 py-3 text-left">
+                            <input type="checkbox" id="selectAll" class="cursor-pointer">
+                        </th>
+                        <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                            @lang('email::app.email.recipient_email')
+                        </th>
+                        <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                            @lang('email::app.email.subject')
+                        </th>
+                        <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                            @lang('email::app.email.status')
+                        </th>
+                        <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                            @lang('email::app.email.created_at')
+                        </th>
+                        <th class="px-4 py-3 text-left text-sm font-medium text-gray-600 dark:text-gray-300">
+                            @lang('admin::app.common.actions')
+                        </th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    @forelse($logs as $log)
+                        <tr class="border-b dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800">
+                            <td class="px-4 py-3">
+                                <input type="checkbox" name="ids[]" value="{{ $log->id }}" class="cursor-pointer">
+                            </td>
+                            <td class="px-4 py-3 text-sm text-gray-800 dark:text-white">
+                                {{ $log->recipient_email }}
+                                @if($log->recipient_name)
+                                    <br><small class="text-gray-500">{{ $log->recipient_name }}</small>
+                                @endif
+                            </td>
+                            <td class="px-4 py-3 text-sm text-gray-800 dark:text-white max-w-xs truncate">
+                                {{ Str::limit($log->subject, 50) }}
+                            </td>
+                            <td class="px-4 py-3">
+                                @if($log->status === 'sent')
+                                    <span class="rounded bg-green-100 px-2 py-1 text-xs text-green-600">
+                                            @lang('email::app.email.status_sent')
+                                        </span>
+                                @elseif($log->status === 'failed')
+                                    <span class="rounded bg-red-100 px-2 py-1 text-xs text-red-600">
+                                            @lang('email::app.email.status_failed')
+                                        </span>
+                                @else
+                                    <span class="rounded bg-yellow-100 px-2 py-1 text-xs text-yellow-600">
+                                            @lang('email::app.email.status_pending')
+                                        </span>
+                                @endif
+                            </td>
+                            <td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
+                                {{ $log->created_at->format('Y-m-d H:i:s') }}
+                            </td>
+                            <td class="px-4 py-3">
+                                <div class="flex gap-2">
+                                    <a href="{{ route('admin.email.logs.show', $log->id) }}"
+                                       class="cursor-pointer text-blue-600 hover:underline">
+                                        @lang('email::app.email.view_details')
+                                    </a>
+                                    <form action="{{ route('admin.email.logs.delete', $log->id) }}"
+                                          method="POST"
+                                          onsubmit="return confirm('确定删除?')"
+                                          style="display:inline;">
+                                        @csrf
+                                        @method('DELETE')
+                                        <button type="submit" class="text-red-600 hover:underline">
+                                            @lang('email::app.email.delete')
+                                        </button>
+                                    </form>
+                                </div>
+                            </td>
+                        </tr>
+                    @empty
+                        <tr>
+                            <td colspan="6" class="px-4 py-8 text-center text-gray-500">
+                                @lang('admin::app.common.no-record')
+                            </td>
+                        </tr>
+                    @endforelse
+                    </tbody>
+                </table>
+            </div>
+
+            @if($logs->hasPages())
+                <div class="border-t px-4 py-3 dark:border-gray-800">
+                    {{ $logs->links() }}
+                </div>
+            @endif
+        </form>
+    </div>
+
+    @push('scripts')
+        <script>
+            // 全选功能
+            document.getElementById('selectAll').addEventListener('change', function() {
+                const checkboxes = document.querySelectorAll('input[name="ids[]"]');
+                checkboxes.forEach(cb => cb.checked = this.checked);
+            });
+        </script>
+    @endpush
+</x-admin::layouts>

+ 85 - 0
packages/Longyi/Email/src/Resources/views/admin/logs/show.blade.php

@@ -0,0 +1,85 @@
+<x-admin::layouts>
+    <x-slot:title>
+        @lang('email::app.email.view_details')
+    </x-slot:title>
+
+    <div class="flex items-center justify-between">
+        <p class="text-xl font-bold text-gray-800 dark:text-white">
+            @lang('email::app.email.view_details')
+        </p>
+        <a href="{{ route('admin.email.logs.index') }}" class="secondary-button">
+            @lang('admin::app.common.back')
+        </a>
+    </div>
+
+    <div class="mt-4 space-y-4">
+        <!-- 基本信息 -->
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <h3 class="mb-3 text-lg font-semibold">@lang('admin::app.common.general')</h3>
+            <div class="grid grid-cols-2 gap-4">
+                <div>
+                    <label class="text-sm text-gray-500">@lang('email::app.email.recipient_email')</label>
+                    <p class="font-medium">{{ $log->recipient_email }}</p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">@lang('email::app.email.recipient_name')</label>
+                    <p class="font-medium">{{ $log->recipient_name ?? '-' }}</p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">@lang('email::app.email.status')</label>
+                    <p>
+                        @if($log->status === 'sent')
+                            <span class="rounded bg-green-100 px-2 py-1 text-xs text-green-600">@lang('email::app.email.status_sent')</span>
+                        @elseif($log->status === 'failed')
+                            <span class="rounded bg-red-100 px-2 py-1 text-xs text-red-600">@lang('email::app.email.status_failed')</span>
+                        @else
+                            <span class="rounded bg-yellow-100 px-2 py-1 text-xs text-yellow-600">@lang('email::app.email.status_pending')</span>
+                        @endif
+                    </p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">@lang('email::app.email.template_used')</label>
+                    <p class="font-medium">{{ $log->template ?? '-' }}</p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">@lang('email::app.email.created_at')</label>
+                    <p class="font-medium">{{ $log->created_at->format('Y-m-d H:i:s') }}</p>
+                </div>
+                <div>
+                    <label class="text-sm text-gray-500">@lang('email::app.email.sent_at')</label>
+                    <p class="font-medium">{{ $log->sent_at?->format('Y-m-d H:i:s') ?? '-' }}</p>
+                </div>
+            </div>
+        </div>
+
+        <!-- 主题 -->
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <h3 class="mb-3 text-lg font-semibold">@lang('email::app.email.subject')</h3>
+            <p class="text-gray-800 dark:text-white">{{ $log->subject }}</p>
+        </div>
+
+        <!-- 错误信息 -->
+        @if($log->error_message)
+            <div class="rounded-lg bg-red-50 p-4 shadow dark:bg-red-900/20">
+                <h3 class="mb-3 text-lg font-semibold text-red-600">@lang('email::app.email.error_message')</h3>
+                <pre class="whitespace-pre-wrap text-sm text-red-600">{{ $log->error_message }}</pre>
+            </div>
+        @endif
+
+        <!-- 邮件内容 -->
+        <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+            <h3 class="mb-3 text-lg font-semibold">@lang('email::app.email.email_content')</h3>
+            <div class="prose max-w-none dark:prose-invert">
+                {!! $log->content !!}
+            </div>
+        </div>
+
+        <!-- 元数据 -->
+        @if($log->metadata)
+            <div class="rounded-lg bg-white p-4 shadow dark:bg-gray-900">
+                <h3 class="mb-3 text-lg font-semibold">@lang('email::app.email.metadata')</h3>
+                <pre class="overflow-x-auto rounded bg-gray-100 p-3 text-sm dark:bg-gray-800">{{ json_encode($log->metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
+            </div>
+        @endif
+    </div>
+</x-admin::layouts>

+ 14 - 0
packages/Longyi/Email/src/Routes/admin-routes.php

@@ -0,0 +1,14 @@
+<?php
+
+use Illuminate\Support\Facades\Route;
+use Longyi\Email\Http\Controllers\LogController;
+
+Route::prefix('email')->group(function () {
+    Route::controller(LogController::class)->prefix('logs')->group(function () {
+        Route::get('', 'index')->name('admin.email.logs.index');
+        Route::get('show/{id}', 'show')->name('admin.email.logs.show');
+        Route::delete('delete/{id}', 'destroy')->name('admin.email.logs.delete');
+        Route::post('mass-delete', 'massDestroy')->name('admin.email.logs.mass_delete');
+        Route::post('clean-old', 'cleanOldLogs')->name('admin.email.logs.clean_old');
+    });
+});