|
<!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',
|
|
'teal-50': '#f0faff',
|
|
'blue-500': '#3b82f6',
|
|
'blue-600': '#2563eb',
|
|
'red-500': '#ef4444',
|
|
'green-600': '#10b981',
|
|
'gray-100': '#f3f4f6',
|
|
'orange-500': '#f97316',
|
|
'orange-600': '#ea580c',
|
|
'purple-600': '#9333ea',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body class="bg-slate-100 p-6 md:p-10">
|
|
|
|
<div id="app" class="max-w-7xl mx-auto bg-white p-8 rounded-2xl shadow-lg border border-gray-200">
|
|
|
|
<h1 class="text-2xl font-bold text-gray-800 mb-6 pb-2 border-b">修改途程</h1>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8 text-sm">
|
|
<div>
|
|
<div class="flex justify-between mb-1.5">
|
|
<span class="text-gray-500">狀態:已發布</span>
|
|
<span class="text-gray-500">版本:ver1</span>
|
|
</div>
|
|
<label class="block mb-1 font-bold text-gray-700">目標產品編號:</label>
|
|
<input type="text" value="FKGZ8940111" readonly class="w-full p-2.5 bg-gray-100 border border-gray-300 rounded-lg text-gray-700 font-mono focus:outline-none">
|
|
</div>
|
|
<div>
|
|
<div class="mb-1.5 h-5"></div> <label class="block mb-1 font-bold text-gray-700"><span class="text-red-500">*</span> 產品名稱:</label>
|
|
<input type="text" value="蔡司邁儿康Pro角膜塑形用硬性透氣接觸鏡-環曲-藍" readonly class="w-full p-2.5 bg-gray-100 border border-gray-300 rounded-lg text-gray-700 focus:outline-none">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 途程站點列表 -->
|
|
<div class="overflow-x-auto rounded-xl border border-gray-200 shadow-sm">
|
|
<table class="min-w-full text-sm text-left text-gray-500">
|
|
<thead class="bg-gray-50 text-gray-700 uppercase font-semibold">
|
|
<tr>
|
|
<th class="px-4 py-3 w-16 text-center">順序</th>
|
|
<th class="px-4 py-3">生產站點名稱</th>
|
|
<th class="px-4 py-3 w-1/4">可用機台</th>
|
|
<th class="px-4 py-3 text-center">標籤</th>
|
|
<th class="px-4 py-3 text-center">APP TYPE</th>
|
|
<th class="px-4 py-3 text-center">配置設定</th>
|
|
<th class="px-4 py-3 text-center">刷單數量</th>
|
|
<th class="px-4 py-3 text-center w-52 bg-teal-50/50 text-teal-800 border-x border-teal-100/50">重工退回站點</th>
|
|
<th class="px-4 py-3 text-center w-20">跳站</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
<tr v-for="(station, index) in routingStations" :key="station.seq" class="hover:bg-slate-50 transition duration-150">
|
|
|
|
<td class="px-4 py-5 text-center font-bold text-gray-800">{{ station.seq }}</td>
|
|
|
|
<td class="px-4 py-5 font-bold text-gray-700">{{ station.name }}</td>
|
|
|
|
<td class="px-4 py-5">
|
|
<div class="flex flex-wrap gap-1">
|
|
<span v-for="m in station.machines" :key="m" class="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded border border-blue-100">
|
|
{{ m }}
|
|
</span>
|
|
<span v-if="station.moreCount" class="px-2 py-0.5 bg-gray-100 text-gray-500 text-xs rounded border border-gray-200">
|
|
+{{ station.moreCount }}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
|
|
<td class="px-4 py-5 text-center text-gray-400">—</td>
|
|
|
|
<td class="px-4 py-5 text-center">
|
|
<span :class="{
|
|
'bg-sky-100 text-sky-700': station.appType === 'insp',
|
|
'bg-indigo-100 text-indigo-700': station.appType === 'ASSEMBLE',
|
|
'bg-amber-100 text-amber-700': station.appType === 'CLOCK',
|
|
'bg-orange-100 text-orange-700': station.appType === 'FEEDING',
|
|
'bg-teal-100 text-teal-700': station.appType === 'IOT',
|
|
'bg-emerald-100 text-emerald-700': station.appType === 'MATERIAL',
|
|
'bg-purple-100 text-purple-700': station.appType === 'SOP',
|
|
'bg-rose-100 text-rose-700': station.appType === 'WAREHOUSE'
|
|
}" class="px-2.5 py-1 rounded text-xs font-semibold uppercase">
|
|
{{ station.appType }}
|
|
</span>
|
|
</td>
|
|
|
|
<!-- 配置設定 -->
|
|
<td class="px-4 py-5 text-center">
|
|
<div v-if="['insp', 'ASSEMBLE', 'CLOCK', 'FEEDING', 'IOT', 'MATERIAL', 'SOP'].includes(station.appType)"
|
|
@click="openConfigDialog(station)"
|
|
class="inline-flex items-center gap-1 cursor-pointer hover:bg-slate-100 px-2.5 py-1 rounded-lg transition duration-150 border border-transparent hover:border-gray-200">
|
|
<template v-if="station.qcStatus === '已配置'">
|
|
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
|
<span class="text-green-600 font-medium decoration-dotted underline underline-offset-4">已配置</span>
|
|
</template>
|
|
<template v-else>
|
|
<span class="w-2 h-2 rounded-full bg-gray-400"></span>
|
|
<span class="text-gray-400 font-medium decoration-dotted underline underline-offset-4">未配置</span>
|
|
</template>
|
|
</div>
|
|
<div v-else class="inline-flex items-center gap-1 text-gray-400">
|
|
—
|
|
</div>
|
|
</td>
|
|
|
|
<td class="px-4 py-5 text-center font-semibold text-blue-600">
|
|
{{ station.swipeQty }}
|
|
</td>
|
|
|
|
<td class="px-4 py-5 bg-teal-50/20 border-x border-teal-100/50 text-center">
|
|
<span v-if="index === 0" class="text-xs text-gray-400 italic">
|
|
第一站無可退回站點
|
|
</span>
|
|
|
|
<select v-else
|
|
v-model="station.reworkReturnSeq"
|
|
class="w-full max-w-[180px] p-1.5 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-teal-600 focus:border-teal-600">
|
|
<option :value="null">-- 無重工需求 --</option>
|
|
<option v-for="prevStation in getPreviousStations(index)" :key="prevStation.seq" :value="prevStation.seq">
|
|
退回第 {{ prevStation.seq }} 站 ({{ prevStation.name }})
|
|
</option>
|
|
</select>
|
|
</td>
|
|
|
|
<td class="px-4 py-5 text-center">
|
|
<input type="checkbox" v-model="station.canSkip" class="w-4 h-4 text-teal-600 border-gray-300 rounded focus:ring-teal-500 focus:ring-2">
|
|
</td>
|
|
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-8 flex justify-end gap-3">
|
|
<button @click="saveRouting" class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-lg transition duration-150 text-sm shadow-sm">
|
|
儲存
|
|
</button>
|
|
<button class="px-6 py-2 bg-white hover:bg-gray-50 text-gray-700 border border-gray-300 rounded-lg transition duration-150 text-sm shadow-sm">
|
|
取消
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ==================== 1. INSP 配置對話框 ==================== -->
|
|
<div v-if="activeDialog === 'insp'" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 max-w-md w-full overflow-visible transform transition-all">
|
|
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-slate-50">
|
|
<h3 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
|
<span class="w-2.5 h-2.5 rounded-full bg-sky-500"></span>
|
|
配置檢驗站設定
|
|
</h3>
|
|
<button @click="closeDialog" class="text-gray-400 hover:text-gray-600 focus:outline-none text-xl">×</button>
|
|
</div>
|
|
|
|
<div class="p-6 space-y-4">
|
|
<div class="relative">
|
|
<label class="block mb-1.5 font-bold text-gray-700 text-sm">
|
|
<span class="text-red-500">*</span> 綁定檢驗表單:
|
|
</label>
|
|
<div class="relative">
|
|
<input type="text" v-model="searchQuery" @focus="showDropdown = true" placeholder="請輸入關鍵字搜尋或點擊選擇..." class="w-full p-2.5 pr-10 border border-gray-300 rounded-lg text-sm bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500 cursor-text">
|
|
<div class="absolute right-3 top-3 flex items-center gap-1.5 text-gray-400">
|
|
<button v-if="tempConfig.formId" @click.stop="clearSelection" class="hover:text-gray-600 text-sm">×</button>
|
|
<svg class="w-4 h-4 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showDropdown" @click="showDropdown = false" class="fixed inset-0 z-10"></div>
|
|
|
|
<ul v-if="showDropdown" class="absolute z-20 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto py-1 text-sm text-gray-700">
|
|
<li v-for="form in filteredInspForms" :key="form.id" @click="selectForm(form)" class="px-4 py-2.5 hover:bg-sky-50 cursor-pointer flex justify-between items-center transition duration-100" :class="{'bg-sky-50 font-semibold text-sky-700': tempConfig.formId === form.id}">
|
|
<span>{{ form.code }} - {{ form.name }}</span>
|
|
<span v-if="tempConfig.formId === form.id" class="text-sky-600 font-bold">✓</span>
|
|
</li>
|
|
<li v-if="filteredInspForms.length === 0" class="px-4 py-3 text-gray-400 text-xs text-center italic">找不到符合「{{ searchQuery }}」的表單</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end gap-2">
|
|
<button @click="closeDialog" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-700 border border-gray-300 rounded-lg text-xs font-semibold transition duration-150">取消</button>
|
|
<button @click="saveInspConfig" :disabled="!tempConfig.formId" class="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition duration-150">確認配置</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== 2. ASSEMBLE 配置對話框 ==================== -->
|
|
<div v-if="activeDialog === 'ASSEMBLE'" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 max-w-xl w-full overflow-hidden transform transition-all flex flex-col max-h-[85vh]">
|
|
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-slate-50 flex-shrink-0">
|
|
<h3 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
|
<span class="w-2.5 h-2.5 rounded-full bg-indigo-500"></span>
|
|
配置組裝步驟設定
|
|
</h3>
|
|
<button @click="closeDialog" class="text-gray-400 hover:text-gray-600 focus:outline-none text-xl">×</button>
|
|
</div>
|
|
|
|
<div class="p-6 overflow-y-auto space-y-4 flex-grow">
|
|
<div class="flex justify-between items-center">
|
|
<p class="text-xs text-gray-500">請設定此組裝站的標準作業步驟:</p>
|
|
<button @click="addAssembleStep" class="px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-semibold transition duration-150 flex items-center gap-1 shadow-sm">
|
|
<span>+</span> 新增步驟
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div v-for="(step, sIdx) in tempAssembleSteps" :key="step.tempId"
|
|
class="p-4 bg-slate-50 border border-gray-200 rounded-xl flex items-start gap-3 relative hover:border-indigo-300 transition duration-150">
|
|
|
|
<div class="w-8 h-8 rounded-lg bg-green-100 text-green-600 flex items-center justify-center flex-shrink-0 font-bold text-lg mt-1 shadow-inner">
|
|
✓
|
|
</div>
|
|
|
|
<div class="flex-grow grid grid-cols-1 gap-2 text-xs">
|
|
<div>
|
|
<label class="block mb-1 text-gray-500 font-semibold">步驟 {{ sIdx + 1 }} 名稱:</label>
|
|
<input type="text" v-model="step.title" placeholder="例如:安裝底座螺絲 (4顆)" class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-800">
|
|
</div>
|
|
<div>
|
|
<label class="block mb-1 text-gray-500 font-semibold">規格要求 / 說明:</label>
|
|
<input type="text" v-model="step.desc" placeholder="例如:扭力要求: 2.5 Nm" class="w-full p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-600">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-1.5 self-center">
|
|
<button @click="moveAssembleStep(sIdx, -1)" :disabled="sIdx === 0" class="p-1 hover:bg-gray-200 rounded text-gray-500 disabled:opacity-30 disabled:hover:bg-transparent">▲</button>
|
|
<button @click="moveAssembleStep(sIdx, 1)" :disabled="sIdx === tempAssembleSteps.length - 1" class="p-1 hover:bg-gray-200 rounded text-gray-500 disabled:opacity-30 disabled:hover:bg-transparent">▼</button>
|
|
<button @click="removeAssembleStep(sIdx)" class="p-1 hover:bg-red-100 hover:text-red-500 rounded text-gray-400 mt-1 transition duration-100">🗑️</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="tempAssembleSteps.length === 0" class="text-center py-8 border border-dashed border-gray-300 rounded-xl text-gray-400 text-sm">
|
|
目前尚未設定組裝步驟,請點選上方「新增步驟」按鈕。
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end gap-2 flex-shrink-0">
|
|
<button @click="closeDialog" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-700 border border-gray-300 rounded-lg text-xs font-semibold transition duration-150">取消</button>
|
|
<button @click="saveAssembleConfig" :disabled="tempAssembleSteps.length === 0" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition duration-150">確認配置</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== 3. CLOCK 配置對話框 ==================== -->
|
|
<div v-if="activeDialog === 'CLOCK'" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 max-w-xl w-full overflow-hidden transform transition-all flex flex-col max-h-[85vh]">
|
|
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-slate-50 flex-shrink-0">
|
|
<h3 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
|
<span class="w-2.5 h-2.5 rounded-full bg-amber-500"></span>
|
|
配置工時報工階段設定
|
|
</h3>
|
|
<button @click="closeDialog" class="text-gray-400 hover:text-gray-600 focus:outline-none text-xl">×</button>
|
|
</div>
|
|
|
|
<div class="p-6 overflow-y-auto space-y-4 flex-grow bg-slate-50/50">
|
|
<div class="flex justify-between items-center">
|
|
<p class="text-xs text-gray-500">請設定報工階段與標準時間:</p>
|
|
<button @click="addClockStep" class="px-3 py-1.5 bg-orange-500 hover:bg-orange-600 text-white rounded-lg text-xs font-semibold transition duration-150 flex items-center gap-1 shadow-sm">
|
|
<span>+</span> 新增階段
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<div v-for="(step, cIdx) in tempClockSteps" :key="step.tempId"
|
|
class="p-4 bg-white border border-gray-200 rounded-2xl flex items-center gap-4 hover:border-orange-300 transition duration-150 shadow-sm">
|
|
|
|
<div class="flex-grow grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
|
|
<div>
|
|
<label class="block mb-1 text-gray-500 font-bold">階段名稱:</label>
|
|
<input type="text" v-model="step.title" placeholder="例如:第一階段粗車反應" class="w-full p-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500 text-gray-800 text-sm">
|
|
</div>
|
|
<div>
|
|
<label class="block mb-1 text-gray-500 font-bold">設定時長 (時 : 分 : 秒):</label>
|
|
<div class="flex items-center gap-1">
|
|
<input type="number" min="0" max="99" v-model.number="step.hours" placeholder="時" class="w-full p-2 border border-gray-300 rounded-lg text-center font-mono focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
<span class="font-bold text-gray-400">:</span>
|
|
<input type="number" min="0" max="59" v-model.number="step.minutes" placeholder="分" class="w-full p-2 border border-gray-300 rounded-lg text-center font-mono focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
<span class="font-bold text-gray-400">:</span>
|
|
<input type="number" min="0" max="59" v-model.number="step.seconds" placeholder="秒" class="w-full p-2 border border-gray-300 rounded-lg text-center font-mono focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-1 text-gray-400 border-l pl-3">
|
|
<button @click="moveClockStep(cIdx, -1)" :disabled="cIdx === 0" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▲</button>
|
|
<button @click="moveClockStep(cIdx, 1)" :disabled="cIdx === tempClockSteps.length - 1" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▼</button>
|
|
<button @click="removeClockStep(cIdx)" class="p-1 hover:bg-red-50 hover:text-red-500 rounded mt-1 transition duration-100">🗑️</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="tempClockSteps.length === 0" class="text-center py-8 border border-dashed border-gray-300 rounded-2xl bg-white text-gray-400 text-sm">
|
|
目前尚未設定工時報工階段,請點選上方「新增階段」按鈕。
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end gap-2 flex-shrink-0">
|
|
<button @click="closeDialog" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-700 border border-gray-300 rounded-lg text-xs font-semibold transition duration-150">取消</button>
|
|
<button @click="saveClockConfig" :disabled="tempClockSteps.length === 0" class="px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg text-xs font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition duration-150">確認配置</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== 4. FEEDING 配置對話框 ==================== -->
|
|
|
|
<div v-if="activeDialog === 'FEEDING'" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 max-w-4xl w-full overflow-hidden transform transition-all flex flex-col max-h-[85vh]">
|
|
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-slate-50 flex-shrink-0">
|
|
<h3 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
|
<span class="w-2.5 h-2.5 rounded-full bg-orange-600"></span>
|
|
配置投料項目
|
|
</h3>
|
|
<button @click="closeDialog" class="text-gray-400 hover:text-gray-600 focus:outline-none text-xl">×</button>
|
|
</div>
|
|
|
|
<div class="p-6 overflow-y-auto space-y-4 flex-grow bg-slate-50/50">
|
|
<div class="flex justify-between items-center">
|
|
<p class="text-xs text-gray-500">請設定投料原料、投入總量與允差範圍:</p>
|
|
<button @click="addFeedingItem" class="px-3 py-1.5 bg-orange-600 hover:bg-orange-700 text-white rounded-lg text-xs font-semibold transition duration-150 flex items-center gap-1 shadow-sm">
|
|
<span>+</span> 新增投料項目
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div v-for="(item, fIdx) in tempFeedingItems" :key="item.tempId"
|
|
class="p-4 bg-white border border-gray-200 rounded-2xl flex items-center gap-4 hover:border-orange-400 transition duration-150 shadow-sm relative">
|
|
|
|
<!-- 內容主體區塊 (12 欄網格) -->
|
|
<div class="flex-grow grid grid-cols-1 md:grid-cols-12 gap-3 text-xs items-end">
|
|
|
|
<!-- 1. 選擇設備 (佔 3 欄) -->
|
|
<div class="md:col-span-3">
|
|
<label class="block mb-1 text-gray-500 font-bold">1. 選擇設備:</label>
|
|
<select v-model="item.deviceId" @change="onIotDeviceChange(item)" class="w-full p-2 border border-gray-300 rounded-lg bg-white text-gray-800 text-xs focus:outline-none focus:ring-2 focus:ring-teal-500">
|
|
<option :value="null">-- 請選擇設備 --</option>
|
|
<option v-for="dev in iotDeviceOptions" :key="dev.id" :value="dev.id">
|
|
{{ dev.code }} ({{ dev.name }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 2. routing key (佔 3 欄) -->
|
|
<div class="md:col-span-3">
|
|
<label class="block mb-1 text-gray-500 font-bold">2. routing key:</label>
|
|
<select v-model="item.routeKey" :disabled="!item.deviceId" class="w-full p-2 border border-gray-300 rounded-lg bg-white text-gray-800 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-teal-500 disabled:bg-gray-100 disabled:text-gray-400">
|
|
<option :value="''">-- 請選擇 routing key --</option>
|
|
<option v-for="keyOpt in getAvailableRouteKeys(item.deviceId)" :key="keyOpt.key" :value="keyOpt.key">
|
|
{{ keyOpt.key }} ({{ keyOpt.label }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 3. 選擇物料 (佔 6 欄) -->
|
|
<div class="md:col-span-6">
|
|
<label class="block mb-1 text-gray-500 font-bold">3.選擇物料:</label>
|
|
<select v-model="item.materialId" @change="onMaterialChange(item)" class="w-full p-2 border border-gray-300 rounded-lg bg-white text-gray-800 text-xs focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
<option :value="null">-- 請選擇物料 --</option>
|
|
<option v-for="opt in feedMaterialOptions" :key="opt.id" :value="opt.id">
|
|
{{ opt.code }} - {{ opt.name }} (預設: {{ opt.defaultUnit }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- 4. 設定投入總量 (佔 6 欄) -->
|
|
<div class="md:col-span-6">
|
|
<label class="block mb-1 text-gray-500 font-bold">4.設定投入總量:</label>
|
|
<div class="flex gap-1">
|
|
<input type="number" step="any" v-model.number="item.targetQty" placeholder="總量" class="flex-grow min-w-0 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
<select v-model="item.unit" class="w-16 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500 flex-shrink-0">
|
|
<option value="kg">kg</option>
|
|
<option value="g">g</option>
|
|
<option value="L">L</option>
|
|
<option value="ml">ml</option>
|
|
<option value="pcs">pcs</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 5. 允差範圍設定 (佔 6 欄) -->
|
|
<div class="md:col-span-6">
|
|
<label class="block mb-1 text-gray-500 font-bold">5.允差範圍設定:</label>
|
|
<div class="flex gap-1 items-center">
|
|
<span class="text-md font-bold text-gray-400">±</span>
|
|
<input type="number" step="any" v-model.number="item.tolerance" placeholder="數值" class="flex-grow min-w-0 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
<select v-model="item.toleranceType" class="w-24 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500 flex-shrink-0">
|
|
<option value="absolute">值 ({{ item.unit }})</option>
|
|
<option value="percent">%</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 6. 系統判定容許區間提示 (佔滿 12 欄) -->
|
|
<div class="md:col-span-12 bg-slate-50 px-2.5 py-1.5 rounded-lg border border-dashed text-[11px] text-gray-500 flex justify-between items-center mt-1" v-if="item.targetQty !== null && item.tolerance !== null">
|
|
<span>系統判定容許區間:</span>
|
|
<span class="font-bold text-gray-700">
|
|
{{ getFeedingRangeString(item) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右側操作按鈕區 (上下移動、刪除) -->
|
|
<div class="flex flex-col gap-1.5 text-gray-400 border-l pl-3 self-stretch justify-center">
|
|
<button @click="moveFeedingItem(fIdx, -1)" :disabled="fIdx === 0" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▲</button>
|
|
<button @click="moveFeedingItem(fIdx, 1)" :disabled="fIdx === tempFeedingItems.length - 1" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▼</button>
|
|
<button @click="removeFeedingItem(fIdx)" class="p-1 hover:bg-red-50 hover:text-red-500 rounded mt-1 transition duration-100">🗑️</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 尚無項目提示 -->
|
|
<div v-if="tempFeedingItems.length === 0" class="text-center py-10 border border-dashed border-gray-300 rounded-2xl bg-white text-gray-400 text-sm">
|
|
目前尚未設定任何投料項目,請點選上方「新增投料項目」按鈕。
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 對話框底部按鈕 -->
|
|
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end gap-2 flex-shrink-0">
|
|
<button @click="closeDialog" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-700 border border-gray-300 rounded-lg text-xs font-semibold transition duration-150">取消</button>
|
|
<button @click="saveFeedingConfig" :disabled="tempFeedingItems.length === 0" class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg text-xs font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition duration-150">確認配置</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- ==================== 5. IOT 配置對話框 ==================== -->
|
|
<div v-if="activeDialog === 'IOT'" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 max-w-3xl w-full overflow-hidden transform transition-all flex flex-col max-h-[85vh]">
|
|
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-slate-50 flex-shrink-0">
|
|
<h3 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
|
<span class="w-2.5 h-2.5 rounded-full bg-teal-600"></span>
|
|
配置 IOT 物聯網監控參數
|
|
</h3>
|
|
<button @click="closeDialog" class="text-gray-400 hover:text-gray-600 focus:outline-none text-xl">×</button>
|
|
</div>
|
|
|
|
<div class="p-6 overflow-y-auto space-y-4 flex-grow bg-slate-50/50">
|
|
<div class="flex justify-between items-center">
|
|
<p class="text-xs text-gray-500">請選擇監控設備、routing key、設定標準數值與標準範圍:</p>
|
|
<button @click="addIotItem" class="px-3 py-1.5 bg-teal-600 hover:bg-teal-700 text-white rounded-lg text-xs font-semibold transition duration-150 flex items-center gap-1 shadow-sm">
|
|
<span>+</span> 新增監控參數
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div v-for="(item, iIdx) in tempIotItems" :key="item.tempId"
|
|
class="p-4 bg-white border border-gray-200 rounded-2xl flex items-center gap-4 hover:border-teal-500 transition duration-150 shadow-sm relative">
|
|
|
|
<div class="flex-grow grid grid-cols-1 md:grid-cols-12 gap-3 text-xs items-end">
|
|
<div class="md:col-span-3">
|
|
<label class="block mb-1 text-gray-500 font-bold">1. 選擇設備:</label>
|
|
<select v-model="item.deviceId" @change="onIotDeviceChange(item)" class="w-full p-2 border border-gray-300 rounded-lg bg-white text-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-teal-500">
|
|
<option :value="null">-- 請選擇設備 --</option>
|
|
<option v-for="dev in iotDeviceOptions" :key="dev.id" :value="dev.id">
|
|
{{ dev.code }} ({{ dev.name }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="md:col-span-3">
|
|
<label class="block mb-1 text-gray-500 font-bold">2. routing key:</label>
|
|
<select v-model="item.routeKey" :disabled="!item.deviceId" class="w-full p-2 border border-gray-300 rounded-lg bg-white text-gray-800 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-teal-500 disabled:bg-gray-100 disabled:text-gray-400">
|
|
<option :value="''">-- 請選擇 routing key --</option>
|
|
<option v-for="keyOpt in getAvailableRouteKeys(item.deviceId)" :key="keyOpt.key" :value="keyOpt.key">
|
|
{{ keyOpt.key }} ({{ keyOpt.label }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="md:col-span-3">
|
|
<label class="block mb-1 text-gray-500 font-bold">標準數值:</label>
|
|
<div class="flex gap-1">
|
|
<input type="number" step="any" v-model.number="item.targetVal" placeholder="例如: 25.0" class="flex-grow min-w-0 p-2 border border-gray-300 rounded-lg text-sm bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-teal-500">
|
|
<input type="text" v-model="item.unit" placeholder="單位" class="w-14 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-teal-500 flex-shrink-0 text-center">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="md:col-span-3">
|
|
<label class="block mb-1 text-gray-500 font-bold">標準範圍 (下限 ~ 上限):</label>
|
|
<div class="flex items-center gap-1">
|
|
<input type="number" step="any" v-model.number="item.minVal" placeholder="下限" class="w-full min-w-0 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-teal-500">
|
|
<span class="text-gray-400 font-bold">~</span>
|
|
<input type="number" step="any" v-model.number="item.maxVal" placeholder="上限" class="w-full min-w-0 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-teal-500">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="md:col-span-12 bg-slate-50 px-2.5 py-1.5 rounded-lg border border-dashed text-[11px] text-gray-500 flex justify-between items-center mt-1" v-if="item.minVal !== null && item.maxVal !== null">
|
|
<span>IOT 系統監控警報區間:</span>
|
|
<span class="font-bold text-teal-700">
|
|
介於 {{ item.minVal }} ~ {{ item.maxVal }} {{ item.unit }} (目標標準值: {{ item.targetVal ?? '未設' }} {{ item.unit }})
|
|
</span>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-1.5 text-gray-400 border-l pl-3 self-stretch justify-center">
|
|
<button @click="moveIotItem(iIdx, -1)" :disabled="iIdx === 0" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▲</button>
|
|
<button @click="moveIotItem(iIdx, 1)" :disabled="iIdx === tempIotItems.length - 1" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▼</button>
|
|
<button @click="removeIotItem(iIdx)" class="p-1 hover:bg-red-50 hover:text-red-500 rounded mt-1 transition duration-100">🗑️</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="tempIotItems.length === 0" class="text-center py-10 border border-dashed border-gray-300 rounded-2xl bg-white text-gray-400 text-sm">
|
|
目前尚未設定任何 IOT 監控參數,請點選上方「新增監控參數」按鈕。
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end gap-2 flex-shrink-0">
|
|
<button @click="closeDialog" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-700 border border-gray-300 rounded-lg text-xs font-semibold transition duration-150">取消</button>
|
|
<button @click="saveIotConfig" :disabled="tempIotItems.length === 0" class="px-4 py-2 bg-teal-600 hover:bg-teal-700 text-white rounded-lg text-xs font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition duration-150">確認配置</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==================== 6. MATERIAL 配置對話框 ==================== -->
|
|
<div v-if="activeDialog === 'MATERIAL'" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 max-w-3xl w-full overflow-hidden transform transition-all flex flex-col max-h-[85vh]">
|
|
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-slate-50 flex-shrink-0">
|
|
<h3 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
|
<span class="w-2.5 h-2.5 rounded-full bg-orange-600"></span>
|
|
配置投料項目
|
|
</h3>
|
|
<button @click="closeDialog" class="text-gray-400 hover:text-gray-600 focus:outline-none text-xl">×</button>
|
|
</div>
|
|
|
|
<div class="p-6 overflow-y-auto space-y-4 flex-grow bg-slate-50/50">
|
|
<div class="flex justify-between items-center">
|
|
<p class="text-xs text-gray-500">請設定投料原料、投入總量與允差範圍:</p>
|
|
<button @click="addFeedingItem" class="px-3 py-1.5 bg-orange-600 hover:bg-orange-700 text-white rounded-lg text-xs font-semibold transition duration-150 flex items-center gap-1 shadow-sm">
|
|
<span>+</span> 新增投料項目
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div v-for="(item, fIdx) in tempFeedingItems" :key="item.tempId"
|
|
class="p-4 bg-white border border-gray-200 rounded-2xl flex items-center gap-4 hover:border-orange-400 transition duration-150 shadow-sm relative">
|
|
|
|
<div class="flex-grow grid grid-cols-1 md:grid-cols-10 gap-4 text-xs items-end">
|
|
<div class="md:col-span-4">
|
|
<label class="block mb-1 text-gray-500 font-bold">1.選擇物料:</label>
|
|
<select v-model="item.materialId" @change="onMaterialChange(item)" class="w-full p-2 border border-gray-300 rounded-lg bg-white text-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
<option :value="null">-- 請選擇物料 --</option>
|
|
<option v-for="opt in feedMaterialOptions" :key="opt.id" :value="opt.id">
|
|
{{ opt.code }} - {{ opt.name }} (預設: {{ opt.defaultUnit }})
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="md:col-span-3">
|
|
<label class="block mb-1 text-gray-500 font-bold">2.設定投入總量:</label>
|
|
<div class="flex gap-1">
|
|
<input type="number" step="any" v-model.number="item.targetQty" placeholder="總量" class="flex-grow min-w-0 p-2 border border-gray-300 rounded-lg text-sm bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
<select v-model="item.unit" class="w-16 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500 flex-shrink-0">
|
|
<option value="kg">kg</option>
|
|
<option value="g">g</option>
|
|
<option value="L">L</option>
|
|
<option value="ml">ml</option>
|
|
<option value="pcs">pcs</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="md:col-span-3">
|
|
<label class="block mb-1 text-gray-500 font-bold">3.允差範圍設定:</label>
|
|
<div class="flex gap-1 items-center">
|
|
<span class="text-md font-bold text-gray-400">±</span>
|
|
<input type="number" step="any" v-model.number="item.tolerance" placeholder="數值" class="flex-grow min-w-0 p-2 border border-gray-300 rounded-lg text-sm bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500">
|
|
|
|
<select v-model="item.toleranceType" class="w-24 p-2 border border-gray-300 rounded-lg text-xs bg-white text-gray-700 focus:outline-none focus:ring-2 focus:ring-orange-500 flex-shrink-0">
|
|
<option value="absolute">值 ({{ item.unit }})</option>
|
|
<option value="percent">%</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="md:col-span-10 bg-slate-50 px-2.5 py-1.5 rounded-lg border border-dashed text-[11px] text-gray-500 flex justify-between items-center mt-1" v-if="item.targetQty !== null && item.tolerance !== null">
|
|
<span>系統判定容許區間:</span>
|
|
<span class="font-bold text-gray-700">
|
|
{{ getFeedingRangeString(item) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-1.5 text-gray-400 border-l pl-3 self-stretch justify-center">
|
|
<button @click="moveFeedingItem(fIdx, -1)" :disabled="fIdx === 0" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▲</button>
|
|
<button @click="moveFeedingItem(fIdx, 1)" :disabled="fIdx === tempFeedingItems.length - 1" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▼</button>
|
|
<button @click="removeFeedingItem(fIdx)" class="p-1 hover:bg-red-50 hover:text-red-500 rounded mt-1 transition duration-100">🗑️</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="tempFeedingItems.length === 0" class="text-center py-10 border border-dashed border-gray-300 rounded-2xl bg-white text-gray-400 text-sm">
|
|
目前尚未設定任何投料項目,請點選上方「新增投料項目」按鈕。
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end gap-2 flex-shrink-0">
|
|
<button @click="closeDialog" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-700 border border-gray-300 rounded-lg text-xs font-semibold transition duration-150">取消</button>
|
|
<button @click="saveFeedingConfig" :disabled="tempFeedingItems.length === 0" class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg text-xs font-semibold disabled:opacity-50 disabled:cursor-not-allowed transition duration-150">確認配置</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ==================== 7. SOP 配置對話框 ==================== -->
|
|
<div v-if="activeDialog === 'SOP'" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
<div class="bg-white rounded-2xl shadow-xl border border-gray-200 max-w-2xl w-full overflow-hidden transform transition-all flex flex-col max-h-[85vh]">
|
|
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-slate-50 flex-shrink-0">
|
|
<h3 class="text-lg font-bold text-gray-800 flex items-center gap-2">
|
|
<span class="w-2.5 h-2.5 rounded-full bg-purple-600"></span>
|
|
配置標準作業指導書 (SOP)
|
|
</h3>
|
|
<button @click="closeDialog" class="text-gray-400 hover:text-gray-600 focus:outline-none text-xl">×</button>
|
|
</div>
|
|
|
|
<div class="p-6 overflow-y-auto space-y-6 flex-grow bg-slate-50/50">
|
|
<!-- 步驟列表區 -->
|
|
<div class="space-y-3">
|
|
<div class="flex justify-between items-center">
|
|
<label class="block font-bold text-gray-700 text-sm">作業步驟列表:</label>
|
|
<button @click="addSopStep" class="px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-xs font-semibold transition duration-150 flex items-center gap-1 shadow-sm">
|
|
<span>+</span> 新增步驟
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-2">
|
|
<div v-for="(step, sIdx) in tempSopSteps" :key="step.tempId" class="flex items-center gap-2 bg-white p-3 border border-gray-200 rounded-xl shadow-sm">
|
|
<span class="text-xs font-bold text-gray-500 whitespace-nowrap">步驟 {{ sIdx + 1 }}</span>
|
|
<input type="text" v-model="step.title" placeholder="請輸入步驟名稱" class="flex-grow p-2 border border-gray-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-purple-500">
|
|
<div class="flex gap-1 text-gray-400">
|
|
<button @click="moveSopStep(sIdx, -1)" :disabled="sIdx === 0" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▲</button>
|
|
<button @click="moveSopStep(sIdx, 1)" :disabled="sIdx === tempSopSteps.length - 1" class="p-1 hover:bg-gray-100 rounded disabled:opacity-30">▼</button>
|
|
<button @click="removeSopStep(sIdx)" class="p-1 hover:bg-red-50 hover:text-red-500 rounded text-red-400">🗑️</button>
|
|
</div>
|
|
</div>
|
|
<div v-if="tempSopSteps.length === 0" class="text-center py-4 border border-dashed border-gray-300 rounded-xl bg-white text-gray-400 text-xs">
|
|
目前尚未設定步驟
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="border-gray-200">
|
|
|
|
<!-- 附件上傳區:文檔與圖檔分開 -->
|
|
<div class="space-y-4">
|
|
<label class="block font-bold text-gray-700 text-sm">SOP 相關附件設定:</label>
|
|
|
|
<!-- 1. PDF 參考文檔區 -->
|
|
<div class="p-4 bg-white border border-gray-200 rounded-xl space-y-2">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-xs font-bold text-gray-700">📄 參考文檔 (PDF)</span>
|
|
<label class="px-2.5 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-xs cursor-pointer border border-gray-300 transition">
|
|
選擇 PDF 檔案
|
|
<input type="file" accept=".pdf" multiple class="hidden" @change="handlePdfUpload">
|
|
</label>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div v-for="(pdf, pIdx) in tempSopPdfs" :key="pIdx" class="flex justify-between items-center bg-gray-50 px-3 py-1.5 rounded-lg text-xs border border-gray-100">
|
|
<span class="text-gray-600 truncate font-mono">📄 {{ pdf.name }}</span>
|
|
<button @click="tempSopPdfs.splice(pIdx, 1)" class="text-red-500 hover:text-red-700 text-xs font-bold">×</button>
|
|
</div>
|
|
<div v-if="tempSopPdfs.length === 0" class="text-gray-400 text-xs italic py-1">尚未上傳任何 PDF 文檔</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. 現場圖檔區 -->
|
|
<div class="p-4 bg-white border border-gray-200 rounded-xl space-y-2">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-xs font-bold text-gray-700">🖼️ 現場圖檔 (圖片)</span>
|
|
<label class="px-2.5 py-1 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg text-xs cursor-pointer border border-gray-300 transition">
|
|
選擇圖片
|
|
<input type="file" accept="image/*" multiple class="hidden" @change="handleImageUpload">
|
|
</label>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2 pt-1">
|
|
<div v-for="(img, imgIdx) in tempSopImages" :key="imgIdx" class="relative w-16 h-16 border border-gray-200 rounded-lg overflow-hidden bg-black flex-shrink-0 group">
|
|
<img :src="img.url" class="w-full h-full object-cover">
|
|
<button @click="tempSopImages.splice(imgIdx, 1)" class="absolute top-1 right-1 bg-black/60 hover:bg-red-500 text-white rounded-full w-4 h-4 flex items-center justify-center text-[10px]">×</button>
|
|
</div>
|
|
<div v-if="tempSopImages.length === 0" class="text-gray-400 text-xs italic py-1">尚未上傳任何現場圖檔</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-slate-50 border-t border-gray-100 flex justify-end gap-2 flex-shrink-0">
|
|
<button @click="closeDialog" class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-700 border border-gray-300 rounded-lg text-xs font-semibold transition duration-150">取消</button>
|
|
<button @click="saveSopConfig" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-xs font-semibold transition duration-150">確認配置</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp, ref, computed } = Vue;
|
|
|
|
createApp({
|
|
setup() {
|
|
const routingStations = ref([
|
|
{ seq: 1, name: '品質檢驗站', machines: ['QC-01 - 光學投影儀', 'QC-02 - 輪廓儀'], moreCount: 1, appType: 'insp', qcStatus: '未配置', swipeQty: 1, reworkReturnSeq: null, canSkip: false, configData: null },
|
|
{ seq: 2, name: '成品組裝站', machines: ['ASM-01 - 自動組裝機', 'ASM-02 - 壓合機'], moreCount: 2, appType: 'ASSEMBLE', qcStatus: '未配置', swipeQty: 1, reworkReturnSeq: null, canSkip: false, configData: null },
|
|
{ seq: 3, name: '工時報工站', machines: ['CLK-01 - 刷卡感應終端'], moreCount: 0, appType: 'CLOCK', qcStatus: '未配置', swipeQty: 1, reworkReturnSeq: null, canSkip: false, configData: null },
|
|
{ seq: 4, name: '進料投料站', machines: ['FED-01 - 震動盤供料機', 'FED-02 - 皮帶輸送線'], moreCount: 1, appType: 'FEEDING', qcStatus: '未配置', swipeQty: 1, reworkReturnSeq: null, canSkip: false, configData: null },
|
|
{ seq: 5, name: '物聯網監控站', machines: ['IOT-G01 - 數據網關', 'IOT-S05 - 溫溼度傳感器'], moreCount: 4, appType: 'IOT', qcStatus: '未配置', swipeQty: 1, reworkReturnSeq: null, canSkip: false, configData: null },
|
|
{ seq: 6, name: '物料領退站', machines: ['MAT-01 - 智能物料架'], moreCount: 1, appType: 'MATERIAL', qcStatus: '未配置', swipeQty: 1, reworkReturnSeq: null, canSkip: false, configData: null },
|
|
{ seq: 7, name: '標準作業指導站', machines: ['SOP-D01 - 電子看板播放終端'], moreCount: 0, appType: 'SOP', qcStatus: '未配置', swipeQty: 1, reworkReturnSeq: null, canSkip: false, configData: null },
|
|
{ seq: 8, name: '成品入庫站', machines: ['WH-R01 - 條碼掃描槍', 'WH-P02 - 標籤列印機'], moreCount: 3, appType: 'WAREHOUSE', qcStatus: '未配置', swipeQty: 1, reworkReturnSeq: null, canSkip: false }
|
|
]);
|
|
|
|
// ==================== 【INSP 配置資料】 ====================
|
|
const inspFormOptions = ref([
|
|
{ id: 101, code: 'FM-QC-001', name: '首件自主檢驗表' },
|
|
{ id: 102, code: 'FM-QC-002', name: '製程巡檢標準表' },
|
|
{ id: 103, code: 'FM-QC-003', name: '隱形眼鏡光學規格檢測表' },
|
|
{ id: 104, code: 'FM-QC-004', name: '外觀瑕疵抽樣檢驗規範' }
|
|
]);
|
|
const searchQuery = ref('');
|
|
const showDropdown = ref(false);
|
|
|
|
const filteredInspForms = computed(() => {
|
|
const query = searchQuery.value.trim().toLowerCase();
|
|
const currentSelected = inspFormOptions.value.find(f => f.id === tempConfig.value.formId);
|
|
const selectedText = currentSelected ? `${currentSelected.code} - ${currentSelected.name}` : '';
|
|
if (!query || query === selectedText.toLowerCase()) return inspFormOptions.value;
|
|
return inspFormOptions.value.filter(form => form.code.toLowerCase().includes(query) || form.name.toLowerCase().includes(query));
|
|
});
|
|
|
|
// ==================== 【FEEDING 物料選單資料】 ====================
|
|
const feedMaterialOptions = ref([
|
|
{ id: 201, code: 'MAT-CL-001', name: '角膜塑形鏡片藍色母粒', defaultUnit: 'g' },
|
|
{ id: 202, code: 'MAT-CL-002', name: '角膜塑形鏡片綠色母粒', defaultUnit: 'g' },
|
|
{ id: 203, code: 'MAT-SL-010', name: '高透氧矽水凝膠 (Silicon Hydrogel)', defaultUnit: 'kg' },
|
|
{ id: 204, code: 'MAT-SOL-50', name: '隱形眼鏡保存緩衝液 (Buffer Solution)', defaultUnit: 'L' },
|
|
{ id: 205, code: 'MAT-PKG-01', name: 'PP 塑膠包裝泡殼', defaultUnit: 'pcs' },
|
|
{ id: 206, code: 'MAT-FOIL-02', name: '鋁箔封口膜', defaultUnit: 'pcs' }
|
|
]);
|
|
|
|
// ==================== 【IOT 設備與 routing key 選單資料】 ====================
|
|
const iotDeviceOptions = ref([
|
|
{
|
|
id: 301,
|
|
code: 'IOT-S05',
|
|
name: '車間環境溫濕度感測器',
|
|
routeKeys: [
|
|
{ key: 'iot.No67.temperature', label: '環境溫度', defaultUnit: '°C' },
|
|
{ key: 'iot.No67.humidity', label: '環境濕度', defaultUnit: '%RH' }
|
|
]
|
|
},
|
|
{
|
|
id: 302,
|
|
code: 'IOT-CNC-01',
|
|
name: '主軸 CNC 車床監控儀',
|
|
routeKeys: [
|
|
{ key: 'iot.No67.spindle_speed', label: '主軸轉速', defaultUnit: 'RPM' },
|
|
{ key: 'iot.No67.vibration', label: '機台振動頻率', defaultUnit: 'mm/s' },
|
|
{ key: 'iot.No67.motor_temp', label: '馬達溫度', defaultUnit: '°C' }
|
|
]
|
|
},
|
|
{
|
|
id: 303,
|
|
code: 'IOT-PRESS-02',
|
|
name: '自動壓合機壓力監測器',
|
|
routeKeys: [
|
|
{ key: 'iot.No67.pressure_bar', label: '即時壓力', defaultUnit: 'bar' },
|
|
{ key: 'iot.No67.hydraulic_oil_temp', label: '液壓油溫', defaultUnit: '°C' }
|
|
]
|
|
}
|
|
]);
|
|
|
|
// ==================== 【彈出視窗控制與暫存資料】 ====================
|
|
const activeDialog = ref(null);
|
|
const currentEditingStation = ref(null);
|
|
|
|
const tempConfig = ref({ formId: null });
|
|
const tempAssembleSteps = ref([]);
|
|
const tempClockSteps = ref([]);
|
|
const tempFeedingItems = ref([]);
|
|
const tempIotItems = ref([]);
|
|
const tempMaterialSteps = ref([]);
|
|
|
|
// SOP 暫存狀態
|
|
const tempSopSteps = ref([]);
|
|
const tempSopPdfs = ref([]);
|
|
const tempSopImages = ref([]);
|
|
|
|
// 開啟配置彈窗
|
|
const openConfigDialog = (station) => {
|
|
currentEditingStation.value = station;
|
|
activeDialog.value = station.appType;
|
|
|
|
if (station.appType === 'insp') {
|
|
showDropdown.value = false;
|
|
if (station.configData) {
|
|
tempConfig.value.formId = station.configData.formId;
|
|
searchQuery.value = `${station.configData.formCode} - ${station.configData.formName}`;
|
|
} else {
|
|
tempConfig.value.formId = null;
|
|
searchQuery.value = '';
|
|
}
|
|
}
|
|
else if (station.appType === 'ASSEMBLE') {
|
|
tempAssembleSteps.value = (station.configData && station.configData.steps) ? JSON.parse(JSON.stringify(station.configData.steps)) : [];
|
|
}
|
|
else if (station.appType === 'CLOCK') {
|
|
tempClockSteps.value = (station.configData && station.configData.steps) ? JSON.parse(JSON.stringify(station.configData.steps)) : [];
|
|
}
|
|
else if (station.appType === 'FEEDING') {
|
|
tempFeedingItems.value = (station.configData && station.configData.items) ? JSON.parse(JSON.stringify(station.configData.items)) : [];
|
|
}
|
|
else if (station.appType === 'IOT') {
|
|
tempIotItems.value = (station.configData && station.configData.items) ? JSON.parse(JSON.stringify(station.configData.items)) : [];
|
|
}
|
|
else if (station.appType === 'MATERIAL') {
|
|
tempMaterialSteps.value = (station.configData && station.configData.steps) ? JSON.parse(JSON.stringify(station.configData.steps)) : [];
|
|
}
|
|
else if (station.appType === 'SOP') {
|
|
tempSopSteps.value = (station.configData && station.configData.steps) ? JSON.parse(JSON.stringify(station.configData.steps)) : [];
|
|
tempSopPdfs.value = (station.configData && station.configData.pdfs) ? JSON.parse(JSON.stringify(station.configData.pdfs)) : [];
|
|
tempSopImages.value = (station.configData && station.configData.images) ? JSON.parse(JSON.stringify(station.configData.images)) : [];
|
|
}
|
|
};
|
|
|
|
const closeDialog = () => {
|
|
activeDialog.value = null;
|
|
currentEditingStation.value = null;
|
|
tempConfig.value.formId = null;
|
|
searchQuery.value = '';
|
|
showDropdown.value = false;
|
|
tempAssembleSteps.value = [];
|
|
tempClockSteps.value = [];
|
|
tempFeedingItems.value = [];
|
|
tempIotItems.value = [];
|
|
tempMaterialSteps.value = [];
|
|
tempSopSteps.value = [];
|
|
tempSopPdfs.value = [];
|
|
tempSopImages.value = [];
|
|
};
|
|
|
|
// INSP 方法...
|
|
const selectForm = (form) => { tempConfig.value.formId = form.id; searchQuery.value = `${form.code} - ${form.name}`; showDropdown.value = false; };
|
|
const clearSelection = () => { tempConfig.value.formId = null; searchQuery.value = ''; showDropdown.value = true; };
|
|
const saveInspConfig = () => {
|
|
if (!currentEditingStation.value) return;
|
|
const selectedForm = inspFormOptions.value.find(f => f.id === tempConfig.value.formId);
|
|
currentEditingStation.value.configData = selectedForm ? { formId: selectedForm.id, formCode: selectedForm.code, formName: selectedForm.name } : null;
|
|
currentEditingStation.value.qcStatus = selectedForm ? '已配置' : '未配置';
|
|
closeDialog();
|
|
};
|
|
|
|
// ASSEMBLE 方法...
|
|
const addAssembleStep = () => { tempAssembleSteps.value.push({ tempId: Date.now(), title: '', desc: '' }); };
|
|
const removeAssembleStep = (index) => { tempAssembleSteps.value.splice(index, 1); };
|
|
const moveAssembleStep = (index, dir) => { const t = index + dir; if (t >= 0 && t < tempAssembleSteps.value.length) [tempAssembleSteps.value[index], tempAssembleSteps.value[t]] = [tempAssembleSteps.value[t], tempAssembleSteps.value[index]]; };
|
|
const saveAssembleConfig = () => {
|
|
if (!currentEditingStation.value) return;
|
|
const finalSteps = tempAssembleSteps.value.filter(s => s.title.trim() !== '');
|
|
currentEditingStation.value.configData = finalSteps.length > 0 ? { steps: finalSteps } : null;
|
|
currentEditingStation.value.qcStatus = finalSteps.length > 0 ? '已配置' : '未配置';
|
|
closeDialog();
|
|
};
|
|
|
|
// CLOCK 方法...
|
|
const addClockStep = () => { tempClockSteps.value.push({ tempId: Date.now(), title: '', hours: 0, minutes: 0, seconds: 0 }); };
|
|
const removeClockStep = (index) => { tempClockSteps.value.splice(index, 1); };
|
|
const moveClockStep = (index, dir) => { const t = index + dir; if (t >= 0 && t < tempClockSteps.value.length) [tempClockSteps.value[index], tempClockSteps.value[t]] = [tempClockSteps.value[t], tempClockSteps.value[index]]; };
|
|
const saveClockConfig = () => {
|
|
if (!currentEditingStation.value) return;
|
|
const finalSteps = tempClockSteps.value.filter(s => s.title.trim() !== '');
|
|
currentEditingStation.value.configData = finalSteps.length > 0 ? { steps: finalSteps } : null;
|
|
currentEditingStation.value.qcStatus = finalSteps.length > 0 ? '已配置' : '未配置';
|
|
closeDialog();
|
|
};
|
|
|
|
// FEEDING 方法...
|
|
const addFeedingItem = () => { tempFeedingItems.value.push({ tempId: Date.now(), materialId: null, materialCode: '', materialName: '', targetQty: null, unit: 'kg', tolerance: null, toleranceType: 'absolute' }); };
|
|
const removeFeedingItem = (index) => { tempFeedingItems.value.splice(index, 1); };
|
|
const moveFeedingItem = (index, dir) => { const t = index + dir; if (t >= 0 && t < tempFeedingItems.value.length) [tempFeedingItems.value[index], tempFeedingItems.value[t]] = [tempFeedingItems.value[t], tempFeedingItems.value[index]]; };
|
|
const onMaterialChange = (item) => {
|
|
const matched = feedMaterialOptions.value.find(o => o.id === item.materialId);
|
|
if (matched) { item.materialCode = matched.code; item.materialName = matched.name; item.unit = matched.defaultUnit; }
|
|
};
|
|
const getFeedingRangeString = (item) => {
|
|
const qty = parseFloat(item.targetQty) || 0; const tol = parseFloat(item.tolerance) || 0;
|
|
if (item.toleranceType === 'absolute') return `${(qty - tol).toFixed(2)} ~ ${(qty + tol).toFixed(2)} ${item.unit}`;
|
|
return `${(qty * (1 - tol / 100)).toFixed(2)} ~ ${(qty * (1 + tol / 100)).toFixed(2)} ${item.unit}`;
|
|
};
|
|
const saveFeedingConfig = () => {
|
|
if (!currentEditingStation.value) return;
|
|
const finalItems = tempFeedingItems.value.filter(i => i.materialId !== null && i.targetQty !== null);
|
|
currentEditingStation.value.configData = finalItems.length > 0 ? { items: finalItems } : null;
|
|
currentEditingStation.value.qcStatus = finalItems.length > 0 ? '已配置' : '未配置';
|
|
closeDialog();
|
|
};
|
|
|
|
// IOT 方法...
|
|
const addIotItem = () => {
|
|
tempIotItems.value.push({
|
|
tempId: Date.now(), deviceId: null, deviceCode: '', deviceName: '',
|
|
routeKey: '', targetVal: null, minVal: null, maxVal: null, unit: ''
|
|
});
|
|
};
|
|
const removeIotItem = (index) => { tempIotItems.value.splice(index, 1); };
|
|
const moveIotItem = (index, dir) => {
|
|
const targetIndex = index + dir;
|
|
if (targetIndex < 0 || targetIndex >= tempIotItems.value.length) return;
|
|
const temp = tempIotItems.value[index];
|
|
tempIotItems.value[index] = tempIotItems.value[targetIndex];
|
|
tempIotItems.value[targetIndex] = temp;
|
|
};
|
|
const onIotDeviceChange = (item) => {
|
|
const matched = iotDeviceOptions.value.find(d => d.id === item.deviceId);
|
|
if (matched) { item.deviceCode = matched.code; item.deviceName = matched.name; item.routeKey = ''; item.unit = ''; }
|
|
else { item.deviceCode = ''; item.deviceName = ''; item.routeKey = ''; }
|
|
};
|
|
const getAvailableRouteKeys = (deviceId) => {
|
|
const matched = iotDeviceOptions.value.find(d => d.id === deviceId);
|
|
return matched ? matched.routeKeys : [];
|
|
};
|
|
const saveIotConfig = () => {
|
|
if (!currentEditingStation.value) return;
|
|
const finalItems = tempIotItems.value.filter(item => item.deviceId !== null && item.routeKey !== '');
|
|
if (finalItems.length > 0) {
|
|
currentEditingStation.value.configData = { items: finalItems };
|
|
currentEditingStation.value.qcStatus = '已配置';
|
|
} else {
|
|
currentEditingStation.value.configData = null;
|
|
currentEditingStation.value.qcStatus = '未配置';
|
|
}
|
|
closeDialog();
|
|
};
|
|
|
|
// MATERIAL 方法 (與 ASSEMBLE 相同)
|
|
const addMaterialStep = () => { tempMaterialSteps.value.push({ tempId: Date.now(), title: '', desc: '' }); };
|
|
const removeMaterialStep = (index) => { tempMaterialSteps.value.splice(index, 1); };
|
|
const moveMaterialStep = (index, dir) => { const t = index + dir; if (t >= 0 && t < tempMaterialSteps.value.length) [tempMaterialSteps.value[index], tempMaterialSteps.value[t]] = [tempMaterialSteps.value[t], tempMaterialSteps.value[index]]; };
|
|
const saveMaterialConfig = () => {
|
|
if (!currentEditingStation.value) return;
|
|
const finalSteps = tempMaterialSteps.value.filter(s => s.title.trim() !== '');
|
|
currentEditingStation.value.configData = finalSteps.length > 0 ? { steps: finalSteps } : null;
|
|
currentEditingStation.value.qcStatus = finalSteps.length > 0 ? '已配置' : '未配置';
|
|
closeDialog();
|
|
};
|
|
|
|
// ==================== 【SOP 專用方法】 ====================
|
|
const addSopStep = () => { tempSopSteps.value.push({ tempId: Date.now(), title: '' }); };
|
|
const removeSopStep = (index) => { tempSopSteps.value.splice(index, 1); };
|
|
const moveSopStep = (index, dir) => {
|
|
const t = index + dir;
|
|
if (t >= 0 && t < tempSopSteps.value.length) {
|
|
[tempSopSteps.value[index], tempSopSteps.value[t]] = [tempSopSteps.value[t], tempSopSteps.value[index]];
|
|
}
|
|
};
|
|
|
|
const handlePdfUpload = (e) => {
|
|
const files = e.target.files;
|
|
Array.from(files).forEach(file => {
|
|
tempSopPdfs.value.push({ name: file.name, fileObj: file });
|
|
});
|
|
e.target.value = '';
|
|
};
|
|
|
|
const handleImageUpload = (e) => {
|
|
const files = e.target.files;
|
|
Array.from(files).forEach(file => {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
tempSopImages.value.push({ name: file.name, url: event.target.result, fileObj: file });
|
|
};
|
|
reader.readAsDataURL(file);
|
|
});
|
|
e.target.value = '';
|
|
};
|
|
|
|
const saveSopConfig = () => {
|
|
if (!currentEditingStation.value) return;
|
|
const validSteps = tempSopSteps.value.filter(s => s.title.trim() !== '');
|
|
const hasData = validSteps.length > 0 || tempSopPdfs.value.length > 0 || tempSopImages.value.length > 0;
|
|
|
|
if (hasData) {
|
|
currentEditingStation.value.configData = {
|
|
steps: validSteps,
|
|
pdfs: tempSopPdfs.value,
|
|
images: tempSopImages.value
|
|
};
|
|
currentEditingStation.value.qcStatus = '已配置';
|
|
} else {
|
|
currentEditingStation.value.configData = null;
|
|
currentEditingStation.value.qcStatus = '未配置';
|
|
}
|
|
closeDialog();
|
|
};
|
|
|
|
// 通用...
|
|
const getPreviousStations = (currentIndex) => routingStations.value.slice(0, currentIndex);
|
|
const saveRouting = () => {
|
|
console.log('保存數據:', JSON.parse(JSON.stringify(routingStations.value)));
|
|
alert('儲存成功!已更新途程及所有站點配置。');
|
|
};
|
|
|
|
return {
|
|
routingStations, inspFormOptions, searchQuery, showDropdown, filteredInspForms,
|
|
feedMaterialOptions, iotDeviceOptions, activeDialog, currentEditingStation,
|
|
tempConfig, tempAssembleSteps, tempClockSteps, tempFeedingItems, tempIotItems, tempMaterialSteps,
|
|
tempSopSteps, tempSopPdfs, tempSopImages,
|
|
getPreviousStations, openConfigDialog, selectForm, clearSelection, closeDialog,
|
|
saveInspConfig, addAssembleStep, removeAssembleStep, moveAssembleStep, saveAssembleConfig,
|
|
addClockStep, removeClockStep, moveClockStep, saveClockConfig,
|
|
addFeedingItem, removeFeedingItem, moveFeedingItem, onMaterialChange, getFeedingRangeString, saveFeedingConfig,
|
|
addIotItem, removeIotItem, moveIotItem, onIotDeviceChange, getAvailableRouteKeys, saveIotConfig,
|
|
addMaterialStep, removeMaterialStep, moveMaterialStep, saveMaterialConfig,
|
|
addSopStep, removeSopStep, moveSopStep, handlePdfUpload, handleImageUpload, saveSopConfig,
|
|
saveRouting
|
|
};
|
|
}
|
|
}).mount('#app');
|
|
</script>
|
|
</body>
|
|
</html>
|