[FEAT] 新增機台日誌溫度趨勢圖與同步指令日誌補全

1. 修改 ProcessCommandAck.php,將「同步商品」與「同步廣告」指令加入日誌紀錄白名單,確保同步結果正確寫入機台日誌。
2. 在 MachineController 新增 temperatureAjax API,從機台日誌中提取歷史溫度數據供圖表使用。
3. 在 web.php 註冊溫度數據 AJAX 查詢路由。
4. 在機台管理頁面 (index.blade.php) 的日誌面板中整合 ApexCharts,並於「機台狀態」分頁內嵌入溫度趨勢曲線圖。
5. 更新 Alpine.js 邏輯,實現圖表與日期篩選的同步連動,並優化面板展開時的圖表渲染。
6. 新增繁體中文、英文、日文的「溫度趨勢」翻譯標籤,並修復語言檔中的重複 key 與翻譯內容。
This commit is contained in:
sky121113 2026-05-14 17:17:55 +08:00
parent db2fd734ee
commit a16b0363a8
7 changed files with 159 additions and 5 deletions

View File

@ -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
]);
}
}

View File

@ -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';

View File

@ -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",

View File

@ -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": "受取成功",

View File

@ -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": "取貨完成",

View File

@ -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 @@
</div>
</div>
<!-- Offcanvas Log Panel -->
<div x-show="showLogPanel" class="fixed inset-0 z-[100] overflow-hidden" style="display: none;"
aria-labelledby="slide-over-title" role="dialog" aria-modal="true">
@ -738,6 +834,21 @@
{{ __('Loading Data') }}...</p>
</div>
<!-- Temperature Trend Chart -->
<div x-show="activeTab === 'status'"
class="mb-6 p-4 rounded-2xl bg-slate-50/50 dark:bg-white/[0.02] border border-slate-100 dark:border-slate-800/50">
<div class="flex items-center justify-between mb-4 px-2">
<h3 class="text-[11px] font-black text-slate-400 uppercase tracking-[0.2em] flex items-center gap-2">
<span class="w-1.5 h-1.5 rounded-full bg-cyan-500 shadow-[0_0_8px_rgba(6,182,212,0.5)]"></span>
{{ __('Temperature Trend') }}
</h3>
<span class="text-[10px] font-bold text-slate-400" x-show="logs.length > 0">
<span x-text="currentMachineName"></span> @ <span x-text="startDate.split(' ')[0]"></span>
</span>
</div>
<div id="temperature-chart" class="min-h-[200px] w-full"></div>
</div>
<!-- Logs Container -->
<div x-show="activeTab !== 'expiry'"
class="luxury-card border border-slate-200 dark:border-slate-800 rounded-2xl overflow-hidden shadow-sm">
@ -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
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
@endpush

View File

@ -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);