Преглед на файлове

Add Broadcast Email Functionality

BrettonYe преди 1 година
родител
ревизия
d55fecfdc3
променени са 95 файла, в които са добавени 1725 реда и са изтрити 484 реда
  1. 112 21
      app/Http/Controllers/Admin/MarketingController.php
  2. 14 2
      app/Notifications/Custom.php
  3. 7 7
      config/common.php
  4. 3 3
      database/seeders/RBACSeeder.php
  5. 0 0
      public/assets/bundle/app.min.css
  6. 2 2
      public/assets/global/fonts/font-awesome/css/all.min.css
  7. 2 2
      public/assets/global/fonts/font-awesome/css/brands.min.css
  8. 2 2
      public/assets/global/fonts/font-awesome/css/fontawesome.min.css
  9. 3 3
      public/assets/global/fonts/font-awesome/css/regular.min.css
  10. 3 3
      public/assets/global/fonts/font-awesome/css/solid.min.css
  11. 2 2
      public/assets/global/fonts/font-awesome/css/svg-with-js.min.css
  12. 2 2
      public/assets/global/fonts/font-awesome/css/v4-font-face.min.css
  13. 2 2
      public/assets/global/fonts/font-awesome/css/v4-shims.min.css
  14. 2 2
      public/assets/global/fonts/font-awesome/css/v5-font-face.min.css
  15. 2 2
      public/assets/global/fonts/font-awesome/js/all.min.js
  16. 2 2
      public/assets/global/fonts/font-awesome/js/brands.min.js
  17. 2 2
      public/assets/global/fonts/font-awesome/js/conflict-detection.min.js
  18. 2 2
      public/assets/global/fonts/font-awesome/js/fontawesome.min.js
  19. 2 2
      public/assets/global/fonts/font-awesome/js/regular.min.js
  20. 2 2
      public/assets/global/fonts/font-awesome/js/solid.min.js
  21. 210 74
      public/assets/global/fonts/font-awesome/js/v4-shims.js
  22. 2 2
      public/assets/global/fonts/font-awesome/js/v4-shims.min.js
  23. BIN
      public/assets/global/fonts/font-awesome/webfonts/fa-brands-400.ttf
  24. BIN
      public/assets/global/fonts/font-awesome/webfonts/fa-brands-400.woff2
  25. BIN
      public/assets/global/fonts/font-awesome/webfonts/fa-regular-400.ttf
  26. BIN
      public/assets/global/fonts/font-awesome/webfonts/fa-regular-400.woff2
  27. BIN
      public/assets/global/fonts/font-awesome/webfonts/fa-solid-900.ttf
  28. BIN
      public/assets/global/fonts/font-awesome/webfonts/fa-solid-900.woff2
  29. BIN
      public/assets/global/fonts/font-awesome/webfonts/fa-v4compatibility.ttf
  30. BIN
      public/assets/global/fonts/font-awesome/webfonts/fa-v4compatibility.woff2
  31. 1 0
      public/assets/global/vendor/summernote/lang/summernote-ar-AR.min.js
  32. 1 0
      public/assets/global/vendor/summernote/lang/summernote-az-AZ.min.js
  33. 1 0
      public/assets/global/vendor/summernote/lang/summernote-bg-BG.min.js
  34. 1 0
      public/assets/global/vendor/summernote/lang/summernote-ca-ES.min.js
  35. 1 0
      public/assets/global/vendor/summernote/lang/summernote-cs-CZ.min.js
  36. 1 0
      public/assets/global/vendor/summernote/lang/summernote-da-DK.min.js
  37. 1 0
      public/assets/global/vendor/summernote/lang/summernote-de-DE.min.js
  38. 1 0
      public/assets/global/vendor/summernote/lang/summernote-el-GR.min.js
  39. 1 0
      public/assets/global/vendor/summernote/lang/summernote-es-ES.min.js
  40. 1 0
      public/assets/global/vendor/summernote/lang/summernote-es-EU.min.js
  41. 1 0
      public/assets/global/vendor/summernote/lang/summernote-fa-IR.min.js
  42. 1 0
      public/assets/global/vendor/summernote/lang/summernote-fi-FI.min.js
  43. 1 0
      public/assets/global/vendor/summernote/lang/summernote-fr-FR.min.js
  44. 1 0
      public/assets/global/vendor/summernote/lang/summernote-gl-ES.min.js
  45. 1 0
      public/assets/global/vendor/summernote/lang/summernote-he-IL.min.js
  46. 1 0
      public/assets/global/vendor/summernote/lang/summernote-hr-HR.min.js
  47. 1 0
      public/assets/global/vendor/summernote/lang/summernote-hu-HU.min.js
  48. 1 0
      public/assets/global/vendor/summernote/lang/summernote-id-ID.min.js
  49. 1 0
      public/assets/global/vendor/summernote/lang/summernote-it-IT.min.js
  50. 1 0
      public/assets/global/vendor/summernote/lang/summernote-ja-JP.min.js
  51. 1 0
      public/assets/global/vendor/summernote/lang/summernote-ko-KR.min.js
  52. 1 0
      public/assets/global/vendor/summernote/lang/summernote-lt-LT.min.js
  53. 1 0
      public/assets/global/vendor/summernote/lang/summernote-lt-LV.min.js
  54. 1 0
      public/assets/global/vendor/summernote/lang/summernote-mn-MN.min.js
  55. 1 0
      public/assets/global/vendor/summernote/lang/summernote-nb-NO.min.js
  56. 1 0
      public/assets/global/vendor/summernote/lang/summernote-nl-NL.min.js
  57. 1 0
      public/assets/global/vendor/summernote/lang/summernote-pl-PL.min.js
  58. 1 0
      public/assets/global/vendor/summernote/lang/summernote-pt-BR.min.js
  59. 1 0
      public/assets/global/vendor/summernote/lang/summernote-pt-PT.min.js
  60. 1 0
      public/assets/global/vendor/summernote/lang/summernote-ro-RO.min.js
  61. 1 0
      public/assets/global/vendor/summernote/lang/summernote-ru-RU.min.js
  62. 1 0
      public/assets/global/vendor/summernote/lang/summernote-sk-SK.min.js
  63. 1 0
      public/assets/global/vendor/summernote/lang/summernote-sl-SI.min.js
  64. 1 0
      public/assets/global/vendor/summernote/lang/summernote-sr-RS-Latin.min.js
  65. 1 0
      public/assets/global/vendor/summernote/lang/summernote-sr-RS.min.js
  66. 1 0
      public/assets/global/vendor/summernote/lang/summernote-sv-SE.min.js
  67. 1 0
      public/assets/global/vendor/summernote/lang/summernote-ta-IN.min.js
  68. 1 0
      public/assets/global/vendor/summernote/lang/summernote-th-TH.min.js
  69. 1 0
      public/assets/global/vendor/summernote/lang/summernote-tr-TR.min.js
  70. 1 0
      public/assets/global/vendor/summernote/lang/summernote-uk-UA.min.js
  71. 1 0
      public/assets/global/vendor/summernote/lang/summernote-uz-UZ.min.js
  72. 1 0
      public/assets/global/vendor/summernote/lang/summernote-vi-VN.min.js
  73. 1 0
      public/assets/global/vendor/summernote/lang/summernote-zh-CN.min.js
  74. 1 0
      public/assets/global/vendor/summernote/lang/summernote-zh-TW.min.js
  75. 16 0
      public/assets/global/vendor/summernote/plugin/databasic/summernote-ext-databasic.css
  76. 291 0
      public/assets/global/vendor/summernote/plugin/databasic/summernote-ext-databasic.js
  77. 82 0
      public/assets/global/vendor/summernote/plugin/hello/summernote-ext-hello.js
  78. 311 0
      public/assets/global/vendor/summernote/plugin/specialchars/summernote-ext-specialchars.js
  79. 0 0
      public/assets/global/vendor/summernote/summernote-bs4.min.css
  80. 1 0
      public/assets/global/vendor/summernote/summernote-bs4.min.js
  81. 0 0
      public/assets/global/vendor/summernote/summernote-bs4.min.js.map
  82. 0 0
      public/assets/global/vendor/summernote/summernote.min.js.map
  83. 20 11
      resources/lang/de/admin.php
  84. 20 11
      resources/lang/en/admin.php
  85. 20 11
      resources/lang/fa/admin.php
  86. 20 11
      resources/lang/ja/admin.php
  87. 20 11
      resources/lang/ko/admin.php
  88. 20 11
      resources/lang/vi/admin.php
  89. 20 11
      resources/lang/zh_CN/admin.php
  90. 3 3
      resources/views/admin/article/info.blade.php
  91. 450 0
      resources/views/admin/article/marketing.blade.php
  92. 5 12
      resources/views/admin/layouts.blade.php
  93. 0 78
      resources/views/admin/marketing/emailList.blade.php
  94. 0 170
      resources/views/admin/marketing/pushList.blade.php
  95. 2 3
      routes/admin.php

+ 112 - 21
app/Http/Controllers/Admin/MarketingController.php

@@ -3,49 +3,140 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
+use App\Models\Level;
 use App\Models\Marketing;
+use App\Models\User;
+use App\Models\UserGroup;
+use App\Models\UserHourlyDataFlow;
+use App\Notifications\Custom;
+use Carbon;
+use Helpers;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Notification;
 use Response;
