From a0c2ab320767423c0be83d99573ffd47721eeca4 Mon Sep 17 00:00:00 2001 From: terrylee Date: Wed, 15 Jul 2026 12:37:35 +0800 Subject: [PATCH 01/16] =?UTF-8?q?feat(app-ui):=20APP=20UI=E5=85=83?= =?UTF-8?q?=E7=B4=A0=E8=A8=AD=E5=AE=9A=20Phase=201=20=E2=80=94=20=E5=BE=8C?= =?UTF-8?q?=E5=8F=B0=E4=B8=89=E9=A0=81(=E5=85=83=E7=B4=A0/=E7=B5=84?= =?UTF-8?q?=E5=90=88/=E6=A9=9F=E5=8F=B0=E7=B6=81=E5=AE=9A)+=20B014=20UISet?= =?UTF-8?q?=20=E4=B8=8B=E7=99=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ui_elements/ui_bundles/ui_bundle_items 三表 + Models(TenantScoped) - config/ui_parts.php 部位定義(Phase 1 啟用 A1/A2/A3 滿版) - UiElementController 三頁 CRUD + 圖片上傳(ImageHandler storeAsWebp) - 機台綁定存 machines.settings['ui_bundle_id'] - B014 getSettings 新增 UISet(部位→絕對圖URL,mirror B005) - 解除 sidebar APP管理 註解 + can:menu.app + 權限白名單 - zh_TW/en 翻譯 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Admin/PermissionController.php | 1 + .../Controllers/Admin/UiElementController.php | 206 ++++++++++++++++++ .../Api/V1/App/MachineController.php | 51 +++++ app/Models/System/UiBundle.php | 33 +++ app/Models/System/UiBundleItem.php | 28 +++ app/Models/System/UiElement.php | 46 ++++ config/ui_parts.php | 34 +++ ..._07_15_120000_create_ui_elements_table.php | 30 +++ ...6_07_15_120001_create_ui_bundles_table.php | 26 +++ ...15_120002_create_ui_bundle_items_table.php | 30 +++ lang/en.json | 16 ++ lang/zh_TW.json | 16 ++ resources/views/admin/app/_tabs.blade.php | 40 ++++ .../admin/app/ui-bundles/content.blade.php | 52 +++++ .../admin/app/ui-bundles/index.blade.php | 75 +++++++ .../admin/app/ui-elements/index.blade.php | 94 ++++++++ .../admin/app/ui-machines/index.blade.php | 55 +++++ .../layouts/partials/sidebar-menu.blade.php | 6 +- routes/web.php | 21 +- 19 files changed, 855 insertions(+), 5 deletions(-) create mode 100644 app/Http/Controllers/Admin/UiElementController.php create mode 100644 app/Models/System/UiBundle.php create mode 100644 app/Models/System/UiBundleItem.php create mode 100644 app/Models/System/UiElement.php create mode 100644 config/ui_parts.php create mode 100644 database/migrations/2026_07_15_120000_create_ui_elements_table.php create mode 100644 database/migrations/2026_07_15_120001_create_ui_bundles_table.php create mode 100644 database/migrations/2026_07_15_120002_create_ui_bundle_items_table.php create mode 100644 resources/views/admin/app/_tabs.blade.php create mode 100644 resources/views/admin/app/ui-bundles/content.blade.php create mode 100644 resources/views/admin/app/ui-bundles/index.blade.php create mode 100644 resources/views/admin/app/ui-elements/index.blade.php create mode 100644 resources/views/admin/app/ui-machines/index.blade.php diff --git a/app/Http/Controllers/Admin/PermissionController.php b/app/Http/Controllers/Admin/PermissionController.php index 1fe5d3a..68a1291 100644 --- a/app/Http/Controllers/Admin/PermissionController.php +++ b/app/Http/Controllers/Admin/PermissionController.php @@ -670,6 +670,7 @@ class PermissionController extends Controller $activeModules = [ 'menu.machines', + 'menu.app', 'menu.warehouses', 'menu.sales', 'menu.analysis', diff --git a/app/Http/Controllers/Admin/UiElementController.php b/app/Http/Controllers/Admin/UiElementController.php new file mode 100644 index 0000000..69030de --- /dev/null +++ b/app/Http/Controllers/Admin/UiElementController.php @@ -0,0 +1,206 @@ + $p['active'] ?? false); + } + + // ================================================================== + // 頁籤一:UI 元素列表(素材庫) + // ================================================================== + + public function uiElements() + { + $elements = UiElement::orderByDesc('id')->get(); + + return view('admin.app.ui-elements.index', [ + 'elements' => $elements, + 'parts' => $this->activeParts(), + ]); + } + + public function storeElement(Request $request) + { + $activeKeys = array_keys($this->activeParts()); + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'part_key' => 'required|string|in:' . implode(',', $activeKeys), + 'file' => 'required|image|mimes:jpeg,png,jpg,gif,webp|max:10240', + ]); + + $part = config("ui_parts.parts.{$validated['part_key']}"); + + // 依部位建議尺寸做居中裁切,存為 webp + $path = $this->storeAsWebp( + $request->file('file'), + 'ui_elements', + 80, + $part['width'] ?? null, + $part['height'] ?? null + ); + + UiElement::create([ + 'part_key' => $validated['part_key'], + 'name' => $validated['name'], + 'type' => 'image', + 'image_url' => Storage::disk('public')->url($path), + 'is_active' => true, + ]); + + return redirect()->route('admin.app.ui-elements')->with('success', __('UI element created')); + } + + public function destroyElement(UiElement $uiElement) + { + // 刪除實體檔(以 URL 反推相對路徑) + if ($uiElement->image_url) { + $relative = str_replace(Storage::disk('public')->url(''), '', $uiElement->image_url); + Storage::disk('public')->delete($relative); + } + + $uiElement->delete(); + + return redirect()->route('admin.app.ui-elements')->with('success', __('UI element deleted')); + } + + // ================================================================== + // 頁籤二:UI 組合編輯 + // ================================================================== + + public function bundles() + { + $bundles = UiBundle::orderBy('id')->get(); + + return view('admin.app.ui-bundles.index', compact('bundles')); + } + + public function storeBundle(Request $request) + { + $validated = $request->validate(['name' => 'required|string|max:255']); + UiBundle::create(['name' => $validated['name']]); + + return redirect()->route('admin.app.ui-bundles')->with('success', __('UI bundle created')); + } + + public function updateBundle(Request $request, UiBundle $uiBundle) + { + $validated = $request->validate(['name' => 'required|string|max:255']); + $uiBundle->update(['name' => $validated['name']]); + + return redirect()->route('admin.app.ui-bundles')->with('success', __('Saved')); + } + + public function destroyBundle(UiBundle $uiBundle) + { + $uiBundle->delete(); + + return redirect()->route('admin.app.ui-bundles')->with('success', __('UI bundle deleted')); + } + + /** + * 組合內容設定頁:每個部位一個下拉,選對應類別的 UI 元素。 + */ + public function editBundleContent(UiBundle $uiBundle) + { + $parts = $this->activeParts(); + + // 每個部位可選的元素(依 part_key 過濾) + $elementsByPart = []; + foreach (array_keys($parts) as $key) { + $elementsByPart[$key] = UiElement::active()->where('part_key', $key)->orderBy('name')->get(); + } + + return view('admin.app.ui-bundles.content', [ + 'bundle' => $uiBundle, + 'parts' => $parts, + 'elementsByPart' => $elementsByPart, + 'itemsByPart' => $uiBundle->itemsByPart(), + ]); + } + + public function updateBundleContent(Request $request, UiBundle $uiBundle) + { + $parts = $this->activeParts(); + $input = $request->input('parts', []); + + foreach (array_keys($parts) as $key) { + $elementId = $input[$key] ?? null; + $elementId = ($elementId === null || $elementId === '' || trim((string) $elementId) === '') ? null : (int) $elementId; + + if ($elementId === null) { + // 未設定 → 移除該部位設定 + $uiBundle->items()->where('part_key', $key)->delete(); + continue; + } + + // 只接受屬於該部位的元素 + $valid = UiElement::where('id', $elementId)->where('part_key', $key)->exists(); + if (!$valid) { + continue; + } + + UiBundleItem::updateOrCreate( + ['ui_bundle_id' => $uiBundle->id, 'part_key' => $key], + ['ui_element_id' => $elementId, 'color_value' => null] + ); + } + + return redirect()->route('admin.app.ui-bundles.content', $uiBundle)->with('success', __('Saved')); + } + + // ================================================================== + // 頁籤三:機台設定(綁定 UI 組合) + // ================================================================== + + public function machines(Request $request) + { + $machines = Machine::orderBy('name')->paginate(20); + $bundles = UiBundle::orderBy('name')->get(); + + // 現有綁定名稱對照 + $bundleNames = $bundles->pluck('name', 'id'); + + return view('admin.app.ui-machines.index', [ + 'machines' => $machines, + 'bundles' => $bundles, + 'bundleNames' => $bundleNames, + ]); + } + + public function updateMachineBundle(Request $request, Machine $machine) + { + $validated = $request->validate([ + 'ui_bundle_id' => 'nullable|integer|exists:ui_bundles,id', + ]); + + $settings = $machine->settings ?? []; + if (empty($validated['ui_bundle_id'])) { + unset($settings['ui_bundle_id']); + } else { + $settings['ui_bundle_id'] = (int) $validated['ui_bundle_id']; + } + $machine->settings = $settings; + $machine->save(); + + return redirect()->route('admin.app.ui-machines')->with('success', __('Saved')); + } +} diff --git a/app/Http/Controllers/Api/V1/App/MachineController.php b/app/Http/Controllers/Api/V1/App/MachineController.php index 731e68e..073c3a4 100644 --- a/app/Http/Controllers/Api/V1/App/MachineController.php +++ b/app/Http/Controllers/Api/V1/App/MachineController.php @@ -546,6 +546,10 @@ class MachineController extends Controller 'SpringSlot51_60' => (bool) $machine->is_spring_slot_51_60, ]; + // 5-7 UISet:後台「APP UI 元素設定」下發(部位 part_key → 圖片 URL / 色值)。 + // 機台端依 part_key 對照本地 view 套用圖片;未綁定或未設定的部位不出現於此。 + $data['UISet'] = $this->buildUiSet($s); + return response()->json([ 'success' => true, 'code' => 200, @@ -553,6 +557,53 @@ class MachineController extends Controller ]); } + /** + * 組出機台綁定的 UI 組合下發內容(part_key => 圖片絕對 URL 或色值)。 + * 僅輸出 config/ui_parts.php 中 active=true 的部位。 + */ + private function buildUiSet(array $settings): object + { + $bundleId = $settings['ui_bundle_id'] ?? null; + if (!$bundleId) { + return (object) []; + } + + $bundle = \App\Models\System\UiBundle::withoutGlobalScopes() + ->with(['items.element']) + ->find($bundleId); + + if (!$bundle) { + return (object) []; + } + + $activeParts = array_filter(config('ui_parts.parts', []), fn($p) => $p['active'] ?? false); + $itemsByPart = $bundle->itemsByPart(); + + $map = []; + foreach ($activeParts as $key => $part) { + $item = $itemsByPart->get($key); + if (!$item) { + continue; + } + + $type = $part['type'] ?? 'image'; + if ($type === 'color') { + if ($item->color_value) { + $map[$key] = $item->color_value; + } + continue; + } + + // image 型:輸出絕對 URL(比照 B005 廣告 t070v04 補完) + $url = optional($item->element)->image_url; + if ($url) { + $map[$key] = str_starts_with($url, 'http') ? $url : asset($url); + } + } + + return (object) $map; + } + /** * B016: Update Machine System Settings (Write-back from machine console) * 機台主控台「系統設定」由系統方 (identity=system) 編輯後回寫雲端。 diff --git a/app/Models/System/UiBundle.php b/app/Models/System/UiBundle.php new file mode 100644 index 0000000..71f011f --- /dev/null +++ b/app/Models/System/UiBundle.php @@ -0,0 +1,33 @@ +hasMany(UiBundleItem::class); + } + + /** + * 以 part_key 為鍵的內容對照(方便下發與表單回填)。 + */ + public function itemsByPart() + { + return $this->items->keyBy('part_key'); + } +} diff --git a/app/Models/System/UiBundleItem.php b/app/Models/System/UiBundleItem.php new file mode 100644 index 0000000..091ea33 --- /dev/null +++ b/app/Models/System/UiBundleItem.php @@ -0,0 +1,28 @@ +belongsTo(UiBundle::class, 'ui_bundle_id'); + } + + public function element() + { + return $this->belongsTo(UiElement::class, 'ui_element_id'); + } +} diff --git a/app/Models/System/UiElement.php b/app/Models/System/UiElement.php new file mode 100644 index 0000000..3cd04a1 --- /dev/null +++ b/app/Models/System/UiElement.php @@ -0,0 +1,46 @@ + 'boolean', + ]; + + /** + * 部位定義(來自 config/ui_parts.php)。 + */ + public function getPartAttribute(): ?array + { + return config("ui_parts.parts.{$this->part_key}"); + } + + /** + * 部位顯示名稱。 + */ + public function getPartLabelAttribute(): string + { + return config("ui_parts.parts.{$this->part_key}.label", $this->part_key); + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } +} diff --git a/config/ui_parts.php b/config/ui_parts.php new file mode 100644 index 0000000..d4ab65d --- /dev/null +++ b/config/ui_parts.php @@ -0,0 +1,34 @@ + [ + // === A 系列:滿版 / 大圖背景 === + 'A1' => ['label' => 'A1 - 頁面跳轉中', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 1920, 'active' => true], + 'A2' => ['label' => 'A2 - 機台自檢中', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 1920, 'active' => true], + 'A3' => ['label' => 'A3 - 機台異常鎖定', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 1920, 'active' => true], + + // --- 以下為舊後台既有部位,Phase 2+ 逐步啟用(active=false 時後台不顯示、不下發)--- + 'A4' => ['label' => 'A4 - 對話框標頭', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 192, 'active' => false], + 'A7' => ['label' => 'A7 - 購買頁廣告底圖', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 620, 'active' => false], + 'A8' => ['label' => 'A8 - 來店廣告底圖', 'group' => 'fullscreen', 'type' => 'image', 'width' => 864, 'height' => 648, 'active' => false], + ], +]; diff --git a/database/migrations/2026_07_15_120000_create_ui_elements_table.php b/database/migrations/2026_07_15_120000_create_ui_elements_table.php new file mode 100644 index 0000000..f10c21f --- /dev/null +++ b/database/migrations/2026_07_15_120000_create_ui_elements_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->onDelete('cascade'); + $table->string('part_key')->index()->comment('部位代碼,對應 config/ui_parts.php,如 A1/A2/A3'); + $table->string('name'); + $table->string('type')->default('image')->comment('image / color'); + $table->string('image_url')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('ui_elements'); + } +}; diff --git a/database/migrations/2026_07_15_120001_create_ui_bundles_table.php b/database/migrations/2026_07_15_120001_create_ui_bundles_table.php new file mode 100644 index 0000000..1e837b9 --- /dev/null +++ b/database/migrations/2026_07_15_120001_create_ui_bundles_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('company_id')->nullable()->constrained()->onDelete('cascade'); + $table->string('name'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('ui_bundles'); + } +}; diff --git a/database/migrations/2026_07_15_120002_create_ui_bundle_items_table.php b/database/migrations/2026_07_15_120002_create_ui_bundle_items_table.php new file mode 100644 index 0000000..f2ded79 --- /dev/null +++ b/database/migrations/2026_07_15_120002_create_ui_bundle_items_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('ui_bundle_id')->constrained()->onDelete('cascade'); + $table->string('part_key'); + $table->foreignId('ui_element_id')->nullable()->constrained()->onDelete('set null'); + $table->string('color_value')->nullable()->comment('色彩型部位(C/T 系列)用,如 #RRGGBBAA'); + $table->timestamps(); + + $table->unique(['ui_bundle_id', 'part_key']); + }); + } + + public function down(): void + { + Schema::dropIfExists('ui_bundle_items'); + } +}; diff --git a/lang/en.json b/lang/en.json index ebb8007..dfe40dd 100644 --- a/lang/en.json +++ b/lang/en.json @@ -2337,6 +2337,22 @@ "Type": "Type", "Type to search or leave blank for system defaults.": "Type to search or leave blank for system defaults.", "UI Elements": "UI Elements", + "UI Bundles": "UI Bundles", + "Machine Binding": "Machine Binding", + "UI element created": "UI element created", + "UI element deleted": "UI element deleted", + "UI bundle created": "UI bundle created", + "UI bundle deleted": "UI bundle deleted", + "No data": "No data", + "Back": "Back", + "Rename": "Rename", + "Content Settings": "Content Settings", + "Not set": "Not set", + "No image selected": "No image selected", + "Current Bundle": "Current Bundle", + "Bind UI Bundle": "Bind UI Bundle", + "Image will be center-cropped to the category size.": "Image will be center-cropped to the category size.", + "(deleted)": "(deleted)", "Unable to read the image archive": "Unable to read the image archive", "Unassigned": "Unassigned", "Unassigned / No Change": "Unassigned / No Change", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 569580e..ed3f24b 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -2338,6 +2338,22 @@ "Type": "類型", "Type to search or leave blank for system defaults.": "輸入關鍵字搜尋,或留空以使用系統預設。", "UI Elements": "UI元素", + "UI Bundles": "UI組合編輯", + "Machine Binding": "機台設定", + "UI element created": "UI 元素已新增", + "UI element deleted": "UI 元素已刪除", + "UI bundle created": "UI 組合已新增", + "UI bundle deleted": "UI 組合已刪除", + "No data": "無資料", + "Back": "返回", + "Rename": "重新命名", + "Content Settings": "內容設定", + "Not set": "未設定", + "No image selected": "尚未選擇圖片", + "Current Bundle": "當前組合", + "Bind UI Bundle": "綁定 UI 組合", + "Image will be center-cropped to the category size.": "圖片會依類別建議尺寸置中裁切。", + "(deleted)": "(已刪除)", "Unable to read the image archive": "無法讀取圖片壓縮檔", "Unassigned": "未指派", "Unassigned / No Change": "未指派 / 不變動", diff --git a/resources/views/admin/app/_tabs.blade.php b/resources/views/admin/app/_tabs.blade.php new file mode 100644 index 0000000..1fb5584 --- /dev/null +++ b/resources/views/admin/app/_tabs.blade.php @@ -0,0 +1,40 @@ +{{-- APP UI元素設定 共用頁籤列。傳入 $active = elements | bundles | machines --}} +@php + $tabs = [ + 'elements' => ['label' => __('UI Elements'), 'route' => 'admin.app.ui-elements'], + 'bundles' => ['label' => __('UI Bundles'), 'route' => 'admin.app.ui-bundles'], + 'machines' => ['label' => __('Machine Binding'), 'route' => 'admin.app.ui-machines'], + ]; +@endphp +
+
+
+

