202508131442

文字公告
This commit is contained in:
allen.yan 2025-08-13 14:44:52 +08:00
parent 2a1ed6e188
commit 993a965878
18 changed files with 629 additions and 13 deletions

View File

@ -8,7 +8,6 @@ enum TextAdColors: string
{
use HasLabels;
case Black = 'black';
case White = 'white';
case Red = 'red';
case Green = 'green';
@ -17,7 +16,6 @@ enum TextAdColors: string
public function labels(): string
{
return match ($this) {
self::Black => '黑色',
self::White => '白色',
self::Red => '紅色',
self::Green => '綠色',

View File

@ -0,0 +1,67 @@
<?php
namespace App\Jobs;
use App\Models\BroadcastTemplate;
use App\Services\SqliteExportService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ExportSqliteBroadcastJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 600;
protected ?int $branchId;
public function __construct(?int $branchId = null)
{
$this->branchId = $branchId;
}
public function handle()
{
$sqlitePath = storage_path('app/database/tempBroadcastTemplate.sqlite');
if (!file_exists(dirname($sqlitePath))) {
mkdir(dirname($sqlitePath), 0755, true);
}
if (!file_exists($sqlitePath)) {
file_put_contents($sqlitePath, '');
}
$connectionName = 'tempsqlite_' . md5($sqlitePath . microtime());
config(["database.connections.{$connectionName}" => [
'driver' => 'sqlite',
'database' => $sqlitePath,
'prefix' => '',
]]);
$exporter = new SqliteExportService($connectionName);
$exporter->exportMultiple([
'broadcast_templates' => [
'query' => fn () => DB::table('broadcast_templates'),
'tableSchema' => function (Blueprint $table) {
$table->id();
$table->string('content')->comment('廣告公告');
$table->enum('color', ['white', 'red', 'green','blue'])->default('white')->comment('顯示顏色');
$table->boolean('is_active')->default(true); // 啟用狀態
$table->timestamps();
},
'transformer' => fn ($row) => [
'content' => $row->content,
'color' => $row->color,
'is_active' => $row->is_active,
'created_at' => $row->created_at,
'updated_at' => $row->updated_at,
],
],
]);
SendSqliteFileJob::dispatch($sqlitePath, $this->branchId);
}
}

View File

@ -0,0 +1,136 @@
<?php
namespace App\Livewire\Forms;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
use WireUi\Traits\WireUiActions;
use Illuminate\Validation\Rules\Enum;
use App\Models\BroadcastTemplate;
use App\Enums\TextAdColors;
class BroadcastTemplateForm extends Component
{
use WireUiActions;
protected $listeners = ['openModal','closeModal', 'deleteBroadcast'];
public bool $canCreate;
public bool $canEdit;
public bool $canDelect;
public bool $showModal = false;
public ?int $textId = null;
public array $colorOptions =[];
public array $fields = [
'content' =>'',
'color' => 'white',
'is_active' => true,
];
public function rules()
{
return [
'fields.content' => 'required|string|max:255',
'fields.color' => ['required',new Enum(TextAdColors::class)],
'fields.is_active' => 'required|boolean',
];
}
public function mount()
{
$this->colorOptions = collect(TextAdColors::cases())->map(fn ($color) => [
'name' => $color->labels(),
'value' => $color->value,
])->toArray();
$this->canCreate = Auth::user()?->can('broadcast-edit') ?? false;
$this->canEdit = Auth::user()?->can('broadcast-edit') ?? false;
$this->canDelect = Auth::user()?->can('broadcast-delete') ?? false;
}
public function openModal($id = null)
{
$this->resetFields();
if ($id) {
$text = BroadcastTemplate::findOrFail($id);
$this->textId = $text->id;
$this->fields = $text->only(array_keys($this->fields));
}
$this->showModal = true;
}
public function closeModal()
{
$this->resetFields();
$this->showModal = false;
}
public function save()
{
$validated = $this->validate($this->rules());
if ($this->textId) {
if ($this->canEdit) {
$text = BroadcastTemplate::findOrFail($this->textId);
$text->update($this->fields);
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '文字公告已更新',
]);
}
} else {
if ($this->canCreate) {
$text = BroadcastTemplate::create($this->fields);
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '文字公告已新增',
]);
}
}
$this->resetFields();
$this->showModal = false;
$this->dispatch('pg:eventRefresh-broadcast-table');
}
public function deleteBroadcast($id)
{
if ($this->canDelect) {
BroadcastTemplate::findOrFail($id)->delete();
$this->notification()->send([
'icon' => 'success',
'title' => '成功',
'description' => '文字公告已刪除',
]);
$this->dispatch('pg:eventRefresh-broadcast-table');
}
}
public function resetFields()
{
foreach ($this->fields as $key => $value) {
if ($key == 'color') {
$this->fields[$key] = 'white';
} else if($key == 'is_active'){
$this->fields[$key] = true;
} else {
$this->fields[$key] = '';
}
}
$this->textId = null;
}
public function render()
{
return view('livewire.forms.broadcast-template-form');
}
}

