diff --git a/app/Livewire/Forms/BroadcastTestForm.php b/app/Livewire/Forms/BroadcastTestForm.php
new file mode 100644
index 0000000..ff62957
--- /dev/null
+++ b/app/Livewire/Forms/BroadcastTestForm.php
@@ -0,0 +1,132 @@
+roomOptions = collect([
+ ['name' => '全部', 'value' => self::ALL_ROOMS_VALUE],
+ ])->merge(
+ Room::where('type', '!=', 'svr')->get()->map(fn ($room) => [
+ 'name' => $room->type->value . $room->name,
+ 'value' => $room->id,
+ ])
+ )->toArray();
+ }
+
+ public function openModal($id = null)
+ {
+ $broadcast = BroadcastTemplate::findOrFail($id);
+ $this->parseVariables($broadcast->content);
+ $this->content = $broadcast->content;
+ $this->showModal = true;
+ }
+
+ public function closeModal()
+ {
+ $this->resetFields();
+ $this->showModal = false;
+ }
+
+ private function parseVariables(string $content): void
+ {
+ preg_match_all('/\*\*(\w+)\*\*/', $content, $matches);
+ $this->variables = [];
+
+ foreach ($matches[1] as $varName) {
+ if (!in_array($varName, self::RESERVED_VARIABLES, true)) {
+ $this->variables[$varName] = '';
+ }
+ }
+ }
+
+ public function send()
+ {
+ $rooms = $this->roomId === self::ALL_ROOMS_VALUE
+ ? Room::where('type', '!=', 'svr')
+ ->where('is_online', 1) // 只發送給在線房間
+ ->get()
+ : Room::where('id', $this->roomId)->get();
+
+ foreach ($rooms as $room) {
+ $message = $this->buildMessage($room);
+
+ try {
+ $client = new TcpSocketClient($room->internal_ip, $room->port);
+ $client->send($this->prefix . $message);
+ } catch (\Throwable $e) {
+ $this->notification()->send([
+ 'icon' => 'error',
+ 'title' => '發送失敗',
+ 'description' => "❌ 房間 {$room->name} 發送失敗:{$e->getMessage()}",
+ ]);
+ continue;
+ }
+ }
+
+ $this->notification()->send([
+ 'icon' => 'success',
+ 'title' => '發送完成',
+ 'description' => $this->roomId === self::ALL_ROOMS_VALUE
+ ? '✅ 已發送至所有包廂'
+ : "✅ 已發送至房間 {$rooms->first()->name}",
+ ]);
+
+ $this->resetFields();
+ $this->showModal = false;
+ $this->dispatch('pg:eventRefresh-text-ads-table');
+ }
+
+ private function buildMessage(Room $room): string
+ {
+ $message = $this->content;
+
+ // 保留變數替換
+ $message = str_replace("**room_name**", $room->type->labels() . '.' . $room->name, $message);
+ $message = str_replace("**date**", now()->format('Y-m-d'), $message);
+ $message = str_replace("**time**", now()->format('H:i'), $message);
+
+ // 自訂變數替換
+ foreach ($this->variables as $key => $value) {
+ $message = str_replace("**{$key}**", $value, $message);
+ }
+
+ return $message;
+ }
+
+ private function resetFields(): void
+ {
+ $this->content = '';
+ $this->roomId = null;
+ $this->variables = [];
+ $this->prefix = '';
+ }
+
+ public function render()
+ {
+ return view('livewire.forms.broadcast-test-form');
+ }
+}
\ No newline at end of file
diff --git a/app/Livewire/Tables/BroadcastTemplateTable.php b/app/Livewire/Tables/BroadcastTemplateTable.php
index 46933ed..a56b333 100644
--- a/app/Livewire/Tables/BroadcastTemplateTable.php
+++ b/app/Livewire/Tables/BroadcastTemplateTable.php
@@ -181,6 +181,11 @@ final class BroadcastTemplateTable extends PowerGridComponent
public function actions(BroadcastTemplate $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.broadcast-test-form', 'openModal', ['id' => $row->id]);
if ($this->canEdit) {
$actions[]=Button::add('edit')
->slot(__('broadcast-templates.edit'))
diff --git a/resources/views/livewire/admin/broadcast-templates.blade.php b/resources/views/livewire/admin/broadcast-templates.blade.php
index b4151ad..4163499 100644
--- a/resources/views/livewire/admin/broadcast-templates.blade.php
+++ b/resources/views/livewire/admin/broadcast-templates.blade.php
@@ -3,4 +3,5 @@