+use Validator;
 
 class MarketingController extends Controller
 {
-    // 邮件群发消息列表
-    public function emailList(Request $request)
+    // 群发消息列表
+    public function index(Request $request)
     {
-        $query = Marketing::whereType(1);
+        $query = Marketing::query();
 
         $request->whenFilled('status', function ($value) use ($query) {
             $query->whereStatus($value);
         });
 
-        return view('admin.marketing.emailList', ['emails' => $query->paginate(15)->appends($request->except('page'))]);
+        return view('admin.article.marketing', [
+            'marketingMessages' => $query->latest()->paginate(15)->appends($request->except('page')),
+            'userGroups' => UserGroup::all()->pluck('name', 'id')->toArray(),
+            'levels' => Level::all()->pluck('name', 'level')->toArray(),
+        ]);
     }
 
-    // 消息通道群发列表
-    public function pushList(Request $request)
+    // 推送消息
+    public function create(string $type, Request $request): JsonResponse
     {
-        $query = Marketing::whereType(2);
+        if ($request->isMethod('GET')) {
+            return Response::json(['status' => 'success', 'count' => $this->userStat($request)]);
+        }
 
-        $request->whenFilled('status', function ($value) use ($query) {
-            $query->whereStatus($value);
-        });
+        $validator = Validator::make($request->all(), ['title' => 'required|string', 'content' => 'required|string']);
 
-        return view('admin.marketing.pushList', ['pushes' => $query->paginate(15)->appends($request->except('page'))]);
-    }
+        if ($validator->fails()) {
+            return Response::json(['status' => 'fail', 'message' => $validator->getMessageBag()->first()]);
+        }
 
-    // 添加推送消息
-    public function addPushMarketing(Request $request): JsonResponse
-    {
         $title = $request->input('title');
         $content = $request->input('content');
 
-        //        if (! sysConfig('is_push_bear')) {
-        //            return Response::json(['status' => 'fail', 'message' => '推送失败:请先启用并配置PushBear']);
-        //        }
-        //
-        //        Notification::send(PushBearChannel::class, new Custom($title, $content));
+        if ($type === 'push') {
+            //            if (! sysConfig('is_push_bear')) {
+            //                return Response::json(['status' => 'fail', 'message' => '推送失败:请先启用并配置PushBear']);
+            //            }
+            //
+            //            Notification::send(PushBearChannel::class, new Custom($title, $content));
+            //            return Response::json(['status' => 'success', 'message' => '发送完成']);
+            return Response::json(['status' => 'fail', 'message' => trans('common.developing')]);
+        }
+
+        if ($type === 'email') {
+            $users = $this->userStat($request);
+            if ($users->isNotEmpty()) {
+                Notification::send($users, new Custom($title, $content, ['mail']));
+                Helpers::addMarketing($users->pluck('id')->toJson(), '1', $title, $content);
+
+                return Response::json(['status' => 'success', 'message' => trans('admin.marketing.processed')]);
+            }
+
+            return Response::json(['status' => 'fail', 'message' => trans('admin.marketing.targeted_users_not_found')]);
+        }
+
+        return Response::json(['status' => 'fail', 'message' => trans('admin.marketing.unknown_sending_type')]);
+    }
+
+    private function userStat(Request $request)
+    {
+        $users = User::query();
+
+        foreach (['id', 'username', 'status', 'enable', 'user_group_id', 'level'] as $field) {
+            $request->whenFilled($field, function ($value) use ($users, $field) {
+                $users->whereIn($field, array_map('trim', explode(',', $value)));
+            });
+        }
+
+        // 流量使用超过N%
+        $request->whenFilled('traffic', function (int $value) use ($users) {
+            $users->whereRaw('(u + d)/transfer_enable >= ?', [$value / 100]);
+        });
+
+        // 过期日期
+        $request->whenFilled('expire_start', function ($value) use ($users) {
+            $users->where('expired_at', '>=', $value);
+        });
+        $request->whenFilled('expire_end', function ($value) use ($users) {
+            $users->where('expired_at', '<=', $value);
+        });
+
+        // 最近N分钟活跃过
+        $request->whenFilled('lastAlive', function ($value) use ($users) {
+            $users->where('t', '>=', Carbon::now()->subMinutes($value)->timestamp);
+        });
+
+        $paidOrderCondition = function ($query) {
+            $query->whereStatus(2)->whereNotNull('goods_id')->where('amount', '>', 0);
+        };
+
+        // 付费服务中
+        $request->whenFilled('paying', function () use ($users) {
+            $users->whereHas('orders', function ($query) {
+                $query->whereStatus(2)->whereNotNull('goods_id')->whereIsExpire(0)->where('amount', '>', 0);
+            });
+        });
+
+        // 曾付费但当前无服务
+        $request->whenFilled('notPaying', function () use ($users, $paidOrderCondition) {
+            $users->whereHas('orders', $paidOrderCondition)->whereDoesntHave('orders', function ($query) use ($paidOrderCondition) {
+                $query->where($paidOrderCondition)->whereIsExpire(0);
+            });
+        });
+
+        // 付费购买过
+        $request->whenFilled('paid', function () use ($users, $paidOrderCondition) {
+            $users->whereHas('orders', $paidOrderCondition);
+        });
+
+        // 从未付费购买过
+        $request->whenFilled('neverPay', function () use ($users, $paidOrderCondition) {
+            $users->whereDoesntHave('orders', $paidOrderCondition);
+        });
+
+        // 1小时内流量异常用户
+        $request->whenFilled('flowAbnormal', function () use ($users) {
+            $users->whereIn('id', (new UserHourlyDataFlow)->trafficAbnormal());
+        });
 
-        return Response::json(['status' => 'fail', 'message' => '功能待开发']);
+        return $request->isMethod('POST') ? $users->get() : $users->count();
     }
 }

+ 14 - 2
app/Notifications/Custom.php

@@ -18,20 +18,32 @@ class Custom extends Notification implements ShouldQueue
 
     private string $content;
 
-    public function __construct(string $title, string $content)
+    private array $channels;
+
+    public function __construct(string $title, string $content, array $channels = ['mail', BarkChannel::class, TelegramChannel::class])
     {
         $this->title = $title;
         $this->content = $content;
+        $this->channels = $channels;
     }
 
     public function via($notifiable): array
     {
-        return $notifiable ?? ['mail', BarkChannel::class, TelegramChannel::class];
+        return $this->channels;
     }
 
     public function toMail($notifiable): MailMessage
     {
+        $emailAddress = config('mail.from.address');
+        $atSignPosition = strpos($emailAddress, '@');
+
+        if ($atSignPosition !== false) {
+            $domain = substr($emailAddress, $atSignPosition + 1);
+            $emailAddress = 'no-reply@'.$domain;
+        }
+
         return (new MailMessage)
+            ->from($emailAddress)
             ->subject($this->title)
             ->markdown('mail.custom', ['content' => $this->content]);
     }

+ 7 - 7
config/common.php

@@ -76,13 +76,13 @@ return [
     ],
 
     'language' => [
-        'de' => ['Deutsch', 'de'],
-        'en' => ['English', 'us'],
-        'fa' => ['فارسی', 'ir'],
-        'ja' => ['日本語', 'jp'],
-        'ko' => ['한국어', 'kr'],
-        'vi' => ['Tiếng Việt', 'vn'],
-        'zh_CN' => ['简体中文', 'cn'],
+        'de' => ['Deutsch', 'de', 'de-DE'],
+        'en' => ['English', 'us', 'en-US'],
+        'fa' => ['فارسی', 'ir', 'fa-IR'],
+        'ja' => ['日本語', 'jp', 'ja-JP'],
+        'ko' => ['한국어', 'kr', 'ko-KR'],
+        'vi' => ['Tiếng Việt', 'vn', 'vi-VN'],
+        'zh_CN' => ['简体中文', 'cn', 'zh-CN'],
     ],
 
     'currency' => [

+ 3 - 3
database/seeders/RBACSeeder.php

@@ -38,9 +38,9 @@ class RBACSeeder extends Seeder
         'admin.log.online' => '【日志系统】在线监控',
         'admin.log.traffic' => '【日志系统】流量日志',
         'log-viewer::dashboard,log-viewer::logs.*' => '【日志系统】运行日志',
-        'admin.marketing.add' => '【客服系统】推送消息',
-        'admin.marketing.email' => '【客服系统】邮件消息列表',
-        'admin.marketing.push' => '【客服系统】推送消息列表',
+        'admin.marketing.index' => '【客服系统】广播列表',
+        'admin.marketing.email' => '【客服系统】群发邮件',
+        'admin.marketing.push' => '【客服系统】推送消息',
         'admin.node.auth.destroy' => '【线路系统】删除授权',
         'admin.node.auth.index' => '【线路系统】授权列表',
         'admin.node.auth.store' => '【线路系统】新建授权',

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
public/assets/bundle/app.min.css


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/css/all.min.css


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/css/brands.min.css


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/css/fontawesome.min.css


+ 3 - 3
public/assets/global/fonts/font-awesome/css/regular.min.css

@@ -1,6 +1,6 @@
 /*!
- * Font Awesome Free 6.1.2 by @fontawesome - https://fontawesome.com
+ * Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
  * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- * Copyright 2022 Fonticons, Inc.
+ * Copyright 2024 Fonticons, Inc.
  */
-:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-family:"Font Awesome 6 Free";font-weight:400}
+:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400}

+ 3 - 3
public/assets/global/fonts/font-awesome/css/solid.min.css

@@ -1,6 +1,6 @@
 /*!
- * Font Awesome Free 6.1.2 by @fontawesome - https://fontawesome.com
+ * Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
  * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- * Copyright 2022 Fonticons, Inc.
+ * Copyright 2024 Fonticons, Inc.
  */
-:host,:root{--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-family:"Font Awesome 6 Free";font-weight:900}
+:host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900}

Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/css/svg-with-js.min.css


+ 2 - 2
public/assets/global/fonts/font-awesome/css/v4-font-face.min.css

@@ -1,6 +1,6 @@
 /*!
- * Font Awesome Free 6.1.2 by @fontawesome - https://fontawesome.com
+ * Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
  * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- * Copyright 2022 Fonticons, Inc.
+ * Copyright 2024 Fonticons, Inc.
  */
 @font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a}

Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/css/v4-shims.min.css


+ 2 - 2
public/assets/global/fonts/font-awesome/css/v5-font-face.min.css

@@ -1,6 +1,6 @@
 /*!
- * Font Awesome Free 6.1.2 by @fontawesome - https://fontawesome.com
+ * Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com
  * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
- * Copyright 2022 Fonticons, Inc.
+ * Copyright 2024 Fonticons, Inc.
  */
 @font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}

Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/js/all.min.js


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/js/brands.min.js


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/js/conflict-detection.min.js


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/js/fontawesome.min.js


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/js/regular.min.js


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/js/solid.min.js


Файловите разлики са ограничени, защото са твърде много
+ 210 - 74
public/assets/global/fonts/font-awesome/js/v4-shims.js


Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
public/assets/global/fonts/font-awesome/js/v4-shims.min.js


BIN
public/assets/global/fonts/font-awesome/webfonts/fa-brands-400.ttf


BIN
public/assets/global/fonts/font-awesome/webfonts/fa-brands-400.woff2


BIN
public/assets/global/fonts/font-awesome/webfonts/fa-regular-400.ttf


BIN
public/assets/global/fonts/font-awesome/webfonts/fa-regular-400.woff2


BIN
public/assets/global/fonts/font-awesome/webfonts/fa-solid-900.ttf


BIN
public/assets/global/fonts/font-awesome/webfonts/fa-solid-900.woff2


BIN
public/assets/global/fonts/font-awesome/webfonts/fa-v4compatibility.ttf


BIN
public/assets/global/fonts/font-awesome/webfonts/fa-v4compatibility.woff2


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-ar-AR.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-az-AZ.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-bg-BG.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-ca-ES.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-cs-CZ.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-da-DK.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-de-DE.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-el-GR.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-es-ES.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-es-EU.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-fa-IR.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-fi-FI.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-fr-FR.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-gl-ES.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-he-IL.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-hr-HR.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-hu-HU.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-id-ID.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-it-IT.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-ja-JP.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-ko-KR.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-lt-LT.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-lt-LV.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-mn-MN.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-nb-NO.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-nl-NL.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-pl-PL.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-pt-BR.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-pt-PT.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-ro-RO.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-ru-RU.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-sk-SK.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-sl-SI.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-sr-RS-Latin.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-sr-RS.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-sv-SE.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-ta-IN.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-th-TH.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-tr-TR.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-uk-UA.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-uz-UZ.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-vi-VN.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-zh-CN.min.js


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/lang/summernote-zh-TW.min.js


+ 16 - 0
public/assets/global/vendor/summernote/plugin/databasic/summernote-ext-databasic.css

@@ -0,0 +1,16 @@
+.ext-databasic {
+	position: relative;
+	display: block;
+	min-height: 50px;
+	background-color: cyan;
+	text-align: center;
+	padding: 20px;
+	border: 1px solid white;
+	border-radius: 10px;
+}
+
+.ext-databasic p {
+	color: white;
+	font-size: 1.2em;
+	margin: 0;
+}

+ 291 - 0
public/assets/global/vendor/summernote/plugin/databasic/summernote-ext-databasic.js

