diff --git a/app/Console/Commands/RotateTextAd.php b/app/Console/Commands/RotateTextAd.php
new file mode 100644
index 0000000..5492837
--- /dev/null
+++ b/app/Console/Commands/RotateTextAd.php
@@ -0,0 +1,72 @@
+orderBy('id')->get();
+
+ if ($ads->isEmpty()) {
+ $this->info('❌ 沒有啟用中的廣告。');
+ return;
+ }
+
+ $nextTime = Cache::get('text_ad_next_time');
+
+ // 還沒到下一次播放時間就跳出
+ if ($nextTime && now()->lt($nextTime)) {
+ $this->info("⏳ 尚未到下一次播放時間(下次播放:{$nextTime})");
+ return;
+ }
+
+ $index = Cache::get('text_ad_current_index', 0) % $ads->count();
+ $ad = $ads[$index];
+
+ $rooms = Room::where('type', '!=', 'svr')
+ ->where('is_online', 1)
+ ->get(['internal_ip', 'port']);
+ // 📨 發送 TCP 廣告
+ try {
+ $prefix = "全部({$ad->color->labels()})-";
+ $content = $prefix . $ad->content;
+ foreach ($rooms as $room) {
+ $client = new TcpSocketClient($room->internal_ip, $room->port);
+ $response = $client->send($content);
+
+ $this->info("✅ 已推送廣告 #{$ad->id} 至 {$room->internal_ip}:{$room->port}");
+ }
+ } catch (\Throwable $e) {
+ $this->error("❌ 發送失敗:{$room->internal_ip}:{$room->port} - " . $e->getMessage());
+ }
+
+ // 🕒 更新下次播放時間
+ Cache::put('text_ad_next_time', now()->addMinutes($ad->duration));
+
+ // 📌 更新下次播放 index
+ Cache::put('text_ad_current_index', ($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/Tables/TextAdTable.php b/app/Livewire/Forms/Tables/TextAdsTable.php
similarity index 97%
rename from app/Livewire/Forms/Tables/TextAdTable.php
rename to app/Livewire/Forms/Tables/TextAdsTable.php
index 3fdd52d..d8ab476 100644
--- a/app/Livewire/Forms/Tables/TextAdTable.php
+++ b/app/Livewire/Forms/Tables/TextAdsTable.php
@@ -17,13 +17,14 @@ use PowerComponents\LivewirePowerGrid\PowerGridFields;
use PowerComponents\LivewirePowerGrid\PowerGridComponent;
use PowerComponents\LivewirePowerGrid\Traits\WithExport;
use PowerComponents\LivewirePowerGrid\Components\SetUp\Exportable;
+use PowerComponents\LivewirePowerGrid\Facades\Rule;
use Livewire\Attributes\On;
use WireUi\Traits\WireUiActions;
-final class TextAdTable extends PowerGridComponent
+final class TextAdsTable extends PowerGridComponent
{
use WithExport, WireUiActions;
- public string $tableName = 'text-ad-table';
+ public string $tableName = 'text-ads-table';
public bool $canCreate;
public bool $canEdit;
@@ -188,6 +189,8 @@ final class TextAdTable extends PowerGridComponent
public function actions(TextAd $row): array
{
$actions = [];
+
+
if ($this->canEdit) {
$actions[]=Button::add('edit')
->slot(__('text_ads.edit'))
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/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 9fda0e0..35336a3 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -15,6 +15,8 @@ class DatabaseSeeder extends Seeder
{
$this->call([
PermissionTableSeeder::class,
+ TextAdPermissionSeeder::class,
+ TextAdSeeder::class,
SongCategorySeeder::class,
FavoriteSongsSeeder::class,
CreateAdminUserSeeder::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..63617fa
--- /dev/null
+++ b/resources/views/livewire/admin/text-ads.blade.php
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ 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 1ec382d..d9b649c 100644
--- a/resources/views/livewire/layout/admin/sidebar.blade.php
+++ b/resources/views/livewire/layout/admin/sidebar.blade.php
@@ -17,6 +17,7 @@ new class extends Component
['label' => 'Branche', 'route' => 'admin.branches', 'icon' => 'building-library', 'permission' => 'room-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 eb1d2b7..7649c6c 100644
--- a/routes/console.php
+++ b/routes/console.php
@@ -6,4 +6,5 @@ use Illuminate\Support\Facades\Schedule;
Schedule::command('app:clear-machine-statuses')->dailyAt('12:00'); // 每天凌晨 12:10 執行
Schedule::command('rooms:check-online-status')->everyMinute(); //每分驗証
+Schedule::command('ads:rotate')->everyMinute();
diff --git a/routes/web.php b/routes/web.php
index 380da98..2f52d0c 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -31,4 +31,5 @@ Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function ()
Route::get('/branches', function () {return view('livewire.admin.branches');})->name('branches');
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