star-cloud/app/Http/Controllers/Admin/UiElementController.php
terrylee b9a82235aa feat(app-ui): Phase3 後台 — 支援 color 型部位(色票選擇器)+ 新增 BG0整頁背景/C0底色/T0文字色
- config 加 background/bgcolor/textcolor 群組 + BG0(image)/C0/T0(color)
- 內容設定頁 color 型渲染勾選套用+色票(input color);updateBundleContent 存 color_value
- B014 下發 color 型已支援(不需改)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 13:34:20 +08:00

281 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Machine\Machine;
use App\Models\System\UiBundle;
use App\Models\System\UiBundleItem;
use App\Models\System\UiElement;
use App\Traits\ImageHandler;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class UiElementController extends Controller
{
use ImageHandler;
/**
* 本階段啟用的部位config/ui_parts.php 內 active=true
*/
private function activeParts(): array
{
return array_filter(config('ui_parts.parts', []), fn($p) => $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 → 解析 UISetAPK 不需改動。
$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);
}
}