@@ -0,0 +1,291 @@
+(function(factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else if (typeof module === 'object' && module.exports) {
+    // Node/CommonJS
+    module.exports = factory(require('jquery'));
+  } else {
+    // Browser globals
+    factory(window.jQuery);
+  }
+}(function($) {
+  // pull in some summernote core functions
+  var ui = $.summernote.ui;
+  var dom = $.summernote.dom;
+
+  // define the popover plugin
+  var DataBasicPlugin = function(context) {
+    var self = this;
+    var options = context.options;
+    var lang = options.langInfo;
+
+    self.icon = '<i class="fa fa-object-group"/>';
+
+    // add context menu button for dialog
+    context.memo('button.databasic', function() {
+      return ui.button({
+        contents: self.icon,
+        tooltip: lang.databasic.insert,
+        click: context.createInvokeHandler('databasic.showDialog'),
+      }).render();
+    });
+
+    // add popover edit button
+    context.memo('button.databasicDialog', function() {
+      return ui.button({
+        contents: self.icon,
+        tooltip: lang.databasic.edit,
+        click: context.createInvokeHandler('databasic.showDialog'),
+      }).render();
+    });
+
+    //  add popover size buttons
+    context.memo('button.databasicSize100', function() {
+      return ui.button({
+        contents: '<span class="note-fontsize-10">100%</span>',
+        tooltip: lang.image.resizeFull,
+        click: context.createInvokeHandler('editor.resize', '1'),
+      }).render();
+    });
+    context.memo('button.databasicSize50', function() {
+      return ui.button({
+        contents: '<span class="note-fontsize-10">50%</span>',
+        tooltip: lang.image.resizeHalf,
+        click: context.createInvokeHandler('editor.resize', '0.5'),
+      }).render();
+    });
+    context.memo('button.databasicSize25', function() {
+      return ui.button({
+        contents: '<span class="note-fontsize-10">25%</span>',
+        tooltip: lang.image.resizeQuarter,
+        click: context.createInvokeHandler('editor.resize', '0.25'),
+      }).render();
+    });
+
+    self.events = {
+      'summernote.init': function(we, e) {
+        // update existing containers
+        $('data.ext-databasic', e.editable).each(function() { self.setContent($(this)); });
+        // TODO: make this an undo snapshot...
+      },
+      'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function() {
+        self.update();
+      },
+      'summernote.dialog.shown': function() {
+        self.hidePopover();
+      },
+    };
+
+    self.initialize = function() {
+      // create dialog markup
+      var $container = options.dialogsInBody ? $(document.body) : context.layoutInfo.editor;
+
+      var body = '<div class="form-group row-fluid">' +
+          '<label>' + lang.databasic.testLabel + '</label>' +
+          '<input class="ext-databasic-test form-control" type="text" />' +
+          '</div>';
+      var footer = '<button href="#" class="btn btn-primary ext-databasic-save">' + lang.databasic.insert + '</button>';
+
+      self.$dialog = ui.dialog({
+        title: lang.databasic.name,
+        fade: options.dialogsFade,
+        body: body,
+        footer: footer,
+      }).render().appendTo($container);
+
+      // create popover
+      self.$popover = ui.popover({
+        className: 'ext-databasic-popover',
+      }).render().appendTo('body');
+      var $content = self.$popover.find('.popover-content');
+
+      context.invoke('buttons.build', $content, options.popover.databasic);
+    };
+
+    self.destroy = function() {
+      self.$popover.remove();
+      self.$popover = null;
+      self.$dialog.remove();
+      self.$dialog = null;
+    };
+
+    self.update = function() {
+      // Prevent focusing on editable when invoke('code') is executed
+      if (!context.invoke('editor.hasFocus')) {
+        self.hidePopover();
+        return;
+      }
+
+      var rng = context.invoke('editor.createRange');
+      var visible = false;
+
+      if (rng.isOnData()) {
+        var $data = $(rng.sc).closest('data.ext-databasic');
+
+        if ($data.length) {
+          var pos = dom.posFromPlaceholder($data[0]);
+
+          self.$popover.css({
+            display: 'block',
+            left: pos.left,
+            top: pos.top,
+          });
+
+          // save editor target to let size buttons resize the container
+          context.invoke('editor.saveTarget', $data[0]);
+
+          visible = true;
+        }
+      }
+
+      // hide if not visible
+      if (!visible) {
+        self.hidePopover();
+      }
+    };
+
+    self.hidePopover = function() {
+      self.$popover.hide();
+    };
+
+    // define plugin dialog
+    self.getInfo = function() {
+      var rng = context.invoke('editor.createRange');
+
+      if (rng.isOnData()) {
+        var $data = $(rng.sc).closest('data.ext-databasic');
+
+        if ($data.length) {
+          // Get the first node on range(for edit).
+          return {
+            node: $data,
+            test: $data.attr('data-test'),
+          };
+        }
+      }
+
+      return {};
+    };
+
+    self.setContent = function($node) {
+      $node.html('<p contenteditable="false">' + self.icon + ' ' + lang.databasic.name + ': ' +
+        $node.attr('data-test') + '</p>');
+    };
+
+    self.updateNode = function(info) {
+      self.setContent(info.node
+        .attr('data-test', info.test));
+    };
+
+    self.createNode = function(info) {
+      var $node = $('<data class="ext-databasic"></data>');
+
+      if ($node) {
+        // save node to info structure
+        info.node = $node;
+        // insert node into editor dom
+        context.invoke('editor.insertNode', $node[0]);
+      }
+
+      return $node;
+    };
+
+    self.showDialog = function() {
+      var info = self.getInfo();
+      var newNode = !info.node;
+      context.invoke('editor.saveRange');
+
+      self
+        .openDialog(info)
+        .then(function(dialogInfo) {
+          // [workaround] hide dialog before restore range for IE range focus
+          ui.hideDialog(self.$dialog);
+          context.invoke('editor.restoreRange');
+
+          // insert a new node
+          if (newNode) {
+            self.createNode(info);
+          }
+
+          // update info with dialog info
+          $.extend(info, dialogInfo);
+
+          self.updateNode(info);
+        })
+        .fail(function() {
+          context.invoke('editor.restoreRange');
+        });
+    };
+
+    self.openDialog = function(info) {
+      return $.Deferred(function(deferred) {
+        var $inpTest = self.$dialog.find('.ext-databasic-test');
+        var $saveBtn = self.$dialog.find('.ext-databasic-save');
+        var onKeyup = function(event) {
+          if (event.keyCode === 13) {
+            $saveBtn.trigger('click');
+          }
+        };
+
+        ui.onDialogShown(self.$dialog, function() {
+          context.triggerEvent('dialog.shown');
+
+          $inpTest.val(info.test).on('input', function() {
+            ui.toggleBtn($saveBtn, $inpTest.val());
+          }).trigger('focus').on('keyup', onKeyup);
+
+          $saveBtn
+            .text(info.node ? lang.databasic.edit : lang.databasic.insert)
+            .click(function(event) {
+              event.preventDefault();
+
+              deferred.resolve({ test: $inpTest.val() });
+            });
+
+          // init save button
+          ui.toggleBtn($saveBtn, $inpTest.val());
+        });
+
+        ui.onDialogHidden(self.$dialog, function() {
+          $inpTest.off('input keyup');
+          $saveBtn.off('click');
+
+          if (deferred.state() === 'pending') {
+            deferred.reject();
+          }
+        });
+
+        ui.showDialog(self.$dialog);
+      });
+    };
+  };
+
+  // Extends summernote
+  $.extend(true, $.summernote, {
+    plugins: {
+      databasic: DataBasicPlugin,
+    },
+
+    options: {
+      popover: {
+        databasic: [
+          ['databasic', ['databasicDialog', 'databasicSize100', 'databasicSize50', 'databasicSize25']],
+        ],
+      },
+    },
+
+    // add localization texts
+    lang: {
+      'en-US': {
+        databasic: {
+          name: 'Basic Data Container',
+          insert: 'insert basic data container',
+          edit: 'edit basic data container',
+          testLabel: 'test input',
+        },
+      },
+    },
+
+  });
+}));

+ 82 - 0
public/assets/global/vendor/summernote/plugin/hello/summernote-ext-hello.js

@@ -0,0 +1,82 @@
+(function(factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else if (typeof module === 'object' && module.exports) {
+    // Node/CommonJS
+    module.exports = factory(require('jquery'));
+  } else {
+    // Browser globals
+    factory(window.jQuery);
+  }
+}(function($) {
+  // Extends plugins for adding hello.
+  //  - plugin is external module for customizing.
+  $.extend($.summernote.plugins, {
+    /**
+     * @param {Object} context - context object has status of editor.
+     */
+    'hello': function(context) {
+      var self = this;
+
+      // ui has renders to build ui elements.
+      //  - you can create a button with `ui.button`
+      var ui = $.summernote.ui;
+
+      // add hello button
+      context.memo('button.hello', function() {
+        // create button
+        var button = ui.button({
+          contents: '<i class="fa fa-child"/> Hello',
+          tooltip: 'hello',
+          click: function() {
+            self.$panel.show();
+            self.$panel.hide(500);
+            // invoke insertText method with 'hello' on editor module.
+            context.invoke('editor.insertText', 'hello');
+          },
+        });
+
+        // create jQuery object from button instance.
+        var $hello = button.render();
+        return $hello;
+      });
+
+      // This events will be attached when editor is initialized.
+      this.events = {
+        // This will be called after modules are initialized.
+        'summernote.init': function(we, e) {
+          // eslint-disable-next-line
+          console.log('summernote initialized', we, e);
+        },
+        // This will be called when user releases a key on editable.
+        'summernote.keyup': function(we, e) {
+          // eslint-disable-next-line
+          console.log('summernote keyup', we, e);
+        },
+      };
+
+      // This method will be called when editor is initialized by $('..').summernote();
+      // You can create elements for plugin
+      this.initialize = function() {
+        this.$panel = $('<div class="hello-panel"/>').css({
+          position: 'absolute',
+          width: 100,
+          height: 100,
+          left: '50%',
+          top: '50%',
+          background: 'red',
+        }).hide();
+
+        this.$panel.appendTo('body');
+      };
+
+      // This methods will be called when editor is destroyed by $('..').summernote('destroy');
+      // You should remove elements on `initialize`.
+      this.destroy = function() {
+        this.$panel.remove();
+        this.$panel = null;
+      };
+    },
+  });
+}));

+ 311 - 0
public/assets/global/vendor/summernote/plugin/specialchars/summernote-ext-specialchars.js

