From a16b0363a8529ec7ed298f09a56483eed8dfd081 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Thu, 14 May 2026 17:17:55 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E6=96=B0=E5=A2=9E=E6=A9=9F=E5=8F=B0?= =?UTF-8?q?=E6=97=A5=E8=AA=8C=E6=BA=AB=E5=BA=A6=E8=B6=A8=E5=8B=A2=E5=9C=96?= =?UTF-8?q?=E8=88=87=E5=90=8C=E6=AD=A5=E6=8C=87=E4=BB=A4=E6=97=A5=E8=AA=8C?= =?UTF-8?q?=E8=A3=9C=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修改 ProcessCommandAck.php,將「同步商品」與「同步廣告」指令加入日誌紀錄白名單,確保同步結果正確寫入機台日誌。 2. 在 MachineController 新增 temperatureAjax API,從機台日誌中提取歷史溫度數據供圖表使用。 3. 在 web.php 註冊溫度數據 AJAX 查詢路由。 4. 在機台管理頁面 (index.blade.php) 的日誌面板中整合 ApexCharts,並於「機台狀態」分頁內嵌入溫度趨勢曲線圖。 5. 更新 Alpine.js 邏輯,實現圖表與日期篩選的同步連動,並優化面板展開時的圖表渲染。 6. 新增繁體中文、英文、日文的「溫度趨勢」翻譯標籤,並修復語言檔中的重複 key 與翻譯內容。 --- .../Controllers/Admin/MachineController.php | 30 +++++ app/Jobs/Machine/ProcessCommandAck.php | 2 +- lang/en.json | 3 + lang/ja.json | 5 +- lang/zh_TW.json | 4 +- .../views/admin/machines/index.blade.php | 119 +++++++++++++++++- routes/web.php | 1 + 7 files changed, 159 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Admin/MachineController.php b/app/Http/Controllers/Admin/MachineController.php index 156128b..39af606 100644 --- a/app/Http/Controllers/Admin/MachineController.php +++ b/app/Http/Controllers/Admin/MachineController.php @@ -237,4 +237,34 @@ class MachineController extends AdminController 'message' => __('All issues marked as resolved.') ]); } + + /** + * AJAX: 取得機台溫度歷史紀錄 (供圖表使用) + */ + public function temperatureAjax(Request $request, Machine $machine) + { + $startDate = $request->get('start_date'); + $endDate = $request->get('end_date'); + + $logs = $machine->logs() + ->where('message', 'Temperature reported: :temp°C') + ->when($startDate, function ($query, $start) { + return $query->where('created_at', '>=', str_replace('T', ' ', $start)); + }) + ->when($endDate, function ($query, $end) { + return $query->where('created_at', '<=', str_replace('T', ' ', $end)); + }) + ->oldest() + ->get(); + + $chartData = [ + 'labels' => $logs->pluck('created_at')->map(fn($date) => $date->format('Y-m-d H:i:s'))->toArray(), + 'values' => $logs->map(fn($log) => (int) ($log->context['temp'] ?? 0))->toArray(), + ]; + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } } diff --git a/app/Jobs/Machine/ProcessCommandAck.php b/app/Jobs/Machine/ProcessCommandAck.php index 919fd69..96efcbb 100644 --- a/app/Jobs/Machine/ProcessCommandAck.php +++ b/app/Jobs/Machine/ProcessCommandAck.php @@ -134,7 +134,7 @@ class ProcessCommandAck implements ShouldQueue ]); // 記錄維護類指令到機台日誌 (MachineLog) - if (in_array($command->command_type, ['reboot', 'reboot_card', 'lock', 'unlock', 'checkout', 'change', 'reload_stock'], true)) { + if (in_array($command->command_type, ['reboot', 'reboot_card', 'lock', 'unlock', 'checkout', 'change', 'reload_stock', 'update_products', 'update_ads'], true)) { $msgKey = $status === 'success' ? 'log.command.success' : 'log.command.failed'; $level = $status === 'success' ? 'info' : 'warning'; diff --git a/lang/en.json b/lang/en.json index 2fe7faa..65ce160 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1607,6 +1607,7 @@ "Taxation System": "Taxation System", "Temperature": "Temperature", "Temperature reported: :temp°C": "Temperature reported: :temp°C", + "Temperature Trend": "Temperature Trend", "Temperature updated to :temp°C": "Temperature updated to :temp°C", "Tenant": "Tenant", "TermID": "TermID", @@ -1827,6 +1828,8 @@ "command.reboot_card": "Reboot Card Reader", "command.reload_stock": "Sync Stock", "command.unlock": "Unlock", + "command.update_ads": "Sync Ads", + "command.update_products": "Sync Products", "companies": "Customer Management", "completed": "Pickup Completed", "consume": "Pickup Successful", diff --git a/lang/ja.json b/lang/ja.json index c341428..9a3ce4f 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1606,7 +1606,8 @@ "Tax System": "税務システム", "Taxation System": "税務システム", "Temperature": "温度", - "Temperature reported: :temp°C": "温度報告: :temp°C", + "Temperature reported: :temp°C": "温度が報告されました: :temp°C", + "Temperature Trend": "温度傾向", "Temperature updated to :temp°C": "温度が :temp°C に更新されました", "Tenant": "顧客 (テナント)", "TermID": "端末ID (TermID)", @@ -1827,6 +1828,8 @@ "command.reboot_card": "カードリーダー再起動", "command.reload_stock": "在庫同期", "command.unlock": "ロック解除", + "command.update_ads": "広告同期", + "command.update_products": "商品同期", "companies": "顧客管理", "completed": "受取完了", "consume": "受取成功", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 8cb70f0..fadc2a9 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1043,7 +1043,6 @@ "Normal": "正常", "Not Used": "不使用", "Not Used Description": "不使用第三方支付介接", - "Normal": "正常", "Note": "備註", "Note (optional)": "備註 (選填)", "Notes": "備註", @@ -1630,6 +1629,7 @@ "Taxation System": "稅務系統", "Temperature": "溫度", "Temperature reported: :temp°C": "回報溫度 : :temp°C", + "Temperature Trend": "溫度趨勢", "Temperature updated to :temp°C": "溫度已更新為::temp°C", "Tenant": "客戶", "TermID": "終端代號 (TermID)", @@ -1856,6 +1856,8 @@ "command.reboot_card": "讀卡機重啟", "command.reload_stock": "同步庫存", "command.unlock": "解鎖", + "command.update_ads": "同步廣告", + "command.update_products": "同步商品", "companies": "客戶管理", "company_id": "公司", "completed": "取貨完成", diff --git a/resources/views/admin/machines/index.blade.php b/resources/views/admin/machines/index.blade.php index 0032e21..191ca97 100644 --- a/resources/views/admin/machines/index.blade.php +++ b/resources/views/admin/machines/index.blade.php @@ -54,6 +54,8 @@ lastPage: 1, totalLogs: 0, + temperatureChart: null, + init() { const now = new Date(); const pad = (n) => String(n).padStart(2, '0'); @@ -62,6 +64,10 @@ this.startDate = formatDate(now, '00:00'); this.endDate = formatDate(now, '23:59'); this.$watch('activeTab', () => this.fetchLogs(1)); + + // 監聽日期變動,同步更新圖表 (僅在當前為狀態分頁時) + this.$watch('startDate', () => { if(this.activeTab === 'status') this.fetchTemperatureData(); }); + this.$watch('endDate', () => { if(this.activeTab === 'status') this.fetchTemperatureData(); }); }, async openLogPanel(id, sn, name) { @@ -72,6 +78,97 @@ this.showLogPanel = true; this.activeTab = 'status'; await this.fetchLogs(); + await this.fetchTemperatureData(); + }, + + async fetchTemperatureData() { + if (this.activeTab !== 'status') return; + try { + let url = `/admin/machines/${this.currentMachineId}/temperature-ajax?`; + if (this.startDate) url += '&start_date=' + this.startDate; + if (this.endDate) url += '&end_date=' + this.endDate; + + const res = await fetch(url); + const data = await res.json(); + if (data.success) { + this.initTemperatureChart(data.data); + } + } catch (e) { console.error('fetchTemperatureData error:', e); } + }, + + initTemperatureChart(chartData) { + this.$nextTick(() => { + const chartEl = document.querySelector("#temperature-chart"); + if (!chartEl) return; + + const isDark = document.documentElement.classList.contains('dark'); + + const options = { + series: [{ + name: "{{ __('Temperature') }}", + data: chartData.values + }], + chart: { + type: 'area', + height: 200, + toolbar: { show: false }, + zoom: { enabled: false }, + animations: { enabled: true }, + background: 'transparent' + }, + colors: ['#06b6d4'], + fill: { + type: 'gradient', + gradient: { + shadeIntensity: 1, + opacityFrom: 0.45, + opacityTo: 0.05, + stops: [20, 100] + } + }, + dataLabels: { enabled: false }, + stroke: { + curve: 'smooth', + width: 3 + }, + grid: { + borderColor: isDark ? '#1e293b' : '#f1f5f9', + strokeDashArray: 4, + padding: { left: 10, right: 10 } + }, + xaxis: { + categories: chartData.labels, + labels: { + show: true, + style: { colors: '#94a3b8', fontSize: '10px', fontWeight: 600 }, + formatter: (val) => { + if (!val) return ''; + return val.split(' ')[1].substring(0, 5); // 僅顯示 HH:mm + } + }, + axisBorder: { show: false }, + axisTicks: { show: false }, + tooltip: { enabled: false } + }, + yaxis: { + labels: { + style: { colors: '#94a3b8', fontSize: '10px', fontWeight: 600 }, + formatter: (val) => val + '°C' + } + }, + tooltip: { + theme: isDark ? 'dark' : 'light', + x: { formatter: (val) => val } + } + }; + + if (this.temperatureChart) { + this.temperatureChart.updateOptions(options); + } else { + this.temperatureChart = new ApexCharts(chartEl, options); + this.temperatureChart.render(); + } + }); }, openEditModal(id, name) { @@ -540,7 +637,6 @@ - + +
+
+