View File

@ -0,0 +1,212 @@
<?php
namespace App\Livewire\Tables;
use App\Enums\TextAdColors;
use App\Models\BroadcastTemplate;
use Illuminate\Support\Str;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Blade;
use Illuminate\Database\Eloquent\Builder;
use PowerComponents\LivewirePowerGrid\Button;
use PowerComponents\LivewirePowerGrid\Column;
use PowerComponents\LivewirePowerGrid\Facades\Filter;
use PowerComponents\LivewirePowerGrid\Facades\PowerGrid;
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 BroadcastTemplateTable extends PowerGridComponent
{
public string $tableName = 'broadcast-table';
public bool $canCreate;
public bool $canEdit;
public bool $canDownload;
public bool $canDelect;
public function boot(): void
{
config(['livewire-powergrid.filter' => 'outside']);
$this->canCreate = Auth::user()?->can('broadcast-create') ?? false;
$this->canEdit = Auth::user()?->can('broadcast-edit') ?? false;
$this->canDownload=Auth::user()?->can('broadcast-delete') ?? false;
$this->canDelect = Auth::user()?->can('broadcast-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();
$header->includeViewOnTop('livewire.headers.broadcast-template');
$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 (<span x-text="window.pgBulkActions.count(\'' . $this->tableName . '\')"></span>)')
->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 BroadcastTemplate::query();
}
public function relationSearch(): array
{
return [];
}
public function fields(): PowerGridFields
{
return PowerGrid::fields()
->add('id')
->add('content')
->add(
'content_short',
fn (BroadcastTemplate $model) =>
'<span title="' . e($model->content) . '">' . e(Str::limit($model->content, 50)) . '</span>'
)
->add('color')
->add('color_str', function (BroadcastTemplate $model) {
if ($this->canEdit) {
return Blade::render(
'<x-select-dropdown type="occurrence" :options=$options :modelId=$modelId :fieldName=$fieldName :selected=$selected/>',
[
'options' => TextAdColors::options(),
'modelId' => intval($model->id),
'fieldName'=>'color',
'selected' => $model->color->value
]
);
}
// 沒有權限就顯示對應的文字
return $model->color->labelPowergridFilter(); // 假設 label() 會回傳顯示文字
} )
->add('is_active')
->add('created_at');
}
public function columns(): array
{
$column=[];
$column[] = Column::make(__('broadcast-templates.id'), 'id');
$column[] = Column::make(__('broadcast-templates.content'), 'content_short', 'broadcast-templates.content')->sortable()->searchable();
$column[] = Column::make(__('broadcast-templates.color'),'color_str', 'text-ads.color')->searchable();
$column[] = Column::make(__('broadcast-templates.is_active'), 'is_active')->toggleable(hasPermission: $this->canEdit, trueLabel: 'yes', falseLabel: 'no');
$column[] = Column::make(__('broadcast-templates.created_at'), 'created_at')->sortable()->searchable();
$column[] = Column::action(__('broadcast-templates.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) {
$template = BroadcastTemplate::find($id);
if ($template) {
$template->delete();
}
}
$this->js('window.pgBulkActions.clearAll()'); // clear the count on the interface.
}
}
#[On('selectChanged')]
public function selectChanged($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){
$template = BroadcastTemplate::find($id);
if ($template) {
$template->{$field} = $value;
$template->save(); // 明確觸發 saving
}
$this->notification()->send([
'icon' => 'success',
'title' => $id.'.'.__('broadcast.'.$field).':'.$value,
'description' => '已經寫入',
]);
}
public function actions(BroadcastTemplate $row): array
{
$actions = [];
if ($this->canEdit) {
$actions[]=Button::add('edit')
->slot(__('broadcast-templates.edit'))
->icon('solid-pencil-square')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('forms.broadcast-template-form', 'openModal', ['id' => $row->id]);
}
if($this->canDelect){
$actions[]=Button::add('delete')
->slot(__('broadcast-templates.delete'))
->icon('solid-trash')
->class('inline-flex items-center gap-1 px-3 py-1 rounded ')
->dispatchTo('forms.broadcast-template-form', 'deleteBroadcast', ['id' => $row->id]);
}
return $actions;
}
/*
public function actionRules($row): array
{
return [
// Hide button edit for ID 1
Rule::button('edit')
->when(fn($row) => $row->id === 1)
->hide(),
];
}
*/
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class BroadcastTemplate extends Model
{
protected $fillable = [
'content',
'color',
'is_active',
];
protected $casts = [
'color' => \App\Enums\TextAdColors::class,
'is_active' => 'boolean',
];
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class TextBroadcast extends Model
{
protected $fillable = ['template_id', 'content', 'target_type'];
public function rooms()
{
return $this->belongsToMany(Room::class, 'text_broadcast_room');
}
public function template()
{
return $this->belongsTo(BroadcastTemplate::class);
}
}

View File

@ -14,7 +14,7 @@ return new class extends Migration
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->enum('color', ['white', 'red', 'green','blue'])->default('white')->comment('顯示顏色');
$table->integer('duration')->default(1)->comment('播放間隔時間(分鐘)');
$table->boolean('is_active')->default(true); // 啟用狀態
$table->timestamps();

View File

@ -0,0 +1,44 @@
<?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('broadcast_templates', function (Blueprint $table) {
$table->id();
$table->text('content')->comment('公告內容'); // e.g., "親愛的 **name**,歡迎光臨!"
$table->enum('color', ['white', 'red', 'green','blue'])->default('white')->comment('顯示顏色');
$table->boolean('is_active')->default(true); // 啟用狀態
$table->timestamps();
});
Schema::create('text_broadcasts', function (Blueprint $table) {
$table->id();
$table->foreignId('template_id')->nullable()->constrained('broadcast_templates')->nullOnDelete();
$table->text('content'); // Final content with variables replaced
$table->enum('target_type', ['all', 'rooms'])->default('all');
$table->timestamps();
});
Schema::create('text_broadcast_room', function (Blueprint $table) {
$table->id();
$table->foreignId('text_broadcast_id')->constrained()->onDelete('cascade');
$table->foreignId('room_id')->constrained()->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('text_broadcast_room');
Schema::dropIfExists('text_broadcasts');
Schema::dropIfExists('broadcast_templates');
}
};

View File

@ -0,0 +1,58 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\BroadcastTemplate;
use App\Enums\TextAdColors;
class BroadcastTemplateSeeder extends Seeder
{
public function run(): void
{
$txs = [
[
'content' => '親愛的 **name**,祝你生日快樂!',
'color' => TextAdColors::White,
],
[
'content' => '**room_name** 歡迎您蒞臨本店,祝您有美好的一天!',
'color' => TextAdColors::White,
],
[
'content' => '**name** 拾獲您的證件,請至櫃台領取',
'color' => TextAdColors::White,
],
[
'content' => '**name** 請移動你的愛車,謝謝!!',
'color' => TextAdColors::White,
],
[
'content' => '**name** 祝你生日快樂!!',
'color' => TextAdColors::White,
],
[
'content' => '**name** 櫃台有您訪客!!',
'color' => TextAdColors::White,
],
[
'content' => '**room_name** 警察臨檢不便之處敬請原諒!!',
'color' => TextAdColors::White,
],
[
'content' => '**name** 您的愛車被拖吊了!!',
'color' => TextAdColors::White,
],
[
'content' => '**name** 請移動您的愛車,謝謝',
'color' => TextAdColors::White,
],
];
foreach ($txs as $tx) {
BroadcastTemplate::create([
'content' => $tx['content'],
'color' => $tx['color'],
'is_active' => true,
]);
}
}
}

View File

@ -20,6 +20,7 @@ class DatabaseSeeder extends Seeder
CreateAdminUserSeeder::class,
TextAdPermissionSeeder::class,
TextAdSeeder::class,
BroadcastTemplateSeeder::class,
]);
}
}