{{ __('APP Management') }}

+

{{ __('UI Elements') }}

+
+
+
+ @foreach($tabs as $key => $tab) + + {{ $tab['label'] }} + + @endforeach +
+
+ +@if(session('success')) +
+ {{ session('success') }} +
+@endif +@if($errors->any()) +
+
    + @foreach($errors->all() as $error)
  • {{ $error }}
  • @endforeach +
+
+@endif diff --git a/resources/views/admin/app/ui-bundles/content.blade.php b/resources/views/admin/app/ui-bundles/content.blade.php new file mode 100644 index 0000000..2896bd1 --- /dev/null +++ b/resources/views/admin/app/ui-bundles/content.blade.php @@ -0,0 +1,52 @@ +@extends('layouts.admin') + +@section('content') +
+ @include('admin.app._tabs', ['active' => 'bundles']) + +
+ ← {{ __('Back') }} + / +

{{ __('Content Settings') }}:{{ $bundle->name }}

+
+ +
+ @csrf @method('PUT') + +
+ @foreach($parts as $key => $part) + @php + $currentId = optional($itemsByPart->get($key))->ui_element_id; + $currentThumb = optional(optional($itemsByPart->get($key))->element)->image_url ?? ''; + @endphp +
+ + +
+ +

{{ __('No image selected') }}

