From 4572ecddbe96798831ea2a631ddbc422ebb76800 Mon Sep 17 00:00:00 2001 From: "allen.yan" Date: Tue, 1 Jul 2025 10:25:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=BB=A3=E5=91=8A=E7=B3=BB?= =?UTF-8?q?=E7=B5=B1=2020250701?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/RotateTextAd.php | 73 ++++++ app/Console/Commands/TestMarqueeMessage.php | 47 ++++ app/Enums/TextAdColors.php | 36 +++ app/Livewire/Forms/TextAdTestForm.php | 81 +++++++ app/Livewire/Forms/TextAdsForm.php | 126 ++++++++++ app/Livewire/Tables/TextAdsTable.php | 215 ++++++++++++++++++ app/Models/TextAd.php | 20 ++ ...025_06_26_114551_create_text_ads_table.php | 31 +++ database/seeders/DatabaseSeeder.php | 6 +- database/seeders/TextAdPermissionSeeder.php | 33 +++ database/seeders/TextAdSeeder.php | 51 +++++ resources/lang/zh-tw/text_ads.php | 25 ++ .../views/livewire/admin/text-ads.blade.php | 7 + .../forms/text-ad-test-form.blade.php | 24 ++ .../livewire/forms/text-ads-form.blade.php | 24 ++ .../views/livewire/header/text-ad.blade.php | 8 + .../livewire/layout/admin/sidebar.blade.php | 1 + routes/console.php | 1 + routes/web.php | 1 + 19 files changed, 808 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/RotateTextAd.php create mode 100644 app/Console/Commands/TestMarqueeMessage.php create mode 100644 app/Enums/TextAdColors.php create mode 100644 app/Livewire/Forms/TextAdTestForm.php create mode 100644 app/Livewire/Forms/TextAdsForm.php create mode 100644 app/Livewire/Tables/TextAdsTable.php create mode 100644 app/Models/TextAd.php create mode 100644 database/migrations/2025_06_26_114551_create_text_ads_table.php create mode 100644 database/seeders/TextAdPermissionSeeder.php create mode 100644 database/seeders/TextAdSeeder.php create mode 100644 resources/lang/zh-tw/text_ads.php create mode 100644 resources/views/livewire/admin/text-ads.blade.php create mode 100644 resources/views/livewire/forms/text-ad-test-form.blade.php create mode 100644 resources/views/livewire/forms/text-ads-form.blade.php create mode 100644 resources/views/livewire/header/text-ad.blade.php diff --git a/app/Console/Commands/RotateTextAd.php b/app/Console/Commands/RotateTextAd.php new file mode 100644 index 0000000..c90be08 --- /dev/null +++ b/app/Console/Commands/RotateTextAd.php @@ -0,0 +1,73 @@ +orderBy('id')->get(); + + if ($ads->isEmpty()) { + $this->info('❌ 沒有啟用中的廣告。'); + return; + } + + $rooms = Room::where('type', '!=', 'svr') + ->where('is_online', 1) + ->get(['id', 'name', 'internal_ip', 'port']); + + foreach ($rooms as $room) { + $nextTimeKey = "text_ad_next_time_{$room->id}"; + $indexKey = "text_ad_index_{$room->id}"; + + $nextTime = Cache::get($nextTimeKey); + + // 還沒到時間,就跳過這個房間 + if ($nextTime && now()->lt($nextTime)) { + $this->info("⏳ 房間 {$room->id} 尚未到下一次播放時間(下次播放:{$nextTime})"); + continue; + } + + $index = Cache::get($indexKey, 0) % $ads->count(); + $ad = $ads[$index]; + $roomCode = str_pad($room->name, 4, '0', STR_PAD_LEFT); + $content = "{$roomCode}({$ad->color->labels()})-" . $ad->content; + + try { + $client = new TcpSocketClient($room->internal_ip, $room->port); + $response = $client->send($content); + + $this->info("✅ 廣告 #{$ad->id} 已推送到房間 {$room->id}:{$room->internal_ip}:{$room->port}"); + } catch (\Throwable $e) { + $this->error("❌ 房間 {$room->id} 發送失敗:{$e->getMessage()}"); + } + + // 更新此房間的播放狀態 + Cache::put($nextTimeKey, now()->addMinutes($ad->duration)); + Cache::put($indexKey, ($index + 1) % $ads->count()); + } + } +} diff --git a/app/Console/Commands/TestMarqueeMessage.php b/app/Console/Commands/TestMarqueeMessage.php new file mode 100644 index 0000000..96545a1 --- /dev/null +++ b/app/Console/Commands/TestMarqueeMessage.php @@ -0,0 +1,47 @@ +argument('ip'); + $port = (int) $this->argument('port'); + $message = $this->argument('message'); + + $client = new TcpSocketClient($ip, $port); + + try { + $this->info("📤 發送中:{$message}"); + $response = $client->send($message); + $this->info("✅ 回應:{$response}"); + } catch (\Exception $e) { + $this->error("❌ 發送失敗:" . $e->getMessage()); + } + } +} diff --git a/app/Enums/TextAdColors.php b/app/Enums/TextAdColors.php new file mode 100644 index 0000000..3c4f2aa --- /dev/null +++ b/app/Enums/TextAdColors.php @@ -0,0 +1,36 @@ + '黑色', + self::White => '白色', + self::Red => '紅色', + self::Green => '綠色', + self::Blue => '藍色', + }; + } + public function colorCode(): string + { + return match ($this) { + self::White => '#FFFFFF', + self::Red => '#FF0000', + self::Green => '#90EE90', + self::Blue => '#ADD8E6', + }; + } +} \ No newline at end of file diff --git a/app/Livewire/Forms/TextAdTestForm.php b/app/Livewire/Forms/TextAdTestForm.php new file mode 100644 index 0000000..d08d72e --- /dev/null +++ b/app/Livewire/Forms/TextAdTestForm.php @@ -0,0 +1,81 @@ +roomOptions = Room::where('type', '!=', 'svr')->get()->map(fn ($room) => [ + 'name' => $room->type->value.$room->name, + 'value' => $room->id, + ])->toArray(); + } + + public function openModal($id = null) + { + $textAd=TextAd::findOrFail($id); + $this->prefix = "({$textAd->color->labels()})-測試:"; + $this->content = $textAd->content; + $this->showModal = true; + } + public function closeModal() + { + $this->textAd=null; + $this->resetFields(); + $this->showModal = false; + } + + public function send() + { + + $room = Room::find($this->roomId); + $roomCode = str_pad($room->name, 4, '0', STR_PAD_LEFT); + try { + + $client = new TcpSocketClient($room->internal_ip, $room->port); + $client->send($roomCode.$this->prefix.$this->content); + + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => "✅ 已送出至房間 {$room->name}", + ]); + } catch (\Throwable $e) { + $this->notification()->send([ + 'icon' => 'error', + 'title' => '失敗', + 'description' => "❌ 發送失敗:{$e->getMessage()}", + ]); + } + $this->resetFields(); + $this->showModal = false; + $this->dispatch('pg:eventRefresh-text-ads-table'); + } + public function resetFields() + { + $this->content=''; + $this->roomId=null; + } + + public function render() + { + return view('livewire.forms.text-ad-test-form'); + } +} \ No newline at end of file diff --git a/app/Livewire/Forms/TextAdsForm.php b/app/Livewire/Forms/TextAdsForm.php new file mode 100644 index 0000000..ee95bbd --- /dev/null +++ b/app/Livewire/Forms/TextAdsForm.php @@ -0,0 +1,126 @@ +'', + 'color' => 'black', + 'duration' => 1, + 'is_active' => 1, + ]; + + + + public function mount() + { + $this->colorOptions = collect(TextAdColors::cases())->map(fn ($color) => [ + 'name' => $color->labels(), + 'value' => $color->value, + ])->toArray(); + $this->canCreate = Auth::user()?->can('song-edit') ?? false; + $this->canEdit = Auth::user()?->can('song-edit') ?? false; + $this->canDelect = Auth::user()?->can('song-delete') ?? false; + } + + public function openModal($id = null) + { + $this->resetFields(); + + if ($id) { + $textAd = TextAd::findOrFail($id); + $this->textAdId = $textAd->id; + $this->fields = $textAd->only(array_keys($this->fields)); + } + + $this->showModal = true; + } + + public function closeModal() + { + $this->resetFields(); + $this->showModal = false; + } + + public function save() + { + if ($this->textAdId) { + if ($this->canEdit) { + $textAd = TextAd::findOrFail($this->textAdId); + $textAd->update($this->fields); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '文字廣吿已更新', + ]); + } + } else { + if ($this->canCreate) { + $textAd = TextAd::create($this->fields); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '文字廣吿已新增', + ]); + } + } + $this->resetFields(); + $this->showModal = false; + $this->dispatch('pg:eventRefresh-text-ads-table'); + } + + public function deleteTextAd($id) + { + if ($this->canDelect) { + TextAd::findOrFail($id)->delete(); + $this->notification()->send([ + 'icon' => 'success', + 'title' => '成功', + 'description' => '文字廣吿已刪除', + ]); + + $this->dispatch('pg:eventRefresh-text-ads-table'); + } + } + + public function resetFields() + { + foreach ($this->fields as $key => $value) { + if ($key == 'color') { + $this->fields[$key] = 'black'; + } else if ($key == 'content'){ + $this->fields[$key] = ''; + } else { + $this->fields[$key] = 1; + } + } + $this->textAdId = null; + } + + public function render() + { + return view('livewire.forms.text-ads-form'); + } +} diff --git a/app/Livewire/Tables/TextAdsTable.php b/app/Livewire/Tables/TextAdsTable.php new file mode 100644 index 0000000..db23dfa --- /dev/null +++ b/app/Livewire/Tables/TextAdsTable.php @@ -0,0 +1,215 @@ + 'outside']); + $this->canCreate = Auth::user()?->can('text-ad-create') ?? false; + $this->canEdit = Auth::user()?->can('text-ad-edit') ?? false; + $this->canDownload=Auth::user()?->can('text-ad-delete') ?? false; + $this->canDelect = Auth::user()?->can('text-ad-delete') ?? false; + } + + public function setUp(): array + { + if($this->canDownload || $this->canDelect){ + $this->showCheckBox(); + } + $actions = []; + if($this->canDownload){ + $actions[]=PowerGrid::exportable(fileName: $this->tableName.'-file') + ->type(Exportable::TYPE_XLS, Exportable::TYPE_CSV); + } + $header = PowerGrid::header()->showSoftDeletes()->showToggleColumns(); + if($this->canCreate){ + $header->includeViewOnTop('livewire.header.text-ad'); + } + $actions[]=$header; + $actions[]=PowerGrid::footer()->showPerPage()->showRecordCount(); + + return $actions; + } + public function header(): array + { + $actions = []; + if ($this->canDelect) { + $actions[]=Button::add('bulk-delete') + ->slot('Bulk delete ()') + ->icon('solid-trash',['id' => 'my-custom-icon-id', 'class' => 'font-bold']) + ->class('inline-flex items-center gap-1 px-3 py-1 rounded ') + ->dispatch('bulkDelete.' . $this->tableName, []); + } + return $actions; + } + + public function datasource(): Builder + { + return TextAd::query(); + } + + public function relationSearch(): array + { + return []; + } + + public function fields(): PowerGridFields + { + return PowerGrid::fields() + ->add('id') + ->add('content') + ->add( + 'content_short', + fn (TextAd $model) => + '' . e(Str::limit($model->content, 50)) . '' + ) + ->add('color') + ->add('color_str', function (TextAd $model) { + if ($this->canEdit) { + return Blade::render( + '', + [ + 'options' => TextAdColors::options(), + 'modelId' => intval($model->id), + 'fieldName'=>'color', + 'selected' => $model->color->value + ] + ); + } + // 沒有權限就顯示對應的文字 + + return $model->color->labelPowergridFilter(); // 假設 label() 會回傳顯示文字 + } ) + ->add('duration') + ->add('is_active') + ->add('created_at'); + } + + public function columns(): array + { + $column=[]; + $column[] = Column::make(__('text_ads.id'), 'id'); + $column[] = Column::make(__('text_ads.content'), 'content_short', 'text_ads.content')->sortable()->searchable(); + $column[] = Column::make(__('text_ads.color'),'color_str', 'text-ads.color')->searchable(); + $column[] = Column::make(__('text_ads.duration'), 'duration')->sortable()->searchable(); + $column[] = Column::make(__('text_ads.is_active'), 'is_active')->toggleable(hasPermission: $this->canEdit, trueLabel: 'yes', falseLabel: 'no'); + $column[] = Column::make(__('text_ads.created_at'), 'created_at')->sortable()->searchable(); + $column[] = Column::action(__('text_ads.actions')); + return $column; + } + + public function filters(): array + { + return [ + ]; + } + + #[On('bulkDelete.{tableName}')] + public function bulkDelete(): void + { + $this->js('alert(window.pgBulkActions.get(\'' . $this->tableName . '\'))'); + if($this->checkboxValues){ + foreach ($this->checkboxValues as $id) { + $textAd = TextAd::find($id); + if ($textAd) { + $textAd->delete(); + } + } + $this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface. + } + } + #[On('categoryChanged')] + public function categoryChanged($value,$fieldName, $modelId): void + { + //dd($value,$fieldName, $modelId); + if (in_array($fieldName, ['color'])) { + $this->noUpdated($modelId,$fieldName,$value); + } + } + #[On('onUpdatedEditable')] + public function onUpdatedEditable($id, $field, $value): void + { + if (in_array($field,[ + '' + ]) && $this->canEdit) { + $this->noUpdated($id,$field,$value); + } + } + #[On('onUpdatedToggleable')] + public function onUpdatedToggleable($id, $field, $value): void + { + if (in_array($field,['is_active']) && $this->canEdit) { + $this->noUpdated($id,$field,$value); + } + } + private function noUpdated($id,$field,$value){ + $textAd = TextAd::find($id); + if ($textAd) { + $textAd->{$field} = $value; + $textAd->save(); // 明確觸發 saving + } + $this->notification()->send([ + 'icon' => 'success', + 'title' => $id.'.'.__('text_ads.'.$field).':'.$value, + 'description' => '已經寫入', + ]); + } + + + public function actions(TextAd $row): array + { + $actions = []; + $actions[] = Button::add('text-ad-test') + ->slot('測試') + ->icon('solid-cog') + ->class('inline-flex items-center gap-1 px-3 py-1 rounded bg-amber-200 text-black') + ->dispatchTo('forms.text-ad-test-form', 'openModal', ['id' => $row->id]); + + if ($this->canEdit) { + $actions[]=Button::add('edit') + ->slot(__('text_ads.edit')) + ->icon('solid-pencil-square') + ->class('inline-flex items-center gap-1 px-3 py-1 rounded ') + ->dispatchTo('forms.text-ads-form', 'openModal', ['id' => $row->id]); + } + if($this->canDelect){ + $actions[]=Button::add('delete') + ->slot(__('text_ads.delete')) + ->icon('solid-trash') + ->class('inline-flex items-center gap-1 px-3 py-1 rounded ') + ->dispatchTo('forms.text-ads-form', 'deleteTextAd', ['id' => $row->id]); + } + + return $actions; + } +} diff --git a/app/Models/TextAd.php b/app/Models/TextAd.php new file mode 100644 index 0000000..617baa2 --- /dev/null +++ b/app/Models/TextAd.php @@ -0,0 +1,20 @@ + \App\Enums\TextAdColors::class, + 'is_active' => 'boolean', + ]; +} diff --git a/database/migrations/2025_06_26_114551_create_text_ads_table.php b/database/migrations/2025_06_26_114551_create_text_ads_table.php new file mode 100644 index 0000000..777d8a0 --- /dev/null +++ b/database/migrations/2025_06_26_114551_create_text_ads_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('content')->comment('廣告內容'); + $table->enum('color', ['black','white', 'red', 'green','blue'])->default('black')->comment('顯示顏色'); + $table->integer('duration')->default(1)->comment('播放間隔時間(分鐘)'); + $table->boolean('is_active')->default(true); // 啟用狀態 + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('text_ads'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 87f4cab..db34847 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -13,10 +13,12 @@ class DatabaseSeeder extends Seeder public function run(): void { (new TransferSqliteTableJob('database/User.data', false))->handle(); - //$this->call([ + $this->call([ + TextAdPermissionSeeder::class, + TextAdSeeder::class, // PermissionTableSeeder::class, // CreateAdminUserSeeder::class, // FavoriteSongsSeeder::class, - //]); + ]); } } diff --git a/database/seeders/TextAdPermissionSeeder.php b/database/seeders/TextAdPermissionSeeder.php new file mode 100644 index 0000000..f418ace --- /dev/null +++ b/database/seeders/TextAdPermissionSeeder.php @@ -0,0 +1,33 @@ + $permission]); + } + + // 把權限加給 Admin 角色 + $adminRole = Role::where('name', 'Admin')->first(); + if ($adminRole) { + $adminRole->givePermissionTo($permissions); + } + } +} diff --git a/database/seeders/TextAdSeeder.php b/database/seeders/TextAdSeeder.php new file mode 100644 index 0000000..10ab4a3 --- /dev/null +++ b/database/seeders/TextAdSeeder.php @@ -0,0 +1,51 @@ + '本公司消費方式:包廂費(原價 * 時數 * 折數)+ 清潔費(一次)+ 總金額10%服務費(一次)。', + 'color' => TextAdColors::Red, + ], + [ + 'content' => '一般專案為包廂計費,免收低消及人頭費;如各族群優惠專案為包廂計費、不限人數K歌一口價。', + 'color' => TextAdColors::Red, + ], + [ + 'content' => '工商專案 上班族趕快來報到 不限人數 包廂計費 歡唱K歌一口價★星期一至星期日 上午08:00 ~ 下午17:00 ★歡唱3小時 小包廂 666元 中包廂 999元 大包廂(臺中公園店為中大包廂) 1299元', + 'color' => TextAdColors::Green, + ], + [ + 'content' => '趁年輕 一定要大膽瘋狂不限人數 包廂計費 歡唱K歌一口價★星期一至星期五 上午08:00 ~ 下午17:00 ★歡唱3小時(員林中山店為4小時) 小包廂 333元 中包廂 666元 大包廂(臺中公園店為中大包廂) 999元', + 'color' => TextAdColors::Blue, + ], + [ + 'content' => '重拾當年意氣風發的活力 不輸少年人啦不限人數 包廂計費 歡唱K歌一口價 ★星期一至星期五 上午08:00 ~ 下午17:00 ★歡唱4小時 小包廂 333元 中包廂 666元 大包廂 999元', + 'color' => TextAdColors::Blue, + ], + [ + 'content' => '各分店皆適用 生日快樂!吹個蠟燭 許個心願吧 壽星們 生日開趴的通通站出來 ★當日壽星限定(須出示相關證件供服務人員確認).享有好禮雙重送 ★好禮一:生日紅酒一瓶🍷.超級巨星6吋特製蛋糕.什錦水果一盤.義式冰淇淋.莓果調酒(五選一) ★好禮二:餐飲券600元(可當日折抵使用)', + 'color' => TextAdColors::White, + ], + ]; + foreach ($ads as $ad) { + TextAd::create([ + 'content' => $ad['content'], + 'color' => $ad['color'], + 'duration' => 1, + 'is_active' => true, + ]); + } + } +} diff --git a/resources/lang/zh-tw/text_ads.php b/resources/lang/zh-tw/text_ads.php new file mode 100644 index 0000000..19c42dd --- /dev/null +++ b/resources/lang/zh-tw/text_ads.php @@ -0,0 +1,25 @@ + '文字廣吿管理', + 'list' => '文字廣吿列表', + 'CreateNew' => '新增文字廣吿', + 'EditArtist' => '編輯文字廣吿', + 'ImportData' => '滙入文字廣吿', + 'create_edit' => '新增 / 編輯', + 'create' => '新增', + 'edit' => '編輯', + 'delete' => '刪除', + 'id' => '編號', + 'content' => '廣告內容', + 'color' => '顏色', + 'duration' => '播放間隔時間(分鐘)', + 'is_active' => '是否推播', + 'created_at' =>'建立於', + + + 'actions' => '操作', + 'view' => '查看', + 'submit' => '提交', + 'cancel' => '取消', +]; \ No newline at end of file diff --git a/resources/views/livewire/admin/text-ads.blade.php b/resources/views/livewire/admin/text-ads.blade.php new file mode 100644 index 0000000..112ecea --- /dev/null +++ b/resources/views/livewire/admin/text-ads.blade.php @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/views/livewire/forms/text-ad-test-form.blade.php b/resources/views/livewire/forms/text-ad-test-form.blade.php new file mode 100644 index 0000000..bd83aa5 --- /dev/null +++ b/resources/views/livewire/forms/text-ad-test-form.blade.php @@ -0,0 +1,24 @@ +
+ + +
+
將發送內容:
+
{{ $content }}
+
+ + + +
+ + +
+
+
+
\ No newline at end of file diff --git a/resources/views/livewire/forms/text-ads-form.blade.php b/resources/views/livewire/forms/text-ads-form.blade.php new file mode 100644 index 0000000..27094b8 --- /dev/null +++ b/resources/views/livewire/forms/text-ads-form.blade.php @@ -0,0 +1,24 @@ + +
+ + + + + +
+ + +
+ + +
+
+
+ \ No newline at end of file diff --git a/resources/views/livewire/header/text-ad.blade.php b/resources/views/livewire/header/text-ad.blade.php new file mode 100644 index 0000000..0d02b72 --- /dev/null +++ b/resources/views/livewire/header/text-ad.blade.php @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/resources/views/livewire/layout/admin/sidebar.blade.php b/resources/views/livewire/layout/admin/sidebar.blade.php index 3185cc5..f9be3c9 100644 --- a/resources/views/livewire/layout/admin/sidebar.blade.php +++ b/resources/views/livewire/layout/admin/sidebar.blade.php @@ -14,6 +14,7 @@ new class extends Component ['label' => 'User', 'route' => 'admin.users', 'icon' => 'user-circle', 'permission' => 'user-list'], ['label' => 'Room', 'route' => 'admin.rooms', 'icon' => 'building-library', 'permission' => 'room-list'], ['label' => 'RoomGrid', 'route' => 'admin.room-grids', 'icon' => 'film', 'permission' => 'room-list'], + ['label' => 'TextAd', 'route' => 'admin.text-ads', 'icon' => 'megaphone', 'permission' => 'text-ad-list'], ]; /** diff --git a/routes/console.php b/routes/console.php index 4afade7..fd48762 100644 --- a/routes/console.php +++ b/routes/console.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Artisan; Schedule::command('app:clear-machine-statuses')->dailyAt('12:00'); // 每天凌晨 12:10 執行 Schedule::command('rooms:check-online-status')->everyMinute(); //每分驗証 +Schedule::command('ads:rotate')->everyMinute(); //首次部署或有新增命令時)建立或更新任務排程 Crontab // 檢查是否已有下列 crontab 設定(crontab -e): //分鐘 小時 日 月 星期 指令 diff --git a/routes/web.php b/routes/web.php index e7ea46e..f7023ff 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,4 +26,5 @@ Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () Route::get('/users', function () {return view('livewire.admin.users');})->name('users'); Route::get('/rooms', function () {return view('livewire.admin.rooms');})->name('rooms'); Route::get('/room-grids', function () {return view('livewire.admin.room-grids');})->name('room-grids'); + Route::get('/text-ads', function () {return view('livewire.admin.text-ads');})->name('text-ads'); }); \ No newline at end of file