- 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>
281 lines
10 KiB
PHP
281 lines
10 KiB
PHP
<?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 → 解析 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);
|
||
}
|
||
}
|