@@ -0,0 +1,311 @@
+(function(factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else if (typeof module === 'object' && module.exports) {
+    // Node/CommonJS
+    module.exports = factory(require('jquery'));
+  } else {
+    // Browser globals
+    factory(window.jQuery);
+  }
+}(function($) {
+  $.extend($.summernote.plugins, {
+    'specialchars': function(context) {
+      var self = this;
+      var ui = $.summernote.ui;
+
+      var $editor = context.layoutInfo.editor;
+      var options = context.options;
+      var lang = options.langInfo;
+
+      var KEY = {
+        UP: 38,
+        DOWN: 40,
+        LEFT: 37,
+        RIGHT: 39,
+        ENTER: 13,
+      };
+      var COLUMN_LENGTH = 15;
+      var COLUMN_WIDTH = 35;
+
+      var currentColumn = 0;
+      var currentRow = 0;
+      var totalColumn = 0;
+      var totalRow = 0;
+
+      // special characters data set
+      var specialCharDataSet = [
+        '&quot;', '&amp;', '&lt;', '&gt;', '&iexcl;', '&cent;',
+        '&pound;', '&curren;', '&yen;', '&brvbar;', '&sect;',
+        '&uml;', '&copy;', '&ordf;', '&laquo;', '&not;',
+        '&reg;', '&macr;', '&deg;', '&plusmn;', '&sup2;',
+        '&sup3;', '&acute;', '&micro;', '&para;', '&middot;',
+        '&cedil;', '&sup1;', '&ordm;', '&raquo;', '&frac14;',
+        '&frac12;', '&frac34;', '&iquest;', '&times;', '&divide;',
+        '&fnof;', '&circ;', '&tilde;', '&ndash;', '&mdash;',
+        '&lsquo;', '&rsquo;', '&sbquo;', '&ldquo;', '&rdquo;',
+        '&bdquo;', '&dagger;', '&Dagger;', '&bull;', '&hellip;',
+        '&permil;', '&prime;', '&Prime;', '&lsaquo;', '&rsaquo;',
+        '&oline;', '&frasl;', '&euro;', '&image;', '&weierp;',
+        '&real;', '&trade;', '&alefsym;', '&larr;', '&uarr;',
+        '&rarr;', '&darr;', '&harr;', '&crarr;', '&lArr;',
+        '&uArr;', '&rArr;', '&dArr;', '&hArr;', '&forall;',
+        '&part;', '&exist;', '&empty;', '&nabla;', '&isin;',
+        '&notin;', '&ni;', '&prod;', '&sum;', '&minus;',
+        '&lowast;', '&radic;', '&prop;', '&infin;', '&ang;',
+        '&and;', '&or;', '&cap;', '&cup;', '&int;',
+        '&there4;', '&sim;', '&cong;', '&asymp;', '&ne;',
+        '&equiv;', '&le;', '&ge;', '&sub;', '&sup;',
+        '&nsub;', '&sube;', '&supe;', '&oplus;', '&otimes;',
+        '&perp;', '&sdot;', '&lceil;', '&rceil;', '&lfloor;',
+        '&rfloor;', '&loz;', '&spades;', '&clubs;', '&hearts;',
+        '&diams;',
+      ];
+
+      context.memo('button.specialchars', function() {
+        return ui.button({
+          contents: '<i class="fa fa-font fa-flip-vertical">',
+          tooltip: lang.specialChar.specialChar,
+          click: function() {
+            self.show();
+          },
+        }).render();
+      });
+
+      /**
+       * Make Special Characters Table
+       *
+       * @member plugin.specialChar
+       * @private
+       * @return {jQuery}
+       */
+      this.makeSpecialCharSetTable = function() {
+        var $table = $('<table/>');
+        $.each(specialCharDataSet, function(idx, text) {
+          var $td = $('<td/>').addClass('note-specialchar-node');
+          var $tr = (idx % COLUMN_LENGTH === 0) ? $('<tr/>') : $table.find('tr').last();
+
+          var $button = ui.button({
+            callback: function($node) {
+              $node.html(text);
+              $node.attr('title', text);
+              $node.attr('data-value', encodeURIComponent(text));
+              $node.css({
+                width: COLUMN_WIDTH,
+                'margin-right': '2px',
+                'margin-bottom': '2px',
+              });
+            },
+          }).render();
+
+          $td.append($button);
+
+          $tr.append($td);
+          if (idx % COLUMN_LENGTH === 0) {
+            $table.append($tr);
+          }
+        });
+
+        totalRow = $table.find('tr').length;
+        totalColumn = COLUMN_LENGTH;
+
+        return $table;
+      };
+
+      this.initialize = function() {
+        var $container = options.dialogsInBody ? $(document.body) : $editor;
+
+        var body = '<div class="form-group row-fluid">' + this.makeSpecialCharSetTable()[0].outerHTML + '</div>';
+
+        this.$dialog = ui.dialog({
+          title: lang.specialChar.select,
+          body: body,
+        }).render().appendTo($container);
+      };
+
+      this.show = function() {
+        var text = context.invoke('editor.getSelectedText');
+        context.invoke('editor.saveRange');
+        this.showSpecialCharDialog(text).then(function(selectChar) {
+          context.invoke('editor.restoreRange');
+
+          // build node
+          var $node = $('<span></span>').html(selectChar)[0];
+
+          if ($node) {
+            // insert video node
+            context.invoke('editor.insertNode', $node);
+          }
+        }).fail(function() {
+          context.invoke('editor.restoreRange');
+        });
+      };
+
+      /**
+       * show image dialog
+       *
+       * @param {jQuery} $dialog
+       * @return {Promise}
+       */
+      this.showSpecialCharDialog = function(text) {
+        return $.Deferred(function(deferred) {
+          var $specialCharDialog = self.$dialog;
+          var $specialCharNode = $specialCharDialog.find('.note-specialchar-node');
+          var $selectedNode = null;
+          var ARROW_KEYS = [KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT];
+          var ENTER_KEY = KEY.ENTER;
+
+          function addActiveClass($target) {
+            if (!$target) {
+              return;
+            }
+            $target.find('button').addClass('active');
+            $selectedNode = $target;
+          }
+
+          function removeActiveClass($target) {
+            $target.find('button').removeClass('active');
+            $selectedNode = null;
+          }
+
+          // find next node
+          function findNextNode(row, column) {
+            var findNode = null;
+            $.each($specialCharNode, function(idx, $node) {
+              var findRow = Math.ceil((idx + 1) / COLUMN_LENGTH);
+              var findColumn = ((idx + 1) % COLUMN_LENGTH === 0) ? COLUMN_LENGTH : (idx + 1) % COLUMN_LENGTH;
+              if (findRow === row && findColumn === column) {
+                findNode = $node;
+                return false;
+              }
+            });
+            return $(findNode);
+          }
+
+          function arrowKeyHandler(keyCode) {
+            // left, right, up, down key
+            var $nextNode;
+            var lastRowColumnLength = $specialCharNode.length % totalColumn;
+
+            if (KEY.LEFT === keyCode) {
+              if (currentColumn > 1) {
+                currentColumn = currentColumn - 1;
+              } else if (currentRow === 1 && currentColumn === 1) {
+                currentColumn = lastRowColumnLength;
+                currentRow = totalRow;
+              } else {
+                currentColumn = totalColumn;
+                currentRow = currentRow - 1;
+              }
+            } else if (KEY.RIGHT === keyCode) {
+              if (currentRow === totalRow && lastRowColumnLength === currentColumn) {
+                currentColumn = 1;
+                currentRow = 1;
+              } else if (currentColumn < totalColumn) {
+                currentColumn = currentColumn + 1;
+              } else {
+                currentColumn = 1;
+                currentRow = currentRow + 1;
+              }
+            } else if (KEY.UP === keyCode) {
+              if (currentRow === 1 && lastRowColumnLength < currentColumn) {
+                currentRow = totalRow - 1;
+              } else {
+                currentRow = currentRow - 1;
+              }
+            } else if (KEY.DOWN === keyCode) {
+              currentRow = currentRow + 1;
+            }
+
+            if (currentRow === totalRow && currentColumn > lastRowColumnLength) {
+              currentRow = 1;
+            } else if (currentRow > totalRow) {
+              currentRow = 1;
+            } else if (currentRow < 1) {
+              currentRow = totalRow;
+            }
+
+            $nextNode = findNextNode(currentRow, currentColumn);
+
+            if ($nextNode) {
+              removeActiveClass($selectedNode);
+              addActiveClass($nextNode);
+            }
+          }
+
+          function enterKeyHandler() {
+            if (!$selectedNode) {
+              return;
+            }
+
+            deferred.resolve(decodeURIComponent($selectedNode.find('button').attr('data-value')));
+            $specialCharDialog.modal('hide');
+          }
+
+          function keyDownEventHandler(event) {
+            event.preventDefault();
+            var keyCode = event.keyCode;
+            if (keyCode === undefined || keyCode === null) {
+              return;
+            }
+            // check arrowKeys match
+            if (ARROW_KEYS.indexOf(keyCode) > -1) {
+              if ($selectedNode === null) {
+                addActiveClass($specialCharNode.eq(0));
+                currentColumn = 1;
+                currentRow = 1;
+                return;
+              }
+              arrowKeyHandler(keyCode);
+            } else if (keyCode === ENTER_KEY) {
+              enterKeyHandler();
+            }
+            return false;
+          }
+
+          // remove class
+          removeActiveClass($specialCharNode);
+
+          // find selected node
+          if (text) {
+            for (var i = 0; i < $specialCharNode.length; i++) {
+              var $checkNode = $($specialCharNode[i]);
+              if ($checkNode.text() === text) {
+                addActiveClass($checkNode);
+                currentRow = Math.ceil((i + 1) / COLUMN_LENGTH);
+                currentColumn = (i + 1) % COLUMN_LENGTH;
+              }
+            }
+          }
+
+          ui.onDialogShown(self.$dialog, function() {
+            $(document).on('keydown', keyDownEventHandler);
+
+            self.$dialog.find('button').tooltip();
+
+            $specialCharNode.on('click', function(event) {
+              event.preventDefault();
+              deferred.resolve(decodeURIComponent($(event.currentTarget).find('button').attr('data-value')));
+              ui.hideDialog(self.$dialog);
+            });
+          });
+
+          ui.onDialogHidden(self.$dialog, function() {
+            $specialCharNode.off('click');
+
+            self.$dialog.find('button').tooltip('destroy');
+
+            $(document).off('keydown', keyDownEventHandler);
+
+            if (deferred.state() === 'pending') {
+              deferred.reject();
+            }
+          });
+
+          ui.showDialog(self.$dialog);
+        });
+      };
+    },
+  });
+}));

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
public/assets/global/vendor/summernote/summernote-bs4.min.css


Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
public/assets/global/vendor/summernote/summernote-bs4.min.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
public/assets/global/vendor/summernote/summernote-bs4.min.js.map


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
public/assets/global/vendor/summernote/summernote.min.js.map


+ 20 - 11
resources/lang/de/admin.php