+
+
+ @endforeach +
+ +
+ +
+
+
+@endsection diff --git a/resources/views/admin/app/ui-bundles/index.blade.php b/resources/views/admin/app/ui-bundles/index.blade.php new file mode 100644 index 0000000..aefd93b --- /dev/null +++ b/resources/views/admin/app/ui-bundles/index.blade.php @@ -0,0 +1,75 @@ +@extends('layouts.admin') + +@section('content') +
+ @include('admin.app._tabs', ['active' => 'bundles']) + +
+ +
+ +
+ + + + + + + + + + @forelse($bundles as $bundle) + + + + + + @empty + + @endforelse + +
{{ __('ID') }}{{ __('Name') }}{{ __('Actions') }}
{{ $bundle->id }} + {{ $bundle->name }} +
+ @csrf @method('PUT') + + + +
+
+ + {{ __('Content Settings') }} +
+ @csrf @method('DELETE') + +
+
{{ __('No data') }}
+
+ + {{-- 新增組合 Modal --}} + +
+@endsection diff --git a/resources/views/admin/app/ui-elements/index.blade.php b/resources/views/admin/app/ui-elements/index.blade.php new file mode 100644 index 0000000..1f9153a --- /dev/null +++ b/resources/views/admin/app/ui-elements/index.blade.php @@ -0,0 +1,94 @@ +@extends('layouts.admin') + +@section('content') +
+ @include('admin.app._tabs', ['active' => 'elements']) + +
+ +
+ +
+ + + + + + + + + + + + @forelse($elements as $element) + + + + + + + + @empty + + @endforelse + +
{{ __('Preview') }}{{ __('Name') }}{{ __('Category') }}{{ __('Created At') }}{{ __('Actions') }}
+ @if($element->image_url) + {{ $element->name }} + @else + + @endif + {{ $element->name }} + + {{ $element->part_label }} + + {{ $element->created_at?->format('Y-m-d H:i') }} +
+ @csrf @method('DELETE') + +
+
{{ __('No data') }}
+
+ + {{-- 新增 UI 元素 Modal --}} + +
+@endsection diff --git a/resources/views/admin/app/ui-machines/index.blade.php b/resources/views/admin/app/ui-machines/index.blade.php new file mode 100644 index 0000000..d1d0045 --- /dev/null +++ b/resources/views/admin/app/ui-machines/index.blade.php @@ -0,0 +1,55 @@ +@extends('layouts.admin') + +@section('content') +
+ @include('admin.app._tabs', ['active' => 'machines']) + +
+ + + + + + + + + + @forelse($machines as $machine) + @php + $currentId = data_get($machine->settings, 'ui_bundle_id'); + $currentName = $currentId ? ($bundleNames[$currentId] ?? __('(deleted)')) : '—'; + @endphp + + + + + + @empty + + @endforelse + +
{{ __('Machine') }}{{ __('Current Bundle') }}{{ __('Bind UI Bundle') }}
+
{{ $machine->name }}
+
{{ $machine->serial_no }}
+
{{ $currentName }} +
+ @csrf @method('PUT') + + +
+
{{ __('No data') }}
+
+ +
+ {{ $machines->links() }} +
+
+@endsection diff --git a/resources/views/layouts/partials/sidebar-menu.blade.php b/resources/views/layouts/partials/sidebar-menu.blade.php index 8109f6c..2c93338 100644 --- a/resources/views/layouts/partials/sidebar-menu.blade.php +++ b/resources/views/layouts/partials/sidebar-menu.blade.php @@ -97,9 +97,8 @@ @endcan -{{-- @can('menu.app') -5. APP管理 +{{-- 5. APP管理 --}}
  • @endcan ---}} @can('menu.warehouses') diff --git a/routes/web.php b/routes/web.php index 11f2c97..cec6181 100644 --- a/routes/web.php +++ b/routes/web.php @@ -87,8 +87,25 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix }); // 4. APP管理 - Route::prefix('app')->name('app.')->group(function () { - Route::get('/ui-elements', [App\Http\Controllers\Admin\AppConfigController::class, 'uiElements'])->name('ui-elements'); + Route::prefix('app')->name('app.')->middleware('can:menu.app')->group(function () { + // 頁籤一:UI 元素列表(素材庫) + Route::get('/ui-elements', [App\Http\Controllers\Admin\UiElementController::class, 'uiElements'])->name('ui-elements'); + Route::post('/ui-elements', [App\Http\Controllers\Admin\UiElementController::class, 'storeElement'])->name('ui-elements.store'); + Route::delete('/ui-elements/{uiElement}', [App\Http\Controllers\Admin\UiElementController::class, 'destroyElement'])->name('ui-elements.destroy'); + + // 頁籤二:UI 組合編輯 + Route::get('/ui-bundles', [App\Http\Controllers\Admin\UiElementController::class, 'bundles'])->name('ui-bundles'); + Route::post('/ui-bundles', [App\Http\Controllers\Admin\UiElementController::class, 'storeBundle'])->name('ui-bundles.store'); + Route::put('/ui-bundles/{uiBundle}', [App\Http\Controllers\Admin\UiElementController::class, 'updateBundle'])->name('ui-bundles.update'); + Route::delete('/ui-bundles/{uiBundle}', [App\Http\Controllers\Admin\UiElementController::class, 'destroyBundle'])->name('ui-bundles.destroy'); + Route::get('/ui-bundles/{uiBundle}/content', [App\Http\Controllers\Admin\UiElementController::class, 'editBundleContent'])->name('ui-bundles.content'); + Route::put('/ui-bundles/{uiBundle}/content', [App\Http\Controllers\Admin\UiElementController::class, 'updateBundleContent'])->name('ui-bundles.content.update'); + + // 頁籤三:機台設定(綁定 UI 組合) + Route::get('/ui-machines', [App\Http\Controllers\Admin\UiElementController::class, 'machines'])->name('ui-machines'); + Route::put('/ui-machines/{machine}', [App\Http\Controllers\Admin\UiElementController::class, 'updateMachineBundle'])->name('ui-machines.update'); + + // 其餘 APP 管理佔位頁(沿用既有) Route::get('/helper', [App\Http\Controllers\Admin\AppConfigController::class, 'helper'])->name('helper'); Route::get('/questionnaire', [App\Http\Controllers\Admin\AppConfigController::class, 'questionnaire'])->name('questionnaire'); Route::get('/games', [App\Http\Controllers\Admin\AppConfigController::class, 'games'])->name('games'); From 7625799ecc8e48fb990eb502c98c05cede497222 Mon Sep 17 00:00:00 2001 From: terrylee Date: Wed, 15 Jul 2026 13:17:54 +0800 Subject: [PATCH 02/16] =?UTF-8?q?feat(app-ui):=20=E6=A9=9F=E5=8F=B0?= =?UTF-8?q?=E7=B6=81=E5=AE=9A=E5=84=B2=E5=AD=98=E5=BE=8C=E4=B8=BB=E5=8B=95?= =?UTF-8?q?=E6=8E=A8=E9=80=81=20update=5Fsettings=EF=BC=8C=E6=A9=9F?= =?UTF-8?q?=E5=8F=B0=E7=AB=8B=E5=8D=B3=E5=A5=97=E7=94=A8=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 沿用 app 既有 update_settings→downloadSetting 處理(APK 不需改);推送失敗(離線)則下次同步套用 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Controllers/Admin/UiElementController.php | 24 +++++++++++++++++-- lang/en.json | 3 +++ lang/zh_TW.json | 3 +++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Admin/UiElementController.php b/app/Http/Controllers/Admin/UiElementController.php index 69030de..bbb9c1b 100644 --- a/app/Http/Controllers/Admin/UiElementController.php +++ b/app/Http/Controllers/Admin/UiElementController.php @@ -186,7 +186,7 @@ class UiElementController extends Controller ]); } - public function updateMachineBundle(Request $request, Machine $machine) + public function updateMachineBundle(Request $request, Machine $machine, \App\Services\Machine\MqttService $mqttService) { $validated = $request->validate([ 'ui_bundle_id' => 'nullable|integer|exists:ui_bundles,id', @@ -201,6 +201,26 @@ class UiElementController extends Controller $machine->settings = $settings; $machine->save(); - return redirect()->route('admin.app.ui-machines')->with('success', __('Saved')); + // 儲存後主動推送,讓機台立即重拉設定並套用 UI 換膚。 + // 沿用 app 端既有的 update_settings 處理(MqttService → downloadSetting → 解析 UISet),APK 不需改動。 + $command = \App\Models\Machine\RemoteCommand::create([ + 'machine_id' => $machine->id, + 'user_id' => auth()->id(), + 'command_type' => 'update_settings', + 'payload' => [], + 'status' => 'pending', + 'remark' => __('Sync UI bundle'), + ]); + + $pushed = $mqttService->pushCommand($machine->serial_no, 'update_settings', [], (string) $command->id); + if (!$pushed) { + $command->update(['status' => 'failed', 'note' => 'MQTT push failed']); + } + + $message = $pushed + ? __('Saved and update pushed to machine.') + : __('Saved. Machine will apply on next sync/reboot.'); + + return redirect()->route('admin.app.ui-machines')->with('success', $message); } } diff --git a/lang/en.json b/lang/en.json index dfe40dd..d3e8d68 100644 --- a/lang/en.json +++ b/lang/en.json @@ -2343,6 +2343,9 @@ "UI element deleted": "UI element deleted", "UI bundle created": "UI bundle created", "UI bundle deleted": "UI bundle deleted", + "Sync UI bundle": "Sync UI bundle", + "Saved and update pushed to machine.": "Saved and update pushed to machine.", + "Saved. Machine will apply on next sync/reboot.": "Saved. Machine will apply on next sync/reboot.", "No data": "No data", "Back": "Back", "Rename": "Rename", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index ed3f24b..4adbaaf 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -2344,6 +2344,9 @@ "UI element deleted": "UI 元素已刪除", "UI bundle created": "UI 組合已新增", "UI bundle deleted": "UI 組合已刪除", + "Sync UI bundle": "同步 UI 組合", + "Saved and update pushed to machine.": "已儲存,並已推送更新到機台(機台將立即重拉並套用)。", + "Saved. Machine will apply on next sync/reboot.": "已儲存。推送未成功(機台可能離線),將於下次同步/重開機時套用。", "No data": "無資料", "Back": "返回", "Rename": "重新命名", From f1c4a9a2b7cfb906e480f360cf3c9d4f65eae203 Mon Sep 17 00:00:00 2001 From: terrylee Date: Thu, 16 Jul 2026 09:38:43 +0800 Subject: [PATCH 03/16] =?UTF-8?q?feat(app-ui):=20=E5=95=9F=E7=94=A8?= =?UTF-8?q?=E9=83=A8=E4=BD=8D=20E1(=E4=BA=A4=E6=98=93=E5=A4=B1=E6=95=97/?= =?UTF-8?q?=E5=AE=A2=E6=9C=8D=E5=9C=96)=20+=20P1~P6(=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E6=8C=89=E9=88=95=E5=9C=96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P 系列 width/height=null 不裁切保留原比例;blade 尺寸標示對 null 做防護 Co-Authored-By: Claude Opus 4.8 (1M context) --- config/ui_parts.php | 11 +++++++++++ .../views/admin/app/ui-bundles/content.blade.php | 2 +- resources/views/admin/app/ui-elements/index.blade.php | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/config/ui_parts.php b/config/ui_parts.php index d4ab65d..9139808 100644 --- a/config/ui_parts.php +++ b/config/ui_parts.php @@ -26,6 +26,17 @@ return [ 'A2' => ['label' => 'A2 - 機台自檢中', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 1920, 'active' => true], 'A3' => ['label' => 'A3 - 機台異常鎖定', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 1920, 'active' => true], + // === E 系列:交易結果 === + 'E1' => ['label' => 'E1 - 交易失敗/客服圖', 'group' => 'transaction', 'type' => 'image', 'width' => 720, 'height' => 1280, 'active' => true], + + // === P 系列:支付方式按鈕圖(不裁切、保留原比例,width/height=null)=== + 'P1' => ['label' => 'P1 - 信用卡支付', 'group' => 'payment', 'type' => 'image', 'width' => null, 'height' => null, 'active' => true], + 'P2' => ['label' => 'P2 - 卡片/電子票證', 'group' => 'payment', 'type' => 'image', 'width' => null, 'height' => null, 'active' => true], + 'P3' => ['label' => 'P3 - 玉山掃碼支付', 'group' => 'payment', 'type' => 'image', 'width' => null, 'height' => null, 'active' => true], + 'P4' => ['label' => 'P4 - 手機支付', 'group' => 'payment', 'type' => 'image', 'width' => null, 'height' => null, 'active' => true], + 'P5' => ['label' => 'P5 - 現金支付', 'group' => 'payment', 'type' => 'image', 'width' => null, 'height' => null, 'active' => true], + 'P6' => ['label' => 'P6 - TapPay 掃碼', 'group' => 'payment', 'type' => 'image', 'width' => null, 'height' => null, 'active' => true], + // --- 以下為舊後台既有部位,Phase 2+ 逐步啟用(active=false 時後台不顯示、不下發)--- 'A4' => ['label' => 'A4 - 對話框標頭', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 192, 'active' => false], 'A7' => ['label' => 'A7 - 購買頁廣告底圖', 'group' => 'fullscreen', 'type' => 'image', 'width' => 1080, 'height' => 620, 'active' => false], diff --git a/resources/views/admin/app/ui-bundles/content.blade.php b/resources/views/admin/app/ui-bundles/content.blade.php index 2896bd1..114090e 100644 --- a/resources/views/admin/app/ui-bundles/content.blade.php +++ b/resources/views/admin/app/ui-bundles/content.blade.php @@ -22,7 +22,7 @@

    {{ __('Image will be center-cropped to the category size.') }}

    From a326283d175f175c09733d989980c865ac661ce1 Mon Sep 17 00:00:00 2001 From: terrylee Date: Thu, 16 Jul 2026 10:11:47 +0800 Subject: [PATCH 04/16] =?UTF-8?q?style(app-ui):=20APP=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E4=B8=89=E9=A0=81=E6=94=B9=E7=94=A8=E7=AB=99=E4=B8=8A=E8=A8=AD?= =?UTF-8?q?=E8=A8=88=E7=B3=BB=E7=B5=B1(luxury=E6=8C=89=E9=88=95/=E6=A8=99?= =?UTF-8?q?=E6=BA=96=E8=A1=A8=E6=A0=BC/=E8=86=A0=E5=9B=8Atabs/=E6=A8=99?= =?UTF-8?q?=E6=BA=96modal/=E5=88=AA=E9=99=A4=E7=A2=BA=E8=AA=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - btn-luxury-primary/secondary + x-delete-confirm-modal - 膠囊式頁籤(對齊 x-tab-nav 外觀) - 「新增」改用 __('Create')=新增(不動全域 New=新值,那是 products 變更紀錄用) Co-Authored-By: Claude Opus 4.8 (1M context) --- lang/en.json | 1 + lang/zh_TW.json | 1 + resources/views/admin/app/_tabs.blade.php | 29 ++--- .../admin/app/ui-bundles/content.blade.php | 28 ++-- .../admin/app/ui-bundles/index.blade.php | 95 +++++++------- .../admin/app/ui-elements/index.blade.php | 120 +++++++++--------- .../admin/app/ui-machines/index.blade.php | 39 +++--- 7 files changed, 157 insertions(+), 156 deletions(-) diff --git a/lang/en.json b/lang/en.json index d3e8d68..6bb4b88 100644 --- a/lang/en.json +++ b/lang/en.json @@ -2337,6 +2337,7 @@ "Type": "Type", "Type to search or leave blank for system defaults.": "Type to search or leave blank for system defaults.", "UI Elements": "UI Elements", + "APP UI Elements": "APP UI Elements", "UI Bundles": "UI Bundles", "Machine Binding": "Machine Binding", "UI element created": "UI element created", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 4adbaaf..ec844ed 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -2338,6 +2338,7 @@ "Type": "類型", "Type to search or leave blank for system defaults.": "輸入關鍵字搜尋,或留空以使用系統預設。", "UI Elements": "UI元素", + "APP UI Elements": "APP UI 元素設定", "UI Bundles": "UI組合編輯", "Machine Binding": "機台設定", "UI element created": "UI 元素已新增", diff --git a/resources/views/admin/app/_tabs.blade.php b/resources/views/admin/app/_tabs.blade.php index 1fb5584..90da629 100644 --- a/resources/views/admin/app/_tabs.blade.php +++ b/resources/views/admin/app/_tabs.blade.php @@ -1,25 +1,22 @@ -{{-- APP UI元素設定 共用頁籤列。傳入 $active = elements | bundles | machines --}} +{{-- APP UI元素設定 共用標題 + 頁籤列。傳入 $active = elements | bundles | machines --}} @php $tabs = [ - 'elements' => ['label' => __('UI Elements'), 'route' => 'admin.app.ui-elements'], - 'bundles' => ['label' => __('UI Bundles'), 'route' => 'admin.app.ui-bundles'], + 'elements' => ['label' => __('UI Elements'), 'route' => 'admin.app.ui-elements'], + 'bundles' => ['label' => __('UI Bundles'), 'route' => 'admin.app.ui-bundles'], 'machines' => ['label' => __('Machine Binding'), 'route' => 'admin.app.ui-machines'], ]; @endphp -
    -
    -
    -

    {{ __('APP Management') }}

    -

    {{ __('UI Elements') }}

    -
    -
    -
    + +
    +

    {{ __('APP UI Elements') }}

    + + @if(session('success')) -
    +
    {{ session('success') }}
    @endif @if($errors->any()) -
    +
      @foreach($errors->all() as $error)
    • {{ $error }}
    • @endforeach
    diff --git a/resources/views/admin/app/ui-bundles/content.blade.php b/resources/views/admin/app/ui-bundles/content.blade.php index 114090e..8b57ad6 100644 --- a/resources/views/admin/app/ui-bundles/content.blade.php +++ b/resources/views/admin/app/ui-bundles/content.blade.php @@ -1,43 +1,43 @@ @extends('layouts.admin') @section('content') -
    +
    @include('admin.app._tabs', ['active' => 'bundles']) -
    - ← {{ __('Back') }} - / -

    {{ __('Content Settings') }}:{{ $bundle->name }}

    +
    + + + {{ __('Back') }} + +

    {{ __('Content Settings') }}:{{ $bundle->name }}

    @csrf @method('PUT') -
    +
    @foreach($parts as $key => $part) @php $currentId = optional($itemsByPart->get($key))->ui_element_id; $currentThumb = optional(optional($itemsByPart->get($key))->element)->image_url ?? ''; @endphp -
    -
    - +
    diff --git a/resources/views/admin/app/ui-bundles/index.blade.php b/resources/views/admin/app/ui-bundles/index.blade.php index aefd93b..6d65541 100644 --- a/resources/views/admin/app/ui-bundles/index.blade.php +++ b/resources/views/admin/app/ui-bundles/index.blade.php @@ -1,75 +1,80 @@ @extends('layouts.admin') @section('content') -
    +
    @include('admin.app._tabs', ['active' => 'bundles']) -
    -
    -
    - - - - - - +
    +
    {{ __('ID') }}{{ __('Name') }}{{ __('Actions') }}
    + + + + + - + @forelse($bundles as $bundle) - - + - @empty - + @endforelse
    {{ __('ID') }}{{ __('Name') }}{{ __('Actions') }}
    {{ $bundle->id }} + {{ $bundle->id }} {{ $bundle->name }} -
    + @csrf @method('PUT') - - - + + +
    - - {{ __('Content Settings') }} -
    - @csrf @method('DELETE') - -
    +
    +
    + + {{ __('Content Settings') }} + +
    {{ __('No data') }}
    {{ __('No data') }}
    {{-- 新增組合 Modal --}} -