|
<!DOCTYPE html>
|
|
<html lang="zh-TW">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>設備檢驗規則</title>
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
'teal-800': '#1e4646',
|
|
'teal-200': '#b2f0f0',
|
|
'teal-600': '#00a399',
|
|
'light-blue': '#4c9aff',
|
|
'dark-red': '#cc0000',
|
|
'gray-500': '#6b7280',
|
|
|
|
/* 優化狀態顏色 */
|
|
'status-ok': '#10b981',
|
|
'status-ng': '#ef4444',
|
|
'status-wip': '#f59e0b',
|
|
'status-idle': '#3b82f6',
|
|
|
|
/* 背景色 */
|
|
'bg-ok': '#dcfce7',
|
|
'bg-ng': '#fee2e2',
|
|
'bg-wip': '#fef3c7',
|
|
'bg-idle': '#dbeafe',
|
|
'bg-primary': '#eff6ff', /* blue-50 */
|
|
|
|
/* 綁定相關 */
|
|
'bind-color': '#007bff',
|
|
'bind-bg': '#e7f3ff',
|
|
|
|
'pm-daily-color': '#059669', /* 綠色 */
|
|
'pm-daily-bg': '#d1fae5',
|
|
'pm-weekly-color': '#c2410c', /* 橘色 */
|
|
'pm-weekly-bg': '#fff7ed',
|
|
'pm-monthly-color': '#6d28d9', /* 紫色 */
|
|
'pm-monthly-bg': '#f3e8ff',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
/* 設備監控頁樣式 (與原檔案相同) */
|
|
.filter-button {
|
|
padding: 8px 16px;
|
|
border-radius: 9999px;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
}
|
|
.action-button-mini {
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
transition: background-color 0.15s, opacity 0.15s;
|
|
}
|
|
.filter-button.active {
|
|
background-color: #4c9aff;
|
|
color: white;
|
|
}
|
|
.filter-button.inactive {
|
|
background-color: #e5e7eb;
|
|
color: #1f2937;
|
|
}
|
|
.required-label::after {
|
|
content: ' *';
|
|
color: #ef4444;
|
|
margin-left: 2px;
|
|
}
|
|
.status-pill {
|
|
padding: 4px 10px;
|
|
border-radius: 4px;
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
}
|
|
.bg-status-ok { background-color: var(--tw-colors-bg-ok); color: var(--tw-colors-status-ok); border: 1px solid var(--tw-colors-status-ok); }
|
|
.bg-status-ng { background-color: var(--tw-colors-bg-ng); color: var(--tw-colors-status-ng); border: 1px solid var(--tw-colors-status-ng); }
|
|
.bg-status-wip { background-color: var(--tw-colors-bg-wip); color: var(--tw-colors-status-wip); border: 1px solid var(--tw-colors-status-wip); }
|
|
.bg-status-idle { background-color: var(--tw-colors-bg-idle); color: var(--tw-colors-status-idle); border: 1px solid var(--tw-colors-status-idle); }
|
|
|
|
.bg-pm-daily, .bg-pm-weekly, .bg-pm-monthly {
|
|
background-color: var(--tw-colors-bind-bg) !important;
|
|
color: var(--tw-colors-bind-color) !important;
|
|
border: 1px solid var(--tw-colors-bind-color) !important;
|
|
}
|
|
/* 動態明細樣式 */
|
|
.detail-row-input {
|
|
width: 100%;
|
|
padding: 6px 8px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 4px;
|
|
appearance: none;
|
|
background-color: white;
|
|
}
|
|
.detail-item-container {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background-color: white;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05);
|
|
border: 1px solid #e5e7eb;
|
|
}
|
|
|
|
/* 保養填寫頁樣式 */
|
|
.detail-input-pm {
|
|
padding: 8px;
|
|
border: 1px solid #d1d5db;
|
|
border-radius: 4px;
|
|
width: 100%;
|
|
}
|
|
/* 狀態轉換標籤樣式 */
|
|
.status-transition-tag {
|
|
background-color: var(--tw-colors-bind-bg);
|
|
color: var(--tw-colors-bind-color);
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
margin-right: 4px;
|
|
border: 1px solid var(--tw-colors-bind-color);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-slate-100 p-10">
|
|
|
|
<div id="app">
|
|
|
|
<div v-if="viewMode === 'list'" class="main-content-wrapper">
|
|
|
|
<h1 class="text-3xl font-bold text-teal-800 mb-6 border-b-4 border-teal-200 pb-3">
|
|
⚙️ 設備稼動規則與保養排程
|
|
</h1>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-xl border border-gray-200">
|
|
|
|
<h2 class="text-xl font-bold text-gray-700 mb-4 border-b pb-2">
|
|
設備稼動規格設定
|
|
</h2>
|
|
|
|
<div class="flex space-x-4 mb-6">
|
|
<button
|
|
@click="activeFilter = '生產機台'"
|
|
:class="['filter-button', activeFilter === '生產機台' ? 'active' : 'inactive']"
|
|
>
|
|
生產設備
|
|
</button>
|
|
<button
|
|
@click="activeFilter = 'IOT設備'"
|
|
:class="['filter-button', activeFilter === 'IOT設備' ? 'active' : 'inactive']"
|
|
>
|
|
IOT設備
|
|
</button>
|
|
<button
|
|
@click="activeFilter = '磅秤'"
|
|
:class="['filter-button', activeFilter === '磅秤' ? 'active' : 'inactive']"
|
|
>
|
|
磅秤
|
|
</button>
|
|
<button
|
|
@click="activeFilter = '全部'"
|
|
:class="['filter-button', activeFilter === '全部' ? 'active' : 'inactive']"
|
|
>
|
|
全部
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex justify-between items-center mb-4">
|
|
<input v-model="searchKeyword" placeholder="搜尋設備編號/名稱..." class="p-2 border rounded-lg w-64">
|
|
</div>
|
|
|
|
<table class="min-w-full table-auto border border-gray-200 text-sm">
|
|
<thead class="bg-gray-100">
|
|
<tr>
|
|
<th class="px-3 py-2 border">設備編號</th>
|
|
<th class="px-3 py-2 border">設備名稱</th>
|
|
<th class="px-3 py-2 border">設備類型</th>
|
|
<th class="px-3 py-2 border w-32">表單配置</th>
|
|
<th class="px-3 py-2 border w-48">狀態變更維護</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="equipment in filteredEquipment" :key="equipment.id" class="hover:bg-teal-50">
|
|
<td class="px-3 py-2 border">{{ equipment.no }}</td>
|
|
<td class="px-3 py-2 border">{{ equipment.name }}</td>
|
|
<td class="px-3 py-2 border">{{ equipment.type }}</td>
|
|
|
|
<td class="px-3 py-2 border text-center">
|
|
<button @click="openDialog('bind_pm', equipment)"
|
|
class="action-button-mini bg-bind-color hover:bg-light-blue text-white w-full">
|
|
配置單據 ({{ equipment.maintenanceForms ? Object.keys(equipment.maintenanceForms).length : 0 }})
|
|
</button>
|
|
</td>
|
|
|
|
<td class="px-3 py-2 border text-center">
|
|
<div v-if="!equipment.maintenanceForms || Object.keys(equipment.maintenanceForms).length === 0"
|
|
class="text-gray-500 font-semibold text-sm">
|
|
N/A
|
|
</div>
|
|
<div v-else class="space-y-1">
|
|
<div v-for="(formId, statusKey) in equipment.maintenanceForms" :key="statusKey"
|
|
class="flex items-center justify-center text-sm text-gray-800">
|
|
|
|
<template v-for="(part, pIndex) in statusKey.split('->')" :key="pIndex">
|
|
<span class="font-normal">
|
|
{{ part.trim() }}
|
|
</span>
|
|
<span v-if="pIndex === 0" class="mx-1.5 font-bold text-gray-800">
|
|
→
|
|
</span>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="dialog.mode === 'status_change'" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-60 z-40 p-4">
|
|
<div class="bg-white p-6 rounded-xl shadow-2xl w-11/12 max-w-md border border-gray-200">
|
|
<h2 class="text-2xl font-bold text-teal-700 mb-4 border-b pb-2">
|
|
Dialog-設備狀態變更 ({{ dialog.data.no }})
|
|
</h2>
|
|
<div class="mb-6 space-y-4">
|
|
<p>當前狀態:<span :class="['status-pill', getStatusColor(dialog.data.status)]">{{ dialog.data.status }}</span></p>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<label class="block mb-1 font-semibold required-label">目標狀態:</label>
|
|
<div class="relative">
|
|
<select v-model="dialog.statusChange.newStatus" class="w-full p-2 border rounded-lg appearance-none bg-white">
|
|
<option v-for="status in availableStatuses(dialog.data.status)" :key="status" :value="status">
|
|
{{ status }}
|
|
</option>
|
|
</select>
|
|
<svg class="w-5 h-5 absolute right-2 top-1/2 transform -translate-y-1/2 pointer-events-none text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block mb-1 font-semibold required-label">變更原因/紀錄:</label>
|
|
<textarea v-model="dialog.statusChange.reason" class="w-full p-2 border rounded-lg" rows="3" placeholder="請說明狀態變更的原因"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border-t pt-4">
|
|
<h3 class="text-sm font-bold text-gray-700 mb-2">最近變更紀錄</h3>
|
|
<div class="h-20 overflow-y-auto bg-gray-50 p-2 text-xs rounded">
|
|
<p v-for="(log, i) in dialog.data.statusLog" :key="i" class="mb-1 text-gray-600">
|
|
[{{ log.time }}] {{ log.oldStatus }} -> {{ log.newStatus }} (原因: {{ log.reason }})
|
|
</p>
|
|
<p v-if="!dialog.data.statusLog || dialog.data.statusLog.length === 0" class="text-gray-400">尚無狀態變更紀錄</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3 mt-4">
|
|
<button @click="closeDialog" class="px-5 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300">取消</button>
|
|
<button @click="saveEquipmentStatusChange" class="px-5 py-2 bg-teal-600 text-white rounded hover:bg-teal-700">確認變更</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="dialog.mode === 'bind_pm'" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-60 z-40 p-4">
|
|
<div class="bg-white p-6 rounded-xl shadow-2xl w-11/12 max-w-xl border border-gray-200">
|
|
<h2 class="text-2xl font-bold text-bind-color mb-4 border-b pb-2">
|
|
📋 配置設備狀態轉換卡控單據 ({{ dialog.data.no }})
|
|
</h2>
|
|
<div class="mb-6 space-y-4">
|
|
<p class="text-sm text-gray-600 mb-4">設備名稱:<span class="font-bold">{{ dialog.data.name }}</span></p>
|
|
|
|
<h3 class="text-lg font-bold text-gray-700 mb-3 border-t pt-3">保養單據卡控規則設定</h3>
|
|
<p class="text-sm text-gray-500 mb-4">請設定當設備**狀態轉換**發生時,需強制執行的保養單據。</p>
|
|
|
|
<div class="space-y-3 max-h-96 overflow-y-auto p-1 border rounded-lg bg-gray-50">
|
|
|
|
<div v-for="(formId, status) in dialog.data.maintenanceForms" :key="status" class="flex items-center space-x-2 bg-white p-3 rounded shadow-sm border border-gray-200">
|
|
|
|
<div class="w-1/3">
|
|
<span class="status-transition-tag">{{ status }}</span>
|
|
</div>
|
|
|
|
<div class="flex-1 relative">
|
|
<select v-model="dialog.data.maintenanceForms[status]" class="detail-row-input">
|
|
<option :value="null">-- 取消綁定此狀態轉換 --</option>
|
|
<option v-for="form in pmFormPool" :key="form.id" :value="form.id">
|
|
{{ form.name }} (週期: {{ form.type }})
|
|
</option>
|
|
</select>
|
|
<svg class="w-4 h-4 absolute right-2 top-1/2 transform -translate-y-1/2 pointer-events-none text-gray-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
|
</div>
|
|
|
|
<button @click="removePMFormBinding(status)" class="text-dark-red hover:text-red-700 p-1">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 6v10h6V6H7z" clip-rule="evenodd" /></svg>
|
|
</button>
|
|
|
|
</div>
|
|
|
|
<div v-if="!dialog.data.maintenanceForms || Object.keys(dialog.data.maintenanceForms).length === 0" class="p-3 text-center text-gray-500">
|
|
目前沒有設定任何單據卡控點。
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<h4 class="text-md font-semibold text-gray-700 pt-3 border-t">新增卡控點</h4>
|
|
<div class="flex space-x-2">
|
|
<div class="relative w-1/3">
|
|
<select v-model="dialog.tempBinding.fromStatus" class="detail-row-input">
|
|
<option value="">-- 起始狀態 --</option>
|
|
<option v-for="status in Object.keys(STATUS_MAP)" :key="status" :value="status">
|
|
{{ status }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="relative w-1/3">
|
|
<select v-model="dialog.tempBinding.toStatus" class="detail-row-input">
|
|
<option value="">-- 目標狀態 --</option>
|
|
<option v-for="status in Object.keys(STATUS_MAP).filter(s => s !== dialog.tempBinding.fromStatus)" :key="status" :value="status">
|
|
{{ status }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="relative w-1/3">
|
|
<button @click="addPMFormBinding" class="px-3 py-1 bg-light-blue text-white rounded-lg hover:bg-blue-600 transition h-full w-full" :disabled="!dialog.tempBinding.fromStatus || !dialog.tempBinding.toStatus">
|
|
新增卡控點
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3 mt-4">
|
|
<button @click="closeDialog" class="px-5 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300">取消</button>
|
|
<button @click="saveMaintenanceBinding" class="px-5 py-2 bg-teal-600 text-white rounded hover:bg-teal-700">確認綁定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="dialog.mode === 'pm_prompt'" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-60 z-50 p-4">
|
|
<div class="bg-white p-6 rounded-xl shadow-2xl w-11/12 max-w-lg border border-gray-200">
|
|
<h2 class="text-2xl font-bold text-orange-600 mb-4 border-b pb-2">
|
|
🚨 狀態變更流程卡控!
|
|
</h2>
|
|
<p class="mb-6 text-gray-700">設備 **{{ dialog.data.no }}** 將由 [{{ dialog.data._pendingOldStatus }}] 變更為 [{{ dialog.data._pendingNewStatus }}],請確認是否需填寫以下**保養單**:</p>
|
|
|
|
<ul class="space-y-2 mb-6">
|
|
<li v-for="pm in dialog.data.pendingPM" :key="pm.id"
|
|
:class="['p-2 rounded border font-semibold', getStatusColor(pm.type)]">
|
|
✅ 點檢表:{{ pm.name }} (週期: {{ pm.type }})
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="flex justify-end space-x-3">
|
|
<button @click="closeDialogAndExecuteStatusChange(dialog.data)" class="px-5 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300">略過 (不推薦)</button>
|
|
<button @click="navigateToPMEntry(dialog.data.no, dialog.data.name, dialog.data.pendingPM?.[0], dialog.data._pendingNewStatus, dialog.data._pendingReason, dialog.data._pendingOldStatus)" class="px-5 py-2 bg-orange-500 text-white rounded hover:bg-orange-600">立即填寫</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="viewMode === 'pm_entry'" class="main-content-wrapper">
|
|
<h1 class="text-3xl font-bold text-teal-800 mb-6 border-b-4 border-teal-200 pb-3">
|
|
📝 設備保養數據填寫
|
|
</h1>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-xl border border-gray-200">
|
|
|
|
<h2 class="text-xl font-bold text-gray-700 mb-4">
|
|
保養工單:<span class="text-light-blue">{{ pmEntryForm.pmFormName }}</span>
|
|
</h2>
|
|
|
|
<div class="grid grid-cols-2 gap-4 mb-6 pb-4 border-b">
|
|
<div><span class="font-semibold">設備編號:</span><span class="text-teal-600">{{ pmEntryForm.equipmentNo }}</span></div>
|
|
<div><span class="font-semibold">設備名稱:</span><span>{{ pmEntryForm.equipmentName }}</span></div>
|
|
<div><span class="font-semibold">保養類型:</span><span>{{ pmEntryForm.pmFormType }}</span></div>
|
|
<div><span class="font-semibold">保養人員:</span><input v-model="pmEntryForm.inspector" class="detail-input-pm w-2/3" placeholder="請輸入姓名/工號"></div>
|
|
</div>
|
|
|
|
<h3 class="text-lg font-bold text-gray-700 mb-3">細項點檢與記錄</h3>
|
|
|
|
<table class="min-w-full table-auto border border-gray-200 text-sm">
|
|
<thead class="bg-gray-100">
|
|
<tr>
|
|
<th class="px-3 py-2 border w-1/4">檢查內容</th>
|
|
<th class="px-3 py-2 border w-1/4">標準/規格</th>
|
|
<th class="px-3 py-2 border w-1/6">方法</th>
|
|
<th class="px-3 py-2 border w-1/6 required-label">結果</th>
|
|
<th class="px-3 py-2 border w-1/6">備註</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(detail, index) in pmEntryForm.details" :key="index">
|
|
<td class="px-3 py-2 border">{{ detail.content }}</td>
|
|
<td class="px-3 py-2 border">{{ detail.standard }}</td>
|
|
<td class="px-3 py-2 border">{{ detail.method }}</td>
|
|
<td class="px-3 py-2 border">
|
|
<div v-if="detail.inputType === 'PASS/FAIL'">
|
|
<select v-model="detail.result" class="detail-input-pm">
|
|
<option value="">-- 選擇 --</option>
|
|
<option value="PASS">PASS</option>
|
|
<option value="FAIL">FAIL</option>
|
|
</select>
|
|
</div>
|
|
<div v-else>
|
|
<input v-model="detail.result" :type="detail.inputType === '數值' ? 'number' : 'text'" class="detail-input-pm" placeholder="填寫數值或文字">
|
|
</div>
|
|
</td>
|
|
<td class="px-3 py-2 border">
|
|
<input v-model="detail.notes" type="text" class="detail-input-pm" placeholder="選填">
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="flex justify-end space-x-3 mt-6">
|
|
<button @click="viewMode = 'list'" class="px-5 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300">取消/返回</button>
|
|
<button @click="submitPMForm" class="px-5 py-2 bg-teal-600 text-white rounded hover:bg-teal-700">提交保養數據並變更狀態</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp, ref, reactive, computed } = Vue;
|
|
|
|
createApp({
|
|
setup() {
|
|
// --- 視圖模式管理 ---
|
|
const viewMode = ref('list'); // 'list' (列表) | 'pm_entry' (保養填寫)
|
|
|
|
const activeFilter = ref('全部');
|
|
const searchKeyword = ref('');
|
|
|
|
// 狀態顏色映射
|
|
const STATUS_MAP = {
|
|
'正常運行': 'bg-status-ok',
|
|
'待機': 'bg-status-idle',
|
|
'維護中': 'bg-status-wip',
|
|
'故障': 'bg-status-ng',
|
|
};
|
|
|
|
// 模擬保養單細項數據
|
|
const mockPmDetails = [
|
|
{ content: '液壓油位', standard: '50%~80%', method: '目視', inputType: '數值', result: '', notes: '' },
|
|
{ content: '清潔濾網', standard: '無灰塵', method: '目視', inputType: 'PASS/FAIL', result: '', notes: '' },
|
|
{ content: '軸承溫度', standard: '< 50°C', method: '量測', inputType: '數值', result: '', notes: '' },
|
|
];
|
|
|
|
// 模擬保養單池 (所有可綁定的保養單)
|
|
const pmFormPool = ref([
|
|
{ id: 1, name: '日點檢-基本', type: '每日', details: mockPmDetails },
|
|
{ id: 2, name: '週保養-潤滑', type: '每週', details: mockPmDetails },
|
|
{ id: 3, name: '月保養-校正', type: '每月', details: mockPmDetails },
|
|
{ id: 4, name: '季點檢-大修', type: '每季', details: mockPmDetails },
|
|
]);
|
|
|
|
// 設備主檔數據
|
|
const equipmentRecords = ref([
|
|
{
|
|
id: 1, no: 'EQP-CNC-001', name: 'CNC機床A', type: '生產機台', status: '正常運行',
|
|
pmHours: 500,
|
|
maintenanceForms: {
|
|
'維護中->正常運行': 1,
|
|
'故障->維護中': 3,
|
|
},
|
|
statusLog: [{time: '14:30', oldStatus: '故障', newStatus: '維護中', reason: '報修單001處理中'}]
|
|
},
|
|
{
|
|
id: 2, no: 'EQP-LPR-002', name: '雷射雕刻機', type: '生產機台', status: '待機',
|
|
pmHours: 120,
|
|
maintenanceForms: {},
|
|
statusLog: [{time: '15:00', oldStatus: '正常運行', newStatus: '待機', reason: '閒置切換'}]
|
|
},
|
|
{
|
|
id: 3, no: 'IOT-TEMP-001', name: '溫度感應器', type: 'IOT設備', status: '正常運行',
|
|
pmHours: 0,
|
|
maintenanceForms: { '維護中->正常運行': 2 },
|
|
statusLog: []
|
|
},
|
|
{
|
|
id: 4, no: 'IOT-HUM-002', name: '濕度監測器', type: 'IOT設備', status: '維護中',
|
|
pmHours: 10,
|
|
maintenanceForms: {},
|
|
statusLog: [{time: '16:00', oldStatus: '正常運行', newStatus: '維護中', reason: '電池更換'}]
|
|
},
|
|
{
|
|
id: 5, no: 'WGH-001', name: '成品磅秤-A', type: '磅秤', status: '故障',
|
|
pmHours: 450,
|
|
maintenanceForms: {},
|
|
statusLog: [{time: '10:00', oldStatus: '正常運行', newStatus: '故障', reason: '開機失敗'}]
|
|
},
|
|
]);
|
|
|
|
const dialog = reactive({
|
|
mode: null,
|
|
data: {},
|
|
statusChange: { newStatus: '', reason: '' },
|
|
tempBinding: { fromStatus: '', toStatus: '' },
|
|
});
|
|
|
|
// 保養填寫表單的數據結構
|
|
const pmEntryForm = reactive({
|
|
equipmentNo: '',
|
|
equipmentName: '',
|
|
pmFormId: '',
|
|
pmFormName: '',
|
|
pmFormType: '',
|
|
inspector: '',
|
|
details: [],
|
|
_pendingNewStatus: '',
|
|
_pendingReason: '',
|
|
_pendingOldStatus: '',
|
|
});
|
|
|
|
|
|
// --- Computed & Helpers ---
|
|
|
|
const getPMFormInfo = (id) => {
|
|
const form = pmFormPool.value.find(f => f.id === id);
|
|
return form ? { name: form.name, type: form.type, id: form.id } : { name: '未知單據', type: 'N/A', id: null };
|
|
};
|
|
|
|
// 狀態顏色輔助函數
|
|
const getStatusColor = (status) => STATUS_MAP[status] || 'bg-gray-500';
|
|
|
|
// 篩選與搜尋邏輯
|
|
const filteredEquipment = computed(() => {
|
|
let records = equipmentRecords.value;
|
|
const keyword = searchKeyword.value.toLowerCase();
|
|
const filterType = activeFilter.value;
|
|
|
|
if (filterType !== '全部') {
|
|
records = records.filter(eq => eq.type === filterType);
|
|
}
|
|
if (keyword) {
|
|
records = records.filter(eq =>
|
|
eq.no.toLowerCase().includes(keyword) ||
|
|
eq.name.toLowerCase().includes(keyword) ||
|
|
eq.status.toLowerCase().includes(keyword) ||
|
|
(eq.pmHours !== undefined && String(eq.pmHours).includes(keyword))
|
|
);
|
|
}
|
|
return records;
|
|
});
|
|
|
|
// 狀態變更彈窗中可選的目標狀態
|
|
const availableStatuses = (currentStatus) => {
|
|
return Object.keys(STATUS_MAP).filter(s => s !== currentStatus);
|
|
};
|
|
|
|
|
|
// --- 單據卡控綁定邏輯 ---
|
|
|
|
const addPMFormBinding = () => {
|
|
const { fromStatus, toStatus } = dialog.tempBinding;
|
|
if (!fromStatus || !toStatus) {
|
|
alert('請選擇起始狀態和目標狀態!');
|
|
return;
|
|
}
|
|
|
|
const key = `${fromStatus}->${toStatus}`;
|
|
if (dialog.data.maintenanceForms[key]) {
|
|
alert(`錯誤:${key} 的卡控點已存在!`);
|
|
return;
|
|
}
|
|
|
|
const defaultFormId = pmFormPool.value[0]?.id || null;
|
|
|
|
dialog.data.maintenanceForms[key] = defaultFormId;
|
|
dialog.tempBinding.fromStatus = '';
|
|
dialog.tempBinding.toStatus = '';
|
|
};
|
|
|
|
const removePMFormBinding = (statusKey) => {
|
|
if (confirm(`確定要移除狀態轉換 ${statusKey} 的保養單據綁定嗎?`)) {
|
|
delete dialog.data.maintenanceForms[statusKey];
|
|
}
|
|
};
|
|
|
|
// 儲存保養綁定
|
|
const saveMaintenanceBinding = () => {
|
|
const data = dialog.data;
|
|
const index = equipmentRecords.value.findIndex(e => e.id === data.id);
|
|
|
|
const cleanedForms = {};
|
|
for (const key in data.maintenanceForms) {
|
|
if (data.maintenanceForms[key] !== null && data.maintenanceForms[key] !== '') {
|
|
if (!pmFormPool.value.find(f => f.id === data.maintenanceForms[key])) {
|
|
alert(`錯誤:狀態轉換 ${key} 綁定的保養單 ID 無效!`);
|
|
return;
|
|
}
|
|
cleanedForms[key] = data.maintenanceForms[key];
|
|
}
|
|
}
|
|
|
|
if (index !== -1) {
|
|
equipmentRecords.value[index].maintenanceForms = cleanedForms;
|
|
alert(`設備 ${data.no} 的保養單據綁定已更新 (${Object.keys(cleanedForms).length} 項卡控點)。`);
|
|
}
|
|
closeDialog();
|
|
};
|
|
|
|
// 儲存 PM 時數設定 (保留程式功能)
|
|
const savePmConfig = () => {
|
|
const data = dialog.data;
|
|
const pmHours = parseFloat(data.pmHours);
|
|
|
|
if (isNaN(pmHours) || pmHours < 0) {
|
|
alert('PM 時數必須是一個非負數值!');
|
|
return;
|
|
}
|
|
|
|
const index = equipmentRecords.value.findIndex(e => e.id === data.id);
|
|
if (index !== -1) {
|
|
equipmentRecords.value[index].pmHours = pmHours;
|
|
alert(`設備 ${data.no} 的 PM 時數已更新為 ${pmHours} Hr。`);
|
|
}
|
|
closeDialog();
|
|
};
|
|
|
|
|
|
// --- 導航與狀態邏輯 ---
|
|
|
|
// 導航到保養單填寫介面
|
|
const navigateToPMEntry = (equipmentNo, equipmentName, pmForm, newStatus, reason, oldStatus) => {
|
|
closeDialog();
|
|
if (!pmForm) return;
|
|
const formTemplate = pmFormPool.value.find(f => f.id === pmForm.id);
|
|
|
|
Object.assign(pmEntryForm, {
|
|
equipmentNo: equipmentNo,
|
|
equipmentName: equipmentName,
|
|
pmFormId: pmForm.id,
|
|
pmFormName: pmForm.name,
|
|
pmFormType: pmForm.type,
|
|
inspector: '',
|
|
details: JSON.parse(JSON.stringify(formTemplate.details)),
|
|
_pendingNewStatus: newStatus,
|
|
_pendingReason: reason,
|
|
_pendingOldStatus: oldStatus,
|
|
});
|
|
|
|
viewMode.value = 'pm_entry';
|
|
};
|
|
|
|
|
|
// 打開 Dialog
|
|
const openDialog = (mode, equipment = {}) => {
|
|
dialog.mode = mode;
|
|
const eqData = JSON.parse(JSON.stringify(equipment));
|
|
dialog.data = eqData;
|
|
|
|
if (mode === 'status_change') {
|
|
dialog.statusChange.newStatus = availableStatuses(eqData.status)[0] || '';
|
|
dialog.statusChange.reason = '';
|
|
} else if (mode === 'bind_pm') {
|
|
if (!dialog.data.maintenanceForms) {
|
|
dialog.data.maintenanceForms = {};
|
|
}
|
|
dialog.tempBinding = { fromStatus: '', toStatus: '' };
|
|
} else if (mode === 'pm_config') {
|
|
dialog.data.pmHours = eqData.pmHours !== undefined ? eqData.pmHours : 0;
|
|
dialog.data.name = eqData.name;
|
|
}
|
|
};
|
|
|
|
// 關閉 Dialog
|
|
const closeDialog = () => {
|
|
dialog.mode = null;
|
|
dialog.data = {};
|
|
};
|
|
|
|
// 略過單據,直接執行狀態變更
|
|
const closeDialogAndExecuteStatusChange = (data) => {
|
|
const index = equipmentRecords.value.findIndex(e => e.id === data.id);
|
|
if (index === -1) return;
|
|
|
|
const currentRecord = equipmentRecords.value[index];
|
|
const newStatus = data._pendingNewStatus;
|
|
const oldStatus = data._pendingOldStatus;
|
|
const reason = data._pendingReason;
|
|
|
|
currentRecord.status = newStatus;
|
|
const logEntry = {
|
|
time: new Date().toLocaleTimeString(),
|
|
oldStatus: oldStatus,
|
|
newStatus: newStatus,
|
|
reason: `[跳過保養單據] ${reason}`
|
|
};
|
|
currentRecord.statusLog.unshift(logEntry);
|
|
alert(`設備 ${data.no} 狀態已直接變更為 ${newStatus}。`);
|
|
|
|
closeDialog();
|
|
};
|
|
|
|
|
|
// 儲存設備狀態變更 (核心邏輯,加入卡控檢查)
|
|
const saveEquipmentStatusChange = () => {
|
|
const data = dialog.data;
|
|
const { newStatus, reason } = dialog.statusChange;
|
|
|
|
if (!newStatus || !reason) {
|
|
alert('請選擇目標狀態並填寫變更原因!');
|
|
return;
|
|
}
|
|
|
|
const index = equipmentRecords.value.findIndex(e => e.id === data.id);
|
|
if (index === -1) return;
|
|
|
|
const currentRecord = equipmentRecords.value[index];
|
|
const oldStatus = currentRecord.status;
|
|
|
|
// 狀態轉換卡控檢查
|
|
const transitionKey = `${oldStatus}->${newStatus}`;
|
|
const requiredFormId = currentRecord.maintenanceForms ? currentRecord.maintenanceForms[transitionKey] : null;
|
|
|
|
if (requiredFormId) {
|
|
const requiredFormInfo = getPMFormInfo(requiredFormId);
|
|
|
|
data._pendingNewStatus = newStatus;
|
|
data._pendingReason = reason;
|
|
data._pendingOldStatus = oldStatus;
|
|
|
|
closeDialog();
|
|
openDialog('pm_prompt', {
|
|
no: data.no, name: data.name, id: data.id,
|
|
pendingPM: [requiredFormInfo],
|
|
_pendingNewStatus: newStatus,
|
|
_pendingReason: reason,
|
|
_pendingOldStatus: oldStatus,
|
|
});
|
|
return;
|
|
}
|
|
|
|
currentRecord.status = newStatus;
|
|
const logEntry = {
|
|
time: new Date().toLocaleTimeString(),
|
|
oldStatus: oldStatus,
|
|
newStatus: newStatus,
|
|
reason: reason
|
|
};
|
|
currentRecord.statusLog.unshift(logEntry);
|
|
alert(`設備 ${data.no} 狀態已變更為 ${newStatus}。`);
|
|
|
|
closeDialog();
|
|
};
|
|
|
|
|
|
// 提交保養單的邏輯
|
|
const submitPMForm = () => {
|
|
if (!pmEntryForm.inspector || pmEntryForm.details.some(d => !d.result)) {
|
|
alert('請填寫保養人員及所有點檢結果!');
|
|
return;
|
|
}
|
|
|
|
alert(`✅ 保養數據提交成功!設備: ${pmEntryForm.equipmentNo}, 清單: ${pmEntryForm.pmFormName}`);
|
|
|
|
const eqIndex = equipmentRecords.value.findIndex(e => e.no === pmEntryForm.equipmentNo);
|
|
if (eqIndex !== -1) {
|
|
const currentRecord = equipmentRecords.value[eqIndex];
|
|
|
|
const newStatus = pmEntryForm._pendingNewStatus;
|
|
const oldStatus = pmEntryForm._pendingOldStatus;
|
|
const reason = `[${pmEntryForm.pmFormName}] 填寫完成,狀態變更為 ${newStatus}。 (原原因: ${pmEntryForm._pendingReason})`;
|
|
|
|
currentRecord.status = newStatus;
|
|
|
|
const logEntry = {
|
|
time: new Date().toLocaleTimeString(),
|
|
oldStatus: oldStatus,
|
|
newStatus: newStatus,
|
|
reason: reason
|
|
};
|
|
currentRecord.statusLog.unshift(logEntry);
|
|
}
|
|
|
|
viewMode.value = 'list';
|
|
};
|
|
|
|
|
|
return {
|
|
Object, // <--- 關鍵!將 Object 全域對象暴露給 template,解讀 Object.keys 報錯
|
|
viewMode,
|
|
activeFilter,
|
|
searchKeyword,
|
|
filteredEquipment,
|
|
dialog,
|
|
pmFormPool,
|
|
pmEntryForm,
|
|
STATUS_MAP,
|
|
openDialog,
|
|
closeDialog,
|
|
getStatusColor,
|
|
availableStatuses,
|
|
saveEquipmentStatusChange,
|
|
saveMaintenanceBinding,
|
|
savePmConfig,
|
|
|
|
addPMFormBinding,
|
|
removePMFormBinding,
|
|
closeDialogAndExecuteStatusChange,
|
|
navigateToPMEntry,
|
|
submitPMForm,
|
|
};
|
|
}
|
|
}).mount('#app');
|
|
</script>
|
|
</body>
|
|
</html>
|