@@ -53,8 +53,7 @@ return [
             'attribute' => 'Helpdesk',
             'ticket' => 'Support-Tickets',
             'article' => 'Wissensdatenbank',
-            'push' => 'Push-Benachrichtigungen',
-            'mail' => 'E-Mail',
+            'marketing' => 'Nachrichtenübermittlung',
         ],
         'node' => [
             'attribute' => 'Knoten',
@@ -387,19 +386,29 @@ return [
         'counts' => 'Insgesamt <code>:num</code> Berechtigungen',
     ],
     'marketing' => [
+        'push_send' => 'Benachrichtigung senden',
+        'email_send' => 'E-Mail senden',
         'email' => [
-            'title' => 'E-Mail-Marketing',
-            'group_send' => 'E-Mail senden',
-            'counts' => 'Insgesamt <code>:num</code> E-Mails',
-        ],
+            'targeted_users_count' => 'Zielgerichtete Benutzeranzahl',
+            'loading_statistics' => 'Lade Statistiken...',
+            'filters' => 'Filter',
+            'expired_date' => 'Abgelaufene Datum',
+            'will_expire_date' => 'Wird Ablaufen Datum',
+            'traffic_usage_over' => 'Verkehrsnutzung Über N%',
+            'recently_active' => 'Kürzlich Aktiv',
+            'paid_servicing' => 'Bezahlte Dienste',
+            'previously_paid' => 'Früher Bezahlt',
+            'ever_paid' => 'Je Bezahlt',
+            'never_paid' => 'Nie Bezahlt',
+            'recent_traffic_abnormal' => 'Verkehr Abnormal in Letzter Stunde',
+        ],
+        'counts' => 'Insgesamt <code>:num</code> E-Mails',
         'send_status' => 'Sende-Status',
         'send_time' => 'Gesendet am',
         'error_message' => 'Fehlermeldungen',
-        'push' => [
-            'title' => 'Push-Benachrichtigungen',
-            'send' => 'Benachrichtigung senden',
-            'counts' => 'Insgesamt <code>:num</code> Nachrichten',
-        ],
+        'processed' => 'Anfrage Bearbeitet',
+        'targeted_users_not_found' => 'Zielgerichtete Benutzer Nicht Gefunden',
+        'unknown_sending_type' => 'Unbekannter Versandtyp',
     ],
     'creating' => 'Hinzufügen...',
     'article' => [

+ 20 - 11
resources/lang/en/admin.php

@@ -53,8 +53,7 @@ return [
             'attribute' => 'Helpdesk',
             'ticket' => 'Support Tickets',
             'article' => 'Knowledge Base',
-            'push' => 'Push Notifications',
-            'mail' => 'Email',
+            'marketing' => 'Message Broadcasting',
         ],
         'node' => [
             'attribute' => 'Nodes',
@@ -387,19 +386,29 @@ return [
         'counts' => 'Total <code>:num</code> Permissions',
     ],
     'marketing' => [
+        'push_send' => 'Send Notification',
+        'email_send' => 'Send Email',
         'email' => [
-            'title' => 'Email Marketing',
-            'group_send' => 'Send Email',
-            'counts' => 'Total <code>:num</code> Emails',
-        ],
+            'targeted_users_count' => 'Targeted Users Count',
+            'loading_statistics' => 'Loading Statistics...',
+            'filters' => 'Filters',
+            'expired_date' => 'Expired Date',
+            'will_expire_date' => 'Will Expire Date',
+            'traffic_usage_over' => 'Traffic Usage Over N%',
+            'recently_active' => 'Recently Active',
+            'paid_servicing' => 'Paid Servicing',
+            'previously_paid' => 'Used to Pay',
+            'ever_paid' => 'Ever Paid',
+            'never_paid' => 'Never Paid',
+            'recent_traffic_abnormal' => 'Traffic Abnormal in Last Hour',
+        ],
+        'counts' => 'Total <code>:num</code> Emails',
         'send_status' => 'Send Status',
         'send_time' => 'Sent On',
         'error_message' => 'Error Messages',
-        'push' => [
-            'title' => 'Push Notifications',
-            'send' => 'Send Notification',
-            'counts' => 'Total <code>:num</code> Messages',
-        ],
+        'processed' => 'Request Processed',
+        'targeted_users_not_found' => 'Targeted Users Not Found',
+        'unknown_sending_type' => 'Unknown Sending Type',
     ],
     'creating' => 'Adding...',
     'article' => [

+ 20 - 11
resources/lang/fa/admin.php

@@ -53,8 +53,7 @@ return [
             'attribute' => 'سیستم پشتیبانی',
             'ticket' => 'تیکت‌های پشتیبانی',
             'article' => 'مدیریت مقالات',
-            'push' => 'ارسال پیام',
-            'mail' => 'ارسال ایمیل گروهی',
+            'marketing' => 'پخش پیام‌ها',
         ],
         'node' => [
             'attribute' => 'سیستم گره‌ها',
@@ -387,19 +386,29 @@ return [
         'counts' => 'مجموع <code>:num</code> دسترسی',
     ],
     'marketing' => [
+        'push_send' => 'ارسال پیام فشاری',
+        'email_send' => 'ارسال ایمیل گروهی',
         'email' => [
-            'title' => 'ایمیل مارکتینگ',
-            'group_send' => 'ارسال ایمیل گروهی',
-            'counts' => 'مجموع <code>:num</code> ایمیل',
-        ],
+            'targeted_users_count' => 'تعداد کاربران هدف',
+            'loading_statistics' => 'در حال بارگذاری آمار...',
+            'filters' => 'فیلترها',
+            'expired_date' => 'تاریخ انقضا',
+            'will_expire_date' => 'تاریخ انقضا آینده',
+            'traffic_usage_over' => 'استفاده از ترافیک بیش از N%',
+            'recently_active' => 'فعالیت اخیر',
+            'paid_servicing' => 'خدمات پرداختی',
+            'previously_paid' => 'قبلاً پرداخت شده',
+            'ever_paid' => 'پرداخت شده',
+            'never_paid' => 'هرگز پرداخت نشده',
+            'recent_traffic_abnormal' => 'ناهنجاری ترافیک در ساعت اخیر',
+        ],
+        'counts' => 'مجموع <code>:num</code> ایمیل',
         'send_status' => 'وضعیت ارسال',
         'send_time' => 'زمان ارسال',
         'error_message' => 'پیام‌های خطا',
-        'push' => [
-            'title' => 'پیام‌های فشاری',
-            'send' => 'ارسال پیام فشاری',
-            'counts' => 'مجموع <code>:num</code> پیام',
-        ],
+        'processed' => 'درخواست پردازش شده',
+        'targeted_users_not_found' => 'کاربران هدف یافت نشد',
+        'unknown_sending_type' => 'نوع ارسال ناشناخته',
     ],
     'creating' => 'در حال افزودن...',
     'article' => [

+ 20 - 11
resources/lang/ja/admin.php

@@ -53,8 +53,7 @@ return [
             'attribute' => 'カスタマーサービスシステム',
             'ticket' => 'サポートチケット',
             'article' => '記事管理',
-            'push' => 'プッシュ通知',
-            'mail' => 'メール送信',
+            'marketing' => 'メッセージの配信',
         ],
         'node' => [
             'attribute' => 'ノードシステム',
@@ -387,19 +386,29 @@ return [
         'counts' => '合計 <code>:num</code> 権限行動',
     ],
     'marketing' => [
+        'push_send' => '通知送信',
+        'email_send' => 'メール送信',
         'email' => [
-            'title' => 'メールマーケティングリスト',
-            'group_send' => 'メール送信',
-            'counts' => '合計 <code>:num</code> メール',
-        ],
+            'targeted_users_count' => 'ターゲットユーザー数',
+            'loading_statistics' => '統計情報を読み込み中...',
+            'filters' => 'フィルター',
+            'expired_date' => '期限切れの日付',
+            'will_expire_date' => '期限が切れる日付',
+            'traffic_usage_over' => 'トラフィック使用量がN%を超えました',
+            'recently_active' => '最近アクティブ',
+            'paid_servicing' => '有料サービス',
+            'previously_paid' => '以前に支払い済み',
+            'ever_paid' => '支払い済み',
+            'never_paid' => '支払いなし',
+            'recent_traffic_abnormal' => '直近1時間のトラフィック異常',
+        ],
+        'counts' => '合計 <code>:num</code> メール',
         'send_status' => '送信状態',
         'send_time' => '送信時間',
         'error_message' => 'エラーメッセージ',
-        'push' => [
-            'title' => 'プッシュ通知リスト',
-            'send' => '通知送信',
-            'counts' => '合計 <code>:num</code> メッセージ',
-        ],
+        'processed' => 'リクエストが処理されました',
+        'targeted_users_not_found' => 'ターゲットユーザーが見つかりません',
+        'unknown_sending_type' => '不明な送信タイプ',
     ],
     'creating' => '追加中...',
     'article' => [

+ 20 - 11
resources/lang/ko/admin.php

@@ -53,8 +53,7 @@ return [
             'attribute' => '고객 서비스 시스템',
             'ticket' => '서비스 티켓',
             'article' => '기사 관리',
-            'push' => '메시지 푸시',
-            'mail' => '이메일 그룹 발송',
+            'marketing' => '메시지 방송',
         ],
         'node' => [
             'attribute' => '노드 시스템',
@@ -387,19 +386,29 @@ return [
         'counts' => '총 <code>:num</code> 권한 행동',
     ],
     'marketing' => [
+        'push_send' => '푸시 메시지 발송',
+        'email_send' => '이메일 그룹 발송',
         'email' => [
-            'title' => '이메일 마케팅 목록',
-            'group_send' => '이메일 그룹 발송',
-            'counts' => '총 <code>:num</code> 메시지',
-        ],
+            'targeted_users_count' => '대상 사용자 수',
+            'loading_statistics' => '통계 정보를 로드 중...',
+            'filters' => '필터',
+            'expired_date' => '만료된 날짜',
+            'will_expire_date' => '만료될 날짜',
+            'traffic_usage_over' => '트래픽 사용량이 N%를 초과함',
+            'recently_active' => '최근 활동',
+            'paid_servicing' => '유료 서비스',
+            'previously_paid' => '이전에 결제됨',
+            'ever_paid' => '결제됨',
+            'never_paid' => '결제하지 않음',
+            'recent_traffic_abnormal' => '최근 1시간 내 트래픽 이상',
+        ],
+        'counts' => '총 <code>:num</code> 메시지',
         'send_status' => '발송 상태',
         'send_time' => '발송 시간',
         'error_message' => '오류 메시지',
-        'push' => [
-            'title' => '푸시 메시지 목록',
-            'send' => '푸시 메시지 발송',
-            'counts' => '총 <code>:num</code> 푸시 메시지',
-        ],
+        'processed' => '요청 처리됨',
+        'targeted_users_not_found' => '대상 사용자를 찾을 수 없음',
+        'unknown_sending_type' => '알 수 없는 발송 유형',
     ],
     'creating' => '추가 중...',
     'article' => [

+ 20 - 11
resources/lang/vi/admin.php

@@ -53,8 +53,7 @@ return [
             'attribute' => 'Dịch vụ khách hàng',
             'ticket' => 'Yêu cầu hỗ trợ',
             'article' => 'Quản lý bài viết',
-            'push' => 'Thông báo đẩy',
-            'mail' => 'Gửi email hàng loạt',
+            'marketing' => 'Phát sóng tin nhắn',
         ],
         'node' => [
             'attribute' => 'Hệ thống nút',
@@ -387,19 +386,29 @@ return [
         'counts' => 'Tổng cộng có <code>:num</code> quyền hạn',
     ],
     'marketing' => [
+        'push_send' => 'Gửi thông báo',
+        'email_send' => 'Gửi email nhóm',
         'email' => [
-            'title' => 'Danh sách email marketing',
-            'group_send' => 'Gửi email nhóm',
-            'counts' => 'Tổng cộng có <code>:num</code> email',
-        ],
+            'targeted_users_count' => 'Số lượng người dùng mục tiêu',
+            'loading_statistics' => 'Đang tải thống kê...',
+            'filters' => 'Bộ lọc',
+            'expired_date' => 'Ngày hết hạn',
+            'will_expire_date' => '',
+            'traffic_usage_over' => 'Sử dụng lưu lượng vượt quá N%',
+            'recently_active' => 'Hoạt động gần đây',
+            'paid_servicing' => '\'Dịch vụ trả phí',
+            'previously_paid' => 'Đã từng thanh toán',
+            'ever_paid' => 'Đã thanh toán',
+            'never_paid' => 'Chưa thanh toán',
+            'recent_traffic_abnormal' => 'Lưu lượng bất thường trong giờ qua',
+        ],
+        'counts' => 'Tổng cộng có <code>:num</code> email',
         'send_status' => 'Trạng thái gửi',
         'send_time' => 'Thời gian gửi',
         'error_message' => 'Thông báo lỗi',
-        'push' => [
-            'title' => 'Danh sách thông báo đẩy',
-            'send' => 'Gửi thông báo',
-            'counts' => 'Tổng cộng có <code>:num</code> thông báo',
-        ],
+        'processed' => 'Yêu cầu đã được xử lý',
+        'targeted_users_not_found' => 'Không tìm thấy người dùng mục tiêu',
+        'unknown_sending_type' => 'Loại gửi không xác định',
     ],
     'creating' => 'Đang thêm...',
     'article' => [

+ 20 - 11
resources/lang/zh_CN/admin.php

@@ -53,8 +53,7 @@ return [
             'attribute' => '客服系统',
             'ticket' => '服务工单',
             'article' => '文章管理',
-            'push' => '消息推送',
-            'mail' => '邮件群发',
+            'marketing' => '消息广播',
         ],
         'node' => [
             'attribute' => '节点系统',
@@ -387,19 +386,29 @@ return [
         'counts' => '共有 <code>:num</code> 条权限行为',
     ],
     'marketing' => [
+        'push_send' => '推送消息',
+        'email_send' => '群发邮件',
         'email' => [
-            'title' => '邮件群发列表',
-            'group_send' => '群发邮件',
-            'counts' => '共有 <code>:num</code> 条消息',
-        ],
+            'targeted_users_count' => '目标用户数',
+            'loading_statistics' => '正在加载统计信息...',
+            'filters' => '过滤条件',
+            'expired_date' => '已过期时间',
+            'will_expire_date' => '将到期时间',
+            'traffic_usage_over' => '流量使用超过N%',
+            'recently_active' => '最近活跃过',
+            'paid_servicing' => '当前付费服务中',
+            'previously_paid' => '曾付费但当前无服务',
+            'ever_paid' => '曾经付费',
+            'never_paid' => '从未付费',
+            'recent_traffic_abnormal' => '小时内流量异常',
+        ],
+        'counts' => '共有 <code>:num</code> 条消息',
         'send_status' => '发送状态',
         'send_time' => '发送时间',
         'error_message' => '错误信息',
-        'push' => [
-            'title' => '推送消息列表',
-            'send' => '推送消息',
-            'counts' => '共有 <code>:num</code> 条推送消息',
-        ],
+        'processed' => '发送请求已受理',
+        'targeted_users_not_found' => '目标用户未找到',
+        'unknown_sending_type' => '未知发送类型',
     ],
     'creating' => '正在添加...',
     'article' => [

+ 3 - 3
resources/views/admin/article/info.blade.php

@@ -37,7 +37,7 @@
                         </div>
                     </div>
                     <div class="form-group row">
-                        <label class="col-form-label col-md-2" for="title"> {{ trans('validation.attributes.title') }} </label>
+                        <label class="col-form-label col-md-2" for="title"> {{ ucfirst(trans('validation.attributes.title')) }} </label>
                         <div class="col-md-4">
                             <input class="form-control" id="title" name="title" type="text" autofocus required />
                         </div>
@@ -88,7 +88,7 @@
                         </div>
                     </div>
                     <div class="form-group row">
-                        <label class="col-form-label col-md-2" for="content"> {{ trans('validation.attributes.content') }} </label>
+                        <label class="col-form-label col-md-2" for="content"> {{ ucfirst(trans('validation.attributes.content')) }} </label>
                         <div class="col-md-10">
                             <textarea class="form-control" name="content">
                                 @isset($article)
@@ -134,7 +134,7 @@
                 quickbars_insert_toolbar: 'quicktable image media',
                 quickbars_selection_toolbar: 'bold italic underline | blocks | bullist numlist | blockquote quicklink',
                 extended_valid_elements: 'button[onclick|class],i[class|aria-hidden]', // Allow more attributes for <a>
-                language: '{{ app()->getLocale() }}',
+                language: '{{ app()->getLocale() !== 'ko' ? app()->getLocale() : 'ko_KR' }}',
                 content_css: [
                     '/assets/bundle/app.min.css',
                     '/assets/global/fonts/font-awesome/css/all.min.css',

+ 450 - 0
resources/views/admin/article/marketing.blade.php

@@ -0,0 +1,450 @@
+@extends('admin.layouts')
+@section('css')
+    <link href="/assets/global/vendor/summernote/summernote-bs4.min.css" rel="stylesheet">
+    <link href="/assets/global/vendor/bootstrap-table/bootstrap-table.min.css" rel="stylesheet">
+    <link href="/assets/global/vendor/bootstrap-select/bootstrap-select.min.css" rel="stylesheet">
+    <link href="/assets/global/vendor/bootstrap-tokenfield/bootstrap-tokenfield.min.css" rel="stylesheet">
+    <link href="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.css" rel="stylesheet">
+    <link href="/assets/global/vendor/bootstrap-markdown/bootstrap-markdown.min.css" rel="stylesheet">
+@endsection
+@section('content')
+    <div class="page-content container-fluid">
+        <div class="panel">
+            <div class="panel-heading">
+                <h3 class="panel-title">{{ trans('admin.menu.customer_service.marketing') }}</h3>
+                <div class="panel-actions">
+                    @can('admin.marketing.email')
+                        <button class="btn btn-primary" data-toggle="modal" data-target="#send_email_modal" type="button">
+                            <i class="fa-solid fa-envelope"></i> {{ trans('admin.marketing.email_send') }}</button>
+                    @endcan
+                    @can('admin.marketing.push')
+                        <button class="btn btn-primary" data-toggle="modal" data-target="#send_push_modal" type="button" disabled>
+                            <i class="fa-solid fa-bell"></i> {{ trans('admin.marketing.push_send') }}</button>
+                    @endcan
+                </div>
+            </div>
+            <div class="panel-body">
+                <form class="form-row">
+                    <div class="form-group col-xxl-1 col-xl-2 col-lg-3 col-md-4 col-sm-6">
+                        <select class="form-control" id="status" name="status" data-plugin="selectpicker" data-style="btn-outline btn-primary"
+                                title="{{ trans('common.status.attribute') }}" onchange="this.form.submit();">
+                            <option value="0">{{ trans('common.to_be_send') }}</option>
+                            <option value="-1">{{ trans('common.failed') }}</option>
+                            <option value="1">{{ trans('common.success') }}</option>
+                        </select>
+                    </div>
+                    <div class="form-group col-lg-3 col-sm-6 btn-group">
+                        <button class="btn btn-primary" type="submit">{{ trans('common.search') }}</button>
+                        <button class="btn btn-danger" type="button" onclick="resetSearchForm()">{{ trans('common.reset') }}</button>
+                    </div>
+                </form>
+                <table class="text-md-center" data-toggle="table" data-mobile-responsive="true">
+                    <thead class="thead-default">
+                        <tr>
+                            <th> #</th>
+                            <th> {{ ucfirst(trans('validation.attributes.title')) }}</th>
+                            <th> {{ trans('admin.marketing.send_status') }}</th>
+                            <th> {{ trans('admin.marketing.send_time') }}</th>
+                            <th> {{ trans('admin.marketing.error_message') }}</th>
+                            <th> {{ trans('common.action') }}</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        @foreach ($marketingMessages as $message)
+                            <tr>
+                                <td> {{ $message->id }} </td>
+                                <td> {{ $message->title }} </td>
+                                <td> {{ $message->status_label }} </td>
+                                <td> {{ $message->created_at }} </td>
+                                <td> {{ $message->error }} </td>
+                                <td>
+                                    <a class="btn btn-primary" data-toggle="collapse" href="#marketing_{{ $loop->iteration }}" aria-expanded="false"
+                                       aria-controls="marketing_{{ $loop->iteration }}">{{ trans('common.view') }}</a>
+                                    @if ($message->type === 1)
+                                        <div class="collapse" id="marketing_{{ $loop->iteration }}">{!! $message->content !!}</div>
+                                    @else
+                                        <div class="collapse" id="marketing_{{ $loop->iteration }}">
+                                            <div class="markdown-content" data-markdown="{{ $message->content }}" data-rendered="false"></div>
+                                        </div>
+                                    @endif
+                                </td>
+                            </tr>
+                        @endforeach
+                    </tbody>
+                </table>
+            </div>
+            <div class="panel-footer">
+                <div class="row">
+                    <div class="col-sm-4">
+                        {!! trans('admin.marketing.counts', ['num' => $marketingMessages->total()]) !!}
+                    </div>
+                    <div class="col-sm-8">
+                        <nav class="Page navigation float-right">
+                            {{ $marketingMessages->links() }}
+                        </nav>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    @can('admin.marketing.email')
+        <div class="modal fade" id="send_email_modal" data-focus-on="input:first" data-backdrop="static" data-keyboard="false" tabindex="-1">
+            <div class="modal-dialog modal-lg modal-center">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <button class="close" data-dismiss="modal" type="button" aria-label="{{ trans('common.close') }}">
+                            <span aria-hidden="true">×</span>
+                        </button>
+                        <h4 class="modal-title">
+                            <i class="icon fa-solid fa-envelopes-bulk"></i>{{ trans('admin.marketing.email_send') }}
+                        </h4>
+                    </div>
+                    <div class="modal-body">
+                        <div class="alert alert-info">
+                            <p class="font-size-18">
+                                <i class="icon fa-solid fa-users-viewfinder"></i> {{ trans('admin.marketing.email.targeted_users_count') }}
+                                <code class="ml-5" id="statistics"></code>
+                            </p>
+                        </div>
+                        <h5>{{ trans('admin.marketing.email.filters') }}</h5>
+                        <form class="form-row" id="filter-form">
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <input class="form-control" name="id" data-plugin="tokenfield" type="text" placeholder="{{ trans('model.user.id') }}" />
+                            </div>
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <input class="form-control" name="username" data-plugin="tokenfield" type="text"
+                                       placeholder="{{ trans('model.user.username') }}" />
+                            </div>
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <div class="input-group">
+                                    <div class="input-group-prepend">
+                                        <span class="input-group-text"><i class="fa-solid fa-calendar-minus"></i></span>
+                                    </div>
+                                    <input class="form-control" name="expire_start" data-plugin="datepicker" type="text"
+                                           placeholder="{{ trans('admin.marketing.email.expired_date') }}" autocomplete="off" />
+                                </div>
+                            </div>
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <div class="input-group">
+                                    <div class="input-group-prepend">
+                                        <span class="input-group-text"><i class="fa-solid fa-calendar-plus"></i></span>
+                                    </div>
+                                    <input class="form-control" name="expire_end" data-plugin="datepicker" type="text"
+                                           placeholder="{{ trans('admin.marketing.email.will_expire_date') }}" autocomplete="off" />
+                                </div>
+                            </div>
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <div class="input-group">
+                                    <input class="form-control" name="traffic" type="number" min="0" max="100"
+                                           placeholder="{{ trans('admin.marketing.email.traffic_usage_over') }}" />
+                                    <div class="input-group-append">
+                                        <span class="input-group-text">%</span>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <div class="input-group">
+                                    <input class="form-control" name="lastAlive" type="number" min="1"
+                                           placeholder="{{ trans('admin.marketing.email.recently_active') }}" />
+                                    <div class="input-group-append">
+                                        <span class="input-group-text">{{ ucfirst(trans('validation.attributes.minute')) }}</span>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="form-group col-auto">
+                                <div class="checkbox-custom checkbox-primary">
+                                    <input id="paying" name="paying" type="checkbox" />
+                                    <label for="paying">{{ trans('admin.marketing.email.paid_servicing') }}</label>
+                                </div>
+                            </div>
+                            <div class="form-group col-auto">
+                                <div class="checkbox-custom checkbox-primary">
+                                    <input id="notPaying" name="notPaying" type="checkbox" />
+                                    <label for="notPaying">{{ trans('admin.marketing.email.previously_paid') }}</label>
+                                </div>
+                            </div>
+                            <div class="form-group col-auto">
+                                <div class="checkbox-custom checkbox-primary">
+                                    <input id="paid" name="paid" type="checkbox" />
+                                    <label for="paid">{{ trans('admin.marketing.email.ever_paid') }}</label>
+                                </div>
+                            </div>
+                            <div class="form-group col-auto">
+                                <div class="checkbox-custom checkbox-primary">
+                                    <input id="neverPay" name="neverPay" type="checkbox" />
+                                    <label for="neverPay">{{ trans('admin.marketing.email.never_paid') }}</label>
+                                </div>
+                            </div>
+                            <div class="form-group col-auto">
+                                <div class="checkbox-custom checkbox-primary">
+                                    <input id="flowAbnormal" name="flowAbnormal" type="checkbox" />
+                                    <label for="flowAbnormal">{{ trans('admin.marketing.email.recent_traffic_abnormal') }}</label>
+                                </div>
+                            </div>
+                            @if ($userGroups)
+                                <div class="form-group col-lg-3 col-md-4 col-6">
+                                    <select class="form-control show-tick" name="user_group_id[]" data-plugin="selectpicker" data-style="btn-outline btn-primary"
+                                            title="{{ trans('model.user.group') }}" multiple>
+                                        @foreach ($userGroups as $key => $group)
+                                            <option value="{{ $key }}">{{ $group }}</option>
+                                        @endforeach
+                                    </select>
+                                </div>
+                            @endif
+                            @if ($levels)
+                                <div class="form-group col-lg-3 col-md-4 col-6">
+                                    <select class="form-control show-tick" name="level[]" data-plugin="selectpicker" data-style="btn-outline btn-primary"
+                                            title="{{ trans('model.common.level') }}" multiple>
+                                        @foreach ($levels as $key => $level)
+                                            <option value="{{ $key }}">{{ $level }}</option>
+                                        @endforeach
+                                    </select>
+                                </div>
+                            @endif
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <select class="form-control show-tick" name="status[]" data-plugin="selectpicker" data-style="btn-outline btn-primary"
+                                        title="{{ trans('model.user.account_status') }}" multiple>
+                                    <option value="-1">{{ trans('common.status.banned') }}</option>
+                                    <option value="0">{{ trans('common.status.inactive') }}</option>
+                                    <option value="1">{{ trans('common.status.normal') }}</option>
+                                </select>
+                            </div>
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <select class="form-control" name="enable" data-plugin="selectpicker" data-style="btn-outline btn-primary"
+                                        title="{{ trans('model.user.proxy_status') }}">
+                                    <option value="1">{{ trans('common.status.enabled') }}</option>
+                                    <option value="0">{{ trans('common.status.banned') }}</option>
+                                </select>
+                            </div>
+                            <div class="form-group col-lg-3 col-md-4 col-6">
+                                <button class="btn btn-primary" type="button" onclick="fetchStatistics()">{{ trans('admin.query') }}</button>
+                                <button class="btn btn-danger" type="button" onclick="resetFilterForm()">{{ trans('common.reset') }}</button>
+                            </div>
+                        </form>
+                        <div class="alert" id="msg" style="display: none;"></div>
+                        <form class="form-horizontal" id="send-email-form">
+                            <div class="form-body">
+                                <div class="form-group">
+                                    <div class="row">
+                                        <label class="col-md-1 control-label" for="title"> {{ ucfirst(trans('validation.attributes.title')) }} </label>
+                                        <div class="col-md-8">
+                                            <input class="form-control" id="title" name="title" type="text" />
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <div class="row">
+                                        <label class="col-md-1 control-label" for="content"> {{ ucfirst(trans('validation.attributes.content')) }} </label>
+                                        <div class="col-md-11">
+                                            <textarea class="form-control" id="content" name="content"></textarea>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </form>
+                    </div>
+                    <div class="modal-footer">
+                        <button class="btn btn-danger mr-auto" data-dismiss="modal">{{ trans('common.cancel') }}</button>
+                        <button class="btn btn-primary" type="button" onclick="sendEmail()">{{ trans('common.send') }}</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    @endcan
+
+    @can('admin.marketing.push')
+        <div class="modal fade" id="send_push_modal" data-focus-on="input:first" data-backdrop="static" data-keyboard="false" tabindex="-1">
+            <div class="modal-dialog modal-lg modal-center">
+                <div class="modal-content">
+                    <div class="modal-header">
+                        <button class="close" data-dismiss="modal" type="button" aria-label="{{ trans('common.close') }}">
+                            <span aria-hidden="true">×</span>
+                        </button>
+                        <h4 class="modal-title">{{ trans('admin.marketing.push_send') }}</h4>
+                    </div>
+                    <div class="modal-body">
+                        <div class="alert alert-danger" id="msg" style="display: none;"></div>
+                        <form class="form-horizontal" action="#" method="post">
+                            <div class="form-body">
+                                <div class="form-group">
+                                    <div class="row">
+                                        <label class="col-md-2 control-label" for="title"> {{ ucfirst(trans('validation.attributes.title')) }} </label>
+                                        <div class="col-md-6">
+                                            <input class="form-control" id="title" name="title" type="text" />
+                                        </div>
+                                    </div>
+                                </div>
+                                <div class="form-group">
+                                    <div class="row">
+                                        <label class="col-md-2 control-label" for="content"> {{ ucfirst(trans('validation.attributes.content')) }} </label>
+                                        <div class="col-md-9">
+                                            <textarea class="form-control" id="content" name="content" data-provide="markdown" data-iconlibrary="fa" rows="10"></textarea>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </form>
+                    </div>
+                    <div class="modal-footer">
+                        <button class="btn btn-danger mr-auto" data-dismiss="modal">{{ trans('common.cancel') }}</button>
+                        <button class="btn btn-primary disabled" type="button" onclick="sendPush()">{{ trans('common.send') }}</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    @endcan
+@endsection
+@section('javascript')
+    <script src="/assets/global/vendor/summernote/summernote-bs4.min.js"></script>
+    @if (app()->getLocale() !== 'en')
+        <script src="/assets/global/vendor/summernote/lang/summernote-{{ config('common.language')[app()->getLocale()][2] }}.min.js"></script>
+    @endif
+    <script src="/assets/global/vendor/bootstrap-table/bootstrap-table.min.js"></script>
+    <script src="/assets/global/vendor/bootstrap-table/extensions/mobile/bootstrap-table-mobile.min.js"></script>
+    <script src="/assets/global/vendor/bootstrap-select/bootstrap-select.min.js"></script>
+    <script src="/assets/global/vendor/bootstrap-tokenfield/bootstrap-tokenfield.min.js"></script>
+    <script src="/assets/global/vendor/bootstrap-datepicker/bootstrap-datepicker.min.js"></script>
+    <script src="/assets/global/vendor/bootstrap-markdown/bootstrap-markdown.min.js"></script>
+    <script src="/assets/global/vendor/marked/marked.min.js"></script>
+    <script src="/assets/global/js/Plugin/bootstrap-select.js"></script>
+    <script src="/assets/global/js/Plugin/bootstrap-tokenfield.js"></script>
+    <script src="/assets/global/js/Plugin/bootstrap-datepicker.js"></script>
+    <script>
+        function resetSearchForm() {
+            window.location.href = window.location.href.split('?')[0]
+        }
+
+        function renderMarkdown(element) {
+            var markdownText = $(element).data('markdown');
+            var htmlContent = marked(markdownText);
+            $(element).html(htmlContent);
+            $(element).data('rendered', true); // Mark as rendered
+
+        }
+        $(document).ready(function() {
+            $('#status').selectpicker('val', @json(Request::query('status')))
+
+            // Render Markdown content when collapse is shown
+            $('.collapse').on('shown.bs.collapse', function() {
+                if ($(this).find('.markdown-content').length > 0 && $(this).find('.markdown-content').data('rendered') === false) {
+                    renderMarkdown($(this).find('.markdown-content'));
+                }
+            });
+
+            @can('admin.marketing.email')
+                fetchStatistics()
+
+                $('#content').summernote({
+                    tabsize: 2,
+                    height: 400,
+                    dialogsInBody: true,
+                    lang: '{{ config('common.language')[app()->getLocale()][2] }}', // default: 'en-US'
+                })
+            @endcan
+        })
+
+        @can('admin.marketing.email')
+            $('[name="expire_start"]').datepicker({
+                format: 'yyyy-mm-dd',
+                endDate: new Date(),
+            })
+
+            $('[name="expire_end"]').datepicker({
+                format: 'yyyy-mm-dd',
+                startDate: new Date(),
+            })
+
+            function resetFilterForm() {
+                const form = $('#filter-form');
+                form[0].reset();
+                form.find('select').selectpicker('refresh');
+                form.find('[data-plugin="tokenfield"]').each(function() {
+                    $(this).tokenfield('setTokens', []);
+                });
+                fetchStatistics()
+            }
+
+            function fetchStatistics() {
+                const filterFormData = $('#filter-form').serializeArray()
+                const filteredFilterFormData = filterFormData.filter(field => field.value.trim() !== '')
+                $.ajax({
+                    url: '{{ route('admin.marketing.create', ['type' => 'email']) }}',
+                    method: 'GET',
+                    data: $.param(filteredFilterFormData),
+                    beforeSend: function() {
+                        $('#statistics').html('{{ trans('admin.marketing.email.loading_statistics') }}')
+                    },
+                    success: function(data) {
+                        $('#statistics').html(data.count)
+                    },
+                    error: function() {
+                        $('#statistics').html('<p>{{ trans('common.request_failed') }}</p>')
+                    },
+                })
+            }
+
+            function sendEmail() {
+                const filterFormData = $('#filter-form').serializeArray()
+                const filteredFilterFormData = filterFormData.filter(field => field.value.trim() !== '')
+
+                const emailFormData = $('#send-email-form').serializeArray()
+                emailFormData.push({
+                    name: '_token',
+                    value: '{{ csrf_token() }}',
+                })
+
+                $.ajax({
+                    url: '{{ route('admin.marketing.create', ['type' => 'email']) }}?' + $.param(filteredFilterFormData),
+                    method: 'POST',
+                    data: $.param(emailFormData),
+                    beforeSend: function() {
+                        $('#msg').show().removeClass('alert-danger alert-success').addClass('alert-info').html('{{ trans('admin.creating') }}')
+                    },
+                    success: function(ret) {
+                        $('#msg').show().html(ret.message)
+                        if (ret.status === 'success') {
+                            $('#msg').removeClass('alert-info alert-danger').addClass('alert-success').show().html(ret.message)
+                        } else {
+                            $('#msg').removeClass('alert-info alert-success').addClass('alert-danger').show().html(ret.message)
+                        }
+                    },
+                    error: function() {
+                        $('#msg').removeClass('alert-info alert-success').addClass('alert-danger').show().html('{{ trans('common.request_failed') }}')
+                    },
+                    complete: function() {},
+                })
+            }
+        @endcan
+
+        @can('admin.marketing.push')
+            // 发送通道消息
+            function sendPush() {
+                const formData = $('#send_push_modal').serializeArray()
+                formData.push({
+                    name: '_token',
+                    value: '{{ csrf_token() }}',
+                })
+                $.ajax({
+                    url: '{{ route('admin.marketing.create', ['type' => 'push']) }}',
+                    method: 'POST',
+                    data: $.param(formData),
+                    beforeSend: function() {
+                        $('#msg').show().html('{{ trans('admin.creating') }}');
+                    },
+                    success: function(ret) {
+                        if (ret.status === 'fail') {
+                            $('#msg').show().html(ret.message);
+                            return false;
+                        }
+                        $('#send_modal').modal('hide');
+                    },
+                    error: function() {
+                        $('#msg').show().html('{{ trans('common.request_failed') }}');
+                    },
+                    complete: function() {},
+                });
+            }
+        @endcan
+    </script>
+@endsection

+ 5 - 12
resources/views/admin/layouts.blade.php

@@ -159,7 +159,7 @@
                         </ul>
                     </li>
                 @endcanany
-                @canany(['admin.ticket.index', 'admin.article.index', 'admin.marketing.push', 'admin.marketing.email'])
+                @canany(['admin.ticket.index', 'admin.article.index', 'admin.marketing.index'])
                     <li class="site-menu-item has-sub {{ request()->routeIs('admin.ticket.*', 'admin.article.*', 'admin.marketing.*') ? 'active open' : '' }}">
                         <a href="javascript:void(0)">
                             <i class="site-menu-icon wb-chat-working" aria-hidden="true"></i>
@@ -197,17 +197,10 @@
                                     </a>
                                 </li>
                             @endcan
-                            @can('admin.marketing.push')
-                                <li class="site-menu-item {{ request()->routeIs('admin.marketing.push') ? 'active open' : '' }}">
-                                    <a href="{{ route('admin.marketing.push') }}">
-                                        <span class="site-menu-title">{{ trans('admin.menu.customer_service.push') }}</span>
-                                    </a>
-                                </li>
-                            @endcan
-                            @can('admin.marketing.email')
-                                <li class="site-menu-item {{ request()->routeIs('admin.marketing.email') ? 'active open' : '' }}">
-                                    <a href="{{ route('admin.marketing.email') }}">
-                                        <span class="site-menu-title">{{ trans('admin.menu.customer_service.mail') }}</span>
+                            @can('admin.marketing.index')
+                                <li class="site-menu-item {{ request()->routeIs('admin.marketing.index') ? 'active open' : '' }}">
+                                    <a href="{{ route('admin.marketing.index') }}">
+                                        <span class="site-menu-title">{{ trans('admin.menu.customer_service.marketing') }}</span>
                                     </a>
                                 </li>
                             @endcan

+ 0 - 78
resources/views/admin/marketing/emailList.blade.php

@@ -1,78 +0,0 @@
-@extends('admin.table_layouts')
-@section('content')
-    <div class="page-content container-fluid">
-        <div class="panel">
-            <div class="panel-heading">
-                <h3 class="panel-title">{{ trans('admin.marketing.email.title') }}</h3>
-                <div class="panel-actions">
-                    <button class="btn btn-primary" onclick="send()">
-                        <i class="icon wb-envelope"></i>{{ trans('admin.marketing.email.group_send') }}</button>
-                </div>
-            </div>
-            <div class="panel-body">
-                <form class="form-row">
-                    <div class="form-group col-lg-3 col-sm-6">
-                        <select class="form-control" id="status" name="status" data-plugin="selectpicker" data-style="btn-outline btn-primary"
-                                title="{{ trans('common.status.attribute') }}">
-                            <option value="0">{{ trans('common.to_be_send') }}</option>
-                            <option value="-1">{{ trans('common.failed') }}</option>
-                            <option value="1">{{ trans('common.success') }}</option>
-                        </select>
-                    </div>
-                    <div class="form-group col-lg-3 col-sm-6 btn-group">
-                        <button class="btn btn-primary" type="submit">{{ trans('common.search') }}</button>
-                        <button class="btn btn-danger" type="button" onclick="resetSearchForm()">{{ trans('common.reset') }}</button>
-                    </div>
-                </form>
-                <table class="text-md-center" data-toggle="table" data-mobile-responsive="true">
-                    <thead class="thead-default">
-                        <tr>
-                            <th> #</th>
-                            <th> {{ trans('validation.attributes.title') }}</th>
-                            <th> {{ trans('validation.attributes.content') }}</th>
-                            <th> {{ trans('admin.marketing.send_status') }}</th>
-                            <th> {{ trans('admin.marketing.send_time') }}</th>
-                            <th> {{ trans('admin.marketing.error_message') }}</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        @foreach ($emails as $email)
-                            <tr>
-                                <td> {{ $email->id }} </td>
-                                <td> {{ $email->title }} </td>
-                                <td> {{ $email->content }} </td>
-                                <td> {{ $email->status_label }} </td>
-                                <td> {{ $email->created_at }} </td>
-                                <td> {{ $email->error }} </td>
-                            </tr>
-                        @endforeach
-                    </tbody>
-                </table>
-            </div>
-            <div class="panel-footer">
-                <div class="row">
-                    <div class="col-sm-4">
-                        {!! trans('admin.marketing.email.counts', ['num' => $emails->total()]) !!}
-                    </div>
-                    <div class="col-sm-8">
-                        <nav class="Page navigation float-right">
-                            {{ $emails->links() }}
-                        </nav>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-@endsection
-@push('javascript')
-    <script>
-        $(document).ready(function() {
-            $('#status').selectpicker('val', @json(Request::query('status')));
-        });
-
-        // 发送邮件
-        function send() {
-            swal.fire(@json(trans('common.sorry')), '{{ trans('common.developing') }}', 'info');
-        }
-    </script>
-@endpush

+ 0 - 170
resources/views/admin/marketing/pushList.blade.php

@@ -1,170 +0,0 @@
-@extends('admin.table_layouts')
-@push('css')
-    <link href="/assets/global/vendor/bootstrap-markdown/bootstrap-markdown.min.css" rel="stylesheet">
-@endpush
-@section('content')
-    <div class="page-content container-fluid">
-        <div class="panel">
-            <div class="panel-heading">
-                <h3 class="panel-title">{{ trans('admin.marketing.push.title') }}</h3>
-                @can('admin.marketing.add')
-                    <div class="panel-actions">
-                        <button class="btn btn-primary disabled" data-toggle="modal" data-target="#send_modal" type="button">
-                            <i class="icon wb-plus"></i>{{ trans('admin.marketing.push.send') }}</button>
-                    </div>
-                @endcan
-            </div>
-            <div class="panel-body">
-                <form class="form-row">
-                    <div class="form-group col-lg-3 col-sm-6">
-                        <select class="form-control" id="status" name="status" data-plugin="selectpicker" data-style="btn-outline btn-primary"
-                                title="{{ trans('common.status.attribute') }}">
-                            <option value="0">{{ trans('common.to_be_send') }}</option>
-                            <option value="-1">{{ trans('common.failed') }}</option>
-                            <option value="1">{{ trans('common.success') }}</option>
-                        </select>
-                    </div>
-                    <div class="form-group col-lg-2 col-sm-6 btn-group">
-                        <button class="btn btn-primary" type="submit">{{ trans('common.search') }}</button>
-                        <button class="btn btn-danger" type="button" onclick="resetSearchForm()">{{ trans('common.reset') }}</button>
-                    </div>
-                </form>
-                {{--                <div class="alert alert-info alert-dismissible" role="alert"> --}}
-                {{--                    <button type="button" class="close" data-dismiss="alert" aria-label="{{ trans('common.close') }}"> --}}
-                {{--                        <span aria-hidden="true">×</span></button> --}}
-                {{--                    仅会推送给关注了您的消息通道的用户 @can('admin.system.index')<a href="{{route('admin.system.index')}}" class="alert-link" target="_blank">设置PushBear</a> @else 设置PushBear @endcan --}}
-                {{--                </div> --}}
-                <table class="text-md-center" data-toggle="table" data-mobile-responsive="true">
-                    <thead class="thead-default">
-                        <tr>
-                            <th> #</th>
-                            <th> {{ trans('validation.attributes.title') }}</th>
-                            <th> {{ trans('validation.attributes.content') }}</th>
-                            <th> {{ trans('admin.marketing.send_status') }}</th>
-                            <th> {{ trans('admin.marketing.send_time') }}</th>
-                            <th> {{ trans('admin.marketing.error_message') }}</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        @foreach ($pushes as $push)
-                            <tr>
-                                <td> {{ $push->id }} </td>
-                                <td> {{ $push->title }} </td>
-                                <td> {{ $push->content }} </td>
-                                <td> {{ $push->status_label }} </td>
-                                <td> {{ $push->created_at }} </td>
-                                <td> {{ $push->error }} </td>
-                            </tr>
-                        @endforeach
-                    </tbody>
-                </table>
-            </div>
-            <div class="panel-footer">
-                <div class="row">
-                    <div class="col-sm-4">
-                        {!! trans('admin.marketing.push.counts', ['num' => $pushes->total()]) !!}
-                    </div>
-                    <div class="col-sm-8">
-                        <nav class="Page navigation float-right">
-                            {{ $pushes->links() }}
-                        </nav>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    @can('admin.marketing.add')
-        <!-- 推送消息 -->
-        <div class="modal fade" id="send_modal" data-focus-on="input:first" data-backdrop="static" data-keyboard="false" tabindex="-1">
-            <div class="modal-dialog modal-lg modal-center">
-                <div class="modal-content">
-                    <div class="modal-header">
-                        <button class="close" data-dismiss="modal" type="button" aria-label="{{ trans('common.close') }}">
-                            <span aria-hidden="true">×</span>
-                        </button>
-                        <h4 class="modal-title">{{ trans('admin.marketing.push.send') }}</h4>
-                    </div>
-                    <div class="modal-body">
-                        <div class="alert alert-danger" id="msg" style="display: none;"></div>
-                        <form class="form-horizontal" action="#" method="post">
-                            <div class="form-body">
-                                <div class="form-group">
-                                    <div class="row">
-                                        <label class="col-md-2 control-label" for="title"> {{ trans('validation.attributes.title') }} </label>
-                                        <div class="col-md-6">
-                                            <input class="form-control" id="title" name="title" type="text" />
-                                        </div>
-                                    </div>
-                                </div>
-                                <div class="form-group">
-                                    <div class="row">
-                                        <label class="col-md-2 control-label" for="content"> {{ trans('validation.attributes.content') }} </label>
-                                        <div class="col-md-9">
-                                            <textarea class="form-control" id="content" name="content" data-provide="markdown" data-iconlibrary="fa" rows="10"></textarea>
-                                        </div>
-                                    </div>
-                                </div>
-                            </div>
-                        </form>
-                    </div>
-                    <div class="modal-footer">
-                        <button class="btn btn-danger mr-auto" data-dismiss="modal">{{ trans('common.cancel') }}</button>
-                        <button class="btn btn-primary disabled" type="button" onclick="return send();">{{ trans('common.send') }}</button>
-                    </div>
-                </div>
-            </div>
-        </div>
-    @endcan
-@endsection
-@push('javascript')
-    <script src="/assets/global/vendor/bootstrap-markdown/bootstrap-markdown.min.js"></script>
-    <script src="/assets/global/vendor/marked/marked.min.js"></script>
-    <script>
-        $(document).ready(function() {
-            $('#status').selectpicker('val', @json(Request::query('status')));
-        });
-
-        @can('admin.marketing.add')
-            // 发送通道消息
-            function send() {
-                const title = $('#title').val();
-
-                if (title.trim() === '') {
-                    $('#msg').show().html('{{ trans('validation.filled', ['attribute' => trans('validation.attributes.title')]) }}');
-                    title.focus();
-                    return false;
-                }
-
-                $.ajax({
-                    url: '{{ route('admin.marketing.add') }}',
-                    method: 'POST',
-                    data: {
-                        _token: '{{ csrf_token() }}',
-                        title: title,
-                        content: $('#content').val()
-                    },
-                    beforeSend: function() {
-                        $('#msg').show().html('{{ trans('admin.creating') }}');
-                    },
-                    success: function(ret) {
-                        if (ret.status === 'fail') {
-                            $('#msg').show().html(ret.message);
-                            return false;
-                        }
-                        $('#send_modal').modal('hide');
-                    },
-                    error: function() {
-                        $('#msg').show().html('{{ trans('common.request_failed') }}');
-                    },
-                    complete: function() {},
-                });
-            }
-
-            // 关闭modal触发
-            $('#send_modal').on('hide.bs.modal', function() {
-                window.location.reload();
-            });
-        @endcan
-    </script>
-@endpush

+ 2 - 3
routes/admin.php

@@ -65,9 +65,8 @@ Route::prefix('admin')->name('admin.')->group(function () {
     Route::resource('ticket', TicketController::class)->except('create', 'show');
     Route::resource('article', ArticleController::class);
     Route::prefix('marketing')->name('marketing.')->controller(MarketingController::class)->group(function () {
-        Route::get('email', 'emailList')->name('email'); // 邮件消息列表
-        Route::get('push', 'pushList')->name('push'); // 推送消息列表
-        Route::post('add', 'addPushMarketing')->name('add'); // 推送消息
+        Route::get('/', 'index')->name('index'); // 营销消息列表
+        Route::match(['get', 'post'], '{type}/create', 'create')->name('create'); // 推送消息
     });
 
     Route::resource('node', NodeController::class)->except('show');

Някои файлове не бяха показани, защото твърде много файлове са промени