單機版 v.0.1.1 20250625
文字廣告 介面 文字廣告 訊息推到各包廂功能
This commit is contained in:
parent
35485bf3f8
commit
f8f4f46096
72
app/Console/Commands/RotateTextAd.php
Normal file
72
app/Console/Commands/RotateTextAd.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\TextAd;
|
||||
use App\Models\Room;
|
||||
use App\Services\TcpSocketClient;
|
||||
|
||||
class RotateTextAd extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'ads:rotate';
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '輪播下一則啟用中的文字廣告';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$ads = TextAd::where('is_active', true)->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());
|
||||
}
|
||||
}
|
47
app/Console/Commands/TestMarqueeMessage.php
Normal file
47
app/Console/Commands/TestMarqueeMessage.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\TcpSocketClient;
|
||||
|
||||
class TestMarqueeMessage extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'tcp:marquee
|
||||
{ip : 目標設備 IP}
|
||||
{port : 目標設備 Port}
|
||||
{message : 要顯示的文字}';
|
||||
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '發送跑馬燈訊息到指定的設備';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$ip = $this->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());
|
||||
}
|
||||
}
|
||||
}
|
36
app/Enums/TextAdColors.php
Normal file
36
app/Enums/TextAdColors.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Enums\Traits\HasLabels;
|
||||
|
||||
enum TextAdColors: string
|
||||
{
|
||||
use HasLabels;
|
||||
|
||||
case Black = 'black';
|
||||
case White = 'white';
|
||||
case Red = 'red';
|
||||
case Green = 'green';
|
||||
case Blue = 'blue';
|
||||
|
||||
public function labels(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Black => '黑色',
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
@ -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'))
|
126
app/Livewire/Forms/TextAdsForm.php
Normal file
126
app/Livewire/Forms/TextAdsForm.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
use Livewire\Component;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
use App\Models\TextAd;
|
||||
use App\Enums\TextAdColors;
|
||||
|
||||
class TextAdsForm extends Component
|
||||
{
|
||||
use WireUiActions;
|
||||
|
||||
protected $listeners = ['openModal','closeModal', 'deleteTextAd'];
|
||||
|
||||
public bool $canCreate;
|
||||
public bool $canEdit;
|
||||
public bool $canDelect;
|
||||
|
||||
public bool $showModal = false;
|
||||
public ?int $textAdId = null;
|
||||
|
||||
public array $colorOptions =[];
|
||||
public array $fields = [
|
||||
'content' =>'',
|
||||
'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');
|
||||
}
|
||||
}
|
20
app/Models/TextAd.php
Normal file
20
app/Models/TextAd.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TextAd extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'content',
|
||||
'color',
|
||||
'duration',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'color' => \App\Enums\TextAdColors::class,
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('text_ads', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
@ -15,6 +15,8 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
$this->call([
|
||||
PermissionTableSeeder::class,
|
||||
TextAdPermissionSeeder::class,
|
||||
TextAdSeeder::class,
|
||||
SongCategorySeeder::class,
|
||||
FavoriteSongsSeeder::class,
|
||||
CreateAdminUserSeeder::class,
|
||||
|
33
database/seeders/TextAdPermissionSeeder.php
Normal file
33
database/seeders/TextAdPermissionSeeder.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class TextAdPermissionSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$permissions = [
|
||||
'text-ad-list',
|
||||
'text-ad-create',
|
||||
'text-ad-edit',
|
||||
'text-ad-delete',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
Permission::firstOrCreate(['name' => $permission]);
|
||||
}
|
||||
|
||||
// 把權限加給 Admin 角色
|
||||
$adminRole = Role::where('name', 'Admin')->first();
|
||||
if ($adminRole) {
|
||||
$adminRole->givePermissionTo($permissions);
|
||||
}
|
||||
}
|
||||
}
|
51
database/seeders/TextAdSeeder.php
Normal file
51
database/seeders/TextAdSeeder.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use App\Models\TextAd;
|
||||
use App\Enums\TextAdColors;
|
||||
|
||||
class TextAdSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$ads = [
|
||||
[
|
||||
'content' => '本公司消費方式:包廂費(原價 * 時數 * 折數)+ 清潔費(一次)+ 總金額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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
25
resources/lang/zh-tw/text_ads.php
Normal file
25
resources/lang/zh-tw/text_ads.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'management' => '文字廣吿管理',
|
||||
'list' => '文字廣吿列表',
|
||||
'CreateNew' => '新增文字廣吿',
|
||||
'EditArtist' => '編輯文字廣吿',
|
||||
'ImportData' => '滙入文字廣吿',
|
||||
'create_edit' => '新增 / 編輯',
|
||||
'create' => '新增',
|
||||
'edit' => '編輯',
|
||||
'delete' => '刪除',
|
||||
'id' => '編號',
|
||||
'content' => '廣告內容',
|
||||
'color' => '顏色',
|
||||
'duration' => '播放間隔時間(分鐘)',
|
||||
'is_active' => '是否推播',
|
||||
'created_at' =>'建立於',
|
||||
|
||||
|
||||
'actions' => '操作',
|
||||
'view' => '查看',
|
||||
'submit' => '提交',
|
||||
'cancel' => '取消',
|
||||
];
|
6
resources/views/livewire/admin/text-ads.blade.php
Normal file
6
resources/views/livewire/admin/text-ads.blade.php
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
<x-layouts.admin>
|
||||
<x-wireui:notifications/>
|
||||
<livewire:forms.tables.text-ads-table />
|
||||
<livewire:forms.text-ads-form />
|
||||
</x-layouts.admin>
|
24
resources/views/livewire/forms/text-ads-form.blade.php
Normal file
24
resources/views/livewire/forms/text-ads-form.blade.php
Normal file
@ -0,0 +1,24 @@
|
||||
<x-wireui:modal-card title="{{ $textAdId ? __('text_ads.EditArtist') : __('text_ads.CreateNew') }}" blur wire:model.defer="showModal">
|
||||
<div class="space-y-4">
|
||||
<x-wireui:input label="{{__('text_ads.content')}}" wire:model.defer="fields.content" />
|
||||
<x-wireui:select
|
||||
label="{{__('text_ads.color')}}"
|
||||
wire:model.defer="fields.color"
|
||||
placeholder="{{__('text_ads.select_color')}}"
|
||||
:options="$colorOptions"
|
||||
option-label="name"
|
||||
option-value="value"
|
||||
/>
|
||||
<x-wireui:input label="{{__('text_ads.duration')}}" type="number" min="1" max="60" wire:model.defer="fields.duration" />
|
||||
|
||||
<x-wireui:toggle label="{{__('text_ads.is_active')}}" wire:model.defer="fields.is_active" />
|
||||
</div>
|
||||
|
||||
<x-slot name="footer">
|
||||
<div class="flex justify-between w-full">
|
||||
<x-wireui:button flat label="{{__('text_ads.cancel')}}" wire:click="closeModal" />
|
||||
<x-wireui:button primary label="{{__('text_ads.submit')}}" wire:click="save" />
|
||||
</div>
|
||||
</x-slot>
|
||||
</x-wireui:modal-card>
|
||||
|
8
resources/views/livewire/header/text-ad.blade.php
Normal file
8
resources/views/livewire/header/text-ad.blade.php
Normal file
@ -0,0 +1,8 @@
|
||||
<x-admin.section-header title="{{ __('text_ads.list') }}">
|
||||
<x-wireui:button
|
||||
wire:click="$dispatchTo('forms.text-ads-form', 'openModal')"
|
||||
icon="plus"
|
||||
label="{{ __('text_ads.CreateNew') }}"
|
||||
class="bg-blue-600 text-white"
|
||||
/>
|
||||
</x-admin.section-header>
|
@ -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'],
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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');
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user