+ + {{ __('Temperature Trend') }} +

+ + @ + +
+
+
+
@@ -1206,4 +1317,8 @@ message="{{ __('Are you sure you want to resolve all recent issues for this machine?') }}" confirmText="{{ __('Confirm') }}" cancelText="{{ __('Cancel') }}" confirmColor="rose" /> - @endsection \ No newline at end of file + @endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 12da720..4015f4a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -54,6 +54,7 @@ Route::middleware(['auth', 'verified', 'tenant.access'])->prefix('admin')->name( Route::get('/{machine}/slots-ajax', [App\Http\Controllers\Admin\MachineController::class, 'slotsAjax'])->name('slots-ajax'); Route::post('/{machine}/slots/expiry', [App\Http\Controllers\Admin\MachineController::class, 'updateSlotExpiry'])->name('slots.expiry.update'); Route::get('/{machine}/logs-ajax', [App\Http\Controllers\Admin\MachineController::class, 'logsAjax'])->name('logs-ajax'); + Route::get('/{machine}/temperature-ajax', [App\Http\Controllers\Admin\MachineController::class, 'temperatureAjax'])->name('temperature-ajax'); Route::post('/{machine}/resolve-logs', [App\Http\Controllers\Admin\MachineController::class, 'resolveLogs'])->name('resolve-logs'); }); Route::resource('machines', App\Http\Controllers\Admin\MachineController::class);