View File

@ -18,6 +18,10 @@ class TextAdPermissionSeeder extends Seeder
'text-ad-create',
'text-ad-edit',
'text-ad-delete',
'broadcast-list',
'broadcast-create',
'broadcast-edit',
'broadcast-delete',
];
foreach ($permissions as $permission) {

View File

@ -0,0 +1,27 @@
<?php
return [
'management' => '文字公告管理',
'list' => '文字公告列表',
'CreateNew' => '新增文字公告',
'EditArtist' => '編輯文字公告',
'ImportData' => '滙入文字公告',
'create_edit' => '新增 / 編輯',
'create' => '新增',
'edit' => '編輯',
'delete' => '刪除',
'id' => '編號',
'name' => '主題',
'content' => '內容',
'description' => '描述',
'color' => '顏色',
'duration' => '播放間隔時間(分鐘)',
'is_active' => '是否推播',
'created_at' =>'建立於',
'actions' => '操作',
'view' => '查看',
'submit' => '提交',
'cancel' => '取消',
];

View File

@ -0,0 +1,6 @@
<x-layouts.admin>
<x-wireui:notifications/>
<livewire:tables.broadcast-template-table />
<livewire:forms.broadcast-template-form />
</x-layouts.admin>

View File

@ -0,0 +1,23 @@
<x-wireui:modal-card title="{{ $textId ? __('broadcast-templates.EditArtist') : __('broadcast-templates.CreateNew') }}" blur wire:model.defer="showModal">
<div class="space-y-4">
<x-wireui:input label="{{__('broadcast-templates.content')}}" wire:model.defer="fields.content" />
<x-wireui:select
label="{{__('broadcast-templates.color')}}"
wire:model.defer="fields.color"
placeholder="{{__('broadcast-templates.select_color')}}"
:options="$colorOptions"
option-label="name"
option-value="value"
/>
<x-wireui:toggle label="{{__('broadcast-templates.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="{{__('broadcast-templates.cancel')}}" wire:click="closeModal" />
<x-wireui:button primary label="{{__('broadcast-templates.submit')}}" wire:click="save" />
</div>
</x-slot>
</x-wireui:modal-card>

View File

@ -0,0 +1,10 @@
<x-admin.section-header title="{{ __('broadcast-templates.list') }}">
@if ($canCreate)
<x-wireui:button
wire:click="$dispatchTo('forms.broadcast-template-form', 'openModal')"
icon="plus"
label="{{ __('broadcast-templates.CreateNew') }}"
class="bg-blue-600 text-white"
/>
@endif
</x-admin.section-header>

View File

@ -1,10 +0,0 @@
<x-admin.section-header title="{{ __('broadcasts.list') }}">
@if ($canCreate)
<x-wireui:button
wire:click="$dispatchTo('forms.broadcast-form', 'openModal')"
icon="plus"
label="{{ __('broadcasts.CreateNew') }}"
class="bg-blue-600 text-white"
/>
@endif
</x-admin.section-header>

View File

@ -17,6 +17,7 @@ new class extends Component
['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'],
['label' => 'BroadcastTemplate', 'route' => 'admin.broadcast-templates', 'icon' => 'megaphone', 'permission' => 'broadcast-list'],
];
/**

View File

@ -31,4 +31,5 @@ Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function ()
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');
Route::get('/broadcast-templates', function () {return view('livewire.admin.broadcast-templates');})->name('broadcast-templates');
});