[FEAT] 重構機台狀態判定邏輯並優化全站多語系支援
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m18s

1. 重構機台在線狀態判定機制:移除資料庫 status 欄位,改由 Model 根據心跳時間動態計算。
2. 修正儀表板 (Dashboard) 與機台管理頁面的多語系顯示問題,解決換行導致翻譯失效的 Bug。
3. 修正個人檔案頁面的麵包屑 (Breadcrumbs) 導航,補齊「個人設定」層級。
4. 更新 IoT API (B010, B600) 的認證機制與日誌處理邏輯。
5. 同步更新繁中、英文、日文語言檔,確保 UI 標籤一致性。
This commit is contained in:
2026-04-07 10:21:07 +08:00
parent 08b6c60d2e
commit bbdc5bad9f
18 changed files with 2476 additions and 1813 deletions

View File

@@ -3,84 +3,93 @@
@section('content')
<script>
window.machineApp = function() {
return {
showLogPanel: false,
activeTab: 'status',
currentMachineId: '',
currentMachineSn: '',
currentMachineName: '',
logs: [],
loading: false,
startDate: '',
endDate: '',
tab: 'list',
viewMode: 'fleet',
selectedMachine: null,
slots: [],
window.machineApp = function () {
return {
showLogPanel: false,
showEditModal: false,
editMachineId: '',
editMachineName: '',
activeTab: 'status',
currentMachineId: '',
currentMachineSn: '',
currentMachineName: '',
logs: [],
loading: false,
startDate: '',
endDate: '',
tab: 'list',
viewMode: 'fleet',
selectedMachine: null,
slots: [],
init() {
const d = new Date();
const today = [
d.getFullYear(),
String(d.getMonth() + 1).padStart(2, '0'),
String(d.getDate()).padStart(2, '0')
].join('-');
this.startDate = today;
this.endDate = today;
this.$watch('activeTab', () => this.fetchLogs());
},
init() {
const d = new Date();
const today = [
d.getFullYear(),
String(d.getMonth() + 1).padStart(2, '0'),
String(d.getDate()).padStart(2, '0')
].join('-');
this.startDate = today;
this.endDate = today;
this.$watch('activeTab', () => this.fetchLogs());
},
async openLogPanel(id, sn, name) {
this.currentMachineId = id;
this.currentMachineSn = sn;
this.currentMachineName = name;
this.slots = [];
this.showLogPanel = true;
this.activeTab = 'status';
await this.fetchLogs();
},
async openLogPanel(id, sn, name) {
this.currentMachineId = id;
this.currentMachineSn = sn;
this.currentMachineName = name;
this.slots = [];
this.showLogPanel = true;
this.activeTab = 'status';
await this.fetchLogs();
},
openEditModal(id, name) {
this.editMachineId = id;
this.editMachineName = name;
this.showEditModal = true;
},
async fetchLogs() {
this.loading = true;
try {
let url = '/admin/machines/' + this.currentMachineId + '/logs-ajax?type=' + this.activeTab;
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.logs = data.data.data || data.data || [];
} catch(e) { console.error('fetchLogs error:', e); }
finally { this.loading = false; }
},
async fetchLogs() {
this.loading = true;
try {
let url = '/admin/machines/' + this.currentMachineId + '/logs-ajax?type=' + this.activeTab;
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.logs = data.data.data || data.data || [];
} catch (e) { console.error('fetchLogs error:', e); }
finally { this.loading = false; }
},
async openCabinet(id) {
this.loading = true;
this.viewMode = 'cabinet';
try {
const res = await fetch('/admin/machines/' + id + '/slots-ajax');
const data = await res.json();
if (data.success) {
this.selectedMachine = data.machine;
this.slots = data.slots;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
} catch(e) { console.error('openCabinet error:', e); }
finally { this.loading = false; }
},
async openCabinet(id) {
this.loading = true;
this.viewMode = 'cabinet';
try {
const res = await fetch('/admin/machines/' + id + '/slots-ajax');
const data = await res.json();
if (data.success) {
this.selectedMachine = data.machine;
this.slots = data.slots;
window.scrollTo({ top: 0, behavior: 'smooth' });
}
} catch (e) { console.error('openCabinet error:', e); }
finally { this.loading = false; }
},
};
};
};
</script>
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()"
@keydown.escape.window="showLogPanel = false">
<div class="space-y-4 pb-20 mt-4" x-data="machineApp()" @keydown.escape.window="showLogPanel = false">
<!-- Top Header & Actions -->
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div class="flex items-center gap-4">
<div>
<h1 class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300">
<h1
class="text-3xl font-black text-slate-800 dark:text-white tracking-tight font-display transition-all duration-300">
{{ __('Machine List') }}
</h1>
<p class="text-sm font-bold text-slate-500 dark:text-slate-400 mt-1 uppercase tracking-widest">
@@ -167,9 +176,13 @@ window.machineApp = function() {
</td>
<td class="px-6 py-6 text-center">
<div class="flex items-center justify-center gap-2">
@if($machine->status === 'online')
<div
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20">
@php
$cStatus = $machine->calculated_status;
@endphp
@if($cStatus === 'online')
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-emerald-500/10 border border-emerald-500/20 tooltip"
title="{{ __('Machine is heartbeat normal') }}">
<div class="relative flex h-2 w-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
@@ -179,17 +192,17 @@ window.machineApp = function() {
class="text-xs font-black text-emerald-600 dark:text-emerald-400 tracking-widest uppercase">{{
__('Online') }}</span>
</div>
@elseif($machine->status === 'error')
<div
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20">
@elseif($cStatus === 'error')
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-rose-500/10 border border-rose-500/20 tooltip"
title="{{ __('Recently reported errors or warnings in logs') }}">
<div class="h-2 w-2 rounded-full bg-rose-500 animate-pulse"></div>
<span
class="text-xs font-black text-rose-600 dark:text-rose-400 tracking-widest uppercase">{{
__('Error') }}</span>
</div>
@else
<div
class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20">
<div class="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-500/10 border border-slate-500/20 tooltip"
title="{{ __('No heartbeat for over 30 seconds') }}">
<div class="h-2 w-2 rounded-full bg-slate-400"></div>
<span
class="text-xs font-black text-slate-500 dark:text-slate-400 tracking-widest uppercase">{{
@@ -230,15 +243,16 @@ window.machineApp = function() {
</td>
<td class="px-6 py-6 text-right">
<div class="flex items-center justify-end gap-2">
<a href="{{ route('admin.machines.edit', $machine->id) }}"
<button type="button"
@click="openEditModal('{{ $machine->id }}', '{{ addslashes($machine->name) }}')"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
title="{{ __('Edit') }}">
title="{{ __('Edit Name') }}">
<svg class="w-4 h-4 stroke-[2.5]" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</a>
</button>
<button type="button"
@click="openLogPanel('{{ $machine->id }}', '{{ $machine->serial_no }}', '{{ addslashes($machine->name) }}')"
class="p-2 rounded-lg bg-slate-50 dark:bg-slate-800 text-slate-400 hover:text-cyan-500 hover:bg-cyan-500/5 dark:hover:bg-cyan-500/10 border border-transparent hover:border-cyan-500/20 transition-all inline-flex group/btn tooltip"
@@ -504,4 +518,57 @@ window.machineApp = function() {
</div>
</div><!-- /Offcanvas -->
@endsection
<!-- Edit Machine Name Modal -->
<div x-show="showEditModal" class="fixed inset-0 z-[100] overflow-y-auto" style="display: none;" role="dialog" aria-modal="true">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<!-- Background Backdrop -->
<div x-show="showEditModal" x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity" @click="showEditModal = false">
</div>
<span class="hidden sm:inline-block sm:align-middle sm:min-h-screen" aria-hidden="true">&#8203;</span>
<!-- Modal Panel -->
<div x-show="showEditModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="inline-block align-bottom bg-white dark:bg-slate-900 rounded-[2.5rem] p-10 text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full border border-slate-100 dark:border-slate-800">
<div>
<h3 class="text-2xl font-black text-slate-800 dark:text-white font-display tracking-tight leading-none mb-2">{{ __('Edit Machine Name') }}</h3>
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Update identification for your asset') }}</p>
<form :action="'/admin/machines/' + editMachineId" method="POST" class="mt-8 space-y-6">
@csrf
@method('PUT')
<div class="space-y-4">
<label class="block text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-[0.1em]">{{ __('New Machine Name') }}</label>
<input type="text" name="name" x-model="editMachineName" required
class="luxury-input block w-full px-6 py-4 text-base font-bold text-slate-800 dark:text-white bg-slate-50/50 dark:bg-slate-900/50"
placeholder="{{ __('Enter machine name...') }}">
</div>
<div class="flex items-center gap-4 pt-4">
<button type="button" @click="showEditModal = false"
class="px-8 py-4 bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-black rounded-2xl border border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 transition-all">
{{ __('Cancel') }}
</button>
<button type="submit"
class="flex-1 bg-cyan-500 hover:bg-cyan-600 text-white font-black py-4 rounded-2xl shadow-lg shadow-cyan-500/30 transition-all duration-300 transform hover:-translate-y-0.5 active:scale-95">
{{ __('Save Changes') }}
</button>
</div>
</form>
</div>
</div>
</div>
</div><!-- /Edit Modal -->
@endsection