$p['active'] ?? false); } // ================================================================== // 頁籤一:UI 元素列表(素材庫) // ================================================================== public function uiElements(Request $request) { $perPage = (int) $request->input('per_page', 10); $perPage = in_array($perPage, [10, 25, 50, 100], true) ? $perPage : 10; $search = trim((string) $request->input('search', '')); $query = UiElement::orderByDesc('id'); if ($search !== '') { $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('part_key', 'like', "%{$search}%"); }); } $elements = $query->paginate($perPage)->withQueryString(); return view('admin.app.ui-elements.index', [ 'elements' => $elements, 'parts' => $this->activeParts(), 'perPage' => $perPage, 'search' => $search, ]); } 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']}"); // crop=true(預設,滿版類)→ 依部位尺寸置中裁切;crop=false(按鈕/icon 類)→ 保留原比例不裁切 $crop = $part['crop'] ?? true; $targetW = $crop ? ($part['width'] ?? null) : null; $targetH = $crop ? ($part['height'] ?? null) : null; $path = $this->storeAsWebp($request->file('file'), 'ui_elements', 80, $targetW, $targetH); 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(Request $request) { $perPage = (int) $request->input('per_page', 10); $perPage = in_array($perPage, [10, 25, 50, 100], true) ? $perPage : 10; $search = trim((string) $request->input('search', '')); $query = UiBundle::orderBy('id'); if ($search !== '') { $query->where('name', 'like', "%{$search}%"); } $bundles = $query->paginate($perPage)->withQueryString(); return view('admin.app.ui-bundles.index', compact('bundles', 'perPage', 'search')); } 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', []); // 圖片型:part_key => ui_element_id $colorInput = $request->input('parts_color', []); // 色彩型:part_key => #RRGGBB $colorEnabled = $request->input('parts_enabled', []); // 色彩型:part_key => "1"(有勾才套用) foreach ($parts as $key => $part) { $type = $part['type'] ?? 'image'; // 色彩型(C 底色 / T 文字色) if ($type === 'color') { $isOn = !empty($colorEnabled[$key]); $color = $colorInput[$key] ?? null; $valid = is_string($color) && preg_match('/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $color); if (!$isOn || !$valid) { $uiBundle->items()->where('part_key', $key)->delete(); continue; } UiBundleItem::updateOrCreate( ['ui_bundle_id' => $uiBundle->id, 'part_key' => $key], ['ui_element_id' => null, 'color_value' => strtoupper($color)] ); continue; } // 圖片型 $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; } // 只接受屬於該部位的元素 if (!UiElement::where('id', $elementId)->where('part_key', $key)->exists()) { 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) { $perPage = (int) $request->input('per_page', 25); $perPage = in_array($perPage, [10, 25, 50, 100], true) ? $perPage : 25; $search = trim((string) $request->input('search', '')); $query = Machine::orderBy('name'); if ($search !== '') { $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('serial_no', 'like', "%{$search}%"); }); } $machines = $query->paginate($perPage)->withQueryString(); $bundles = UiBundle::orderBy('name')->get(); $bundleNames = $bundles->pluck('name', 'id'); return view('admin.app.ui-machines.index', [ 'machines' => $machines, 'bundles' => $bundles, 'bundleNames' => $bundleNames, 'perPage' => $perPage, 'search' => $search, ]); } public function updateMachineBundle(Request $request, Machine $machine, \App\Services\Machine\MqttService $mqttService) { $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(); // 儲存後主動推送,讓機台立即重拉設定並套用 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); } }